[
  {
    "path": ".actrc",
    "content": "-P ubuntu-latest=ludwigai/ludwig-ray\n"
  },
  {
    "path": ".deepsource.toml",
    "content": "version = 1\n\ntest_patterns = [\n  \"tests/**\"\n]\n\n[[analyzers]]\nname = \"python\"\nenabled = true\nruntime_version = \"3.x.x\"\n"
  },
  {
    "path": ".devcontainer/Dockerfile",
    "content": "FROM python:3.12-slim\n\nRUN apt-get update && export DEBIAN_FRONTEND=noninteractive \\\n    && apt-get install -y --no-install-recommends \\\n        git \\\n        build-essential \\\n        curl \\\n        libsndfile1 \\\n        ffmpeg \\\n        sox \\\n    && apt-get clean && rm -rf /var/lib/apt/lists/*\n\n# Create non-root user\nARG USERNAME=vscode\nARG USER_UID=1000\nARG USER_GID=$USER_UID\nRUN groupadd --gid $USER_GID $USERNAME \\\n    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME\n\nUSER $USERNAME\nENV PATH=\"/home/$USERNAME/.local/bin:$PATH\"\n\n# Pre-install pip tools so editable install is fast\nRUN pip install --user --no-cache-dir --upgrade pip setuptools wheel\n"
  },
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n\t\"name\": \"Ludwig Dev\",\n\t\"build\": {\n\t\t\"dockerfile\": \"Dockerfile\",\n\t\t\"context\": \"..\"\n\t},\n\t\"customizations\": {\n\t\t\"vscode\": {\n\t\t\t\"settings\": {\n\t\t\t\t\"python.defaultInterpreterPath\": \"/usr/local/bin/python\"\n\t\t\t},\n\t\t\t\"extensions\": [\n\t\t\t\t\"ms-python.python\",\n\t\t\t\t\"ms-python.vscode-pylance\",\n\t\t\t\t\"charliermarsh.ruff\"\n\t\t\t]\n\t\t}\n\t},\n\t\"postCreateCommand\": \"pip install --user -e '.[test]'\",\n\t\"remoteUser\": \"vscode\",\n\t\"features\": {\n\t\t\"ghcr.io/devcontainers/features/git:1\": {}\n\t}\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n1. Click on '....'\n1. Scroll down to '....'\n1. See error\n\nPlease provide code, yaml config file and a sample of data in order to entirely reproduce the issue.\nIssues that are not reproducible will be ignored.\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Environment (please complete the following information):**\n\n- OS: [e.g. iOS]\n- Version [e.g. 22]\n- Python version\n- Ludwig version\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is. Ex. I'm always frustrated when [...]\n\n**Describe the use case**\nA clear and concise description of what the use case for this feature is.\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "# Code Pull Requests\n\nPlease provide the following:\n\n- a clear explanation of what your code does\n- if applicable, a reference to an issue\n- a reproducible test for your PR (code, config and data sample)\n\n# Documentation Pull Requests\n\nNote that the documentation HTML files are in `docs/` while the Markdown sources are in `mkdocs/docs`.\n\nIf you are proposing a modification to the documentation you should change only the Markdown files.\n\n`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` .\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: docker\n\non:\n  schedule:\n    - cron: \"0 10 * * *\" # everyday at 10am\n  push:\n    branches: [\"main\", \"release-*\"]\n    tags: [\"v*.*.*\"]\n\njobs:\n  start-runner:\n    name: Start self-hosted EC2 runner\n    if: >\n      github.event_name == 'schedule' && github.repository == 'ludwig-ai/ludwig' ||\n      github.event_name == 'push' && github.repository == 'ludwig-ai/ludwig' ||\n      github.event_name == 'pull_request' && github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && !github.event.pull_request.head.repo.fork\n    runs-on: ubuntu-latest\n    outputs:\n      label: ${{ steps.start-ec2-runner.outputs.label }}\n      ec2-instance-id: ${{ steps.start-ec2-runner.outputs.ec2-instance-id }}\n\n    steps:\n      - name: Configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v4\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: ${{ secrets.AWS_REGION }}\n\n      - name: Start EC2 runner\n        id: start-ec2-runner\n        uses: machulav/ec2-github-runner@v2.3.2\n        with:\n          mode: start\n          github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}\n          ec2-image-id: ami-0759580dedc953d1f\n          ec2-instance-type: r5.large\n          subnet-id: subnet-0983be43\n          security-group-id: sg-4cba0d08\n          aws-resource-tags: >\n            [\n              {\"Key\": \"Name\", \"Value\": \"ludwig-github-${{ github.head_ref || github.sha }}\"},\n              {\"Key\": \"GitHubRepository\", \"Value\": \"${{ github.repository }}\"},\n              {\"Key\": \"GitHubHeadRef\", \"Value\": \"${{ github.head_ref }}\"},\n              {\"Key\": \"GitHubSHA\", \"Value\": \"${{ github.sha }}\"}\n            ]\n\n  docker:\n    name: Build docker image ${{ matrix.docker-image }} (push=${{ github.event_name != 'pull_request' }})\n    if: needs.start-runner.result != 'skipped'\n    needs: start-runner # required to start the main job when the runner is ready\n    runs-on: ${{ needs.start-runner.outputs.label }} # run the job on the newly created runners\n\n    # we want an ongoing run of this workflow to be canceled by a later commit\n    # so that there is only one concurrent run of this workflow for each branch\n    concurrency:\n      group: docker-${{ matrix.docker-image }}-${{ github.head_ref || github.sha }}\n      cancel-in-progress: true\n\n    strategy:\n      fail-fast: false\n      matrix:\n        docker-image:\n          - ludwig\n          - ludwig-gpu\n          - ludwig-ray\n          - ludwig-ray-gpu\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          # list of Docker images to use as base name for tags\n          images: |\n            ludwigai/${{ matrix.docker-image }}\n          # generate Docker tags based on the following events/attributes\n          tags: |\n            type=schedule\n            type=ref,event=branch\n            type=ref,event=pr\n            type=semver,pattern={{version}}\n            type=semver,pattern={{major}}.{{minor}}\n            type=semver,pattern={{major}}\n            type=sha\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to DockerHub\n        if: github.event_name != 'pull_request'\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v6\n        with:\n          context: .\n          file: ./docker/${{ matrix.docker-image }}/Dockerfile\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n\n  stop-runner:\n    name: Stop self-hosted EC2 runner\n\n    # required to stop the runner even if the error happened in the previous job\n    if: always() && needs.start-runner.result != 'skipped'\n    needs:\n      - start-runner # required to get output from the start-runner job\n      - docker # required to wait when the main job is done\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Configure AWS credentials\n        uses: aws-actions/configure-aws-credentials@v4\n        with:\n          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}\n          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n          aws-region: ${{ secrets.AWS_REGION }}\n\n      - name: Stop EC2 runner\n        uses: machulav/ec2-github-runner@v2.3.2\n        with:\n          mode: stop\n          github-token: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }}\n          label: ${{ needs.start-runner.outputs.label }}\n          ec2-instance-id: ${{ needs.start-runner.outputs.ec2-instance-id }}\n"
  },
  {
    "path": ".github/workflows/pytest.yml",
    "content": "name: pytest\n\non:\n  push:\n    branches: [\"main\", \"release-*\"]\n  pull_request:\n    branches: [\"main\", \"release-*\"]\n\nconcurrency:\n  group: pytest-${{ github.head_ref || github.sha }}\n  cancel-in-progress: true\n\njobs:\n  unit-tests:\n    runs-on: ubuntu-latest\n    env:\n      AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }}\n      AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }}\n      KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}\n      KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}\n      IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }}\n\n    name: Unit Tests\n    timeout-minutes: 60\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python 3.12\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup Linux\n        run: |\n          sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev\n\n      - name: pip cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-unit-${{ hashFiles('requirements*.txt', '.github/workflows/pytest.yml') }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install -U pip setuptools\n          pip install torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n          pip install '.[test]' --extra-index-url https://download.pytorch.org/whl/cpu\n          pip list\n\n      - name: Unit Tests\n        run: |\n          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\n\n      - name: Regression Tests\n        run: |\n          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\n\n      - name: Upload Test Results\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: Unit Test Results\n          path: pytest*.xml\n\n  integration-tests:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        test-markers:\n          - \"integration_tests_a\"\n          - \"integration_tests_b\"\n          - \"integration_tests_c\"\n          - \"integration_tests_d\"\n          - \"integration_tests_e\"\n          - \"integration_tests_f\"\n\n    env:\n      AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }}\n      AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }}\n      KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}\n      KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}\n      IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }}\n      MARKERS: ${{ matrix.test-markers }}\n\n    name: Integration (${{ matrix.test-markers }})\n    services:\n      minio:\n        image: fclairamb/minio-github-actions\n        env:\n          MINIO_ACCESS_KEY: minio\n          MINIO_SECRET_KEY: minio123\n        ports:\n          - 9000:9000\n\n    timeout-minutes: 90\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python 3.12\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup Linux\n        run: |\n          sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev\n\n      - name: pip cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-integration-${{ hashFiles('requirements*.txt', '.github/workflows/pytest.yml') }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install -U pip setuptools\n          pip install torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n          pip install '.[test]' --extra-index-url https://download.pytorch.org/whl/cpu\n          pip list\n\n      - name: Free Disk Space\n        uses: jlumbroso/free-disk-space@main\n        with:\n          tool-cache: false\n          android: true\n          dotnet: true\n          haskell: true\n          large-packages: false\n          docker-images: true\n          swap-storage: true\n\n      - name: Integration Tests\n        run: |\n          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\n\n      - name: Upload Test Results\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: Integration Test Results (${{ matrix.test-markers }})\n          path: pytest.xml\n\n  distributed-tests:\n    runs-on: ubuntu-latest\n    env:\n      AWS_ACCESS_KEY_ID: ${{ secrets.LUDWIG_TESTS_AWS_ACCESS_KEY_ID }}\n      AWS_SECRET_ACCESS_KEY: ${{ secrets.LUDWIG_TESTS_AWS_SECRET_ACCESS_KEY }}\n      KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}\n      KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}\n      IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }}\n\n    name: Distributed Tests\n    services:\n      minio:\n        image: fclairamb/minio-github-actions\n        env:\n          MINIO_ACCESS_KEY: minio\n          MINIO_SECRET_KEY: minio123\n        ports:\n          - 9000:9000\n\n    timeout-minutes: 120\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python 3.12\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup Linux\n        run: |\n          sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev\n\n      - name: pip cache\n        uses: actions/cache@v4\n        with:\n          path: ~/.cache/pip\n          key: ${{ runner.os }}-pip-distributed-${{ hashFiles('requirements*.txt', '.github/workflows/pytest.yml') }}\n\n      - name: Install dependencies\n        run: |\n          python -m pip install -U pip setuptools\n          pip install torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n          pip install '.[test]' --extra-index-url https://download.pytorch.org/whl/cpu\n          pip list\n\n      - name: Free Disk Space\n        uses: jlumbroso/free-disk-space@main\n        with:\n          tool-cache: false\n          android: true\n          dotnet: true\n          haskell: true\n          large-packages: false\n          docker-images: true\n          swap-storage: true\n\n      - name: Distributed Unit Tests\n        run: |\n          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\" --junitxml pytest-unit.xml tests/ludwig\n\n      - name: Distributed Integration Tests\n        run: |\n          RUN_PRIVATE=$IS_NOT_FORK LUDWIG_TEST_SUITE_TIMEOUT_S=7200 pytest -v --timeout 300 --durations 100 -m \"distributed and not slow and not combinatorial and not llm\" --junitxml pytest-integration.xml tests/integration_tests\n\n      - name: Upload Test Results\n        if: always()\n        uses: actions/upload-artifact@v4\n        with:\n          name: Distributed Test Results\n          path: pytest*.xml\n\n  test-minimal-install:\n    name: Minimal Install\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Python 3.12\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup Linux\n        run: |\n          sudo apt-get update && sudo apt-get install -y cmake libsndfile1\n\n      - name: Install dependencies\n        run: |\n          python -m pip install -U pip setuptools\n          pip install torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n          pip install '.' --extra-index-url https://download.pytorch.org/whl/cpu\n          pip list\n\n      - name: Check Install\n        run: |\n          ludwig check_install\n\n  event_file:\n    name: \"Event File\"\n    runs-on: ubuntu-latest\n    steps:\n      - name: Upload\n        uses: actions/upload-artifact@v4\n        with:\n          name: Event File\n          path: ${{ github.event_path }}\n"
  },
  {
    "path": ".github/workflows/pytest_slow.yml",
    "content": "# This workflow will install Python dependencies and run all tests marked as `slow` on a single Python version.\n\nname: pytest (slow)\n\non:\n  push:\n    branches: [\"main\", \"release-*\"]\n\njobs:\n  slow-pytest:\n    name: py-slow\n    runs-on: ubuntu-latest\n    env:\n      # Use Minio credentials for all S3 operations in tests.\n      # PyArrow/Ray S3 clients use these env vars directly, so they must point to Minio.\n      AWS_ACCESS_KEY_ID: minio\n      AWS_SECRET_ACCESS_KEY: minio123\n      AWS_ENDPOINT_URL: http://localhost:9000\n      KAGGLE_USERNAME: ${{ secrets.KAGGLE_USERNAME }}\n      KAGGLE_KEY: ${{ secrets.KAGGLE_KEY }}\n      IS_NOT_FORK: ${{ !(github.event.pull_request.base.repo.full_name == 'ludwig-ai/ludwig' && github.event.pull_request.head.repo.fork) }}\n\n    services:\n      minio:\n        image: fclairamb/minio-github-actions\n        env:\n          MINIO_ACCESS_KEY: minio\n          MINIO_SECRET_KEY: minio123\n        ports:\n          - 9000:9000\n\n    timeout-minutes: 150\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python 3.12\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Setup Linux\n        if: runner.os == 'linux'\n        run: |\n          sudo apt-get update && sudo apt-get install -y cmake libsndfile1 libsox-dev\n\n      - name: Install dependencies\n        run: |\n          python --version\n          pip --version\n          python -m pip install -U pip\n          pip install torch==2.6.0 torchvision torchaudio\n          pip install '.[test]'\n          pip list\n        shell: bash\n\n      - name: Create Minio test bucket\n        run: |\n          python -c \"\n          import boto3\n          s3 = boto3.client('s3', endpoint_url='http://localhost:9000',\n                            aws_access_key_id='minio', aws_secret_access_key='minio123')\n          try:\n              s3.create_bucket(Bucket='ludwig-tests')\n          except s3.exceptions.BucketAlreadyOwnedByYou:\n              pass\n          \"\n        shell: bash\n\n      - name: Tests\n        run: |\n          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/\n"
  },
  {
    "path": ".github/workflows/schema.yml",
    "content": "name: Publish JSON Schema\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"ludwig/schema/**\"\n      - \"ludwig/config_validation/**\"\n      - \"ludwig/constants.py\"\n  release:\n    types: [published]\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  publish-schema:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Set up Python\n        uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Install Ludwig\n        run: pip install -e \".[test]\"\n\n      - name: Export schemas\n        run: |\n          mkdir -p schema-out\n          ludwig export_schema --model-type combined -o schema-out/ludwig-config.json\n          ludwig export_schema --model-type ecd -o schema-out/ludwig-config-ecd.json\n          ludwig export_schema --model-type llm -o schema-out/ludwig-config-llm.json\n\n      - name: Generate index.html\n        run: |\n          cat > schema-out/index.html << 'HTMLEOF'\n          <!DOCTYPE html>\n          <html lang=\"en\">\n          <head>\n            <meta charset=\"utf-8\">\n            <title>Ludwig JSON Schema</title>\n            <style>body{font-family:system-ui,sans-serif;max-width:640px;margin:2rem auto;padding:0 1rem}a{color:#0066cc}</style>\n          </head>\n          <body>\n            <h1>Ludwig JSON Schema</h1>\n            <p>JSON Schema files for <a href=\"https://github.com/ludwig-ai/ludwig\">Ludwig</a> config validation and IDE auto-complete.</p>\n            <ul>\n              <li><a href=\"ludwig-config.json\">ludwig-config.json</a> — Combined (ECD + LLM)</li>\n              <li><a href=\"ludwig-config-ecd.json\">ludwig-config-ecd.json</a> — ECD only</li>\n              <li><a href=\"ludwig-config-llm.json\">ludwig-config-llm.json</a> — LLM only</li>\n            </ul>\n            <h2>Usage</h2>\n            <p>Add to your Ludwig YAML config:</p>\n            <pre># yaml-language-server: $schema=https://ludwig-ai.github.io/schema/ludwig-config.json</pre>\n            <p>Or see <a href=\"https://www.schemastore.org/json/\">SchemaStore</a> for automatic IDE integration.</p>\n          </body>\n          </html>\n          HTMLEOF\n\n      - name: Publish to ludwig-ai/schema\n        uses: cpina/github-action-push-to-another-repository@v1.7.2\n        env:\n          SSH_DEPLOY_KEY: ${{ secrets.SCHEMA_REPO_DEPLOY_KEY }}\n        with:\n          source-directory: schema-out\n          destination-github-username: ludwig-ai\n          destination-repository-name: schema\n          target-branch: main\n          commit-message: \"Update Ludwig JSON schema from ${{ github.sha }}\"\n"
  },
  {
    "path": ".github/workflows/test-results.yml",
    "content": "name: test results\n\non:\n  workflow_run:\n    workflows: [\"pytest\"]\n    types:\n      - completed\n\njobs:\n  test-results:\n    name: Test Results\n    runs-on: ubuntu-latest\n    if: github.event.workflow_run.conclusion != 'skipped'\n\n    steps:\n      - name: Download and Extract Artifacts\n        env:\n          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}\n        run: |\n           mkdir -p artifacts && cd artifacts\n\n           artifacts_url=${{ github.event.workflow_run.artifacts_url }}\n\n           gh api \"$artifacts_url\" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact\n           do\n             IFS=$'\\t' read name url <<< \"$artifact\"\n             gh api $url > \"$name.zip\"\n             unzip -d \"$name\" \"$name.zip\"\n           done\n\n      - name: Publish Unit Test Results\n        uses: EnricoMi/publish-unit-test-result-action@v2\n        with:\n          commit: ${{ github.event.workflow_run.head_sha }}\n          event_file: artifacts/Event File/event.json\n          event_name: ${{ github.event.workflow_run.event }}\n          files: \"artifacts/**/*.xml\"\n"
  },
  {
    "path": ".github/workflows/upload-pypi.yml",
    "content": "name: Upload to PyPI\n\non:\n  # Triggers the workflow when a release or draft of a release is published,\n  # or a pre-release is changed to a release\n  release:\n    types: [released]\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  pypi-publish:\n    name: upload release to PyPI\n    runs-on: ubuntu-latest\n    # Specifying a GitHub environment is optional, but strongly encouraged\n    environment: release\n    permissions:\n      # IMPORTANT: this permission is mandatory for trusted publishing\n      id-token: write\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          submodules: \"recursive\"\n\n      - uses: actions/setup-python@v5\n        with:\n          python-version: \"3.12\"\n\n      - name: Build source distribution\n        run: |\n          pip install setuptools\n          python setup.py sdist\n\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".gitignore",
    "content": "###################\n# ludwig specific #\n###################\n\n*.lock_preprocessing\nresults/\nludwig/results/\nresults_*/\nludwig_arm64/\n\n# ailabs-utils\nailabs_util\ndocker_assets\n\n# data\nmnist_data/\nprofile_images/\n./profile_images/\n\n###########\n# General #\n###########\n\n# Mac stuff\n.DS_Store\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nenv*\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\n./downloads/\n./dataset/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*,cover\n.hypothesis/\n\n# Data\n*.csv\n*.hdf5\n*.meta.json\n*.parquet\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv*\nENV/\n\n# Spyder project settings\n.spyderproject\n\n# Rope project settings\n.ropeproject\n\n# PyCharm\n.idea\n\n# ctags\ntags\n\n# examples\nexamples/*/data/\nexamples/*/visualizations/\n\n# benchmarking configs\nludwig/benchmarking/configs/\n\n# Aim tracking\n.aim/\n\n# Comet\n.comet.config\n\n# Test-generated artifacts (image/audio features)\n*.png\n*.wav\ngenerated_audio/\ngenerated_images/\n"
  },
  {
    "path": ".nojekyll",
    "content": ""
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# Apply to all files without committing:\n#   pre-commit run --all-files\n# Apply to changed files:\n#   pre-commit run\n# Update this file:\n#   pre-commit autoupdate\n# Run a specific hook:\n#   pre-commit run <hook id>\n\nci:\n  autofix_prs: true\n  autoupdate_commit_msg: \"[pre-commit.ci] pre-commit suggestions\"\n  autoupdate_schedule: weekly\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: check-ast\n      - id: fix-byte-order-marker\n      - id: check-case-conflict\n      - id: check-executables-have-shebangs\n      - id: check-json\n      - id: check-toml\n      - id: check-yaml\n      - id: debug-statements\n      - id: detect-private-key\n      - id: end-of-file-fixer\n      - id: trailing-whitespace\n      - id: mixed-line-ending\n  - repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n      - id: pyupgrade\n        args: [--py312-plus]\n  - repo: https://github.com/PyCQA/docformatter\n    rev: v1.7.7\n    hooks:\n      - id: docformatter\n        args: [--in-place, --wrap-summaries=115, --wrap-descriptions=120]\n  - repo: https://github.com/PyCQA/isort\n    rev: 8.0.0\n    hooks:\n      - id: isort\n        name: Format imports\n  - repo: https://github.com/pycqa/flake8\n    rev: 7.3.0\n    hooks:\n      - id: flake8\n  - repo: https://github.com/psf/black\n    rev: 26.1.0\n    hooks:\n      - id: black\n        name: Format code\n  - repo: https://github.com/asottile/blacken-docs\n    rev: 1.20.0\n    hooks:\n      - id: blacken-docs\n        args: [--line-length=120]\n  - repo: https://github.com/hukkin/mdformat\n    rev: 1.0.0\n    hooks:\n      - id: mdformat\n        additional_dependencies:\n          - mdformat-gfm==1.0.0\n          - mdformat_frontmatter==2.0.10\n        exclude: CHANGELOG.md\n  - repo: https://github.com/yoheimuta/protolint\n    rev: v0.56.4\n    hooks:\n      - id: protolint\n"
  },
  {
    "path": ".protolint.yaml",
    "content": "# Adapted from\n# https://github.com/yoheimuta/protolint/blob/master/_example/config/.protolint.yaml\n---\n# Lint directives.\nlint:\n  # Linter files to walk.\n  files:\n    # The specific files to exclude.\n    exclude:\n      # NOTE: UNIX paths will be properly accepted by both UNIX and Windows.\n      - ../proto/invalidFileName.proto\n\n  # Linter rules.\n  # Run `protolint list` to see all available rules.\n  rules:\n    # Set the default to all linters. This option works the other way around as no_default does.\n    # If you want to enable this option, delete the comment out below and no_default.\n    # all_default: true\n\n    # The specific linters to add.\n    add:\n      - FIELD_NAMES_LOWER_SNAKE_CASE\n      - MESSAGE_NAMES_UPPER_CAMEL_CASE\n      - MAX_LINE_LENGTH\n      - INDENT\n      - FIELD_NAMES_EXCLUDE_PREPOSITIONS\n      - FILE_NAMES_LOWER_SNAKE_CASE\n      - IMPORTS_SORTED\n      - PACKAGE_NAME_LOWER_CASE\n      - ORDER\n      - PROTO3_FIELDS_AVOID_REQUIRED\n      - PROTO3_GROUPS_AVOID\n      - REPEATED_FIELD_NAMES_PLURALIZED\n      - QUOTE_CONSISTENT\n\n  # Linter rules option.\n  rules_option:\n    # MAX_LINE_LENGTH rule option.\n    max_line_length:\n      # Enforces a maximum line length\n      max_chars: 120\n      # Specifies the character count for tab characters\n      tab_chars: 2\n\n    # FILE_NAMES_LOWER_SNAKE_CASE rule option.\n    file_names_lower_snake_case:\n      excludes:\n        - ../proto/invalidFileName.proto\n\n    # QUOTE_CONSISTENT rule option.\n    quote_consistent:\n      # Available quote are \"double\" or \"single\".\n      quote: double\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n    \"editor.rulers\": [\n        120\n    ],\n    \"editor.formatOnSave\": true,\n    \"[python]\": {\n        \"editor.defaultFormatter\": \"ms-python.black-formatter\",\n        \"editor.formatOnSave\": true\n    },\n    \"black-formatter.args\": [\n        \"--line-length\",\n        \"120\"\n    ],\n    \"flake8.args\": [\n        \"--config=setup.cfg\"\n    ],\n    \"python.testing.unittestEnabled\": false,\n    \"python.testing.pytestEnabled\": false,\n    \"python.envFile\": \"${workspaceFolder}/.env\"\n}\n"
  },
  {
    "path": "CODEOWNERS",
    "content": "# Default code owners for the entire repository\n* @w4nderlust @tgaddair @justinxzhao @arnavgarg1 @geoffreyangus @jeffkinnison @Infernaught @alexsherstinsky\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of conduct\n\nLudwig adopts the [Linux Foundation code of conduct](https://lfprojects.org/policies/code-of-conduct/).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nEveryone is welcome to contribute, and we value everybody’s contribution. Code is thus not the only\nway to help the community. Answering questions, helping others, reaching out and improving the\ndocumentation are immensely valuable contributions as well.\n\nIt also helps us if you spread the word: reference the library from blog posts on the awesome\nprojects it made possible, shout out on X every time it has helped you, or simply star the\nrepo to say \"thank you\".\n\nCheck out the official [ludwig docs](https://ludwig-ai.github.io/ludwig-docs/) to get oriented\naround the codebase, and join the community!\n\n## Open Issues\n\nIssues are listed at: <https://github.com/ludwig-ai/ludwig/issues>\n\nIf you would like to work on any of them, make sure it is not already assigned to someone else.\n\nYou can self-assign it by commenting on the Issue page with one of the keywords: `#take` or\n`#self-assign`.\n\nWork on your self-assigned issue and eventually create a Pull Request.\n\n## Creating Pull Requests\n\n1. Fork the [repository](https://github.com/ludwig-ai/ludwig) by clicking on the \"Fork\" button on\n   the repository's page. This creates a copy of the code under your GitHub user account.\n\n1. Clone your fork to your local disk, and add the base repository as a remote:\n\n   ```bash\n   git clone git@github.com:<your Github handle>/ludwig.git\n   cd ludwig\n   git remote add upstream https://github.com/ludwig-ai/ludwig.git\n   ```\n\n1. Create a new branch to hold your development changes:\n\n   ```bash\n   git checkout -b a-descriptive-name-for-my-changes\n   ```\n\n   *Do not*\\* work on the `master` branch.\n\n1. Set up a development environment by running the following command in a virtual environment:\n\n   ```bash\n   pip install -e .\n   ```\n\n   The above command will install only the packages in \"requirements.txt\" in the developer mode. If you would like to\n   be able to potentially make changes to the overall Ludwig codebase, then use the following command:\n\n   ```bash\n   pip install -e .[full]\n   ```\n\n   Please note that in certain Shell environments (e.g., the `Z shell`), the dependencies in brackets have to be quoted:\n\n   ```bash\n   pip install -e .\"[full]\"\n   ```\n\n   If you do not need access to the entire Ludwig codebase, but just want to be able to run `pytest` on the essential\n   functionality, then you would replace the above command with:\n\n   ```bash\n   pip install -e .[test]\n   ```\n\n   (Please use `pip install -e .\"[test]\"` where your Shell environment requires quotes around the square brackets.)\n\n   For the full list of the optional dependencies available in Ludwig, please see\n   [Installation Guide](https://ludwig.ai/latest/getting_started/installation/) and \"setup.py\" in the root of the Ludwig\n   repository.\n\n1. On MacOS with Apple Silicon, if this installation approach runs into errors, you may need to install the following\n   prerequisites:\n\n   ```bash\n   brew install cmake libomp\n   ```\n\n   This step requires `homebrew` to be installed on your development machine.\n\n1. Install and run `pre-commit`:\n\n   ```bash\n   pip install pre-commit\n   pre-commit install\n   ```\n\n1. Develop features on your branch.\n\n1. Format your code by running pre-commits so that your newly added files look nice:\n\n   ```bash\n   pre-commit run\n   ```\n\n   Pre-commits also run automatically when committing.\n\n1. Once you're happy with your changes, make a commit to record your changes locally:\n\n   ```bash\n   git add .\n   git commit\n   ```\n\n   It is a good idea to sync your copy of the code with the original repository regularly. This\n   way you can quickly account for changes:\n\n   ```bash\n   git fetch upstream\n   git rebase upstream/master\n   ```\n\n   Push the changes to your account using:\n\n   ```bash\n   git push -u origin a-descriptive-name-for-my-changes\n   ```\n\n1. Once you are satisfied, go the webpage of your fork on GitHub. Click on \"Pull request\" to send\n   your contribution to the project maintainers for review.\n\n## Other tips\n\n- Add unit tests for any new code you write.\n- 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.\n\n## Attribution\n\nThis contributing guideline is adapted from `huggingface`, available at <https://github.com/huggingface/datasets/blob/master/CONTRIBUTING.md>.\n\n## Code of Conduct\n\nPlease be mindful of and adhere to the Linux Foundation's\n[Code of Conduct](https://lfprojects.org/policies/code-of-conduct) when contributing to Ludwig.\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n--------------------------------------------------------------------------\n\nCode in ludwig/api_annotations.py adapted from\nhttps://github.com/ray-project/ray (Apache-2.0 License)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------------------------------------------------------------\n\nCode in ludwig/utils/structural_warnings.py adapted from\nhttps://github.com/ray-project/ray (Apache-2.0 License)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\n--------------------------------------------------------------------------\n\nCode in ludwig/utils/logging_utils.py adapted from\nhttps://github.com/ray-project/ray (Apache-2.0 License)\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    https://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include *.txt\nrecursive-include ludwig/datasets *.yaml\nrecursive-include ludwig/automl/defaults *.yaml\nrecursive-include ludwig/schema/metadata/configs *.yaml\n"
  },
  {
    "path": "NOTICE",
    "content": "Ludwig includes derived work from TensorFlow(https://github.com/tensorflow/tensorflow) under the Apache License 2.0:\n\nCopyright 2016 The prometheus-operator Authors\n\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\n\nyou may not use this file except in compliance with the License.\n\nYou may obtain a copy of the License at\n\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\n\nUnless required by applicable law or agreed to in writing, software\n\ndistributed under the License is distributed on an \"AS IS\" BASIS,\n\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\nSee the License for the specific language governing permissions and\n\nlimitations under the License.\n\n\nThe derived work can be found in the files: ludwig/models/modules/convolutional_modules.py\n\n\n------\n\nLudwig includes derived work from Keras(https://github.com/keras-team/keras) under the MIT License:\n\n\nCOPYRIGHT\n\nAll contributions by François Chollet:\n\nCopyright (c) 2015 - 2018, François Chollet.\n\nAll rights reserved.\n\n\nAll contributions by Google:\n\nCopyright (c) 2015 - 2018, Google, Inc.\n\nAll rights reserved.\n\n\nAll contributions by Microsoft:\n\nCopyright (c) 2017 - 2018, Microsoft, Inc.\n\nAll rights reserved.\n\n\nAll other contributions:\n\nCopyright (c) 2015 - 2018, the respective contributors.\n\nAll rights reserved.\n\n\nEach contributor holds copyright over their respective contributions.\n\nThe project versioning (Git) records all such contribution source information.\n\nLICENSE\n\nThe MIT License (MIT)\n\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n\nof this software and associated documentation files (the \"Software\"), to deal\n\nin the Software without restriction, including without limitation the rights\n\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n\ncopies of the Software, and to permit persons to whom the Software is\n\nfurnished to do so, subject to the following conditions:\n\n\nThe above copyright notice and this permission notice shall be included in all\n\ncopies or substantial portions of the Software.\n\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n\nSOFTWARE.\n\n\nThe derived work can be found in the files: mkdocs/code_docs_autogen.py\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <a href=\"https://ludwig.ai\">\n    <img src=\"https://github.com/ludwig-ai/ludwig-docs/raw/main/docs/images/ludwig_hero_smaller.jpg\" height=\"150\">\n  </a>\n</p>\n\n<div align=\"center\">\n\n_Declarative deep learning framework built for scale and efficiency._\n\n[![PyPI version](https://badge.fury.io/py/ludwig.svg)](https://badge.fury.io/py/ludwig)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/CBgdrGnZjy)\n[![DockerHub](https://img.shields.io/docker/pulls/ludwigai/ludwig.svg)](https://hub.docker.com/r/ludwigai)\n[![Downloads](https://pepy.tech/badge/ludwig)](https://pepy.tech/project/ludwig)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ludwig-ai/ludwig/blob/main/LICENSE)\n[![X](https://img.shields.io/twitter/follow/ludwig_ai.svg?style=social&logo=twitter)](https://twitter.com/ludwig_ai)\n\n</div>\n\n# 📖 What is Ludwig?\n\nLudwig is a **low-code** framework for building **custom** AI models like **LLMs** and other deep neural networks.\n\nKey features:\n\n- 🛠 **Build custom models with ease:** a declarative YAML configuration file is all you need to train a state-of-the-art LLM on your data. Support for multi-task and multi-modality learning. Comprehensive config validation detects invalid parameter combinations and prevents runtime failures.\n- ⚡ **Optimized for scale and efficiency:** automatic batch size selection, distributed training ([DDP](https://pytorch.org/tutorials/beginner/ddp_series_theory.html), [DeepSpeed](https://github.com/microsoft/DeepSpeed)), parameter efficient fine-tuning ([PEFT](https://github.com/huggingface/peft)), 4-bit quantization (QLoRA), paged and 8-bit optimizers, and larger-than-memory datasets.\n- 📐 **Expert level control:** retain full control of your models down to the activation functions. Support for hyperparameter optimization, explainability, and rich metric visualizations.\n- 🧱 **Modular and extensible:** experiment with different model architectures, tasks, features, and modalities with just a few parameter changes in the config. Think building blocks for deep learning.\n- 🚢 **Engineered for production:** prebuilt [Docker](https://hub.docker.com/u/ludwigai) containers, native support for running with [Ray](https://www.ray.io/) on [Kubernetes](https://github.com/ray-project/kuberay), export models to [Torchscript](https://pytorch.org/docs/stable/jit.html) and [Triton](https://developer.nvidia.com/triton-inference-server), upload to [HuggingFace](https://huggingface.co/models) with one command.\n\nLudwig is hosted by the\n[Linux Foundation AI & Data](https://lfaidata.foundation/).\n\n**Tech stack:** Python 3.12 | PyTorch 2.6 | Pydantic 2 | Transformers 5 | Ray 2.54\n\n![img](https://raw.githubusercontent.com/ludwig-ai/ludwig-docs/main/docs/images/ludwig_legos_unanimated.gif)\n\n# 💾 Installation\n\nInstall from PyPI. Be aware that Ludwig requires Python 3.12+.\n\n```shell\npip install ludwig\n```\n\nOr install with all optional dependencies:\n\n```shell\npip install ludwig[full]\n```\n\nPlease see [contributing](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md) for more detailed installation instructions.\n\n# 🚂 Getting Started\n\nWant to take a quick peek at some of Ludwig's features? Check out this Colab Notebook 🚀 [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1lB4ALmEyvcMycE3Mlnsd7I3bc0zxvk39)\n\nLooking to fine-tune LLMs? Check out these notebooks:\n\n1. 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)\n1. 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)\n1. 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)\n\nFor a full tutorial, check out the official [getting started guide](https://ludwig.ai/latest/getting_started/), or take a look at end-to-end [Examples](https://ludwig.ai/latest/examples).\n\n## Large Language Model Fine-Tuning\n\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing)\n\nLet's fine-tune a pretrained LLM to follow instructions like a chatbot (\"instruction tuning\").\n\n### Prerequisites\n\n- [HuggingFace API Token](https://huggingface.co/docs/hub/security-tokens)\n- Access approval to your chosen base model (e.g., [Llama-3.1-8B](https://huggingface.co/meta-llama/Llama-3.1-8B))\n- GPU with at least 12 GiB of VRAM (in our tests, we used an Nvidia T4)\n\n### Running\n\nWe'll use the [Stanford Alpaca](https://crfm.stanford.edu/2023/03/13/alpaca.html) dataset, which will be formatted as a table-like file that looks like this:\n\n|                    instruction                    |      input       |                      output                       |\n| :-----------------------------------------------: | :--------------: | :-----------------------------------------------: |\n|       Give three tips for staying healthy.        |                  | 1.Eat a balanced diet and make sure to include... |\n| Arrange the items given below in the order to ... | cake, me, eating |                  I eating cake.                   |\n| Write an introductory paragraph about a famous... |  Michelle Obama  | Michelle Obama is an inspirational woman who r... |\n|                        ...                        |       ...        |                        ...                        |\n\nCreate a YAML config file named `model.yaml` with the following:\n\n```yaml\nmodel_type: llm\nbase_model: meta-llama/Llama-3.1-8B\n\nquantization:\n  bits: 4\n\nadapter:\n  type: lora\n\nprompt:\n  template: |\n    Below is an instruction that describes a task, paired with an input that may provide further context.\n    Write a response that appropriately completes the request.\n\n    ### Instruction:\n    {instruction}\n\n    ### Input:\n    {input}\n\n    ### Response:\n\ninput_features:\n  - name: prompt\n    type: text\n\noutput_features:\n  - name: output\n    type: text\n\ntrainer:\n  type: finetune\n  learning_rate: 0.0001\n  batch_size: 1\n  gradient_accumulation_steps: 16\n  epochs: 3\n  learning_rate_scheduler:\n    decay: cosine\n    warmup_fraction: 0.01\n\npreprocessing:\n  sample_ratio: 0.1\n\nbackend:\n  type: local\n```\n\nAnd now let's train the model:\n\n```bash\nexport HUGGING_FACE_HUB_TOKEN = \"<api_token>\"\n\nludwig train --config model.yaml --dataset \"ludwig://alpaca\"\n```\n\n## Supervised ML\n\nLet's build a neural network that predicts whether a given movie critic's review on [Rotten Tomatoes](https://www.kaggle.com/stefanoleone992/rotten-tomatoes-movies-and-critic-reviews-dataset) was positive or negative.\n\nOur dataset will be a CSV file that looks like this:\n\n|     movie_title      | content_rating |              genres              | runtime | top_critic | review_content                                                                                                                                                                                                   | recommended |\n| :------------------: | :------------: | :------------------------------: | :-----: | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |\n| 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           |\n|       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           |\n|   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           |\n|         ...          |      ...       |               ...                |   ...   | ...        | ...                                                                                                                                                                                                              | ...         |\n\nDownload a sample of the dataset from [here](https://ludwig.ai/latest/data/rotten_tomatoes.csv).\n\n```bash\nwget https://ludwig.ai/latest/data/rotten_tomatoes.csv\n```\n\nNext create a YAML config file named `model.yaml` with the following:\n\n```yaml\ninput_features:\n  - name: genres\n    type: set\n    preprocessing:\n      tokenizer: comma\n  - name: content_rating\n    type: category\n  - name: top_critic\n    type: binary\n  - name: runtime\n    type: number\n  - name: review_content\n    type: text\n    encoder:\n      type: embed\noutput_features:\n  - name: recommended\n    type: binary\n```\n\nThat's it! Now let's train the model:\n\n```bash\nludwig train --config model.yaml --dataset rotten_tomatoes.csv\n```\n\n**Happy modeling**\n\nTry applying Ludwig to your data. [Reach out on Discord](https://discord.gg/CBgdrGnZjy)\nif you have any questions.\n\n# ❓ Why you should use Ludwig\n\n- **Minimal machine learning boilerplate**\n\n  Ludwig takes care of the engineering complexity of machine learning out of\n  the box, enabling research scientists to focus on building models at the\n  highest level of abstraction. Data preprocessing, hyperparameter\n  optimization, device management, and distributed training for\n  `torch.nn.Module` models come completely free.\n\n- **Easily build your benchmarks**\n\n  Creating a state-of-the-art baseline and comparing it with a new model is a\n  simple config change.\n\n- **Easily apply new architectures to multiple problems and datasets**\n\n  Apply new models across the extensive set of tasks and datasets that Ludwig\n  supports. Ludwig includes a\n  [full benchmarking toolkit](https://arxiv.org/abs/2111.04260) accessible to\n  any user, for running experiments with multiple models across multiple\n  datasets with just a simple configuration.\n\n- **Highly configurable data preprocessing, modeling, and metrics**\n\n  Any and all aspects of the model architecture, training loop, hyperparameter\n  search, and backend infrastructure can be modified as additional fields in\n  the declarative configuration to customize the pipeline to meet your\n  requirements. For details on what can be configured, check out\n  [Ludwig Configuration](https://ludwig.ai/latest/configuration/)\n  docs.\n\n- **Multi-modal, multi-task learning out-of-the-box**\n\n  Mix and match tabular data, text, images, and even audio into complex model\n  configurations without writing code.\n\n- **Rich model exporting and tracking**\n\n  Automatically track all trials and metrics with tools like Tensorboard,\n  Comet ML, Weights & Biases, MLFlow, and Aim Stack.\n\n- **Automatically scale training to multi-GPU, multi-node clusters**\n\n  Go from training on your local machine to the cloud without code changes.\n\n- **Low-code interface for state-of-the-art models, including pre-trained Huggingface Transformers**\n\n  Ludwig also natively integrates with pre-trained models, such as the ones\n  available in [Huggingface Transformers](https://huggingface.co/docs/transformers/index).\n  Users can choose from a vast collection of state-of-the-art pre-trained\n  PyTorch models to use without needing to write any code at all. For example,\n  training a BERT-based sentiment analysis model with Ludwig is as simple as:\n\n  ```shell\n  ludwig train --dataset sst5 --config_str \"{input_features: [{name: sentence, type: text, encoder: bert}], output_features: [{name: label, type: category}]}\"\n  ```\n\n- **Low-code interface for AutoML**\n\n  [Ludwig AutoML](https://ludwig.ai/latest/user_guide/automl/)\n  allows users to obtain trained models by providing just a dataset, the\n  target column, and a time budget.\n\n  ```python\n  auto_train_results = ludwig.automl.auto_train(dataset=my_dataset_df, target=target_column_name, time_limit_s=7200)\n  ```\n\n- **Easy productionisation**\n\n  Ludwig makes it easy to serve deep learning models, including on GPUs.\n  Launch a REST API for your trained Ludwig model.\n\n  ```shell\n  ludwig serve --model_path=/path/to/model\n  ```\n\n  Ludwig supports exporting models to efficient Torchscript bundles.\n\n  ```shell\n  ludwig export_torchscript -–model_path=/path/to/model\n  ```\n\n# 📚 Tutorials\n\n- [Text Classification](https://ludwig.ai/latest/examples/text_classification)\n- [Tabular Data Classification](https://ludwig.ai/latest/examples/adult_census_income)\n- [Image Classification](https://ludwig.ai/latest/examples/mnist)\n- [Multimodal Classification](https://ludwig.ai/latest/examples/multimodal_classification)\n\n# 🔬 Example Use Cases\n\n- [Named Entity Recognition Tagging](https://ludwig.ai/latest/examples/ner_tagging)\n- [Natural Language Understanding](https://ludwig.ai/latest/examples/nlu)\n- [Machine Translation](https://ludwig.ai/latest/examples/machine_translation)\n- [Chit-Chat Dialogue Modeling through seq2seq](https://ludwig.ai/latest/examples/seq2seq)\n- [Sentiment Analysis](https://ludwig.ai/latest/examples/sentiment_analysis)\n- [One-shot Learning with Siamese Networks](https://ludwig.ai/latest/examples/oneshot)\n- [Visual Question Answering](https://ludwig.ai/latest/examples/visual_qa)\n- [Spoken Digit Speech Recognition](https://ludwig.ai/latest/examples/speech_recognition)\n- [Speaker Verification](https://ludwig.ai/latest/examples/speaker_verification)\n- [Binary Classification (Titanic)](https://ludwig.ai/latest/examples/titanic)\n- [Timeseries forecasting](https://ludwig.ai/latest/examples/forecasting)\n- [Timeseries forecasting (Weather)](https://ludwig.ai/latest/examples/weather)\n- [Movie rating prediction](https://ludwig.ai/latest/examples/movie_ratings)\n- [Multi-label classification](https://ludwig.ai/latest/examples/multi_label)\n- [Multi-Task Learning](https://ludwig.ai/latest/examples/multi_task)\n- [Simple Regression: Fuel Efficiency Prediction](https://ludwig.ai/latest/examples/fuel_efficiency)\n- [Fraud Detection](https://ludwig.ai/latest/examples/fraud)\n\n# 💡 More Information\n\nRead our publications on [Ludwig](https://arxiv.org/pdf/1909.07930.pdf), [declarative ML](https://arxiv.org/pdf/2107.08148.pdf), and [Ludwig’s SoTA benchmarks](https://openreview.net/pdf?id=hwjnu6qW7E4).\n\nLearn more about [how Ludwig works](https://ludwig.ai/latest/user_guide/how_ludwig_works/), [how to get started](https://ludwig.ai/latest/getting_started/), and work through more [examples](https://ludwig.ai/latest/examples).\n\nIf you are interested in [contributing](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md), have questions, comments, or thoughts to share, or if you just want to be in the\nknow, please consider [joining our Community Discord](https://discord.gg/CBgdrGnZjy) and follow us on [X](https://twitter.com/ludwig_ai)!\n\n# 🤝 Join the community to build Ludwig with us\n\nLudwig is an actively managed open-source project that relies on contributions from folks just like\nyou. Consider joining the active group of Ludwig contributors to make Ludwig an even\nmore accessible and feature rich framework for everyone to use!\n\n<a href=\"https://github.com/ludwig-ai/ludwig/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=ludwig-ai/ludwig\" />\n</a><br/>\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=ludwig-ai/ludwig&type=Date)](https://star-history.com/#ludwig-ai/ludwig&Date)\n\n# 👋 Getting Involved\n\n- [Discord](https://discord.gg/CBgdrGnZjy)\n- [X (Twitter)](https://twitter.com/ludwig_ai)\n- [Medium](https://medium.com/ludwig-ai)\n- [GitHub Issues](https://github.com/ludwig-ai/ludwig/issues)\n"
  },
  {
    "path": "README_KR.md",
    "content": "<p align=\"center\">\n  <a href=\"https://ludwig.ai\">\n    <img src=\"https://github.com/ludwig-ai/ludwig-docs/raw/main/docs/images/ludwig_hero_smaller.jpg\" height=\"150\">\n  </a>\n</p>\n\n<div align=\"center\">\n\n_확장성과 효율성을 위해 설계된 선언적 딥러닝 프레임워크_\n\n[![PyPI version](https://badge.fury.io/py/ludwig.svg)](https://badge.fury.io/py/ludwig)\n[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/CBgdrGnZjy)\n[![DockerHub](https://img.shields.io/docker/pulls/ludwigai/ludwig.svg)](https://hub.docker.com/r/ludwigai)\n[![Downloads](https://pepy.tech/badge/ludwig)](https://pepy.tech/project/ludwig)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ludwig-ai/ludwig/blob/main/LICENSE)\n[![X](https://img.shields.io/twitter/follow/ludwig_ai.svg?style=social&logo=twitter)](https://twitter.com/ludwig_ai)\n\n</div>\n\n# 📖 Ludwig란?\n\nLudwig는 **LLM** 및 기타 심층 신경망과 같은 **맞춤형** AI 모델을 구축하기 위한 **로우코드** 프레임워크입니다.\n\n주요 기능:\n\n- 🛠 **손쉬운 맞춤형 모델 구축:** 선언적 YAML 설정 파일만으로 최신 LLM을 데이터에 맞춰 학습시킬 수 있습니다. 멀티태스크 및 멀티모달 학습을 지원합니다. 포괄적인 설정 검증으로 잘못된 매개변수 조합을 감지하고 런타임 오류를 방지합니다.\n- ⚡ **확장성과 효율성 최적화:** 자동 배치 크기 선택, 분산 학습([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비트 옵티마이저, 메모리 초과 데이터셋 지원.\n- 📐 **전문가 수준의 제어:** 활성화 함수까지 모델을 완전히 제어할 수 있습니다. 하이퍼파라미터 최적화, 설명 가능성, 풍부한 메트릭 시각화를 지원합니다.\n- 🧱 **모듈식 및 확장 가능:** 설정에서 몇 가지 매개변수만 변경하여 다양한 모델 아키텍처, 태스크, 피처, 모달리티를 실험할 수 있습니다. 딥러닝을 위한 빌딩 블록이라고 생각하세요.\n- 🚢 **프로덕션을 위한 설계:** 사전 빌드된 [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)에 업로드.\n\nLudwig는 [Linux Foundation AI & Data](https://lfaidata.foundation/)에서 호스팅합니다.\n\n**기술 스택:** Python 3.12 | PyTorch 2.6 | Pydantic 2 | Transformers 5 | Ray 2.54\n\n![img](https://raw.githubusercontent.com/ludwig-ai/ludwig-docs/master/docs/images/ludwig_legos_unanimated.gif)\n\n# 💾 설치\n\nPyPI에서 설치합니다. Ludwig는 Python 3.12 이상을 요구합니다.\n\n```shell\npip install ludwig\n```\n\n모든 선택적 의존성을 포함하여 설치:\n\n```shell\npip install ludwig[full]\n```\n\n더 자세한 설치 방법은 [기여 가이드](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md)를 참조하세요.\n\n# 🚂 시작하기\n\nLudwig의 기능을 빠르게 살펴보고 싶으시다면 이 Colab 노트북을 확인하세요 🚀 [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1lB4ALmEyvcMycE3Mlnsd7I3bc0zxvk39)\n\nLLM 미세 조정을 원하시나요? 다음 노트북을 확인하세요:\n\n1. 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)\n1. 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)\n1. 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)\n\n전체 튜토리얼은 공식 [시작 가이드](https://ludwig.ai/latest/getting_started/)를 확인하시거나, 엔드투엔드 [예제](https://ludwig.ai/latest/examples)를 살펴보세요.\n\n## 대규모 언어 모델 미세 조정\n\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing)\n\n사전 학습된 LLM을 챗봇처럼 지시를 따르도록 미세 조정(\"인스트럭션 튜닝\")해 봅시다.\n\n### 사전 요구 사항\n\n- [HuggingFace API 토큰](https://huggingface.co/docs/hub/security-tokens)\n- 선택한 베이스 모델에 대한 접근 승인 (예: [Llama-3.1-8B](https://huggingface.co/meta-llama/Llama-3.1-8B))\n- 최소 12 GiB VRAM의 GPU (테스트에서는 Nvidia T4를 사용했습니다)\n\n### 실행\n\n[Stanford Alpaca](https://crfm.stanford.edu/2023/03/13/alpaca.html) 데이터셋을 사용합니다. 다음과 같은 테이블 형식의 파일로 구성됩니다:\n\n|                    instruction                    |      input       |                      output                       |\n| :-----------------------------------------------: | :--------------: | :-----------------------------------------------: |\n|       Give three tips for staying healthy.        |                  | 1.Eat a balanced diet and make sure to include... |\n| Arrange the items given below in the order to ... | cake, me, eating |                  I eating cake.                   |\n| Write an introductory paragraph about a famous... |  Michelle Obama  | Michelle Obama is an inspirational woman who r... |\n|                        ...                        |       ...        |                        ...                        |\n\n`model.yaml`이라는 YAML 설정 파일을 다음 내용으로 생성하세요:\n\n```yaml\nmodel_type: llm\nbase_model: meta-llama/Llama-3.1-8B\n\nquantization:\n  bits: 4\n\nadapter:\n  type: lora\n\nprompt:\n  template: |\n    Below is an instruction that describes a task, paired with an input that may provide further context.\n    Write a response that appropriately completes the request.\n\n    ### Instruction:\n    {instruction}\n\n    ### Input:\n    {input}\n\n    ### Response:\n\ninput_features:\n  - name: prompt\n    type: text\n\noutput_features:\n  - name: output\n    type: text\n\ntrainer:\n  type: finetune\n  learning_rate: 0.0001\n  batch_size: 1\n  gradient_accumulation_steps: 16\n  epochs: 3\n  learning_rate_scheduler:\n    decay: cosine\n    warmup_fraction: 0.01\n\npreprocessing:\n  sample_ratio: 0.1\n\nbackend:\n  type: local\n```\n\n이제 모델을 학습시켜 봅시다:\n\n```bash\nexport HUGGING_FACE_HUB_TOKEN = \"<api_token>\"\n\nludwig train --config model.yaml --dataset \"ludwig://alpaca\"\n```\n\n## 지도 학습 ML\n\n[Rotten Tomatoes](https://www.kaggle.com/stefanoleone992/rotten-tomatoes-movies-and-critic-reviews-dataset) 영화 평론가의 리뷰가 긍정적인지 부정적인지 예측하는 신경망을 만들어 봅시다.\n\n데이터셋은 다음과 같은 CSV 파일입니다:\n\n|     movie_title      | content_rating |              genres              | runtime | top_critic | review_content                                                                                                                                                                                                   | recommended |\n| :------------------: | :------------: | :------------------------------: | :-----: | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |\n| 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           |\n|       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           |\n|   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           |\n|         ...          |      ...       |               ...                |   ...   | ...        | ...                                                                                                                                                                                                              | ...         |\n\n[여기](https://ludwig.ai/latest/data/rotten_tomatoes.csv)에서 데이터셋 샘플을 다운로드하세요.\n\n```bash\nwget https://ludwig.ai/latest/data/rotten_tomatoes.csv\n```\n\n다음으로 `model.yaml`이라는 YAML 설정 파일을 생성하세요:\n\n```yaml\ninput_features:\n  - name: genres\n    type: set\n    preprocessing:\n      tokenizer: comma\n  - name: content_rating\n    type: category\n  - name: top_critic\n    type: binary\n  - name: runtime\n    type: number\n  - name: review_content\n    type: text\n    encoder:\n      type: embed\noutput_features:\n  - name: recommended\n    type: binary\n```\n\n이게 전부입니다! 이제 모델을 학습시켜 봅시다:\n\n```bash\nludwig train --config model.yaml --dataset rotten_tomatoes.csv\n```\n\n**즐거운 모델링 되세요**\n\nLudwig를 여러분의 데이터에 적용해 보세요. 질문이 있으시면 [Discord에서 문의](https://discord.gg/CBgdrGnZjy)해 주세요.\n\n# ❓ Ludwig를 사용해야 하는 이유\n\n- **최소한의 머신러닝 보일러플레이트**\n\n  Ludwig는 머신러닝의 엔지니어링 복잡성을 기본으로 처리하여, 연구자들이 가장 높은 수준의 추상화에서 모델 구축에 집중할 수 있게 합니다. `torch.nn.Module` 모델에 대한 데이터 전처리, 하이퍼파라미터 최적화, 디바이스 관리, 분산 학습이 완전히 무료로 제공됩니다.\n\n- **손쉬운 벤치마크 구축**\n\n  최신 기준 모델을 만들고 새 모델과 비교하는 것이 간단한 설정 변경만으로 가능합니다.\n\n- **새로운 아키텍처를 여러 문제와 데이터셋에 쉽게 적용**\n\n  Ludwig가 지원하는 광범위한 태스크 및 데이터셋 세트에 새 모델을 적용하세요. Ludwig에는 간단한 설정만으로 여러 데이터셋에서 여러 모델 실험을 실행할 수 있는 [전체 벤치마킹 도구](https://arxiv.org/abs/2111.04260)가 모든 사용자에게 제공됩니다.\n\n- **데이터 전처리, 모델링, 메트릭의 높은 설정 가능성**\n\n  모델 아키텍처, 학습 루프, 하이퍼파라미터 검색, 백엔드 인프라의 모든 측면을 선언적 설정에서 추가 필드로 수정하여 파이프라인을 요구 사항에 맞게 커스터마이즈할 수 있습니다. 설정 가능한 항목에 대한 자세한 내용은 [Ludwig 설정](https://ludwig.ai/latest/configuration/) 문서를 확인하세요.\n\n- **멀티모달, 멀티태스크 학습 기본 지원**\n\n  코드 작성 없이 테이블 데이터, 텍스트, 이미지, 오디오까지 복잡한 모델 설정으로 혼합하여 사용할 수 있습니다.\n\n- **풍부한 모델 내보내기 및 추적**\n\n  Tensorboard, Comet ML, Weights & Biases, MLFlow, Aim Stack 등의 도구로 모든 시도와 메트릭을 자동으로 추적합니다.\n\n- **멀티 GPU, 멀티 노드 클러스터로 학습 자동 확장**\n\n  로컬 머신에서 클라우드로 코드 변경 없이 전환할 수 있습니다.\n\n- **사전 학습된 Huggingface Transformers를 포함한 최신 모델의 로우코드 인터페이스**\n\n  Ludwig는 [Huggingface Transformers](https://huggingface.co/docs/transformers/index)에서 제공하는 사전 학습된 모델과 네이티브로 통합됩니다. 사용자는 코드를 전혀 작성하지 않고도 방대한 최신 사전 학습 PyTorch 모델을 사용할 수 있습니다. 예를 들어, Ludwig로 BERT 기반 감성 분석 모델을 학습시키는 것은 다음과 같이 간단합니다:\n\n  ```shell\n  ludwig train --dataset sst5 --config_str \"{input_features: [{name: sentence, type: text, encoder: bert}], output_features: [{name: label, type: category}]}\"\n  ```\n\n- **AutoML을 위한 로우코드 인터페이스**\n\n  [Ludwig AutoML](https://ludwig.ai/latest/user_guide/automl/)을 사용하면 데이터셋, 대상 컬럼, 시간 예산만 제공하여 학습된 모델을 얻을 수 있습니다.\n\n  ```python\n  auto_train_results = ludwig.automl.auto_train(dataset=my_dataset_df, target=target_column_name, time_limit_s=7200)\n  ```\n\n- **손쉬운 프로덕션화**\n\n  Ludwig는 GPU를 포함한 딥러닝 모델 서빙을 쉽게 만들어 줍니다. 학습된 Ludwig 모델에 대한 REST API를 실행하세요.\n\n  ```shell\n  ludwig serve --model_path=/path/to/model\n  ```\n\n  Ludwig는 효율적인 Torchscript 번들로 모델 내보내기를 지원합니다.\n\n  ```shell\n  ludwig export_torchscript --model_path=/path/to/model\n  ```\n\n# 📚 튜토리얼\n\n- [텍스트 분류](https://ludwig.ai/latest/examples/text_classification)\n- [테이블 데이터 분류](https://ludwig.ai/latest/examples/adult_census_income)\n- [이미지 분류](https://ludwig.ai/latest/examples/mnist)\n- [멀티모달 분류](https://ludwig.ai/latest/examples/multimodal_classification)\n\n# 🔬 예제 사용 사례\n\n- [개체명 인식 태깅](https://ludwig.ai/latest/examples/ner_tagging)\n- [자연어 이해](https://ludwig.ai/latest/examples/nlu)\n- [기계 번역](https://ludwig.ai/latest/examples/machine_translation)\n- [seq2seq를 통한 대화 모델링](https://ludwig.ai/latest/examples/seq2seq)\n- [감성 분석](https://ludwig.ai/latest/examples/sentiment_analysis)\n- [시아미즈 네트워크를 이용한 원샷 학습](https://ludwig.ai/latest/examples/oneshot)\n- [시각적 질의응답](https://ludwig.ai/latest/examples/visual_qa)\n- [음성 숫자 인식](https://ludwig.ai/latest/examples/speech_recognition)\n- [화자 인증](https://ludwig.ai/latest/examples/speaker_verification)\n- [이진 분류 (타이타닉)](https://ludwig.ai/latest/examples/titanic)\n- [시계열 예측](https://ludwig.ai/latest/examples/forecasting)\n- [시계열 예측 (날씨)](https://ludwig.ai/latest/examples/weather)\n- [영화 평점 예측](https://ludwig.ai/latest/examples/movie_ratings)\n- [다중 레이블 분류](https://ludwig.ai/latest/examples/multi_label)\n- [멀티태스크 학습](https://ludwig.ai/latest/examples/multi_task)\n- [단순 회귀: 연비 예측](https://ludwig.ai/latest/examples/fuel_efficiency)\n- [사기 탐지](https://ludwig.ai/latest/examples/fraud)\n\n# 💡 추가 정보\n\n[Ludwig](https://arxiv.org/pdf/1909.07930.pdf), [선언적 ML](https://arxiv.org/pdf/2107.08148.pdf), [Ludwig의 SoTA 벤치마크](https://openreview.net/pdf?id=hwjnu6qW7E4)에 대한 논문을 읽어보세요.\n\n[Ludwig의 작동 방식](https://ludwig.ai/latest/user_guide/how_ludwig_works/), [시작 가이드](https://ludwig.ai/latest/getting_started/), 더 많은 [예제](https://ludwig.ai/latest/examples)를 확인하세요.\n\n[기여](https://github.com/ludwig-ai/ludwig/blob/main/CONTRIBUTING.md)에 관심이 있으시거나, 질문, 의견, 공유하고 싶은 생각이 있으시거나, 최신 정보를 받고 싶으시다면 [Discord 커뮤니티에 참여](https://discord.gg/CBgdrGnZjy)하시고 [X](https://twitter.com/ludwig_ai)에서 팔로우해 주세요!\n\n# 🤝 함께 Ludwig를 만들어 갈 커뮤니티에 참여하세요\n\nLudwig는 여러분과 같은 분들의 기여에 의존하는 활발하게 관리되는 오픈소스 프로젝트입니다. Ludwig를 모든 사람이 사용할 수 있는 더 접근 가능하고 기능이 풍부한 프레임워크로 만들기 위해 활발한 Ludwig 기여자 그룹에 참여하는 것을 고려해 주세요!\n\n<a href=\"https://github.com/ludwig-ai/ludwig/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=ludwig-ai/ludwig\" />\n</a><br/>\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=ludwig-ai/ludwig&type=Date)](https://star-history.com/#ludwig-ai/ludwig&Date)\n\n# 👋 참여하기\n\n- [Discord](https://discord.gg/CBgdrGnZjy)\n- [X (Twitter)](https://twitter.com/ludwig_ai)\n- [Medium](https://medium.com/ludwig-ai)\n- [GitHub Issues](https://github.com/ludwig-ai/ludwig/issues)\n"
  },
  {
    "path": "RELEASES.md",
    "content": "# Releasing\n\n## Release procedure\n\n1. Update version number in `ludwig/globals.py`\n1. Update version number in `setup.py`\n1. Commit\n1. Tag the commit with the version number `vX.Y.Z` with a meaningful message\n1. Push with `--tags`\n1. If a non-patch release, edit the release notes\n1. Create a release for Pypi: `python setup.py sdist`\n1. Release on Pypi: `twine upaload --repository pypi dist/ludwig-X.Y.Z.tar.gz`\n\n## Release policy\n\nLudwig follows [Semantic Versioning](https://semver.org).\nIn general, for major and minor releases, maintainers should all agree on the release.\nFor patches, in particular time sensitive ones, a single maintainer can release without a full consensus, but this practice should be reserved for critical situations.\n"
  },
  {
    "path": "docker/README.md",
    "content": "# Ludwig Docker Images\n\nThese images provide Ludwig, a toolbox to train and evaluate deep learning models\nwithout the need to write code. Ludwig Docker images contain the full set of pre-requisite\npackages to support these capabilities\n\n- text features\n- image features\n- audio features\n- visualizations\n- hyperparameter optimization\n- distributed training\n- model serving\n\n## Repositories\n\nThese four repositories contain a version of Ludwig with full features built\nfrom the project's `master` branch.\n\n- `ludwigai/ludwig` Ludwig packaged with PyTorch\n- `ludwigai/ludwig-gpu` Ludwig packaged with gpu-enabled version of PyTorch\n- `ludwigai/ludwig-ray` Ludwig packaged with PyTorch\n  and Ray 2.3.1 (https://github.com/ray-project/ray)\n- `ludwigai/ludwig-ray-gpu` Ludwig packaged with gpu-enabled versions of PyTorch\n  and Ray 2.3.1 (https://github.com/ray-project/ray)\n\n## Image Tags\n\n- `master` - built from Ludwig's `master` branch\n- `nightly` - nightly build of Ludwig's software.\n- `sha-<commit point>` - version of Ludwig software at designated git sha1\n  7-character commit point.\n\n## Running Containers\n\nExamples of using the `ludwigai/ludwig:master` image to:\n\n- run the `ludwig cli` command or\n- run Python program containing Ludwig api or\n- view Ludwig results with Tensorboard\n\nFor purposes of the examples assume this host directory structure\n\n```\n/top/level/directory/path/\n    data/\n        train.csv\n    src/\n        config.yaml\n        ludwig_api_program.py\n```\n\n### Run Ludwig CLI\n\n```\n# set shell variable to parent directory\nparent_path=/top/level/directory/path\n\n# invoke docker run command to execute the ludwig cli\n# map host directory ${parent_path}/data to container /data directory\n# map host directory ${parent_path}/src to container /src directory\ndocker run -v ${parent_path}/data:/data  \\\n    -v ${parent_path}/src:/src \\\n    ludwigai/ludwig:master \\\n    experiment --config /src/config.yaml \\\n        --dataset /data/train.csv \\\n        --output_directory /src/results\n```\n\nExperiment results can be found in host directory `/top/level/directory/path/src/results`\n\n### Run Python program using Ludwig APIs\n\n```\n# set shell variable to parent directory\nparent_path=/top/level/directory/path\n\n# invoke docker run command to execute Python interpreter\n# map host directory ${parent_path}/data to container /data directory\n# map host directory ${parent_path}/src to container /src directory\n# set current working directory to container /src directory\n# change default entrypoint from ludwig to python\ndocker run  -v ${parent_path}/data:/data  \\\n    -v ${parent_path}/src:/src \\\n    -w /src \\\n    --entrypoint python \\\n    ludwigai/ludwig:master /src/ludwig_api_program.py\n```\n\nLudwig results can be found in host\ndirectory `/top/level/directory/path/src/results`\n\n### View Ludwig Tensorboard results\n\n```\n# set shell variable to parent directory\nparent_path=/top/level/directory/path\n\n# invoke docker run command to execute Tensorboard\n# map host directory ${parent_path}/src to container /src directory\n# set up mapping from localhost port 6006 to container port 6006\n# change default entrypoint from ludwig to tensorboard\n# --logdir container location of tenorboard logs /src/results/<experiment_name>_<model_name>/model/logs\n# --bind_all Tensorboard serves on all public container interfaces\ndocker run  -v ${parent_path}/src:/src \\\n    -p 6006:6006 \\\n    --entrypoint tensorboard \\\n    ludwigai/ludwig:master \\\n      --logdir /src/results/experiment_run/model/logs \\\n      --bind_all\n```\n\nPoint browser to `http://localhost:6006` to see Tensorboard dashboard.\n\n### Devcontainer\n\nIf you want to contribute to Ludwig, you can setup a Docker container with all the dependencies\ninstalled as a full featured development environment. This can be done using devcontainers with VS Code:\nhttps://code.visualstudio.com/docs/devcontainers/containers\n\nYou can find the `devcontainer.json` file within the top level `.devcontainer` folder.\n"
  },
  {
    "path": "docker/ludwig/Dockerfile",
    "content": "#\n# Ludwig Docker image with full set of pre-requiste packages to support these capabilities\n#   text features\n#   image features\n#   audio features\n#   visualizations\n#   hyperparameter optimization\n#   distributed training\n#   model serving\n#\n\nFROM python:3.12-slim\n\nRUN apt-get -y update && apt-get -y install \\\n    git \\\n    libsndfile1 \\\n    build-essential \\\n    g++ \\\n    cmake \\\n    ffmpeg \\\n    sox \\\n    libsox-dev\nRUN pip install -U pip\n\nWORKDIR /ludwig\n\nRUN pip install --no-cache-dir torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n\nCOPY . .\nRUN pip install --no-cache-dir '.[full]'\n\nWORKDIR /data\n\nENTRYPOINT [\"ludwig\"]\n"
  },
  {
    "path": "docker/ludwig-gpu/Dockerfile",
    "content": "#\n# Ludwig Docker image with full set of pre-requiste packages to support these capabilities\n#   text features\n#   image features\n#   audio features\n#   visualizations\n#   hyperparameter optimization\n#   distributed training\n#   model serving\n#\n\nFROM pytorch/pytorch:2.6.0-cuda12.4-cudnn9-devel\n\nRUN apt-get -y update && DEBIAN_FRONTEND=\"noninteractive\" apt-get -y install \\\n    git \\\n    libsndfile1 \\\n    cmake \\\n    ffmpeg \\\n    sox \\\n    libsox-dev\nRUN pip install -U pip\n\nWORKDIR /ludwig\n\nCOPY . .\nRUN pip install --no-cache-dir '.[full]'\n\nWORKDIR /data\n\nENTRYPOINT [\"ludwig\"]\n"
  },
  {
    "path": "docker/ludwig-ray/Dockerfile",
    "content": "#\n# Ludwig Docker image with Ray support and full dependencies including:\n#   text features\n#   image features\n#   audio features\n#   visualizations\n#   hyperparameter optimization\n#   distributed training\n#   model serving\n#\n\nFROM rayproject/ray:2.44.1-py312\n\nRUN sudo apt-get update && DEBIAN_FRONTEND=\"noninteractive\" sudo apt-get install -y \\\n\tbuild-essential \\\n\twget \\\n\tgit \\\n\tcurl \\\n\tlibsndfile1 \\\n\tcmake \\\n\ttzdata \\\n\trsync \\\n\tvim \\\n\tffmpeg \\\n\tsox \\\n\tlibsox-dev\nRUN pip install -U pip\n\nWORKDIR /ludwig\n\nRUN pip install --no-cache-dir torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cpu\n\nCOPY . .\nRUN pip install --no-cache-dir '.[full]' --extra-index-url https://download.pytorch.org/whl/cpu\n"
  },
  {
    "path": "docker/ludwig-ray-gpu/Dockerfile",
    "content": "#\n# Ludwig Docker image with Ray support and full dependencies including:\n#   text features\n#   image features\n#   audio features\n#   visualizations\n#   hyperparameter optimization\n#   distributed training\n#   model serving\n#\n\nFROM rayproject/ray:2.44.1-py312-cu124\n\nRUN sudo apt-get update && \\\n    DEBIAN_FRONTEND=\"noninteractive\" sudo apt-get install -y \\\n    build-essential \\\n    wget \\\n    git \\\n    curl \\\n    libsndfile1 \\\n    cmake \\\n    tzdata \\\n    rsync \\\n    vim \\\n    ffmpeg \\\n    sox \\\n    libsox-dev\nRUN pip install -U pip\n\nWORKDIR /ludwig\n\nRUN pip install --no-cache-dir torch==2.6.0 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu124\n\nCOPY . .\nRUN pip install --no-cache-dir '.[full]'\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Examples\n\nThis directory contains example programs demonstrating Ludwig's Python APIs.\n\n| Directory       | Examples Provided                                                                                                                                                                                                                                                                                                               |\n| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| hyperopt        | Demonstrates Ludwig's to hyper-parameter optimization capability.                                                                                                                                                                                                                                                               |\n| 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.                                                                                                                      |\n| 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. |\n| 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.                                                                                 |\n| serve           | Demonstrates running Ludwig http model server. A sample Python program illustrates how to invoke the REST API to get predictions from input features.                                                                                                                                                                           |\n| class_imbalance | Demonstrates using our class balancing feature to over-sample an imbalanced dataset.                                                                                                                                                                                                                                            |\n"
  },
  {
    "path": "examples/calibration/README.md",
    "content": "# Calibration Examples\n\nDrawing on the methods in\nOn Calibration of Modern Neural Networks (Chuan Guo, Geoff Pleiss, Yu Sun, Kilian Q. Weinberger), Ludwig supports\ntemperature scaling for binary and category output features. Temperature scaling brings a model’s output probabilities\ncloser to the true likelihood while preserving the same accuracy and top k predictions.\n\nTo enable calibration, add calibration: True to any binary or category feature:\n\n```\noutput_features:\n - name: Cover_Type\n   type: category\n   calibration: True\n```\n\nWith calibration enabled, Ludwig will attempt to find a scale factor (temperature) which will bring the probabilities\ncloser to the true likelihoods using the validation set. This calibration phase is run after training is complete. If\nno validation set is provided, the training set is used for calibration.\n\nTo visualize the effects of calibration in Ludwig, you can use Calibration Plots, which bin the data based on model\nprobability and plot the model probability (X) versus the observed rate (Y) for each bin.\n"
  },
  {
    "path": "examples/calibration/train_forest_cover_calibrated.py",
    "content": "#!/usr/bin/env python\n\nimport copy\nimport logging\nimport shutil\n\nimport numpy as np\nimport yaml\n\nimport ludwig.visualize\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import forest_cover\n\n# clean out prior results\nshutil.rmtree(\"./results_forest_cover\", ignore_errors=True)\nshutil.rmtree(\"./visualizations_forest_cover\", ignore_errors=True)\n\n# Download and prepare the dataset\ndataset = forest_cover.load()\n\nconfig_yaml = \"\"\"\ninput_features:\n  - name: Elevation\n    type: number\n  - name: Aspect\n    type: number\n  - name: Slope\n    type: number\n  - name: Horizontal_Distance_To_Hydrology\n    type: number\n  - name: Vertical_Distance_To_Hydrology\n    type: number\n  - name: Horizontal_Distance_To_Roadways\n    type: number\n  - name: Hillshade_9am\n    type: number\n  - name: Hillshade_Noon\n    type: number\n  - name: Hillshade_3pm\n    type: number\n  - name: Horizontal_Distance_To_Fire_Points\n    type: number\n  - name: Wilderness_Area\n    type: category\n  - name: Soil_Type\n    type: category\noutput_features:\n  - name: Cover_Type\n    type: category\ncombiner:\n  type: transformer\ntrainer:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n\"\"\"\n\nuncalibrated_config = yaml.safe_load(config_yaml)\n\nscaled_config = copy.deepcopy(uncalibrated_config)\nscaled_config[\"output_features\"][0][\"calibration\"] = True\n\nuncalibrated_model = LudwigModel(config=uncalibrated_config, logging_level=logging.INFO)\nuncalibrated_model.train(\n    dataset,\n    model_name=\"uncalibrated\",\n    experiment_name=\"forest_cover_calibration\",\n    output_directory=\"results_forest_cover\",\n)\n\nscaled_model = LudwigModel(config=scaled_config, logging_level=logging.INFO)\nscaled_model.train(\n    dataset, model_name=\"scaled\", experiment_name=\"forest_cover_calibration\", output_directory=\"results_forest_cover\"\n)\n\n# Generates predictions and performance statistics for the test set.\nuncalibrated_test_stats, uncalibrated_test_predictions, _ = uncalibrated_model.evaluate(\n    dataset, collect_predictions=True, collect_overall_stats=True\n)\n\nscaled_test_stats, scaled_test_predictions, _ = scaled_model.evaluate(\n    dataset, collect_predictions=True, collect_overall_stats=True\n)\n\nuncalibrated_probs = np.stack(uncalibrated_test_predictions[\"Cover_Type_probabilities\"], axis=0)\nscaled_probs = np.stack(scaled_test_predictions[\"Cover_Type_probabilities\"], axis=0)\n\nludwig.visualize.calibration_1_vs_all(\n    probabilities_per_model=[uncalibrated_probs, scaled_probs],\n    model_names=[\"Uncalibrated\", \"Calibrated\"],\n    ground_truth=dataset[\"Cover_Type\"],\n    metadata=uncalibrated_model.training_set_metadata,\n    output_feature_name=\"Cover_Type\",\n    top_n_classes=[7, 7],\n    labels_limit=7,\n    output_directory=\"visualizations_forest_cover\",\n    file_format=\"png\",\n)\n"
  },
  {
    "path": "examples/calibration/train_mushroom_edibility_calibrated.py",
    "content": "#!/usr/bin/env python\n\nimport copy\nimport logging\nimport shutil\n\nimport numpy as np\nimport yaml\n\nimport ludwig.visualize\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import mushroom_edibility\n\n# clean out prior results\nshutil.rmtree(\"./results_mushroom_edibility\", ignore_errors=True)\nshutil.rmtree(\"./visualizations_mushroom_edibility\", ignore_errors=True)\n\n# Download and prepare the dataset\ndataset = mushroom_edibility.load()\n\n# This dataset has no split, so add a split column\ndataset.split = np.random.choice(3, len(dataset), p=(0.7, 0.1, 0.2))\n\nconfig_yaml = \"\"\"\ninput_features:\n  - name: cap-shape\n    type: category\n  - name: cap-surface\n    type: category\n  - name: cap-color\n    type: category\n  - name: bruises?\n    type: category\n  - name: odor\n    type: category\n  - name: gill-attachment\n    type: category\n  - name: gill-spacing\n    type: category\n  - name: gill-size\n    type: category\n  - name: gill-color\n    type: category\n  - name: stalk-shape\n    type: category\n  - name: stalk-root\n    type: category\n  - name: stalk-surface-above-ring\n    type: category\n  - name: stalk-surface-below-ring\n    type: category\n  - name: stalk-color-above-ring\n    type: category\n  - name: stalk-color-below-ring\n    type: category\n  - name: veil-type\n    type: category\n  - name: veil-color\n    type: category\n  - name: ring-number\n    type: category\n  - name: ring-type\n    type: category\n  - name: spore-print-color\n    type: category\n  - name: population\n    type: category\n  - name: habitat\n    type: category\noutput_features:\n  - name: class\n    type: category\ncombiner:\n  type: concat\ntrainer:\n  batch_size: 256\n  learning_rate: .0001\n  epochs: 10\n\"\"\"\n\nuncalibrated_config = yaml.safe_load(config_yaml)\n\nscaled_config = copy.deepcopy(uncalibrated_config)\nscaled_config[\"output_features\"][0][\"calibration\"] = True\n\nuncalibrated_model = LudwigModel(config=uncalibrated_config, logging_level=logging.INFO)\nuncalibrated_model.train(\n    dataset,\n    model_name=\"uncalibrated\",\n    experiment_name=\"mushroom_edibility_calibration\",\n    output_directory=\"results_mushroom_edibility\",\n)\n\nscaled_model = LudwigModel(config=scaled_config, logging_level=logging.INFO)\nscaled_model.train(\n    dataset,\n    model_name=\"scaled\",\n    experiment_name=\"mushroom_edibility_calibration\",\n    output_directory=\"results_mushroom_edibility\",\n)\n\n# Generates predictions and performance statistics for the test set.\nuncalibrated_test_stats, uncalibrated_test_predictions, _ = uncalibrated_model.evaluate(\n    dataset, collect_predictions=True, collect_overall_stats=True\n)\n\nscaled_test_stats, scaled_test_predictions, _ = scaled_model.evaluate(\n    dataset, collect_predictions=True, collect_overall_stats=True\n)\n\nuncalibrated_probs = np.stack(uncalibrated_test_predictions[\"class_probabilities\"], axis=0)\nscaled_probs = np.stack(scaled_test_predictions[\"class_probabilities\"], axis=0)\n\nludwig.visualize.calibration_1_vs_all(\n    probabilities_per_model=[uncalibrated_probs, scaled_probs],\n    model_names=[\"Uncalibrated\", \"Calibrated\"],\n    ground_truth=dataset[\"class\"],\n    metadata=uncalibrated_model.training_set_metadata,\n    output_feature_name=\"class\",\n    top_n_classes=[3, 3],\n    labels_limit=3,\n    output_directory=\"visualizations_mushroom_edibility\",\n    file_format=\"png\",\n)\n"
  },
  {
    "path": "examples/class_imbalance/README.md",
    "content": "# Credit Card Fraud Detection Example\n\nThis 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.\n\n### Preparatory Steps\n\nCreate and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials).\n\nThe Imbalanced Insurance dataset is hosted by Kaggle, and as such Ludwig will need to authenticate you through the Kaggle API to download the dataset.\n\n### Examples\n\n| File                         | Description                                                                                                    |\n| ---------------------------- | -------------------------------------------------------------------------------------------------------------- |\n| model_training.py            | Demonstrates the use of oversampling by training two different models: one with no oversampling, and one with. |\n| model_training_results.ipynb | Example for extracting training statistics and generating visualizations.                                      |\n\nEnter `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.\n\n```\n./results/\n    balance_example_standard_model/\n    balance_example_balanced_model/\n```\n\nThe only difference between these two models is that the balanced model uses a small amount of oversampling in addition to the other configuration parameters.\nThe way this is done is by specifying the ratio that you want the minority class to have in relation to the majority class.\nFor 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.\nIn 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%.\nThough this doesn't seem like much, it is enough to get some small performance improvements without experiencing performance degradation due to over-fitting.\n\nHere are the performance differences in the two models followed by some plots showing different metrics over training:\n\n|  Metric  | Standard Model | Balanced Model |\n| :------: | :------------: | :------------: |\n|   Loss   |     0.3649     |     0.2758     |\n| Accuracy |     0.7732     |     0.8237     |\n| ROC AUC  |     0.8533     |     0.8660     |\n\nHere are the learning curve plots from both models:\n\n![](../images/balance_example_accuracy_curves.png)\n\n![](../images/balance_example_roc_auc_curves.png)\n\nHere is the comparison of model performances on ROC_AUC and Accuracy:\n\n![](../images/compare_performance_Response.png)\n\nThe 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.\n"
  },
  {
    "path": "examples/class_imbalance/balanced_model_config.yaml",
    "content": "input_features:\n  - name: Gender\n    type: category\n  - name: Age\n    type: number\n  - name: Driving_License\n    type: binary\n  - name: Region_Code\n    type: number\n  - name: Previously_Insured\n    type: binary\n  - name: Vehicle_Age\n    type: category\n  - name: Vehicle_Damage\n    type: category\n  - name: Annual_Premium\n    type: number\n  - name: Policy_Sales_Channel\n    type: number\n  - name: Vintage\n    type: number\noutput_features:\n  - name: Response\n    type: binary\npreprocessing:\n  oversample_minority: 0.26\ntrainer:\n  learning_rate: 0.0001\n  learning_rate_scheduler:\n    decay: exponential\n    decay_rate: 0.9\n    decay_steps: 30000\n    staircase: True\n  epochs: 50\n"
  },
  {
    "path": "examples/class_imbalance/model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Class Imbalance Model Training Example\n#\n# This example trains a model utilizing a standard config, and then a config using oversampling\n\nimport logging\nimport shutil\n\n# Import required libraries\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import imbalanced_insurance\nfrom ludwig.visualize import compare_performance\n\n# clean out old results\nshutil.rmtree(\"./results\", ignore_errors=True)\nshutil.rmtree(\"./visualizations\", ignore_errors=True)\n\n# list models to train\nlist_of_model_ids = [\"standard_model\", \"balanced_model\"]\nlist_of_eval_stats = []\n\ntraining_set, val_set, test_set = imbalanced_insurance.load()\n\n# Train models\nfor model_id in list_of_model_ids:\n    print(\">>>> training: \", model_id)\n\n    # Define Ludwig model object that drive model training\n    model = LudwigModel(config=model_id + \"_config.yaml\", logging_level=logging.WARN)\n\n    # initiate model training\n    train_stats, _, _ = model.train(\n        training_set=training_set,\n        validation_set=val_set,\n        test_set=test_set,\n        experiment_name=\"balance_example\",\n        model_name=model_id,\n        skip_save_model=True,\n    )\n\n    # evaluate model on test_set\n    eval_stats, _, _ = model.evaluate(test_set)\n\n    # save eval stats for later use\n    list_of_eval_stats.append(eval_stats)\n\n    print(\">>>>>>> completed: \", model_id, \"\\n\")\n\n\ncompare_performance(\n    list_of_eval_stats,\n    \"Response\",\n    model_names=list_of_model_ids,\n    output_directory=\"./visualizations\",\n    file_format=\"png\",\n)\n"
  },
  {
    "path": "examples/class_imbalance/model_training_results.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"8c1e31e4-d8d4-4e83-8f4c-f868365d14d7\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Model Analysis\\n\",\n    \"\\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    \"```\\n\",\n    \"python model_training.py\\n\",\n    \"```\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"a3b3b6c1-5d11-4a03-9dfa-b070e45b2adb\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Import required libraries\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 226,\n   \"id\": \"6a6bbd43-1333-4a0e-b895-c3e393d5ee07\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"from ludwig.utils.data_utils import load_json\\n\",\n    \"from ludwig.visualize import learning_curves\\n\",\n    \"import pandas as pd\\n\",\n    \"import numpy as np\\n\",\n    \"import os.path\\n\",\n    \"import matplotlib.pyplot as plt\\n\",\n    \"%matplotlib inline\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"b5e4d240-7607-4036-9573-6b452523c18f\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Learning Curves\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"725dde7d-f7a5-4836-8cf1-a3f50acafd30\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Create Plotting Data Function \"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 227,\n   \"id\": \"d8c35325-cfab-4ead-8699-1e98e55a8c7b\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"def create_plot_ready_data(list_of_train_stats, model_names, metric, target):\\n\",\n    \"    # List of splits to evaluate statistics for\\n\",\n    \"    list_of_splits = ['training', 'validation', 'test']    \\n\",\n    \"    \\n\",\n    \"    # Empty list to fill with dfs for each models' stats\\n\",\n    \"    list_of_train_stats_df = []\\n\",\n    \"    \\n\",\n    \"    # For each models' stats, create a df with columns of stats for each split listed above\\n\",\n    \"    for name, stats in zip(model_names, list_of_train_stats):\\n\",\n    \"        list_of_dfs = []\\n\",\n    \"        for split in list_of_splits:\\n\",\n    \"                df = pd.DataFrame(stats[split][target])\\n\",\n    \"                df.columns = [split + '_' + c for c in df.columns]\\n\",\n    \"                list_of_dfs.append(df)\\n\",\n    \"    \\n\",\n    \"        combined_df = pd.concat(list_of_dfs, axis=1)\\n\",\n    \"        combined_df.name = name\\n\",\n    \"        combined_df['epoch'] = combined_df.index + 1\\n\",\n    \"        list_of_train_stats_df.append(combined_df)\\n\",\n    \"    \\n\",\n    \"    # holding ready for plot ready data\\n\",\n    \"    plot_ready_list = []\\n\",\n    \"    \\n\",\n    \"    # consolidate the multiple training statistics dataframes\\n\",\n    \"    for df in list_of_train_stats_df:\\n\",\n    \"        for col in ['training', 'validation']:\\n\",\n    \"            df2 = df[['epoch', col + '_{}'.format(metric)]].copy()\\n\",\n    \"            df2.columns = ['epoch', '{}'.format(metric)]\\n\",\n    \"            df2['split'] = col\\n\",\n    \"            df2['model'] = df.name\\n\",\n    \"            plot_ready_list.append(df2)\\n\",\n    \"\\n\",\n    \"    return pd.concat(plot_ready_list, axis=0, ignore_index=True)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"id\": \"722f5f48-7026-427e-9bb7-388fec15a24f\",\n   \"metadata\": {\n    \"tags\": []\n   },\n   \"source\": [\n    \"### Create Plotting Data\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 228,\n   \"id\": \"6b48aef8-11a8-4bba-8d52-114adb9cb2f2\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"standard_stats = load_json(os.path.join('results/balance_example_standard_model','training_statistics.json'))\\n\",\n    \"balanced_stats = load_json(os.path.join('results/balance_example_balanced_model','training_statistics.json'))\\n\",\n    \"\\n\",\n    \"accuracy_learning_curves = create_plot_ready_data([standard_stats, balanced_stats], ['standard_model', 'balanced_model'], 'accuracy', 'Response')\\n\",\n    \"roc_auc_learning_curves = create_plot_ready_data([standard_stats, balanced_stats], ['standard_model', 'balanced_model'], 'roc_auc', 'Response')\\n\",\n    \"loss_learning_curves = create_plot_ready_data([standard_stats, balanced_stats], ['standard_model', 'balanced_model'], 'loss', 'Response')\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 229,\n   \"id\": \"a06aaa41-e692-4348-aab5-6bd78711f6f1\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 720x432 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {\n      \"needs_background\": \"light\"\n     },\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"fig = plt.figure(figsize=(10,6))\\n\",\n    \"sns.set_style(style='dark')\\n\",\n    \"ax = sns.lineplot(x='epoch', y='accuracy',\\n\",\n    \"             style='split',\\n\",\n    \"             hue='model',\\n\",\n    \"             data=accuracy_learning_curves)\\n\",\n    \"ax.set_title('Accuracy Learning Curves', fontdict={'fontsize': 16})\\n\",\n    \"ax.grid(visible=True, which='major', color='black', linewidth=0.075)\\n\",\n    \"ax.grid(visible=True, which='minor', color='black', linewidth=0.075)\\n\",\n    \"ax.set_xlabel(\\\"Epoch\\\", fontsize = 15)\\n\",\n    \"ax.set_ylabel(\\\"Accuracy\\\", fontsize = 15);\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 230,\n   \"id\": \"b6ef7fce-b49e-49a6-a4b0-436d078f72ed\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 720x432 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"fig = plt.figure(figsize=(10,6))\\n\",\n    \"sns.set_style(style='dark')\\n\",\n    \"ax = sns.lineplot(x='epoch', y='roc_auc',\\n\",\n    \"             style='split',\\n\",\n    \"             hue='model',\\n\",\n    \"             data=roc_auc_learning_curves)\\n\",\n    \"ax.set_title('ROC AUC Learning Curves', fontdict={'fontsize': 16})\\n\",\n    \"ax.grid(visible=True, which='major', color='black', linewidth=0.075)\\n\",\n    \"ax.grid(visible=True, which='minor', color='black', linewidth=0.075)\\n\",\n    \"ax.set_xlabel(\\\"Epoch\\\", fontsize = 15)\\n\",\n    \"ax.set_ylabel(\\\"ROC AUC\\\", fontsize = 15);\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 231,\n   \"id\": \"9bfd8dc1-b0e0-451c-8f2b-de9e92e976c0\",\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 720x432 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"fig = plt.figure(figsize=(10,6))\\n\",\n    \"sns.set_style(style='dark')\\n\",\n    \"ax = sns.lineplot(x='epoch', y='loss',\\n\",\n    \"             style='split',\\n\",\n    \"             hue='model',\\n\",\n    \"             data=loss_learning_curves)\\n\",\n    \"ax.set_title('Loss Learning Curves', fontdict={'fontsize': 16})\\n\",\n    \"ax.grid(visible=True, which='major', color='black', linewidth=0.075)\\n\",\n    \"ax.grid(visible=True, which='minor', color='black', linewidth=0.075)\\n\",\n    \"ax.set_xlabel(\\\"Epoch\\\", fontsize = 15)\\n\",\n    \"ax.set_ylabel(\\\"Loss\\\", fontsize = 15);\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"id\": \"11855d06-552d-4bc2-ba74-668422aace0c\",\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"base39\",\n   \"language\": \"python\",\n   \"name\": \"base39\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.10\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 5\n}\n"
  },
  {
    "path": "examples/class_imbalance/standard_model_config.yaml",
    "content": "input_features:\n  - name: Gender\n    type: category\n  - name: Age\n    type: number\n  - name: Driving_License\n    type: binary\n  - name: Region_Code\n    type: number\n  - name: Previously_Insured\n    type: binary\n  - name: Vehicle_Age\n    type: category\n  - name: Vehicle_Damage\n    type: category\n  - name: Annual_Premium\n    type: number\n  - name: Policy_Sales_Channel\n    type: number\n  - name: Vintage\n    type: number\noutput_features:\n  - name: Response\n    type: binary\ntrainer:\n  learning_rate: 0.0001\n  learning_rate_scheduler:\n    decay: exponential\n    decay_rate: 0.9\n    decay_steps: 30000\n    staircase: True\n  epochs: 50\n"
  },
  {
    "path": "examples/forecasting/README.md",
    "content": "- Download and unpack hourly weather data from https://www.kaggle.com/selfishgene/historical-hourly-weather-data\n- `ludwig train --config config.yaml --dataset temperature.csv`\n- `ludwig forecast -n 10 --model_path results/experiment_run/model --dataset temperature.csv`\n"
  },
  {
    "path": "examples/forecasting/config.yaml",
    "content": "input_features:\n  - name: Seattle\n    type: timeseries\n    preprocessing:\n      window_size: 10\n    encoder:\n      type: passthrough\noutput_features:\n  - name: Seattle_next\n    type: timeseries\n    column: Seattle\n    preprocessing:\n      horizon: 2\ncombiner:\n  type: concat\n  flatten_inputs: true\npreprocessing:\n  split:\n    type: datetime\n    column: datetime\n"
  },
  {
    "path": "examples/getting_started/rotten_tomatoes.yaml",
    "content": "input_features:\n  - name: genres\n    type: set\n    preprocessing:\n      tokenizer: comma\n  - name: content_rating\n    type: category\n  - name: top_critic\n    type: binary\n  - name: runtime\n    type: number\n  - name: review_content\n    type: text\n    encoder:\n      type: embed\noutput_features:\n  - name: recommended\n    type: binary\ntrainer:\n  epochs: 3\n"
  },
  {
    "path": "examples/getting_started/run.sh",
    "content": "#!/usr/bin/env bash\n\n# Fail fast if an error occurs\nset -e\n\n# Get the directory of this script, which contains the config file\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\n# Download the data\nwget https://ludwig.ai/latest/data/rotten_tomatoes.csv\nwget https://ludwig.ai/latest/data/rotten_tomatoes_test.csv\n\n# Check the first 5 rows\nhead -n 5 rotten_tomatoes.csv\n\n# Train\nludwig train --config ${SCRIPT_DIR}/rotten_tomatoes.yaml --dataset rotten_tomatoes.csv\n\n# Predict and Evaluate\nludwig predict --model_path results/experiment_run/model --dataset rotten_tomatoes_test.csv\n"
  },
  {
    "path": "examples/hyperopt/README.md",
    "content": "# Hyperparameter Optimization\n\nDemonstrates hyperparameter optimization using Ludwig's in-built capabilities.\n\n### Preparatory Steps\n\n- Create `data` directory\n- Download [Kaggle wine quality data set](https://www.kaggle.com/rajyellow46/wine-quality) into the `data` directory. Directory should\n  appear as follows:\n\n```\nhyperopt/\n    data/\n        winequalityN.csv\n```\n\n### Description\n\nJupyter notebook `model_hyperopt_example.ipynb` demonstrates several hyperparameter optimization capabilities. Key features demonstrated in the notebook:\n\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 (Note: takes about 35 minutes)\n- Demonstrate various Ludwig visualizations for hyperparameter optimization\n"
  },
  {
    "path": "examples/hyperopt/model_hyperopt_example.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Hyperparameter Optimization In Ludwig\\n\",\n    \"\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"Demonstrates hyper-parameter tuning capabilities of Ludwig. The following steps occur in this notebook:\\n\",\n    \"* Training data is prepared for use\\n\",\n    \"* Programmatically create Ludwig config dictionary from the training data dataframe\\n\",\n    \"* Setup parameter space for hyperparameter optimization\\n\",\n    \"* Perform two hyperparameter runs\\n\",\n    \"  * Parallel workers using random search strategy\\n\",\n    \"  * Serial processing using random search strategy\\n\",\n    \"  * Parallel workers using grid search strategy\\n\",\n    \"* Demonstrate various Ludwig visualizations for hyperparameter optimization\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Import required libraries\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"pycharm\": {\n     \"is_executing\": false\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"import warnings\\n\",\n    \"warnings.simplefilter('ignore')\\n\",\n    \"\\n\",\n    \"import shutil\\n\",\n    \"import datetime\\n\",\n    \"\\n\",\n    \"import pandas as pd\\n\",\n    \"import numpy as np\\n\",\n    \"\\n\",\n    \"from ludwig.hyperopt.run import hyperopt\\n\",\n    \"from ludwig.visualize import hyperopt_results_to_dataframe, hyperopt_hiplot_cli, hyperopt_report_cli\\n\",\n    \"\\n\",\n    \"from sklearn.model_selection import train_test_split\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Retrieve data for training\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"(6497, 13)\"\n      ]\n     },\n     \"execution_count\": 2,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"train_df = pd.read_csv('./data/winequalityN.csv')\\n\",\n    \"train_df.shape\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Standardize column names to replace spaces(\\\" \\\") with underscore(\\\"_\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"new_col = []\\n\",\n    \"for i in range(len(train_df.columns)):\\n\",\n    \"    new_col.append(train_df.columns[i].replace(' ', '_'))\\n\",\n    \"    \\n\",\n    \"train_df.columns = new_col\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Data Set Overview\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"type                     object\\n\",\n       \"fixed_acidity           float64\\n\",\n       \"volatile_acidity        float64\\n\",\n       \"citric_acid             float64\\n\",\n       \"residual_sugar          float64\\n\",\n       \"chlorides               float64\\n\",\n       \"free_sulfur_dioxide     float64\\n\",\n       \"total_sulfur_dioxide    float64\\n\",\n       \"density                 float64\\n\",\n       \"pH                      float64\\n\",\n       \"sulphates               float64\\n\",\n       \"alcohol                 float64\\n\",\n       \"quality                   int64\\n\",\n       \"dtype: object\"\n      ]\n     },\n     \"execution_count\": 4,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"train_df.dtypes\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Create training and test data sets\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"3      30\\n\",\n       \"4     216\\n\",\n       \"5    2138\\n\",\n       \"6    2836\\n\",\n       \"7    1079\\n\",\n       \"8     193\\n\",\n       \"9       5\\n\",\n       \"Name: quality, dtype: int64\"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"train_df['quality'].value_counts().sort_index()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"categorical variables: ['type'] \\n\",\n      \"\\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      \"\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# isolate the predictor variables only\\n\",\n    \"predictor_vars = list(set(train_df.columns) - set(['quality']))\\n\",\n    \"\\n\",\n    \"#extract categorical variables\\n\",\n    \"categorical_vars = []\\n\",\n    \"for p in predictor_vars:\\n\",\n    \"    if train_df[p].dtype == 'object':\\n\",\n    \"        categorical_vars.append(p)\\n\",\n    \"        \\n\",\n    \"print(\\\"categorical variables:\\\", categorical_vars,'\\\\n')\\n\",\n    \"\\n\",\n    \"# get numerical variables\\n\",\n    \"numerical_vars = list(set(predictor_vars) - set(categorical_vars))\\n\",\n    \"\\n\",\n    \"print(\\\"numerical variables:\\\", numerical_vars,\\\"\\\\n\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>count</th>\\n\",\n       \"      <th>mean</th>\\n\",\n       \"      <th>std</th>\\n\",\n       \"      <th>min</th>\\n\",\n       \"      <th>25%</th>\\n\",\n       \"      <th>50%</th>\\n\",\n       \"      <th>75%</th>\\n\",\n       \"      <th>max</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>fixed_acidity</th>\\n\",\n       \"      <td>6487.0</td>\\n\",\n       \"      <td>7.216579</td>\\n\",\n       \"      <td>1.296750</td>\\n\",\n       \"      <td>3.80000</td>\\n\",\n       \"      <td>6.40000</td>\\n\",\n       \"      <td>7.00000</td>\\n\",\n       \"      <td>7.70000</td>\\n\",\n       \"      <td>15.90000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>volatile_acidity</th>\\n\",\n       \"      <td>6489.0</td>\\n\",\n       \"      <td>0.339691</td>\\n\",\n       \"      <td>0.164649</td>\\n\",\n       \"      <td>0.08000</td>\\n\",\n       \"      <td>0.23000</td>\\n\",\n       \"      <td>0.29000</td>\\n\",\n       \"      <td>0.40000</td>\\n\",\n       \"      <td>1.58000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>citric_acid</th>\\n\",\n       \"      <td>6494.0</td>\\n\",\n       \"      <td>0.318722</td>\\n\",\n       \"      <td>0.145265</td>\\n\",\n       \"      <td>0.00000</td>\\n\",\n       \"      <td>0.25000</td>\\n\",\n       \"      <td>0.31000</td>\\n\",\n       \"      <td>0.39000</td>\\n\",\n       \"      <td>1.66000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>residual_sugar</th>\\n\",\n       \"      <td>6495.0</td>\\n\",\n       \"      <td>5.444326</td>\\n\",\n       \"      <td>4.758125</td>\\n\",\n       \"      <td>0.60000</td>\\n\",\n       \"      <td>1.80000</td>\\n\",\n       \"      <td>3.00000</td>\\n\",\n       \"      <td>8.10000</td>\\n\",\n       \"      <td>65.80000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>chlorides</th>\\n\",\n       \"      <td>6495.0</td>\\n\",\n       \"      <td>0.056042</td>\\n\",\n       \"      <td>0.035036</td>\\n\",\n       \"      <td>0.00900</td>\\n\",\n       \"      <td>0.03800</td>\\n\",\n       \"      <td>0.04700</td>\\n\",\n       \"      <td>0.06500</td>\\n\",\n       \"      <td>0.61100</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>free_sulfur_dioxide</th>\\n\",\n       \"      <td>6497.0</td>\\n\",\n       \"      <td>30.525319</td>\\n\",\n       \"      <td>17.749400</td>\\n\",\n       \"      <td>1.00000</td>\\n\",\n       \"      <td>17.00000</td>\\n\",\n       \"      <td>29.00000</td>\\n\",\n       \"      <td>41.00000</td>\\n\",\n       \"      <td>289.00000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>total_sulfur_dioxide</th>\\n\",\n       \"      <td>6497.0</td>\\n\",\n       \"      <td>115.744574</td>\\n\",\n       \"      <td>56.521855</td>\\n\",\n       \"      <td>6.00000</td>\\n\",\n       \"      <td>77.00000</td>\\n\",\n       \"      <td>118.00000</td>\\n\",\n       \"      <td>156.00000</td>\\n\",\n       \"      <td>440.00000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>density</th>\\n\",\n       \"      <td>6497.0</td>\\n\",\n       \"      <td>0.994697</td>\\n\",\n       \"      <td>0.002999</td>\\n\",\n       \"      <td>0.98711</td>\\n\",\n       \"      <td>0.99234</td>\\n\",\n       \"      <td>0.99489</td>\\n\",\n       \"      <td>0.99699</td>\\n\",\n       \"      <td>1.03898</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>pH</th>\\n\",\n       \"      <td>6488.0</td>\\n\",\n       \"      <td>3.218395</td>\\n\",\n       \"      <td>0.160748</td>\\n\",\n       \"      <td>2.72000</td>\\n\",\n       \"      <td>3.11000</td>\\n\",\n       \"      <td>3.21000</td>\\n\",\n       \"      <td>3.32000</td>\\n\",\n       \"      <td>4.01000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>sulphates</th>\\n\",\n       \"      <td>6493.0</td>\\n\",\n       \"      <td>0.531215</td>\\n\",\n       \"      <td>0.148814</td>\\n\",\n       \"      <td>0.22000</td>\\n\",\n       \"      <td>0.43000</td>\\n\",\n       \"      <td>0.51000</td>\\n\",\n       \"      <td>0.60000</td>\\n\",\n       \"      <td>2.00000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>alcohol</th>\\n\",\n       \"      <td>6497.0</td>\\n\",\n       \"      <td>10.491801</td>\\n\",\n       \"      <td>1.192712</td>\\n\",\n       \"      <td>8.00000</td>\\n\",\n       \"      <td>9.50000</td>\\n\",\n       \"      <td>10.30000</td>\\n\",\n       \"      <td>11.30000</td>\\n\",\n       \"      <td>14.90000</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>quality</th>\\n\",\n       \"      <td>6497.0</td>\\n\",\n       \"      <td>5.818378</td>\\n\",\n       \"      <td>0.873255</td>\\n\",\n       \"      <td>3.00000</td>\\n\",\n       \"      <td>5.00000</td>\\n\",\n       \"      <td>6.00000</td>\\n\",\n       \"      <td>6.00000</td>\\n\",\n       \"      <td>9.00000</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\"\n      ],\n      \"text/plain\": [\n       \"                       count        mean        std      min       25%  \\\\\\n\",\n       \"fixed_acidity         6487.0    7.216579   1.296750  3.80000   6.40000   \\n\",\n       \"volatile_acidity      6489.0    0.339691   0.164649  0.08000   0.23000   \\n\",\n       \"citric_acid           6494.0    0.318722   0.145265  0.00000   0.25000   \\n\",\n       \"residual_sugar        6495.0    5.444326   4.758125  0.60000   1.80000   \\n\",\n       \"chlorides             6495.0    0.056042   0.035036  0.00900   0.03800   \\n\",\n       \"free_sulfur_dioxide   6497.0   30.525319  17.749400  1.00000  17.00000   \\n\",\n       \"total_sulfur_dioxide  6497.0  115.744574  56.521855  6.00000  77.00000   \\n\",\n       \"density               6497.0    0.994697   0.002999  0.98711   0.99234   \\n\",\n       \"pH                    6488.0    3.218395   0.160748  2.72000   3.11000   \\n\",\n       \"sulphates             6493.0    0.531215   0.148814  0.22000   0.43000   \\n\",\n       \"alcohol               6497.0   10.491801   1.192712  8.00000   9.50000   \\n\",\n       \"quality               6497.0    5.818378   0.873255  3.00000   5.00000   \\n\",\n       \"\\n\",\n       \"                            50%        75%        max  \\n\",\n       \"fixed_acidity           7.00000    7.70000   15.90000  \\n\",\n       \"volatile_acidity        0.29000    0.40000    1.58000  \\n\",\n       \"citric_acid             0.31000    0.39000    1.66000  \\n\",\n       \"residual_sugar          3.00000    8.10000   65.80000  \\n\",\n       \"chlorides               0.04700    0.06500    0.61100  \\n\",\n       \"free_sulfur_dioxide    29.00000   41.00000  289.00000  \\n\",\n       \"total_sulfur_dioxide  118.00000  156.00000  440.00000  \\n\",\n       \"density                 0.99489    0.99699    1.03898  \\n\",\n       \"pH                      3.21000    3.32000    4.01000  \\n\",\n       \"sulphates               0.51000    0.60000    2.00000  \\n\",\n       \"alcohol                10.30000   11.30000   14.90000  \\n\",\n       \"quality                 6.00000    6.00000    9.00000  \"\n      ]\n     },\n     \"execution_count\": 7,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"train_df.describe().T\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"unique values for type is 2\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"for p in categorical_vars:\\n\",\n    \"    print(\\\"unique values for\\\",p,\\\"is\\\",train_df[p].nunique())\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Create config\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# template for config\\n\",\n    \"config = {'input_features':[], 'output_features': [], 'trainer':{}}\\n\",\n    \"\\n\",\n    \"# setup input features for categorical variables\\n\",\n    \"for p in categorical_vars:\\n\",\n    \"    a_feature = {'name': p.replace(' ','_'), 'type': 'category', 'representation': 'sparse'}\\n\",\n    \"    config['input_features'].append(a_feature)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"# setup input features for numerical variables\\n\",\n    \"for p in numerical_vars:\\n\",\n    \"    a_feature = {'name': p.replace(' ','_'), 'type': 'number', \\n\",\n    \"                'preprocessing': {'missing_value_strategy': 'fill_with_mean', 'normalization': 'zscore'}}\\n\",\n    \"    config['input_features'].append(a_feature)\\n\",\n    \"\\n\",\n    \"# set up output variable\\n\",\n    \"config['output_features'].append({'name': 'quality', 'type':'category'})\\n\",\n    \"\\n\",\n    \"# set up trainer\\n\",\n    \"config['trainer'] = {'epochs': 20}\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 10,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"config:\\n\"\n     ]\n    },\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"{'input_features': [{'name': 'type',\\n\",\n       \"   'type': 'category',\\n\",\n       \"   'representation': 'sparse'},\\n\",\n       \"  {'name': 'sulphates',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'fixed_acidity',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'total_sulfur_dioxide',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'density',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'pH',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'residual_sugar',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'free_sulfur_dioxide',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'alcohol',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'citric_acid',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'volatile_acidity',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}},\\n\",\n       \"  {'name': 'chlorides',\\n\",\n       \"   'type': 'number',\\n\",\n       \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n       \"    'normalization': 'zscore'}}],\\n\",\n       \" 'output_features': [{'name': 'quality', 'type': 'category'}],\\n\",\n       \" 'trainer': {'epochs': 20}}\"\n      ]\n     },\n     \"execution_count\": 10,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"# View the config\\n\",\n    \"print(\\\"config:\\\")\\n\",\n    \"config\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Define hyperparameter search space\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 11,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"SEED=13\\n\",\n    \"\\n\",\n    \"hyperopt_configs = {\\n\",\n    \"    \\\"parameters\\\": {\\n\",\n    \"        \\\"trainer.learning_rate\\\": {\\n\",\n    \"            \\\"type\\\": \\\"float\\\",\\n\",\n    \"            \\\"space\\\": \\\"loguniform\\\",\\n\",\n    \"            \\\"lower\\\": 0.0001,\\n\",\n    \"            \\\"upper\\\": 0.01,\\n\",\n    \"            \\\"q\\\": 3,\\n\",\n    \"        },\\n\",\n    \"        \\\"trainer.batch_size\\\": {\\n\",\n    \"            \\\"type\\\": \\\"int\\\",\\n\",\n    \"            \\\"space\\\": \\\"qlograndint\\\",\\n\",\n    \"            \\\"base\\\" : 2,\\n\",\n    \"            \\\"lower\\\": 32,\\n\",\n    \"            \\\"upper\\\": 256,\\n\",\n    \"            \\\"q\\\": 5,\\n\",\n    \"        },\\n\",\n    \"        \\\"quality.fc_size\\\": {\\n\",\n    \"            \\\"type\\\": \\\"int\\\",\\n\",\n    \"            'space': 'qrandint',\\n\",\n    \"            \\\"lower\\\": 32,\\n\",\n    \"            \\\"upper\\\": 256,\\n\",\n    \"            \\\"q\\\": 5,\\n\",\n    \"        },\\n\",\n    \"        \\\"quality.num_fc_layers\\\": {\\n\",\n    \"            'type': 'int',\\n\",\n    \"            'space': 'qrandint',\\n\",\n    \"            'lower': 1,\\n\",\n    \"            'upper': 5,\\n\",\n    \"            'q': 4,\\n\",\n    \"        }\\n\",\n    \"    },\\n\",\n    \"    \\\"goal\\\": \\\"minimize\\\",\\n\",\n    \"    'output_feature': \\\"quality\\\",\\n\",\n    \"    'validation_metrics': 'loss'\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"# add hyperopt parameter space to the config\\n\",\n    \"config['hyperopt'] = hyperopt_configs\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Train with optimal hyperparameters on the whole data set\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 12,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# clean out old results\\n\",\n    \"shutil.rmtree('./results_ray', ignore_errors=True)\\n\",\n    \"shutil.rmtree('./results_random_serial', ignore_errors=True)\\n\",\n    \"shutil.rmtree('./visualizations', ignore_errors=True)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Random Search with ray  executors\\n\",\n    \"\\n\",\n    \"This executor will use a local run cluster with 3 samples (should take less than 30 seconds)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 13,\n   \"metadata\": {\n    \"scrolled\": false\n   },\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"{   'executor': {'time_budget_s': 1000, 'type': 'ray'},\\n\",\n      \"    'goal': 'minimize',\\n\",\n      \"    'metric': 'loss',\\n\",\n      \"    'output_feature': 'quality',\\n\",\n      \"    'parameters': {   'quality.fc_size': {   'lower': 32,\\n\",\n      \"                                             'q': 5,\\n\",\n      \"                                             'space': 'qrandint',\\n\",\n      \"                                             'type': 'int',\\n\",\n      \"                                             'upper': 256},\\n\",\n      \"                      'quality.num_fc_layers': {   'lower': 1,\\n\",\n      \"                                                   'q': 4,\\n\",\n      \"                                                   'space': 'qrandint',\\n\",\n      \"                                                   'type': 'int',\\n\",\n      \"                                                   'upper': 5},\\n\",\n      \"                      'trainer.batch_size': {   'base': 2,\\n\",\n      \"                                                'lower': 32,\\n\",\n      \"                                                'q': 5,\\n\",\n      \"                                                'space': 'qlograndint',\\n\",\n      \"                                                'type': 'int',\\n\",\n      \"                                                'upper': 256},\\n\",\n      \"                      'trainer.learning_rate': {   'lower': 0.0001,\\n\",\n      \"                                                   'q': 3,\\n\",\n      \"                                                   'space': 'loguniform',\\n\",\n      \"                                                   'type': 'float',\\n\",\n      \"                                                   'upper': 0.01}},\\n\",\n      \"    'sampler': {'num_samples': 3, 'type': 'ray'},\\n\",\n      \"    'split': 'validation',\\n\",\n      \"    'validation_metrics': 'loss'}\\n\",\n      \"\\n\",\n      \"\\n\",\n      \"Initializing new Ray cluster...\\n\",\n      \"CPU times: user 492 ms, sys: 207 ms, total: 699 ms\\n\",\n      \"Wall time: 12 s\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"%%time\\n\",\n    \"%%capture\\n\",\n    \"print(\\\"starting:\\\", datetime.datetime.now())\\n\",\n    \"config['hyperopt']['executor'] = {'type': 'ray', 'time_budget_s': 1000}\\n\",\n    \"config['hyperopt']['sampler'] = {'type': 'ray', 'num_samples': 3}\\n\",\n    \"results_ray = hyperopt(\\n\",\n    \"    config,\\n\",\n    \"    dataset=train_df.sample(4000, random_state=42),  # limit number records for demonstration purposes\\n\",\n    \"    output_directory='results_ray'  # location to place results\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Random Search with serial executor\\n\",\n    \"\\n\",\n    \"Run the serialize executor with 2 samples (should take less then 3 minutes)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 14,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"hyperopt_configs = {\\n\",\n    \"    \\\"parameters\\\": {\\n\",\n    \"        \\\"trainer.learning_rate\\\": {\\n\",\n    \"            \\\"type\\\": \\\"float\\\",\\n\",\n    \"            \\\"low\\\": 0.0001,\\n\",\n    \"            \\\"high\\\": 0.01,\\n\",\n    \"            \\\"space\\\": \\\"log\\\",\\n\",\n    \"            \\\"steps\\\": 3,\\n\",\n    \"        },\\n\",\n    \"        \\\"trainer.batch_size\\\": {\\n\",\n    \"            \\\"type\\\": \\\"int\\\",\\n\",\n    \"            \\\"low\\\": 32,\\n\",\n    \"            \\\"high\\\": 256,\\n\",\n    \"            \\\"space\\\": \\\"log\\\",\\n\",\n    \"            \\\"steps\\\": 5,\\n\",\n    \"            \\\"base\\\" : 2\\n\",\n    \"        },\\n\",\n    \"        \\\"quality.fc_size\\\": {\\n\",\n    \"            \\\"type\\\": \\\"int\\\",\\n\",\n    \"            \\\"low\\\": 32,\\n\",\n    \"            \\\"high\\\": 256,\\n\",\n    \"            \\\"steps\\\": 5\\n\",\n    \"        },\\n\",\n    \"        \\\"quality.num_fc_layers\\\": {\\n\",\n    \"            'type': 'int',\\n\",\n    \"            'low': 1,\\n\",\n    \"            'high': 5,\\n\",\n    \"            'space': 'linear',\\n\",\n    \"            'steps': 4\\n\",\n    \"        }\\n\",\n    \"    },\\n\",\n    \"    \\\"goal\\\": \\\"minimize\\\",\\n\",\n    \"    'output_feature': \\\"quality\\\",\\n\",\n    \"    'validation_metrics': 'loss'\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"# add hyperopt parameter space to the config\\n\",\n    \"config['hyperopt'] = hyperopt_configs\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 15,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"starting: 2022-04-09 15:09:04.768901\\n\",\n      \"Note: steps_per_checkpoint (was 40) is now set to the number of steps per epoch: 40.\\n\",\n      \"\\n\",\n      \"Note: steps_per_checkpoint (was 52) is now set to the number of steps per epoch: 52.\\n\",\n      \"\\n\",\n      \"CPU times: user 17min 20s, sys: 48 s, total: 18min 8s\\n\",\n      \"Wall time: 2min 42s\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"%%time\\n\",\n    \"print(\\\"starting:\\\", datetime.datetime.now())\\n\",\n    \"config['hyperopt']['executor'] = {'type': 'serial'}\\n\",\n    \"config['hyperopt']['sampler'] = {'type': 'random', 'num_samples': 2}\\n\",\n    \"results_random_serial = hyperopt(\\n\",\n    \"    config,\\n\",\n    \"    dataset= train_df.sample(4000, random_state=42),  # limit number records for demonstration purposes\\n\",\n    \"    output_directory='hyperopt_results',\\n\",\n    \"    experiment_name='random_serial',\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Note:\\n\",\n    \"`results_ray`, `results_random_serial` are `HyperoptResults` object with the ordered_trials, so will convert to dictionary to visualize.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 20,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"def hyperopt_results_dict(results):\\n\",\n    \"    return [{\\\"metric_score\\\": t.metric_score, \\\"parameters\\\": t.parameters} for t in results.ordered_trials]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Convert hyperparameter optimization results to dataframe\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Results For Ray executor\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 21,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>loss</th>\\n\",\n       \"      <th>trainer.learning_rate</th>\\n\",\n       \"      <th>trainer.batch_size</th>\\n\",\n       \"      <th>quality.fc_size</th>\\n\",\n       \"      <th>quality.num_fc_layers</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>1.291179</td>\\n\",\n       \"      <td>0.001393</td>\\n\",\n       \"      <td>170</td>\\n\",\n       \"      <td>60</td>\\n\",\n       \"      <td>4</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>1.300332</td>\\n\",\n       \"      <td>0.000203</td>\\n\",\n       \"      <td>45</td>\\n\",\n       \"      <td>100</td>\\n\",\n       \"      <td>4</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>2</th>\\n\",\n       \"      <td>1.956202</td>\\n\",\n       \"      <td>0.000527</td>\\n\",\n       \"      <td>75</td>\\n\",\n       \"      <td>45</td>\\n\",\n       \"      <td>0</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\"\n      ],\n      \"text/plain\": [\n       \"       loss  trainer.learning_rate  trainer.batch_size  quality.fc_size  \\\\\\n\",\n       \"0  1.291179               0.001393                 170               60   \\n\",\n       \"1  1.300332               0.000203                  45              100   \\n\",\n       \"2  1.956202               0.000527                  75               45   \\n\",\n       \"\\n\",\n       \"   quality.num_fc_layers  \\n\",\n       \"0                      4  \\n\",\n       \"1                      4  \\n\",\n       \"2                      0  \"\n      ]\n     },\n     \"execution_count\": 21,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"df1 = hyperopt_results_to_dataframe(\\n\",\n    \"    hyperopt_results_dict(results_ray),\\n\",\n    \"    hyperopt_configs['parameters'],\\n\",\n    \"    hyperopt_configs['validation_metrics']\\n\",\n    \")\\n\",\n    \"df1\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Results for Random Search with serial executor\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 22,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>loss</th>\\n\",\n       \"      <th>quality.fc_size</th>\\n\",\n       \"      <th>quality.num_fc_layers</th>\\n\",\n       \"      <th>trainer.batch_size</th>\\n\",\n       \"      <th>trainer.learning_rate</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>1.300466</td>\\n\",\n       \"      <td>75</td>\\n\",\n       \"      <td>4</td>\\n\",\n       \"      <td>55</td>\\n\",\n       \"      <td>0.005918</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>1.362126</td>\\n\",\n       \"      <td>45</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"      <td>72</td>\\n\",\n       \"      <td>0.002479</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\"\n      ],\n      \"text/plain\": [\n       \"       loss  quality.fc_size  quality.num_fc_layers  trainer.batch_size  \\\\\\n\",\n       \"0  1.300466               75                      4                  55   \\n\",\n       \"1  1.362126               45                      1                  72   \\n\",\n       \"\\n\",\n       \"   trainer.learning_rate  \\n\",\n       \"0               0.005918  \\n\",\n       \"1               0.002479  \"\n      ]\n     },\n     \"execution_count\": 22,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"df2 = hyperopt_results_to_dataframe(\\n\",\n    \"    hyperopt_results_dict(results_random_serial),\\n\",\n    \"    hyperopt_configs['parameters'],\\n\",\n    \"    hyperopt_configs['validation_metrics']\\n\",\n    \")\\n\",\n    \"df2\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Example Hyperopt Visualizations\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Report results of the a hyperparameter optimization run\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 31,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"%matplotlib inline\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 34,\n   \"metadata\": {\n    \"scrolled\": false\n   },\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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//79Gj16tM6dO6ezZ89q2LBhuuSSSzRw4EAtXLhQkvTA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668rJiZGY8aM0bZt21ShQgX99ttveuONN1SpUqU8//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/FarzAeeQtgLzM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lZaWppCQkHzbXbx4UTVq1FDjxo3zbe/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\",\n      \"text/plain\": [\n       \"<Figure size 1440x1440 with 24 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"hyperopt_report_cli(\\n\",\n    \"    'hyperopt_results/random_serial/hyperopt_statistics.json',\\n\",\n    \"    output_directory='./visualizations'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"#### Generate parallel coordinates plot on hyperparameter optimization\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 33,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"hyperopt_hiplot_cli(\\n\",\n    \"    'hyperopt_results/random_serial/hyperopt_statistics.json',\\n\",\n    \"    output_directory='./visualizations'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"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.\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 29,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"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gAABAgSmKyDIOl2elBariaquW9NtNYKra9f8nSe1q/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/d9OxbW2GTdu3JTig78ECBAgQIBA8wqUC7JG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Ddm3dB7LBAgQIECAAAECBAgQIECAAAECBFqyQEODrBE2jCBmjKuuumqawOHBBx+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//z95jzzrrrNYetMUWW1hblZ9f+5dffrm1kdQfb/ZZdsUllliilkXOvrfffru55557bF36rDTNNNOYhRZayGy55ZamT58+tfyNdkJtDak2Nb/9mDkm+5JsgLIvffDBB0bRYSQ72TT0jGfllVc2v/71rzNfuhabZ555xpZXPSov25acWFVWvCSrqaee2u9mh/3UeeIqS7ELqY5Ye3aonF0//W3MdeXKp5R1dXT21uliHFk7mzT1QwACEIAABCAAAQhAAAJlIhASlXWisc5MY+9zfyhsXMM/+tGZZNJ8b6wW1oGcFbkbyhRHy48++sgceOCB1ulHzc8222zmlFNOydmT7pm9CL5dQQZH1q6gXI42Uufs0UcfbZ577jk72Isuuig6QqSjJUV+1FFHWWc/d8zfyiFSjqhy/GuUZFQ97bTTzJgxYzKzHHLIIWbRRRfNPOcOymHv4IMPth/lqLf99tu7Uy23sWWL6Lc6F6qjFWXirLPOMi+88ELDMf3qV78ygwcP7uCgLGfME088sWGZrBNy1rz44ovtKRl9f//732dlyzy2xx57mJVWWinznA7G8JbzlBz1NZasJKdpzcNGKa+s9OKA9G6rtOGGG5ptttmmQzY5dGvOtkqLL764kTN5VhSMlOvqxhtvNH/729+MIpM0Sv369TOHHXaY0fUZklL1TozM1a+8cnNjue2228wll1xiH+64Y/5WDt+ap1ns/XzsQ6AKBPJc3ym6sQosGSMEIACBIgjk0ctF/U5P/a1dxLipAwIQqA6BPHrOUUm5R3Z1NNum3EOm9C2lrBtPjA6PtS+5Nu+++25z6aWX1l5Cd8fdVi9c655bdg8/7bfffubNN9/0D7Xc/+1vf2vWXXddm0/9lu3nvffea1hOUUp33nlns+qqqzbMk8fWkGpTc52ImWMKXKEAFq2SeOvl/hlmmKFDVt8G3OGE90F2Ib0Y/otf/MI7Om43dZ74FcbahVwd/lhC7dl55OzacduY66qIsq6Ortg6XYwja1fQpg0IQAACEIAABCAAAQhAoEwERo4caXRf2CjZiKx6w9Q5ZGg/xbG1ao6s//vf/8yhhx5aW3peoHFk/Wm6uRv2FEfhn2rrvD0cWTuPbdlqjpmzegtcUSFvvfVWc+edd9aGHGr4qxWo29FS6fvuu68ZPny4PSNj8cILL2ydY+Usq6gSSlre65hjjjGK0lqf1Ke//vWvNrKmzqkORXCYaaaZbCTJV155xRxwwAENHVn1BfLss89apzUX5jvUkTWlbGq/HYdQHa3vPRmvnXOlomsutdRSZvrpp7eRWV1UZtW7+eabm0033dQ1YZ0/u9KRVRGjFTm6PsXy1g+FE044ocPDjqmmmspGoZ144olt9NNpp522oSNrjKzkFC0ju5KM+orkqYieWr5NrF3ED52vH6+vr3V+9tlnt3Na18t///tfI5m7tNlmmxn991PKdaUHLCeddJL9naRItZKDotVqvsjBVk6uLrKsnJ733HNPv+mG+zF6R5XFylxlY+Smcors/Oc//1m7NrkoI4pS+9prr9V+Q66yyiq1qMMuL1sIVJFAnus7RTdWkS1jhgAEIBBDII9ezuvIWv+7Vf1L/a0dM0bKQAAC1SaQR8+JVMo9cgjplHvIlL6llHXjitHhKfYltauXq4888kjXBWvrky1Q49EqOs7mIRvKmWeeaVe1cZn/+Mc/GkU4zZOcI6vq1wu5WqFHSfYwvVys5xxyCFaUVmfHkQ1Edsg555zT5vX/5LU1FOHIGjvHZFuVA6dLsqtqJTa9lKsVsRRh1SVxkD3If2HXX8XN2bbk9CobseOo8jqmoCfaupQ6T1w9KXahFHt2Xjm7/mobc1258illXR1dtXW6GEfWriJOOxCAAAQgAAEIQAACEIBAmQjI1qDVkbPSRGONED9MN910Rv9loJAhQjfS7qZcjq1yKJHDhm5u3Tnn/Oq2yq9z748YNTbv2KWBJxkXkVXHVNZttS+n2X0PfH/sse/Gnhu39OwPZmw+M9aJ1nxvxp4Ye7zHuP7asmPzjf33ww9jz41NtszYcicfO5Nd5kX1ufZdO+MKq6rs9pVf5cZ8M67OySfuYevyx6g6mo1fhgIZjOQ4o3EpqT0ta3z42Ghz7nPW+F37qt/l036e9tX/ev5uvLbSsX/cZ7d1/Luqfd2wC434qu2ubt+N220btS8HJBmUlG+ZZZYxO+20k0UovnJQk6FO52SgkkHLcXfb2Pmn8kqqW/vdTf6Ou9s24j8hjV9ztkePn1kdJpm00n/XXXedXYreyU5jdOnkk0+2znmx45cDqSKEqk7pZkVedfNPznIyoMpArfOrrrqqXd7Lb//hhx+2TqyufS2HNWDAAOs46PSHvhzkrKj63Rg0n+VQqGiWY6N22/pVh5Lq79+/v3UObDT/XVnpSOV37au8+jlw4ECrfxvNfy0Zr8gTSiqrfstxVH1U31ROTrxy4FX9ja4/GeEb6Wi9gOC3r2XELhkbYVJJhmU5IM4555w1+SsaxtVXX23bU7t6qKClwRxHteWPU/W4z2779ttvW4dR9VkPHDQ/lBQxVs5LyicHY/VNSflUv7a+bNz3tOMvGelhheNtC4/9o/okKzneqg5Xn3i5fmur6KYyBCu/lorbYYcdzAILLGD5uPk/evRo22fH35V/8MEHzWWXXWabVHnJSu1pTumz2tR8mGSSSaz8VE51KJKFIgSvv/76NiqFP//UlvSxHAdUh5a4UxRc7ausHgaIndrSEnhypNQ5/ReLCy+80Dz//PP2sxy3FWlW9bvx69zZZ59tP6ufYi/uYqzx/ulPf6o59coZU/PV8ZfDr5aLU1sbb7yxWXvttTvMP9c31x+NU07AjfgLnPLqt8j33/8w9rvyZ7Y+HZd8HX/XvuP/6aefWgd0X+aqR+2svvrqduk699lt3fhVR73cNE7/GlP7Yqm8Yui37yJEqz458m633XZ2DGpn6NCh5txzz1Vx2xd9f/bq1avl+DVWvw2VbzZ+N//cfFLbbpwqq+Q+u60/flfe5aN9+Hfm/Pvi63F6fNKfj/u+ajb/9L2u72U9MFZ0Jf/6k2489dRT7cNUzefFFlvMRvNm/nP9Oz1nlR/6D/3/4+8BNy/4/hv3e1q/aZz+/fyrb8fOE2N6Tdbx/kfXUP3vD73I51YC0O9F7Tf7/aHyasvx1/eLfmu639r6XfjbsZHvtNqB//tDv5d1D1LfvupRfTru+t+sfdeu2yL/8eUvOYuPz1+MleDf+P6D+Tfh21/dda9tnt+fmv+6jzvnnHPstaF7d9meFHlS56THjj/++Np98GqrrWa22mqrmp6zF8/YP3770j2+/pEt1UX41Ooy2267be3+T46E7kVJldE9pOwlqk/t67xvF5O9xNkANC+V392jq29a9t61//TTT9v7U9WlcUmHa1y61mXDcWU1BpXVEvAq6+4NdFztyWaj47IByV4iHe7GqzyyT6nPyqPjjzzySM0WJ92uyKlzzDGHstrz9913n13lRZ9lX5IjpWwxuv/XmMRLDpSqT/fqsrHIpqFzsgWcccYZNqCGzq+55pr2Xt71R+NS0jnlF0NtNWYl5dO+mOolYeXbeuut7fgVXVO2EuXRy80au7auvOrWeZVTHr28O2jQoFq9qsvZIV1b6r/skBqnOyZbg8ajfqh/qktt6L/qcPx1zn1WXjmWOjuab1NTvbLRaY6pLtmKNMdUn+p46qmnzHnnnWfbV33OTqG8eh4gu6HmpexnmluufbV51113mWuvvdb2Q/nFRA6t2lddsl1JJrJtyXbl+qxzsumqXXHT5/XWW89ssMEGdtxqQ6ycPU12E70Moxe1Xfv333+/tUOqLWeH1DzRZ/3XfaPstrIPu/64rexCeqlb/XHyc/w1LrXxj3/8o2bPdufUT9UhzuqT8rryjqfO+3JWWfFz81R16L+uCzn3al/1uDZUXr+N9BtL5zSmHXfc0cwzzzz2sxu/xufPP7WvpOvYXZMq+9sff1epLjd+ta2VsJqNX3l1Xkn9a2R/yxq/a8cWHvvHfXZb9UXlvvxWc/t74xxZdd7x135nt9/u8dN+4+sP+TP/uf47V/+if9A/7vtb39V8//L7g99fPP9z9xh8/06437/yGdG9s+wFumb1//8DcdvATAxHcWsAAAAASUVORK5CYII=\",\n      \"text/plain\": [\n       \"<IPython.core.display.Image object>\"\n      ]\n     },\n     \"execution_count\": 29,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"from IPython.display import Image\\n\",\n    \"Image(filename='./images/parallel_coordinates_plot.png')\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.8.12\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/insurance_lite/config.yaml",
    "content": "input_features:\n  - name: image_path\n    type: image\n    preprocessing:\n      height: 224\n      width: 224\n      in_memory: true\n      num_channels: 3\n    encoder: vit\n    img_height: 224\n    in_channels: 3\n    use_pretrained: true\n    hidden_size: 768\n    num_hidden_layers: 12\n    num_attention_heads: 12\n    intermediate_size: 3072\n  - name: insurance_company\n    type: category\n    preprocessing:\n      missing_value_strategy: fill_with_const\n      fill_value: UNKNOWN\n  - name: cost_of_vehicle\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: expiry_date\n    type: date\n    preprocessing:\n      missing_value_strategy: fill_with_const\n      fill_value: \"\"\n      datetime_format: \"%Y-%m-%d\"\n  - name: min_coverage\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: max_coverage\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: condition\n    type: category\n    preprocessing:\n      missing_value_strategy: fill_with_const\n      fill_value: UNKNOWN\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  output_size: 256\noutput_features:\n  - name: amount\n    type: number\n    preprocessing:\n      normalization: zscore\ntrainer:\n  epochs: 10\n  early_stop: 0\n  batch_size: 8\npreprocessing:\n  split:\n    type: random\n    probabilities: [0.7, 0.1, 0.2]\n"
  },
  {
    "path": "examples/insurance_lite/train.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example on multi-modal data.\n\n# Import required libraries\nimport logging\nimport os\nimport shutil\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import insurance_lite\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\n# Download and prepare the dataset\ndataset = insurance_lite.load()\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=\"./config.yaml\", logging_level=logging.INFO, backend=\"local\")\n\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(dataset=dataset, experiment_name=\"simple_experiment\", model_name=\"simple_model\")\n\n# list contents of output directory\nprint(\"contents of output directory:\", output_directory)\nfor item in os.listdir(output_directory):\n    print(\"\\t\", item)\n"
  },
  {
    "path": "examples/kfold_cv/README.md",
    "content": "# K-Ffold Cross Validation Example\n\nThis directory contains two examples of performing a k-fold cross validation analysis with Ludwig.\n\n## Classification Example\n\nThis example illustrates running the k-fold cv with the `ludwig experiment` cli.\n\nTo run this example execute this bash script:\n\n```\n./k-fold_cv_classification.sh\n```\n\nThis bash script performs these steps:\n\n- Download and prepare data for training and create a Ludwig config file\n- Execute `ludwig experiment` to run the 5-fold cross validation\n- Display results from the 5-fold cross validation analysis\n\nSample output:\n\n```\nCleaning out old results\nDownloading data set\nPreparing data for training\nSaving training and test data sets\nPreparing Ludwig config\nCompleted data preparation\nTraining: 100%|████████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 23.14it/s]\nEvaluation train: 100%|████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 98.62it/s]\nEvaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 321.03it/s]\nTraining: 100%|███████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 190.18it/s]\nEvaluation train: 100%|███████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 331.68it/s]\nEvaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 298.08it/s]\n<<<< DELETED LINES >>>>>\nTraining: 100%|███████████████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 248.00it/s]\nEvaluation train: 100%|███████████████████████████████████████████████████████████████████████| 12/12 [00:00<00:00, 400.31it/s]\nEvaluation test : 100%|█████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 340.35it/s]\nEvaluation: 100%|████████████████████████████████████████████████████████████████████████████████| 3/3 [00:00<00:00, 27.87it/s]\nretrieving results from  results\n#\n# K-fold Cross Validation Results\n#\n{'combined': {'accuracy_mean': 0.9736263736263737,\n              'accuracy_std': 0.011206636293610508,\n              'loss_mean': 0.06359774886251807,\n              'loss_std': 0.011785678840394689},\n 'diagnosis': {'accuracy_mean': 0.9736263736263737,\n               'accuracy_std': 0.011206636293610508,\n               'average_precision_macro_mean': 0.995842104045726,\n               'average_precision_macro_std': 0.002339014329647542,\n               'average_precision_micro_mean': 0.995842104045726,\n               'average_precision_micro_std': 0.002339014329647542,\n               'average_precision_samples_mean': 0.995842104045726,\n               'average_precision_samples_std': 0.002339014329647542,\n               'loss_mean': 0.06359774886251807,\n               'loss_std': 0.011785678840394689,\n               'roc_auc_macro_mean': 0.9973999160508542,\n               'roc_auc_macro_std': 0.0011259319854886507,\n               'roc_auc_micro_mean': 0.9973999160508542,\n               'roc_auc_micro_std': 0.0011259319854886507}}\n```\n\n## Regression Example\n\nThis 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:\n\n- Download and prepare data for training and create a Ludwig config data structure from a pandas dataframe structure\n- Use `ludwig.api.kfold_cross_validate()` function to run the 5-fold cross validation\n- Display results from the 5-fold cross validation analysis\n\nExpected output from running the example:\n![](../images/regression_kfold_cv_example_results.png)\n"
  },
  {
    "path": "examples/kfold_cv/display_kfold_cv_results.py",
    "content": "#!/usr/bin/env python\n\n\nimport argparse\nimport os.path\nimport pprint\nimport sys\n\nfrom ludwig.utils.data_utils import load_json\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Display K-fold cross validation results\",\n        prog=\"display_kfold_cv_results\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------------------\n    # Experiment naming parameters\n    # ----------------------------\n    parser.add_argument(\n        \"--results_directory\", type=str, default=\"results\", help=\"directory that contains the K-fold cv results\"\n    )\n\n    args = parser.parse_args(sys.argv[1:])\n    results_directory = args.results_directory\n\n    print(\"Retrieving results from \", results_directory)\n\n    kfold_cv_stats = load_json(os.path.join(results_directory, \"kfold_training_statistics.json\"))\n\n    print(\"#\\n# K-fold Cross Validation Results\\n#\")\n    pprint.pprint(kfold_cv_stats[\"overall\"])\n"
  },
  {
    "path": "examples/kfold_cv/k-fold_cv_classification.sh",
    "content": "#!/bin/bash\n\n#\n# Download and prepare training data\n#\npython prepare_classification_data_set.py\n\n#\n# Run 5-fold cross validation\n#\nludwig experiment \\\n  --config config.yaml \\\n  --dataset data/train.csv \\\n  --output_directory results \\\n  --logging_level 'error' \\\n  -kf 5\n\n#\n# Display results from K-fold cv\n#\npython display_kfold_cv_results.py --results_directory results\n"
  },
  {
    "path": "examples/kfold_cv/prepare_classification_data_set.py",
    "content": "#!/usr/bin/env python\n\n\n# Download and prepare training data set\n# Create Ludwig config file\n#\n# Based on the\n# [UCI Wisconsin Breast Cancer data set](https://archive.ics.uci.edu/ml/datasets/breast+cancer+wisconsin+(original))\n#\nimport os.path\nimport shutil\n\nimport pandas as pd\nimport requests\nimport yaml\nfrom sklearn.model_selection import train_test_split\n\nfrom ludwig.constants import TRAINER\n\n# Constants\nDATA_SET_URL = \"https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data\"\nDATA_SET = \"wdbc.data\"\nDATA_DIR = \"./data\"\nRESULTS_DIR = \"results\"\n\n# Clean out previous results\nprint(\"Cleaning out old results\")\nif os.path.isfile(DATA_SET):\n    os.remove(DATA_SET)\nif os.path.isfile(\"config.yaml\"):\n    os.remove(\"config.yaml\")\n\nshutil.rmtree(RESULTS_DIR, ignore_errors=True)\nshutil.rmtree(DATA_DIR, ignore_errors=True)\n\n# Retrieve data from UCI Machine Learning Repository\n# Download required data\nprint(\"Downloading data set\")\nr = requests.get(DATA_SET_URL)\nif r.status_code == 200:\n    with open(DATA_SET, \"w\") as f:\n        f.write(r.content.decode(\"utf-8\"))\n\n# create pandas dataframe from downloaded data\nprint(\"Preparing data for training\")\nraw_df = pd.read_csv(DATA_SET, header=None, sep=\",\", skipinitialspace=True)\nraw_df.columns = [\"ID\", \"diagnosis\"] + [\"X\" + str(i) for i in range(1, 31)]\n\n# convert diagnosis attribute to binary format\nraw_df[\"diagnosis\"] = raw_df[\"diagnosis\"].map({\"M\": 1, \"B\": 0})\n\n# Create train/test split\nprint(\"Saving training and test data sets\")\ntrain_df, test_df = train_test_split(raw_df, train_size=0.8, random_state=17)\nos.mkdir(DATA_DIR)\ntrain_df.to_csv(os.path.join(DATA_DIR, \"train.csv\"), index=False)\ntest_df.to_csv(os.path.join(DATA_DIR, \"test.csv\"), index=False)\n\nprint(\"Preparing Ludwig config\")\n# Create ludwig input_features\nnum_features = [\"X\" + str(i) for i in range(1, 31)]\ninput_features = []\n\n# setup input features for number variables\nfor 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# Create ludwig output features\noutput_features = [{\"name\": \"diagnosis\", \"type\": \"binary\", \"num_fc_layers\": 2, \"output_size\": 64}]\n\n# setup ludwig config\nconfig = {\n    \"input_features\": input_features,\n    \"output_features\": output_features,\n    TRAINER: {\"epochs\": 20, \"batch_size\": 32},\n}\n\nwith open(\"config.yaml\", \"w\") as f:\n    yaml.dump(config, f)\n\nprint(\"Completed data preparation\")\n"
  },
  {
    "path": "examples/kfold_cv/regression_example.ipynb",
    "content": "{\n    \"cells\": [\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"# K-fold cross validation - Regression Model\\n\",\n                \"Based on the [Ludwig regression example](https://ludwig-ai.github.io/ludwig-docs/examples/#simple-regression-fuel-efficiency-prediction)  \\n\",\n                \"\\n\",\n                \"[Data set](https://archive.ics.uci.edu/ml/datasets/auto+mpg)\\n\",\n                \"\\n\",\n                \"This example demonstrates teh following:\\n\",\n                \"\\n\",\n                \"- Download a data set and create a pandas dataframe\\n\",\n                \"- Create a training and hold-out test data sets\\n\",\n                \"- Create a Ludwig config data structure from the pandas dataframe\\n\",\n                \"- Run a 5-fold cross validation analysis with the training data\\n\",\n                \"- Use Ludwig APIs to train and assess model performance on hold-out test data set\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 1,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"import logging\\n\",\n                \"import os\\n\",\n                \"import os.path\\n\",\n                \"import shutil\\n\",\n                \"import tempfile\\n\",\n                \"\\n\",\n                \"import matplotlib.pyplot as plt\\n\",\n                \"import numpy as np\\n\",\n                \"import pandas as pd\\n\",\n                \"import requests\\n\",\n                \"import scipy.stats as stats\\n\",\n                \"import seaborn as sns\\n\",\n                \"from sklearn.model_selection import train_test_split\\n\",\n                \"\\n\",\n                \"from ludwig.api import kfold_cross_validate, LudwigModel\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Contstants\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 2,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"DATA_SET_URL = 'http://archive.ics.uci.edu/ml/machine-learning-databases/auto-mpg/auto-mpg.data'\\n\",\n                \"DATA_SET = 'auto_mpg.data'\\n\",\n                \"RESULTS_DIR = 'results'\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Clean out previous results\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 3,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"if os.path.isfile(DATA_SET):\\n\",\n                \"    os.remove(DATA_SET)\\n\",\n                \"    \\n\",\n                \"shutil.rmtree(RESULTS_DIR, ignore_errors=True)\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Retrieve data from UCI Machine Learning Repository\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Download required data\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 4,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"r = requests.get(DATA_SET_URL)\\n\",\n                \"if r.status_code == 200:\\n\",\n                \"    with open(DATA_SET,'w') as f:\\n\",\n                \"        f.write(r.content.decode(\\\"utf-8\\\"))\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Create Pandas DataFrame from downloaded data\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 5,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"(398, 8)\"\n                        ]\n                    },\n                    \"execution_count\": 5,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"raw_df = pd.read_csv(DATA_SET,\\n\",\n                \"                     header=None,\\n\",\n                \"                      na_values = \\\"?\\\", comment='\\\\t',\\n\",\n                \"                      sep=\\\" \\\", skipinitialspace=True)\\n\",\n                \"\\n\",\n                \"\\n\",\n                \"raw_df.columns = ['MPG','Cylinders','Displacement','Horsepower','Weight',\\n\",\n                \"                'Acceleration', 'ModelYear', 'Origin']\\n\",\n                \"raw_df.shape\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 6,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/html\": [\n                            \"<div>\\n\",\n                            \"<style scoped>\\n\",\n                            \"    .dataframe tbody tr th:only-of-type {\\n\",\n                            \"        vertical-align: middle;\\n\",\n                            \"    }\\n\",\n                            \"\\n\",\n                            \"    .dataframe tbody tr th {\\n\",\n                            \"        vertical-align: top;\\n\",\n                            \"    }\\n\",\n                            \"\\n\",\n                            \"    .dataframe thead th {\\n\",\n                            \"        text-align: right;\\n\",\n                            \"    }\\n\",\n                            \"</style>\\n\",\n                            \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n                            \"  <thead>\\n\",\n                            \"    <tr style=\\\"text-align: right;\\\">\\n\",\n                            \"      <th></th>\\n\",\n                            \"      <th>MPG</th>\\n\",\n                            \"      <th>Cylinders</th>\\n\",\n                            \"      <th>Displacement</th>\\n\",\n                            \"      <th>Horsepower</th>\\n\",\n                            \"      <th>Weight</th>\\n\",\n                            \"      <th>Acceleration</th>\\n\",\n                            \"      <th>ModelYear</th>\\n\",\n                            \"      <th>Origin</th>\\n\",\n                            \"    </tr>\\n\",\n                            \"  </thead>\\n\",\n                            \"  <tbody>\\n\",\n                            \"    <tr>\\n\",\n                            \"      <th>0</th>\\n\",\n                            \"      <td>18.0</td>\\n\",\n                            \"      <td>8</td>\\n\",\n                            \"      <td>307.0</td>\\n\",\n                            \"      <td>130.0</td>\\n\",\n                            \"      <td>3504.0</td>\\n\",\n                            \"      <td>12.0</td>\\n\",\n                            \"      <td>70</td>\\n\",\n                            \"      <td>1</td>\\n\",\n                            \"    </tr>\\n\",\n                            \"    <tr>\\n\",\n                            \"      <th>1</th>\\n\",\n                            \"      <td>15.0</td>\\n\",\n                            \"      <td>8</td>\\n\",\n                            \"      <td>350.0</td>\\n\",\n                            \"      <td>165.0</td>\\n\",\n                            \"      <td>3693.0</td>\\n\",\n                            \"      <td>11.5</td>\\n\",\n                            \"      <td>70</td>\\n\",\n                            \"      <td>1</td>\\n\",\n                            \"    </tr>\\n\",\n                            \"    <tr>\\n\",\n                            \"      <th>2</th>\\n\",\n                            \"      <td>18.0</td>\\n\",\n                            \"      <td>8</td>\\n\",\n                            \"      <td>318.0</td>\\n\",\n                            \"      <td>150.0</td>\\n\",\n                            \"      <td>3436.0</td>\\n\",\n                            \"      <td>11.0</td>\\n\",\n                            \"      <td>70</td>\\n\",\n                            \"      <td>1</td>\\n\",\n                            \"    </tr>\\n\",\n                            \"    <tr>\\n\",\n                            \"      <th>3</th>\\n\",\n                            \"      <td>16.0</td>\\n\",\n                            \"      <td>8</td>\\n\",\n                            \"      <td>304.0</td>\\n\",\n                            \"      <td>150.0</td>\\n\",\n                            \"      <td>3433.0</td>\\n\",\n                            \"      <td>12.0</td>\\n\",\n                            \"      <td>70</td>\\n\",\n                            \"      <td>1</td>\\n\",\n                            \"    </tr>\\n\",\n                            \"    <tr>\\n\",\n                            \"      <th>4</th>\\n\",\n                            \"      <td>17.0</td>\\n\",\n                            \"      <td>8</td>\\n\",\n                            \"      <td>302.0</td>\\n\",\n                            \"      <td>140.0</td>\\n\",\n                            \"      <td>3449.0</td>\\n\",\n                            \"      <td>10.5</td>\\n\",\n                            \"      <td>70</td>\\n\",\n                            \"      <td>1</td>\\n\",\n                            \"    </tr>\\n\",\n                            \"  </tbody>\\n\",\n                            \"</table>\\n\",\n                            \"</div>\"\n                        ],\n                        \"text/plain\": [\n                            \"    MPG  Cylinders  Displacement  Horsepower  Weight  Acceleration  ModelYear  \\\\\\n\",\n                            \"0  18.0          8         307.0       130.0  3504.0          12.0         70   \\n\",\n                            \"1  15.0          8         350.0       165.0  3693.0          11.5         70   \\n\",\n                            \"2  18.0          8         318.0       150.0  3436.0          11.0         70   \\n\",\n                            \"3  16.0          8         304.0       150.0  3433.0          12.0         70   \\n\",\n                            \"4  17.0          8         302.0       140.0  3449.0          10.5         70   \\n\",\n                            \"\\n\",\n                            \"   Origin  \\n\",\n                            \"0       1  \\n\",\n                            \"1       1  \\n\",\n                            \"2       1  \\n\",\n                            \"3       1  \\n\",\n                            \"4       1  \"\n                        ]\n                    },\n                    \"execution_count\": 6,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"raw_df.head()\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Create train/test split\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 7,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"name\": \"stdout\",\n                    \"output_type\": \"stream\",\n                    \"text\": [\n                        \"(318, 8)\\n\",\n                        \"(80, 8)\\n\"\n                    ]\n                }\n            ],\n            \"source\": [\n                \"train_df, test_df = train_test_split(raw_df, train_size=0.8, random_state=17)\\n\",\n                \"print(train_df.shape)\\n\",\n                \"print(test_df.shape)\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Setup Ludwig config\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 8,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"num_features = ['Cylinders', 'Displacement', 'Horsepower', 'Weight', 'Acceleration', 'ModelYear']\\n\",\n                \"cat_features = ['Origin']\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Create Ludwig input_features\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 9,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"input_features = []\\n\",\n                \"# setup input features for number variables\\n\",\n                \"for p in num_features:\\n\",\n                \"    a_feature = {'name': p, 'type': 'number', \\n\",\n                \"                'preprocessing': {'missing_value_strategy': 'fill_with_mean', 'normalization': 'zscore'}}\\n\",\n                \"    input_features.append(a_feature)\\n\",\n                \"\\n\",\n                \"# setkup input features for categorical variables\\n\",\n                \"for p in cat_features:\\n\",\n                \"    a_feature = {'name': p, 'type': 'category'}\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Create Ludwig output features\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 10,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"output_features =[\\n\",\n                \"    {\\n\",\n                \"        'name': 'MPG',\\n\",\n                \"        'type': 'number',\\n\",\n                \"        'num_fc_layers': 2,\\n\",\n                \"        'fc_size': 64\\n\",\n                \"    }\\n\",\n                \"]\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 11,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"{'input_features': [{'name': 'Cylinders',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}},\\n\",\n                            \"  {'name': 'Displacement',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}},\\n\",\n                            \"  {'name': 'Horsepower',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}},\\n\",\n                            \"  {'name': 'Weight',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}},\\n\",\n                            \"  {'name': 'Acceleration',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}},\\n\",\n                            \"  {'name': 'ModelYear',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'preprocessing': {'missing_value_strategy': 'fill_with_mean',\\n\",\n                            \"    'normalization': 'zscore'}}],\\n\",\n                            \" 'output_features': [{'name': 'MPG',\\n\",\n                            \"   'type': 'number',\\n\",\n                            \"   'num_fc_layers': 2,\\n\",\n                            \"   'fc_size': 64}],\\n\",\n                            \" 'training': {'epochs': 100, 'batch_size': 32}}\"\n                        ]\n                    },\n                    \"execution_count\": 11,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"config = {\\n\",\n                \"    'input_features' : input_features,\\n\",\n                \"    'output_features': output_features,\\n\",\n                \"    'trainer': {\\n\",\n                \"        'epochs': 100,\\n\",\n                \"        'batch_size': 32\\n\",\n                \"    }\\n\",\n                \"}\\n\",\n                \"config\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Perform K-fold Cross Validation analysis\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 12,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"name\": \"stdout\",\n                    \"output_type\": \"stream\",\n                    \"text\": [\n                        \"starting 5-fold cross validation\\n\",\n                        \"training on fold 1\\n\",\n                        \"CPU times: user 40.7 s, sys: 5.38 s, total: 46 s\\n\",\n                        \"Wall time: 40.6 s\\n\"\n                    ]\n                }\n            ],\n            \"source\": [\n                \"%%time\\n\",\n                \"with tempfile.TemporaryDirectory() as tmpdir:\\n\",\n                \"    data_csv_fp = os.path.join(tmpdir,'train.csv')\\n\",\n                \"    train_df.to_csv(data_csv_fp, index=False)\\n\",\n                \"\\n\",\n                \"    (\\n\",\n                \"        kfold_cv_stats, \\n\",\n                \"        kfold_split_indices \\n\",\n                \"    ) = kfold_cross_validate(\\n\",\n                \"        num_folds=5,\\n\",\n                \"        config=config,\\n\",\n                \"        dataset=data_csv_fp,\\n\",\n                \"        output_directory=tmpdir,\\n\",\n                \"        logging_level=logging.ERROR\\n\",\n                \"    )\\n\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 13,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"{'loss_mean': 8.111681,\\n\",\n                            \" 'loss_std': 2.4598064,\\n\",\n                            \" 'error_mean': 0.0380627,\\n\",\n                            \" 'error_std': 0.5965346,\\n\",\n                            \" 'mean_squared_error_mean': 8.111682,\\n\",\n                            \" 'mean_squared_error_std': 2.4598064,\\n\",\n                            \" 'mean_absolute_error_mean': 2.0598435,\\n\",\n                            \" 'mean_absolute_error_std': 0.2779836,\\n\",\n                            \" 'r2_mean': 0.8666786,\\n\",\n                            \" 'r2_std': 0.03552912}\"\n                        ]\n                    },\n                    \"execution_count\": 13,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"kfold_cv_stats['overall']['MPG']\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Train model and assess model performance\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 14,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": [\n                \"model = LudwigModel(\\n\",\n                \"    config=config,\\n\",\n                \"    logging_level=logging.ERROR\\n\",\n                \")\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 15,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"name\": \"stdout\",\n                    \"output_type\": \"stream\",\n                    \"text\": [\n                        \"CPU times: user 8.34 s, sys: 1.78 s, total: 10.1 s\\n\",\n                        \"Wall time: 15 s\\n\"\n                    ]\n                }\n            ],\n            \"source\": [\n                \"%%time\\n\",\n                \"training_stats = model.train(\\n\",\n                \"    training_set=train_df,\\n\",\n                \"    output_directory=RESULTS_DIR,\\n\",\n                \")\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 16,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"name\": \"stderr\",\n                    \"output_type\": \"stream\",\n                    \"text\": [\n                        \"/opt/project/ludwig/data/preprocessing.py:1045: SettingWithCopyWarning: \\n\",\n                        \"A value is trying to be set on a copy of a slice from a DataFrame.\\n\",\n                        \"Try using .loc[row_indexer,col_indexer] = value instead\\n\",\n                        \"\\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\",\n                        \"  computed_fill_value,\\n\"\n                    ]\n                }\n            ],\n            \"source\": [\n                \"test_stats, mpg_hat_df, _ = model.evaluate(dataset=test_df, collect_predictions=True, collect_overall_stats=True)\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 17,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"{'MPG': {'loss': 8.303831,\\n\",\n                            \"  'error': -0.45136052,\\n\",\n                            \"  'mean_squared_error': 8.303831,\\n\",\n                            \"  'mean_absolute_error': 2.2274728,\\n\",\n                            \"  'r2': 0.8558148},\\n\",\n                            \" 'combined': {'loss': 8.303831}}\"\n                        ]\n                    },\n                    \"execution_count\": 17,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"test_stats\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 18,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"name\": \"stderr\",\n                    \"output_type\": \"stream\",\n                    \"text\": [\n                        \"/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\",\n                        \"  FutureWarning\\n\"\n                    ]\n                },\n                {\n                    \"data\": {\n                        \"image/png\": \"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\",\n                        \"text/plain\": [\n                            \"<Figure size 432x288 with 1 Axes>\"\n                        ]\n                    },\n                    \"metadata\": {\n                        \"needs_background\": \"light\"\n                    },\n                    \"output_type\": \"display_data\"\n                }\n            ],\n            \"source\": [\n                \"a = plt.axes(aspect='equal')\\n\",\n                \"sns.scatterplot(test_df['MPG'].values, mpg_hat_df['MPG_predictions'].values,\\n\",\n                \"               s=50)\\n\",\n                \"plt.xlabel('True Values [MPG]')\\n\",\n                \"plt.ylabel('Predictions [MPG]')\\n\",\n                \"lims = [0, 50]\\n\",\n                \"plt.xlim(lims)\\n\",\n                \"plt.ylim(lims)\\n\",\n                \"_ = plt.plot(lims, lims)\\n\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"## Compare K-fold Cross Validation metrics against hold-out test metrics\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### Hold-out Test Metrics\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 19,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"{'loss': 8.303831,\\n\",\n                            \" 'error': -0.45136052,\\n\",\n                            \" 'mean_squared_error': 8.303831,\\n\",\n                            \" 'mean_absolute_error': 2.2274728,\\n\",\n                            \" 'r2': 0.8558148}\"\n                        ]\n                    },\n                    \"execution_count\": 19,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"test_stats['MPG']\"\n            ]\n        },\n        {\n            \"cell_type\": \"markdown\",\n            \"metadata\": {},\n            \"source\": [\n                \"### K-fold Cross Validation Metrics\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": 20,\n            \"metadata\": {},\n            \"outputs\": [\n                {\n                    \"data\": {\n                        \"text/plain\": [\n                            \"{'loss_mean': 8.111681,\\n\",\n                            \" 'loss_std': 2.4598064,\\n\",\n                            \" 'error_mean': 0.0380627,\\n\",\n                            \" 'error_std': 0.5965346,\\n\",\n                            \" 'mean_squared_error_mean': 8.111682,\\n\",\n                            \" 'mean_squared_error_std': 2.4598064,\\n\",\n                            \" 'mean_absolute_error_mean': 2.0598435,\\n\",\n                            \" 'mean_absolute_error_std': 0.2779836,\\n\",\n                            \" 'r2_mean': 0.8666786,\\n\",\n                            \" 'r2_std': 0.03552912}\"\n                        ]\n                    },\n                    \"execution_count\": 20,\n                    \"metadata\": {},\n                    \"output_type\": \"execute_result\"\n                }\n            ],\n            \"source\": [\n                \"kfold_cv_stats['overall']['MPG']\"\n            ]\n        },\n        {\n            \"cell_type\": \"code\",\n            \"execution_count\": null,\n            \"metadata\": {},\n            \"outputs\": [],\n            \"source\": []\n        }\n    ],\n    \"metadata\": {\n        \"kernelspec\": {\n            \"display_name\": \"Python 3\",\n            \"language\": \"python\",\n            \"name\": \"python3\"\n        },\n        \"language_info\": {\n            \"codemirror_mode\": {\n                \"name\": \"ipython\",\n                \"version\": 3\n            },\n            \"file_extension\": \".py\",\n            \"mimetype\": \"text/x-python\",\n            \"name\": \"python\",\n            \"nbconvert_exporter\": \"python\",\n            \"pygments_lexer\": \"ipython3\",\n            \"version\": \"3.6.9\"\n        }\n    },\n    \"nbformat\": 4,\n    \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/lbfgs/config.yaml",
    "content": "input_features:\n  - name: RESOURCE\n    type: category\n  - name: MGR_ID\n    type: category\n  - name: ROLE_ROLLUP_1\n    type: category\n  - name: ROLE_ROLLUP_2\n    type: category\n  - name: ROLE_DEPTNAME\n    type: category\n  - name: ROLE_TITLE\n    type: category\n  - name: ROLE_FAMILY_DESC\n    type: category\n  - name: ROLE_FAMILY\n    type: category\n  - name: ROLE_CODE\n    type: category\noutput_features:\n  - name: ACTION\n    type: binary\npreprocessing:\n  split:\n    type: fixed\ndefaults:\n  category:\n    encoder:\n      type: sparse\ntrainer:\n  batch_size: 32769 # entire training set\n  train_steps: 1\n  steps_per_checkpoint: 1\n  learning_rate: 1\n  regularization_lambda: 0.0000057\n  optimizer:\n    type: lbfgs\n    max_iter: 100\n    tolerance_grad: 0.0001\n    history_size: 10\n"
  },
  {
    "path": "examples/lbfgs/model.py",
    "content": "import logging\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import amazon_employee_access_challenge\n\ndf = amazon_employee_access_challenge.load()\n\nmodel = LudwigModel(config=\"config.yaml\", logging_level=logging.INFO)\n\ntraining_statistics, preprocessed_data, output_directory = model.train(\n    df,\n    skip_save_processed_input=True,\n    skip_save_log=True,\n    skip_save_progress=True,\n    skip_save_training_description=True,\n    skip_save_training_statistics=True,\n)\n\n# Predict on unlabeled test\nconfig = model.config\nconfig[\"preprocessing\"] = {}\nmodel.config = config\nunlabeled_test = df[df.split == 2].reset_index(drop=True)\npreds, _ = model.predict(unlabeled_test)\n\n# Save predictions to csv\naction = preds.ACTION_probabilities_True\nsubmission = pd.merge(unlabeled_test.reset_index(drop=True).id.astype(int), action, left_index=True, right_index=True)\nsubmission.rename(columns={\"ACTION_probabilities_True\": \"Action\", \"id\": \"Id\"}, inplace=True)\nsubmission.to_csv(\"submission.csv\", index=False)\n"
  },
  {
    "path": "examples/llama2_7b_finetuning_4bit/README.md",
    "content": "# Llama2-7b Fine-Tuning 4bit (QLoRA)\n\n[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/drive/1c3AO8l_H6V_x37RwQ8V7M6A-RmcBf2tG?usp=sharing)\n\nThis example shows how to fine-tune [Llama2-7b](https://huggingface.co/meta-llama/Llama-2-7b-hf) to follow instructions.\nInstruction tuning is the first step in adapting a general purpose Large Language Model into a chatbot.\n\nThis example uses no distributed training or big data functionality. It is designed to run locally on any machine\nwith GPU availability.\n\n## Prerequisites\n\n- [HuggingFace API Token](https://huggingface.co/docs/hub/security-tokens)\n- Access approval to [Llama2-7b-hf](https://huggingface.co/meta-llama/Llama-2-7b-hf)\n- GPU with at least 12 GiB of VRAM (in our tests, we used an Nvidia T4)\n\n## Running\n\n### Command Line\n\nSet your token environment variable from the terminal, then run the API script:\n\n```bash\nexport HUGGING_FACE_HUB_TOKEN=\"<api_token>\"\n./run_train.sh\n```\n\n### Python API\n\nSet your token environment variable from the terminal, then run the API script:\n\n```bash\nexport HUGGING_FACE_HUB_TOKEN=\"<api_token>\"\npython train_alpaca.py\n```\n\n## Upload to HuggingFace\n\nYou can upload to the HuggingFace Hub from the command line:\n\n```bash\nludwig upload hf_hub -r <your_org>/<model_name> -m <path/to/model>\n```\n"
  },
  {
    "path": "examples/llama2_7b_finetuning_4bit/llama2_7b_4bit.yaml",
    "content": "model_type: llm\nbase_model: meta-llama/Llama-2-7b-hf\n\nquantization:\n  bits: 4\n\nadapter:\n  type: lora\n\ninput_features:\n  - name: instruction\n    type: text\n\noutput_features:\n  - name: output\n    type: text\n\ntrainer:\n  type: finetune\n  learning_rate: 0.0003\n  batch_size: 2\n  gradient_accumulation_steps: 8\n  epochs: 3\n  learning_rate_scheduler:\n    warmup_fraction: 0.01\n\nbackend:\n  type: local\n"
  },
  {
    "path": "examples/llama2_7b_finetuning_4bit/run_train.sh",
    "content": "#!/usr/bin/env bash\n\n# Fail fast if an error occurs\nset -e\n\n# Get the directory of this script, which contains the config file\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\n# Train\nludwig train --config ${SCRIPT_DIR}/llama2_7b_4bit.yaml --dataset ludwig://alpaca\n"
  },
  {
    "path": "examples/llama2_7b_finetuning_4bit/train_alpaca.py",
    "content": "import logging\nimport os\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\nconfig = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: meta-llama/Llama-2-7b-hf\n\nquantization:\n  bits: 4\n\nadapter:\n  type: lora\n\ninput_features:\n  - name: instruction\n    type: text\n\noutput_features:\n  - name: output\n    type: text\n\ntrainer:\n    type: finetune\n    learning_rate: 0.0003\n    batch_size: 2\n    gradient_accumulation_steps: 8\n    epochs: 3\n    learning_rate_scheduler:\n      warmup_fraction: 0.01\n\nbackend:\n  type: local\n\"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=\"ludwig://alpaca\",\n    experiment_name=\"alpaca_instruct_4bit\",\n    model_name=\"llama2_7b\",\n)\n\n# list contents of output directory\nprint(\"contents of output directory:\", output_directory)\nfor item in os.listdir(output_directory):\n    print(\"\\t\", item)\n"
  },
  {
    "path": "examples/llm_base_model_dequantization/README.md",
    "content": "# Convert quantized base model to fp16\n\nLudwig has utility functions to convert nf4 quantized bitsandbytes base models back to fp16\nfor more efficient inference. This is desireable since inference with bitsandbytes is slow because\nevery forward pass through the model requires dequantizing the model weights from nf4 to fp16 layer\nby layer and then quantizing it back to nf4 to keep memory usage constant.\n\nBy dequantizing the base model in fp16 upfront, you can get the same effect of the quantized weights\nwithout sacrificing on inference performance.\n\n## Visual Illustration\n\n### Without dequantization upfront\n\n| **Request 1:**                             | **Request 2:**                             | **Request 3:**                             |\n| ------------------------------------------ | ------------------------------------------ | ------------------------------------------ |\n| - Quantized bitsandbytes model             | - Quantized bitsandbytes model             | - Quantized bitsandbytes model             |\n| - Dequantization of layer 1 (nf4 to fp16)  | - Dequantization of layer 1 (nf4 to fp16)  | - Dequantization of layer 1 (nf4 to fp16)  |\n| - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) |\n| - Quantization of layer 1 (fp16 to nf4)    | - Quantization of layer 1 (fp16 to nf4)    | - Quantization of layer 1 (fp16 to nf4)    |\n| - Dequantization of layer 2 (nf4 to fp16)  | - Dequantization of layer 2 (nf4 to fp16)  | - Dequantization of layer 2 (nf4 to fp16)  |\n| - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) | - Forward Pass (using dequantized weights) |\n| - Quantization of layer 2 (fp16 to nf4)    | - Quantization of layer 2 (fp16 to nf4)    | - Quantization of layer 2 (fp16 to nf4)    |\n| - ...                                      | - ...                                      | - ...                                      |\n| - Final Output                             | - Final Output                             | - Final Output                             |\n\n### With dequantization upfront\n\n| **Request 1:**                   | **Request 2:**                   | **Request 3:**                   |\n| -------------------------------- | -------------------------------- | -------------------------------- |\n| - Dequantized base model in fp16 | - Dequantized base model in fp16 | - Dequantized base model in fp16 |\n| - Forward pass through layer 1   | - Forward pass through layer 1   | - Forward pass through layer 1   |\n| - Forward pass through layer 2   | - Forward pass through layer 2   | - Forward pass through layer 2   |\n| - ...                            | - ...                            | - ...                            |\n| - Final Output                   | - Final Output                   | - Final Output                   |\n\n## Running the example script\n\nThe example `phi_2_dequantization.py` shows how you how you can quantize and then dequantized Phi-2. This process\ncan 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.\n\nBeneath the surface, this script:\n\n1. Loads the base model in 4 bit nf4 quantization\n1. Dequantizes the model layer by layer back into fp16 in-place.\n1. Write the new dequantized weights to disk at `save_path`\n1. Write the tokenizer to disk at `save_path`\n\nMake sure you update the paths at the top of the file for base model, save path, and huggingface repo ID!\n\n## Bonus\n\nIf desired, you can also use Ludwig to push the new dequantized model weights straight to HuggingFace hub!\n\n```python\nfrom ludwig.utils.hf_utils import upload_folder_to_hfhub\n\nupload_folder_to_hfhub(repo_id=hfhub_repo_id, folder_path=save_path)\n```\n\n### Dequantized base models already on huggingface hub\n\n- [CodeLlama 7b Instruct](https://huggingface.co/arnavgrg/codallama-7b-instruct-nf4-fp16-upscaled)\n- [CodeLlama 13b Instruct](https://huggingface.co/arnavgrg/codellama-13b-instruct-nf4-fp16-upscaled)\n- [CodeLlama 70b Instruct](https://huggingface.co/arnavgrg/codellama-70b-instruct-nf4-fp16-upscaled)\n- [Llama 2 7b](https://huggingface.co/arnavgrg/llama-2-7b-nf4-fp16-upscaled)\n- [Llama 2 7b Chat](https://huggingface.co/arnavgrg/llama-2-7b-chat-nf4-fp16-upscaled)\n- [Llama 2 13b Chat](https://huggingface.co/arnavgrg/llama-2-13b-chat-nf4-fp16-upscaled)\n- [Llama 2 70b Chat](https://huggingface.co/arnavgrg/llama-2-70b-chat-nf4-fp16-upscaled)\n- [Mistral 7b](https://huggingface.co/arnavgrg/mistral-7b-nf4-fp16-upscaled)\n- [Mistral 7b Instruct](https://huggingface.co/arnavgrg/mistral-7b-instruct-nf4-fp16-upscaled)\n- [NousMistral Yarn 7b 128K](https://huggingface.co/arnavgrg/NousResearch-Yarn-Mistral-7b-128k-nf4-fp16-upscaled)\n- [Microsoft Phi-2](https://huggingface.co/arnavgrg/phi-2-nf4-fp16-upscaled)\n- [Zephyr 7b Beta](https://huggingface.co/arnavgrg/zephyr-7b-beta-nf4-fp16-upscaled)\n"
  },
  {
    "path": "examples/llm_base_model_dequantization/phi_2_dequantization.py",
    "content": "import logging\nimport os\n\nimport yaml\nfrom huggingface_hub import whoami\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.utils.hf_utils import upload_folder_to_hfhub\n\nhf_username = whoami().get(\"name\")\nbase_model_name = \"microsoft/phi-2\"\ndequantized_path = \"microsoft-phi-2-dequantized\"\nsave_path = \"/home/ray/\" + dequantized_path\nhfhub_repo_id = os.path.join(hf_username, dequantized_path)\n\n\nconfig = yaml.safe_load(f\"\"\"\n    model_type: llm\n    base_model: {base_model_name}\n\n    quantization:\n      bits: 4\n\n    input_features:\n      - name: instruction\n        type: text\n\n    output_features:\n      - name: output\n        type: text\n\n    trainer:\n        type: none\n\n    backend:\n      type: local\n  \"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\nmodel.save_dequantized_base_model(save_path=save_path)\n\n# Optional: Upload to Huggingface Hub\nupload_folder_to_hfhub(repo_id=hfhub_repo_id, folder_path=save_path)\n"
  },
  {
    "path": "examples/llm_few_shot_learning/simple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example\n#\n# This is a simple example of how to use the LLM model type to train\n# a zero shot classification model. It uses the facebook/opt-350m model\n# as the base LLM model.\n\n# Import required libraries\nimport logging\nimport shutil\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\nreview_label_pairs = [\n    {\"review\": \"I loved this movie!\", \"label\": \"positive\"},\n    {\"review\": \"The food was okay, but the service was terrible.\", \"label\": \"negative\"},\n    {\"review\": \"I can't believe how rude the staff was.\", \"label\": \"negative\"},\n    {\"review\": \"This book was a real page-turner.\", \"label\": \"positive\"},\n    {\"review\": \"The hotel room was dirty and smelled bad.\", \"label\": \"negative\"},\n    {\"review\": \"I had a great experience at this restaurant.\", \"label\": \"positive\"},\n    {\"review\": \"The concert was amazing!\", \"label\": \"positive\"},\n    {\"review\": \"The traffic was terrible on my way to work this morning.\", \"label\": \"negative\"},\n    {\"review\": \"The customer service was excellent.\", \"label\": \"positive\"},\n    {\"review\": \"I was disappointed with the quality of the product.\", \"label\": \"negative\"},\n    {\"review\": \"The scenery on the hike was breathtaking.\", \"label\": \"positive\"},\n    {\"review\": \"I had a terrible experience at this hotel.\", \"label\": \"negative\"},\n    {\"review\": \"The coffee at this cafe was delicious.\", \"label\": \"positive\"},\n    {\"review\": \"The weather was perfect for a day at the beach.\", \"label\": \"positive\"},\n    {\"review\": \"I would definitely recommend this product.\", \"label\": \"positive\"},\n    {\"review\": \"The wait time at the doctor's office was ridiculous.\", \"label\": \"negative\"},\n    {\"review\": \"The museum was a bit underwhelming.\", \"label\": \"neutral\"},\n    {\"review\": \"I had a fantastic time at the amusement park.\", \"label\": \"positive\"},\n    {\"review\": \"The staff at this store was extremely helpful.\", \"label\": \"positive\"},\n    {\"review\": \"The airline lost my luggage and was very unhelpful.\", \"label\": \"negative\"},\n    {\"review\": \"This album is a must-listen for any music fan.\", \"label\": \"positive\"},\n    {\"review\": \"The food at this restaurant was just okay.\", \"label\": \"neutral\"},\n    {\"review\": \"I was pleasantly surprised by how great this movie was.\", \"label\": \"positive\"},\n    {\"review\": \"The car rental process was quick and easy.\", \"label\": \"positive\"},\n    {\"review\": \"The service at this hotel was top-notch.\", \"label\": \"positive\"},\n]\n\ndf = pd.DataFrame(review_label_pairs)\ndf[\"split\"] = [0] * 15 + [2] * 10\n\nconfig = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\ngeneration:\n    temperature: 0.1\n    top_p: 0.75\n    top_k: 40\n    num_beams: 4\n    max_new_tokens: 64\nprompt:\n    task: \"Classify the sample input as either negative, neutral, or positive.\"\n    retrieval:\n        type: semantic\n        k: 3\n        model_name: paraphrase-MiniLM-L3-v2\ninput_features:\n-\n    name: review\n    type: text\noutput_features:\n-\n    name: label\n    type: category\n    preprocessing:\n        fallback_label: \"neutral\"\n    decoder:\n        type: category_extractor\n        match:\n            \"negative\":\n                type: contains\n                value: \"positive\"\n            \"neural\":\n                type: contains\n                value: \"neutral\"\n            \"positive\":\n                type: contains\n                value: \"positive\"\npreprocessing:\n    split:\n        type: fixed\n    \"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=df, experiment_name=\"simple_experiment\", model_name=\"simple_model\", skip_save_processed_input=True\n)\n\ntraining_set, val_set, test_set, _ = preprocessed_data\n\n# batch prediction\npreds, _ = model.predict(test_set, skip_save_predictions=False)\nprint(preds)\n"
  },
  {
    "path": "examples/llm_finetuning/README.md",
    "content": "# LLM Fine-tuning\n\nThese examples show you how to fine-tune Large Language Models by taking advantage of model parallelism\nwith [DeepSpeed](https://www.deepspeed.ai/), allowing Ludwig to scale to very large models with billions of\nparameters.\n\nThe 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,\nand fine-tuning the weights to improve performance of the LLM on the task. Ludwig will do this for you without no machine learning\ncode, just configuration.\n\n## Prerequisites\n\n- Installed Ludwig with `ludwig[distributed]` dependencies\n- Have a CUDA-enabled version of PyTorch installed\n- Have access to a machine or cluster of machines with multiple GPUs\n- The IMDB dataset used in these examples comes from Kaggle, so make sure you have your credentials set (e.g., `$HOME/.kaggle.kaggle.json`)\n\n## Running DeepSpeed on Ray\n\nThis is the recommended way to use DeepSpeed, which supports auto-batch size tuning and distributed data processing.\nThere is some overhead from using Ray with small datasets (\\<100MB), but in most cases performance should be comparable\nto using native DeepSpeed.\n\nFrom the head node of your Ray cluster:\n\n```bash\n./run_train_dsz3_ray.sh\n```\n\n### Python API\n\nIf you want to run Ludwig programatically (from a notebook or as part of a larger workflow), you can run the following\nPython script using the Ray cluster launcher from your local machine.\n\n```bash\nray submit cluster.yaml train_imdb_ray.py\n```\n\nIf running directly on the Ray head node, you can omit the `ray submit` portion and run like an ordinary Python script:\n\n```bash\npython train_imdb_ray.py\n```\n\n## Running DeepSpeed Native\n\nThis mode is suitable for datasets small enough to fit in memory on a single machine, as it doesn't make use of\ndistributed data processing (requires use of the Ray backend).\n\nThe following example assumes you have 4 GPUs available, but can easily be modified to support your preferred\nsetup.\n\nFrom a terminal on your machine:\n\n```bash\n./run_train_dsz3.sh\n```\n"
  },
  {
    "path": "examples/llm_finetuning/imdb_deepspeed_zero3.yaml",
    "content": "input_features:\n  - name: review\n    type: text\n    encoder:\n      type: auto_transformer\n      pretrained_model_name_or_path: bigscience/bloom-3b\n      trainable: true\n      adapter: lora\n\noutput_features:\n  - name: sentiment\n    type: category\n\ntrainer:\n  batch_size: 4\n  epochs: 3\n  gradient_accumulation_steps: 8\n\nbackend:\n  type: deepspeed\n  zero_optimization:\n    stage: 3\n    offload_optimizer:\n      device: cpu\n      pin_memory: true\n"
  },
  {
    "path": "examples/llm_finetuning/imdb_deepspeed_zero3_ray.yaml",
    "content": "input_features:\n  - name: review\n    type: text\n    encoder:\n      type: auto_transformer\n      pretrained_model_name_or_path: bigscience/bloom-3b\n      trainable: true\n      adapter: lora\n\noutput_features:\n  - name: sentiment\n    type: category\n\ntrainer:\n  batch_size: 4\n  epochs: 3\n  gradient_accumulation_steps: 8\n\nbackend:\n  type: ray\n  trainer:\n    use_gpu: true\n    strategy:\n      type: deepspeed\n      zero_optimization:\n        stage: 3\n        offload_optimizer:\n          device: cpu\n          pin_memory: true\n"
  },
  {
    "path": "examples/llm_finetuning/run_train_dsz3.sh",
    "content": "#!/usr/bin/env bash\n\n# Fail fast if an error occurs\nset -e\n\n# Get the directory of this script, which contains the config file\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\n# Train\ndeepspeed --no_python --no_local_rank --num_gpus 4 ludwig train --config ${SCRIPT_DIR}/imdb_deepspeed_zero3.yaml --dataset ludwig://imdb\n"
  },
  {
    "path": "examples/llm_finetuning/run_train_dsz3_ray.sh",
    "content": "#!/usr/bin/env bash\n\n# Fail fast if an error occurs\nset -e\n\n# Get the directory of this script, which contains the config file\nSCRIPT_DIR=$( cd -- \"$( dirname -- \"${BASH_SOURCE[0]}\" )\" &> /dev/null && pwd )\n\n# Train\nludwig train --config ${SCRIPT_DIR}/imdb_deepspeed_zero3_ray.yaml --dataset ludwig://imdb\n"
  },
  {
    "path": "examples/llm_finetuning/train_imdb_ray.py",
    "content": "import logging\nimport os\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\nconfig = yaml.safe_load(\"\"\"\ninput_features:\n  - name: review\n    type: text\n\n    encoder:\n      type: auto_transformer\n      pretrained_model_name_or_path: bigscience/bloom-3b\n      trainable: true\n      adapter:\n        type: lora\n\noutput_features:\n  - name: sentiment\n    type: category\n\ntrainer:\n  batch_size: 4\n  epochs: 3\n\nbackend:\n  type: ray\n  trainer:\n    use_gpu: true\n    strategy:\n      type: deepspeed\n      zero_optimization:\n        stage: 3\n        offload_optimizer:\n          device: cpu\n          pin_memory: true\n\"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=\"ludwig://imdb\",\n    experiment_name=\"imdb_sentiment\",\n    model_name=\"bloom3b\",\n)\n\n# list contents of output directory\nprint(\"contents of output directory:\", output_directory)\nfor item in os.listdir(output_directory):\n    print(\"\\t\", item)\n"
  },
  {
    "path": "examples/llm_instruction_tuning/train_alpaca_ray.py",
    "content": "import logging\nimport os\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\nconfig = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: bigscience/bloomz-3b\n\nadapter:\n  type: lora\n\ninput_features:\n  - name: instruction\n    type: text\n\noutput_features:\n  - name: output\n    type: text\n\ntrainer:\n    type: finetune\n    batch_size: 4\n    epochs: 3\n\nbackend:\n  type: ray\n  trainer:\n    use_gpu: true\n    strategy:\n      type: deepspeed\n      zero_optimization:\n        stage: 3\n        offload_optimizer:\n          device: cpu\n          pin_memory: true\n\"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=\"ludwig://alpaca\",\n    experiment_name=\"alpaca_instruct\",\n    model_name=\"bloom560m\",\n)\n\n# list contents of output directory\nprint(\"contents of output directory:\", output_directory)\nfor item in os.listdir(output_directory):\n    print(\"\\t\", item)\n"
  },
  {
    "path": "examples/llm_text_generation/simple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example\n#\n# This is a simple example of how to use the LLM model type to train\n# a model on a simple question and answer dataset. It uses the\n# facebook/opt-350m model as the base LLM model.\n\n# Import required libraries\nimport logging\nimport shutil\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\nqa_pairs = [\n    {\"Question\": \"What is the capital of Uzbekistan?\", \"Answer\": \"Tashkent\"},\n    {\"Question\": \"Who is the founder of Microsoft?\", \"Answer\": \"Bill Gates\"},\n    {\"Question\": \"What is the tallest building in the world?\", \"Answer\": \"Burj Khalifa\"},\n    {\"Question\": \"What is the currency of Brazil?\", \"Answer\": \"Real\"},\n    {\"Question\": \"What is the boiling point of mercury in Celsius?\", \"Answer\": \"-38.83\"},\n    {\"Question\": \"What is the most commonly spoken language in the world?\", \"Answer\": \"Mandarin\"},\n    {\"Question\": \"What is the diameter of the Earth?\", \"Answer\": \"12,742 km\"},\n    {\"Question\": 'Who wrote the novel \"1984\"?', \"Answer\": \"George Orwell\"},\n    {\"Question\": \"What is the name of the largest moon of Neptune?\", \"Answer\": \"Triton\"},\n    {\"Question\": \"What is the speed of light in meters per second?\", \"Answer\": \"299,792,458 m/s\"},\n    {\"Question\": \"What is the smallest country in Africa by land area?\", \"Answer\": \"Seychelles\"},\n    {\"Question\": \"What is the largest organ in the human body?\", \"Answer\": \"Skin\"},\n    {\"Question\": 'Who directed the film \"The Godfather\"?', \"Answer\": \"Francis Ford Coppola\"},\n    {\"Question\": \"What is the name of the smallest planet in our solar system?\", \"Answer\": \"Mercury\"},\n    {\"Question\": \"What is the largest lake in Africa?\", \"Answer\": \"Lake Victoria\"},\n    {\"Question\": \"What is the smallest country in Asia by land area?\", \"Answer\": \"Maldives\"},\n    {\"Question\": \"Who is the current president of Russia?\", \"Answer\": \"Vladimir Putin\"},\n    {\"Question\": \"What is the chemical symbol for gold?\", \"Answer\": \"Au\"},\n    {\"Question\": \"What is the name of the famous Swiss mountain known for skiing?\", \"Answer\": \"The Matterhorn\"},\n    {\"Question\": \"What is the largest flower in the world?\", \"Answer\": \"Rafflesia arnoldii\"},\n]\n\ndf = pd.DataFrame(qa_pairs)\n\nconfig = yaml.safe_load(\"\"\"\n        input_features:\n            - name: Question\n              type: text\n        output_features:\n            - name: Answer\n              type: text\n        model_type: llm\n        generation:\n            temperature: 0.1\n            top_p: 0.75\n            top_k: 40\n            num_beams: 4\n            max_new_tokens: 5\n        base_model: facebook/opt-350m\n    \"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=df, experiment_name=\"simple_experiment\", model_name=\"simple_model\", skip_save_processed_input=True\n)\n\ntraining_set, val_set, test_set, _ = preprocessed_data\n\n# batch prediction\npreds, _ = model.predict(test_set, skip_save_predictions=False)\nprint(preds)\n"
  },
  {
    "path": "examples/llm_zero_shot_learning/simple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example\n#\n# This is a simple example of how to use the LLM model type to train\n# a zero shot classification model. It uses the facebook/opt-350m model\n# as the base LLM model.\n\n# Import required libraries\nimport logging\nimport shutil\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\nreview_label_pairs = [\n    {\"review\": \"I loved this movie!\", \"label\": \"positive\"},\n    {\"review\": \"The food was okay, but the service was terrible.\", \"label\": \"negative\"},\n    {\"review\": \"I can't believe how rude the staff was.\", \"label\": \"negative\"},\n    {\"review\": \"This book was a real page-turner.\", \"label\": \"positive\"},\n    {\"review\": \"The hotel room was dirty and smelled bad.\", \"label\": \"negative\"},\n    {\"review\": \"I had a great experience at this restaurant.\", \"label\": \"positive\"},\n    {\"review\": \"The concert was amazing!\", \"label\": \"positive\"},\n    {\"review\": \"The traffic was terrible on my way to work this morning.\", \"label\": \"negative\"},\n    {\"review\": \"The customer service was excellent.\", \"label\": \"positive\"},\n    {\"review\": \"I was disappointed with the quality of the product.\", \"label\": \"negative\"},\n    {\"review\": \"The scenery on the hike was breathtaking.\", \"label\": \"positive\"},\n    {\"review\": \"I had a terrible experience at this hotel.\", \"label\": \"negative\"},\n    {\"review\": \"The coffee at this cafe was delicious.\", \"label\": \"positive\"},\n    {\"review\": \"The weather was perfect for a day at the beach.\", \"label\": \"positive\"},\n    {\"review\": \"I would definitely recommend this product.\", \"label\": \"positive\"},\n    {\"review\": \"The wait time at the doctor's office was ridiculous.\", \"label\": \"negative\"},\n    {\"review\": \"The museum was a bit underwhelming.\", \"label\": \"neutral\"},\n    {\"review\": \"I had a fantastic time at the amusement park.\", \"label\": \"positive\"},\n    {\"review\": \"The staff at this store was extremely helpful.\", \"label\": \"positive\"},\n    {\"review\": \"The airline lost my luggage and was very unhelpful.\", \"label\": \"negative\"},\n    {\"review\": \"This album is a must-listen for any music fan.\", \"label\": \"positive\"},\n    {\"review\": \"The food at this restaurant was just okay.\", \"label\": \"neutral\"},\n    {\"review\": \"I was pleasantly surprised by how great this movie was.\", \"label\": \"positive\"},\n    {\"review\": \"The car rental process was quick and easy.\", \"label\": \"positive\"},\n    {\"review\": \"The service at this hotel was top-notch.\", \"label\": \"positive\"},\n]\n\ndf = pd.DataFrame(review_label_pairs)\n\nconfig = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\ngeneration:\n    temperature: 0.1\n    top_p: 0.75\n    top_k: 40\n    num_beams: 4\n    max_new_tokens: 64\nprompt:\n    task: \"Classify the sample input as either negative, neutral, or positive.\"\ninput_features:\n-\n    name: review\n    type: text\noutput_features:\n-\n    name: label\n    type: category\n    preprocessing:\n        fallback_label: \"neutral\"\n    decoder:\n        type: category_extractor\n        match:\n            \"negative\":\n                type: contains\n                value: \"positive\"\n            \"neutral\":\n                type: contains\n                value: \"neutral\"\n            \"positive\":\n                type: contains\n                value: \"positive\"\n    \"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=df, experiment_name=\"simple_experiment\", model_name=\"simple_model\", skip_save_processed_input=True\n)\n\ntraining_set, val_set, test_set, _ = preprocessed_data\n\n# batch prediction\npreds, _ = model.predict(test_set, skip_save_predictions=False)\nprint(preds)\n"
  },
  {
    "path": "examples/mnist/README.md",
    "content": "# MNIST Hand-written Digit Classification\n\nThis 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).\n\n### Examples\n\n| File                               | Description                                                                                                                |\n| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |\n| simple_model_training.py           | Demonstrates using Ludwig api for training a model.                                                                        |\n| advance_model_training.py          | Demonstrates a method to assess alternative model architectures.                                                           |\n| 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. |\n| visualize_model_test_results.ipynb | Example for extracting training statistics and generate custom visualizations.                                             |\n"
  },
  {
    "path": "examples/mnist/advanced_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Multiple Model Training Example\n#\n# This example trains multiple models and extracts training statistics\n\nimport glob\nimport logging\nimport os\nimport shutil\nfrom collections import namedtuple\n\nimport yaml\n\n# ## Import required libraries\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import TRAINER\nfrom ludwig.datasets import mnist\nfrom ludwig.visualize import learning_curves\n\n# clean out old results\nshutil.rmtree(\"./results\", ignore_errors=True)\nshutil.rmtree(\"./visualizations\", ignore_errors=True)\n\nfile_list = glob.glob(\"./data/*.json\")\nfile_list += glob.glob(\"./data/*.hdf5\")\nfor f in file_list:\n    try:\n        os.remove(f)\n    except FileNotFoundError:\n        pass\n\n# read in base config\nwith open(\"./config.yaml\") as f:\n    base_model = yaml.safe_load(f.read())\n\n# Specify named tuple to keep track of training results\nTrainingResult = namedtuple(\"TrainingResult\", [\"name\", \"train_stats\"])\n\n# specify alternative architectures to test\nFullyConnectedLayers = namedtuple(\"FullyConnectedLayers\", [\"name\", \"fc_layers\"])\n\nlist_of_fc_layers = [\n    FullyConnectedLayers(name=\"Option1\", fc_layers=[{\"output_size\": 64}]),\n    FullyConnectedLayers(name=\"Option2\", fc_layers=[{\"output_size\": 128}, {\"output_size\": 64}]),\n    FullyConnectedLayers(name=\"Option3\", fc_layers=[{\"output_size\": 128}]),\n]\n\n#\nlist_of_train_stats = []\n\n# load and split MNIST dataset\ntraining_set, test_set, _ = mnist.load(split=True)\n\n# ## Train models\nfor model_option in list_of_fc_layers:\n    print(\">>>> training: \", model_option.name)\n\n    # set up Python dictionary to hold model training parameters\n    config = base_model.copy()\n    config[\"input_features\"][0][\"fc_layers\"] = model_option.fc_layers\n    config[TRAINER][\"epochs\"] = 5\n\n    # Define Ludwig model object that drive model training\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    # initiate model training\n    train_stats, _, _ = model.train(\n        training_set=training_set,\n        test_set=test_set,\n        experiment_name=\"multiple_experiment\",\n        model_name=model_option.name,\n    )\n\n    # save training stats for later use\n    list_of_train_stats.append(TrainingResult(name=model_option.name, train_stats=train_stats))\n\n    print(\">>>>>>> completed: \", model_option.name, \"\\n\")\n\n\n# generating learning curves from training\noption_names = [trs.name for trs in list_of_train_stats]\ntrain_stats = [trs.train_stats for trs in list_of_train_stats]\nlearning_curves(\n    train_stats, \"Survived\", model_names=option_names, output_directory=\"./visualizations\", file_format=\"png\"\n)\n"
  },
  {
    "path": "examples/mnist/assess_model_performance.py",
    "content": "#!/usr/bin/env python\n\n#\n# Load a previously saved model and make predictions on the test data set\n#\n\nimport os.path\n\n# ## Import required libraries\nimport pandas as pd\nfrom sklearn.metrics import accuracy_score\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import mnist\n\n# create data set for predictions\ntest_data = {\"image_path\": [], \"label\": []}\ndataset = mnist.Mnist()\ntest_dir = os.path.join(dataset.processed_dataset_path, \"testing\")\nfor 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\ntest_df = pd.DataFrame(test_data)\nprint(test_df.head())\n\n# retrieve a trained model\nmodel = LudwigModel.load(\"./results/multiple_experiment_Option3/model\")\n\n# make predictions\npred_df, _ = model.predict(dataset=test_df)\nprint(pred_df.head())\n\n# print accuracy on test data set\nprint(\"predicted accuracy\", accuracy_score(test_df[\"label\"], pred_df[\"label_predictions\"]))\n"
  },
  {
    "path": "examples/mnist/config.yaml",
    "content": "input_features:\n  - name: image_path\n    type: image\n    preprocessing:\n      num_processes: 4\n    encoder: stacked_cnn\n    conv_layers:\n      - num_filters: 32\n        filter_size: 3\n        pool_size: 2\n        pool_stride: 2\n      - num_filters: 64\n        filter_size: 3\n        pool_size: 2\n        pool_stride: 2\n        dropout: 0.4\n    fc_layers:\n      - output_size: 128\n        dropout: 0.4\n\noutput_features:\n  - name: label\n    type: category\n\ntrainer:\n  epochs: 5\n"
  },
  {
    "path": "examples/mnist/simple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example\n#\n# This example is the API example for this Ludwig command line example\n# (https://ludwig-ai.github.io/ludwig-docs/latest/examples/mnist/).\nimport logging\nimport shutil\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import mnist\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\n# set up Python dictionary to hold model training parameters\nwith open(\"./config.yaml\") as f:\n    config = yaml.safe_load(f.read())\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config, logging_level=logging.INFO)\n\n# load and split MNIST dataset\ntraining_set, test_set, _ = mnist.load(split=True)\n\n# initiate model training\ntrain_stats, _, output_directory = model.train(  # training statistics  # location for training results saved to disk\n    training_set=training_set,\n    test_set=test_set,\n    experiment_name=\"simple_image_experiment\",\n    model_name=\"single_model\",\n    skip_save_processed_input=True,\n)\n"
  },
  {
    "path": "examples/mnist/visualize_model_test_results.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Ludwig Visualization Demonstration\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import warnings\\n\",\n    \"warnings.simplefilter('ignore')\\n\",\n    \"from ludwig.api import LudwigModel\\n\",\n    \"from ludwig.datasets import mnist\\n\",\n    \"from ludwig.visualize import compare_performance,  compare_classifiers_performance_from_pred, \\\\\\n\",\n    \"    confusion_matrix\\n\",\n    \"from ludwig.utils.data_utils import load_json\\n\",\n    \"import pandas as pd\\n\",\n    \"import os\\n\",\n    \"import os.path\\n\",\n    \"import shutil\\n\",\n    \"\\n\",\n    \"shutil.rmtree('./viz2', ignore_errors=True)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Prepare test data set for use\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"name\": \"stdout\",\n     \"output_type\": \"stream\",\n     \"text\": [\n      \"                                          image_path label\\n\",\n      \"0  /opt/project/examples/mnist/data/mnist_png/tes...     0\\n\",\n      \"1  /opt/project/examples/mnist/data/mnist_png/tes...     0\\n\",\n      \"2  /opt/project/examples/mnist/data/mnist_png/tes...     0\\n\",\n      \"3  /opt/project/examples/mnist/data/mnist_png/tes...     0\\n\",\n      \"4  /opt/project/examples/mnist/data/mnist_png/tes...     0\\n\"\n     ]\n    }\n   ],\n   \"source\": [\n    \"# create test dataframe\\n\",\n    \"test_data = {'image_path': [], 'label': []}\\n\",\n    \"dataset = mnist.Mnist()\\n\",\n    \"test_dir = os.path.join(dataset.processed_dataset_path, 'testing')\\n\",\n    \"for label in os.listdir(test_dir):\\n\",\n    \"    files = os.listdir(os.path.join(test_dir, label))\\n\",\n    \"    test_data['image_path'] += [os.path.join(test_dir, label, f) for f in files]\\n\",\n    \"    test_data['label'] += len(files) * [label]\\n\",\n    \"\\n\",\n    \"# collect data into a data frame\\n\",\n    \"test_df = pd.DataFrame(test_data)\\n\",\n    \"print(test_df.head())\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Generate predictions the test data set for the different neural network options\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# get list of models to visualize results\\n\",\n    \"models_list = ['Option1', 'Option2', 'Option3']\\n\",\n    \"test_stats_list = []\\n\",\n    \"preds_list = []\\n\",\n    \"\\n\",\n    \"for m in models_list:\\n\",\n    \"    # retrieve a trained model\\n\",\n    \"    model = LudwigModel.load('./results/multiple_experiment_'+ m + '/model')\\n\",\n    \"\\n\",\n    \"    # make predictions\\n\",\n    \"    test_stats, pred_df, _ = model.evaluate(dataset=test_df, collect_predictions=True, collect_overall_stats=True)\\n\",\n    \"    \\n\",\n    \"    # collect test statsitics\\n\",\n    \"    preds_list.append(pred_df['label_predictions'].astype('int'))\\n\",\n    \"    test_stats_list.append(test_stats)\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Show model performance on test data set\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"# overall model performance\\n\",\n    \"compare_performance(\\n\",\n    \"  test_stats_list,\\n\",\n    \"  'label',\\n\",\n    \"  model_names=models_list,\\n\",\n    \"  output_directory='./viz2',\\n\",\n    \"  file_format='png'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"# Classifiction performance metrics by model\\n\",\n    \"train_metadata_json = load_json('./results/multiple_experiment_Option1/model/training_set_metadata.json')\\n\",\n    \"compare_classifiers_performance_from_pred(\\n\",\n    \"  preds_list,\\n\",\n    \"  test_df['label'].to_numpy().astype('int'),\\n\",\n    \"  train_metadata_json,\\n\",\n    \"  'label',\\n\",\n    \"  10,\\n\",\n    \"  model_names=models_list,\\n\",\n    \"  output_directory='./viz2',\\n\",\n    \"  file_format='png'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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+PHjefTRRxk7diyZmZnk5OSYthopERQUxH333Wd6XckJLyFE5SM1VzvT6/XodDpSU1MZO3YsDz74IMnJyfTo0QMfHx9q1qyJj48P+fn5eHt788ADD1C9enVHhy2EKCdJrnailCIrK4vhw4czcuRIhg0bhl6vZ/LkyRw+fJjnnnuOP/74g/z8fDRN48qVKyxZsgSdTufo0IUQNiDJ1c527drFu+++y4gRIxgyZAhXrlxhzJgxhIWFMXr0aNPz8vPzy70ZnxDCeUjN1Q5+/fVXGjRogKenJz169MDDw4N58+ZhMBiIiIjg/fff5+9//zsXL15kzpw5ANSoUcPBUQshbElmrjaWmppKaGgojRo1IjAwkOHDhxMQEEBWVhavvPIKY8eOpW/fvqSlpTFp0iSWLl2Kj4+Po8MWQthY9dmzZ892dBCuIjs7m4YNG1K7dm1TrwB3d3feffddvLy8yMnJYcuWLbi7u9OxY0cGDRpEnTp1HB22EMIOZJ2rjej1eiZNmsThw4d5+umn6dy5My1atKBly5Z8/vnn+Pj40Lx5c9LS0li0aBFZWVk3LcMSQrgWKQvYyNWrV9m0aRN79uxhzJgxtG3bljVr1nDixAnCw8Pp2rUrAMnJyXh7e9OoUSMHRyyEsCcpC9hIrVq1aN68OQaDgdWrV3P33XcTHBzMlStXOHjwIDk5ObRs2RIfHx8pBQhRBcj30nI4fPgwCQkJptt169ald+/ePPbYY3z88cf89ttvDBgwgBYtWvDjjz9y7do1B0YrhKhIshSrHIqKioiJiaFatWr0798fKE6woaGh5OXlsX37dsaPH09YWBi1atXC09PTwRELISqKzFytoJRCKUXnzp1ZsmQJixcvZsOGDabH6tWrR0BAACkpKRiNRnx9ffH29q7wOP/973/L9jFOLjc319EhlIn8PllOkqsVNE1D0zTOnDnDI488wrx583j33XfZuHGjqdlKbm4u+fn5Fv/xGAwGm8b4ww8/8OKLL5KammqzMX/77TcOHTpEZmamzcb8/fff+fHHHykoKLDZmBcuXODHH3/EaDTa7Od6/vx5jh8/TmFhoc3G3LFjBwsXLiQjI8Mm4wGcOHGC+Ph4Tpw4YbOf6ZEjR4iPjweKf/clwVpGTmhZac2aNbz//vuEh4fj7+9Py5Ytef/997lw4QK7d+8mISGBGTNm0Lhx41LHSUlJMXXHMhgMNlmetWfPHhYuXEhmZiZXr16lW7du5R5z9+7dzJo1i5SUFLZt20anTp3KfWLu+++/5/XXX+fo0aPs37+fwMBA6tevX64xd+zYwezZs0lOTubEiRNcunSJFi1alOsKuG3btjFjxgx++uknDh48iF6vJyAggJo1a1o95qFDh5g/fz5RUVG0bNnS6nH+KjExkZiYGPLz8zl8+DBt2rShXr16Vo9nNBrJzc3lhRde4OjRo1SrVo22bduiaRpGo1G6tpmjRJkYDAallFIffvih2rFjx02PnT17Vm3atEmtWLFCXbhwwexYO3fuVA888ICaNGmS6b6ioqJyxbd371712GOPqV9//VUVFBSoUaNGqUOHDpVrzAMHDqjQ0FB18uRJpZRS48aNU3v37i3XmEePHlVhYWHq559/VkopNWvWLDVt2rRyjXnlyhX17LPPqt9++00ppVRcXJwaMmSIWrp0qcrJybFqzIKCAvXSSy+pI0eOKKWU2rJli3rzzTfV22+/bfWYSin12WefqWXLlimllEpLS1N79uxRJ06cUNnZ2VaNd+XKFfXMM8+os2fPKqWUmjZtmtq8ebP63//9X5WXl2d1nEopFRsbqz799FM1ZcoUtXz58nKNVZVIWaCMqlWrxsWLF9m7d+9NHax+//13AgMD6du3L08//TTNmzcvdZzc3Fy+/PJLXnvtNWrUqMHkyZMBqF69erm+dhoMBv7xj39w7733cuPGDe655x5+++03wPp6WcOGDZkzZw4PPPAA//73vzl58iRffvklM2fOZMuWLVaP+9xzz3H//fcDEB0dTVZWVrm+yrq5uZGbm8u///1voHiXh6ZNm5KZmcmuXbusHvfatWv8/vvvAISEhNCzZ08KCwv59ttvrf7sf20r+dJLL7F27Vq+/PJL5syZQ1ZWVpnHc3NzIy8vj/Pnz3Pt2jUOHTpEQkICCxYs4IMPPihXbdfNzY3U1FQGDx7MqVOniImJYdGiRSilMBqNVo/r6qQsUAZKKYqKili8eDHBwcF06dKF5ORkpk+fzvnz57n33nvx9PS06OtSjRo16NSpE23atKFTp04kJiaSmJhIaGhouUoDzZs3p3HjxhiNRmrVqoWmacTExBAUFETDhg2tGtPHx4e77roLgJUrV9K2bVtmz55NZmYmO3fu5JFHHsHDw6NMY/r6+tK8eXNq1qyJwWAgJyeHr7/+mj59+uDh4UFmZmaZx3R3d6egoIDExERyc3P57rvvyM3NpU2bNhw5coRevXqVaTwoToINGjRg/fr1+Pn50bRpU/z8/Lh69Sr79+8nNDTUqq/HtWrVYuHChRw7dow+ffowceJEWrVqxY8//oinp6fZf5z/m7u7O3Xq1CE2NpZvv/2WPn368MYbb+Dt7c3Ro0e55557rP7/36BBA1JTUxk0aBB//vknn376KQEBAfTo0UNKA6WQmWsZaJpGjRo1uH79Ounp6URFRbFu3Truu+8+Jk+ejJ+fX5l+2XQ6HXXq1MHHx4c5c+aQn59vmsH+/PPPJCcnWx1rSYLu1q0bw4YNY9euXTaZaYwdO5Zx48YBMGTIEK5du2bVSbPq1aublqYppfDy8qJu3br4+PiwYcMGFi9eTF5eXpnH7devH926dePgwYPk5eWxcOFCnnjiCTIyMqxeZ9yhQweCgoJISEjg8OHDVK9enf79+5Oens6ZM2esGrNly5ZMnTqVkydPcunSJQCaNWuG0WjkypUrVo0ZFhbG8uXLeeihh0zfCDp37sz169f5888/rRoTihN3SkoKq1evZtWqVTz33HOkpqayatUqq8esCmSdaxmdP3/e9FV49OjRPProozZpF1i/fn3mzJnDW2+9RVhYGEajkZUrV9ogYrjvvvv4/PPPGT16dLl2OVD/tfXM1q1bycjIMG22aC03Nzfc3Nxo3LgxixYtYu/evcTExFCrVq0yj+Xl5cWAAQPo16+f6R+Y+Pj4cvVycHd3p3///miaxscff8z58+epWbMmGRkZ5bqMuVu3bkRHR/Pee+/RpEkTAE6fPs2YMWOsHrNu3bp06tSJLVu2UKNGDfLz87l06VK5TprpdDr8/Pz44IMPmDlzJsHBwRw4cKDMs+sqx2HV3kosJydH5ebm3nSf0Wi0ydjLly9Xjz76qDpz5oxNxisRHR2tLl68aJOx8vPz1erVq1Xfvn1NJ1DKw2g0qvz8fNWrVy/VvXt3lZKSUv4g/yMuLk716dPHJj/P/Px8tX//fjVhwgQ1depU08m48vrpp5/UokWLVExMjE3izMrKUitWrFDDhw9XzzzzjPrll1/KPebly5fVjz/+aLpdcmJX3Jk0bnEiWVlZTJgwgalTp5o2KiwvZYeNDgsLC9m3bx/NmjXD39/fZuOuW7eOtm3bcu+999pszD///JOioiKbzrIMBgOapjl9V7OSMogtrwy0x++Tq5Lk6mSq8nYv8ocrXIkkVyGEsAPn/l4jhBCVlCRXIYTTy8/PJzs729FhlEmVXoqVlPgDmZfLfjWMEOJm9ZvUpVuvrnYb/1JyLLkFLbi/bWi5lhNWpCqdXDMvZ7F05ApHhyFEpffiipF2G/vGjRsUGb3QeX2LPnkXTQL/Ybf3siUpCwghnNrllGX4ea3Hp/YuruY9YvP2nPYiyVUI4bRKZq1e7r9QTSuiYZ1E9MmvOTosi0hyFUI4rZJZa4nKNHuV5CqEcEp/nbWWqEyzV0muQgin9N+z1hKVZfbqsOQaHBx8U2u1gwcP8vzzzwOY2vj9tZ1bv379TK3Z/vran376ieDgYE6fPl2B0Qsh7Ol2s9YSlWX2WqHJtaCgwOKO6H5+fnz00UelPufMmTNER0ezePFi7r//fnJycqQzuhAu4E6z1hKVYfZaIck1OTmZN998k7CwMC5cuGDRa3r06MG5c+c4f/78bR8/f/48L7zwAv/85z954IEHADh69ChhYWG89957XL582VbhCyEqUF5eHkVG79vOWktU04poUDvRtKWPM7Jbcs3NzWXt2rU8+eSTzJgxg4CAADZs2GDqkG42sGrVGD16NB9//PFtHx83bhwzZ86kQ4cOpvt69OjBqlWr8PLyYuzYsTz77LN89913Nt22WQhhXwUFBdSukWL2ebVrnic/P78CIrKO3a7QCgoKomXLlsybN4+AgACLXvPf7eb69evHhx9+yMWLF295bufOnYmLiyMoKOimy+F8fHyIiooiKiqK48eP89prr/HBBx/w7bfflu8DCSEqjEJhpPQSn8K5G/rZbea6ZMkSdDod48ePZ+nSpbfs4VOvXr2bGjFkZWXdsme9m5sbzzzzDJ988skt48+cOROAOXPm3PLYuXPn+Mc//sHUqVP5n//5H+bNm2eLjySEqCBKgUEZzR7OzG7JNSgoiMWLF/PVV1/h5eXFuHHjiIqKMp3x79ixIwkJCUBxZ/cNGzbQsWPHW8YZPHgw+/fvv2XTNk3TWLRoEefPn+fdd98Fijf1GzZsGDNmzMDf35/169czf/582rVrZ6+PKYSwAyNGijCUehjMzGwdze6NW+rXr8/IkSMZOXIkp06dMn2FHzduHLNnz2bAgAEopejatSsDBgy45fU1a9ZkxIgRzJ8//5bH3N3d+fDDD3nqqado2LAhnTp1IiYmxuIyhBDCORkBg5k+/kalwIk3rqjSOxEkfLFRumIJYQMvrhjJwBH9bDJWdnY26Zf/SUPv0v828wpakK997rS70FbploNCCOekAIOZE1ZGJz+hJclVCOF0jEpRaOaEVZEkVyGEKBsFZk9XGXHqkqskVyGE81Eoi8oCzrzhiyRXG9t6+YTNx+zdpL3NxxTCmRWvFjDzHAXVnXjqKslVCOF0jGgUmvnSb0CjRgXFYw1JrkIIp6MonpmWxtzjjibJVQjhdIqXYpU+c3Xu67MkuQohnJBBaaBKvzpfmXnc0SS5CiGcjkIzO3NVTr0QS5KrEMIJKTSMZvtKSXIVQogyKT6hZSZ5mnvcwZy7aFEOr776Kp07d6ZfP9s0kxBCVByD0ihQ1Us9ipz6EgIXTq5Dhgxh2bJljg5DCGGFkrJA6YfMXB3i4Ycfpm7duo4OQwhhhZITWqUdliTX232DvXr1KqNGjSI0NJRRo0aRlZVV/J5KMW/ePEJCQujfvz8///yz6TXr168nNDSU0NBQ1q+/8660f+WyyVUIUXkZ0ChU1Us9iixYinW7b7CxsbF07tyZbdu20blzZ2JjYwFISkriwoULbNu2jblz5zJ79mygOBkvXbqU1atXExcXx9KlS00JuTSSXIUQTqd45lrN7GHO7b7BJiYmMmjQIAAGDRrEjh07brpf0zTat29f3LQ7PZ09e/bQpUsX6tWrR926denSpQs//PCD2feW1QJCCKejlIbBzMy0mqpGcnIyEydONN0XGRlJZGRkqa/LyMjA19cXgEaNGpGRkQGAXq/Hz8/P9Dw/Pz/0ev0t9+t0OvR6vdnPIMlVCOF0LFnnakQjICCAdevWWf0+mqahafY5MeayZYFJkybxxBNPkJKSQrdu3YiLi3N0SEIICxmwYCmWlZe/NmjQgPT0dADS09Px8fEBimekaWlppuelpaWh0+luuV+v16PT6cy+j8sm17fffps9e/bw888/k5SUREREhKNDEkJYSCkNo6pm9rBGcHAw8fHxAMTHx9OrV6+b7ldKceLECby8vPD19SUoKIg9e/aQlZVFVlYWe/bsISgoyOz7SFlACOF0Sk5olcb85bHF32APHTpEZmYm3bp1Y/z48YwZM4YJEyawZs0amjRpwuLFiwHo3r07u3fvJiQkBA8PDxYsWABAvXr1GDduHEOHDgXghRdeoF69embfW5KrEMLpGNGKO2OVwmDBOG+//fZt71+x4tZtuzVNY9asWbd9/tChQ03J1VKSXIUQTseoNApV6enJzczjjubc0QkhqiRlwRVYTr4RgSRXW7PHZoKvJP9o8zEB/hnQ1vaD2mNZi3L2PyNhawrz61zNPe5oklyFEE7H+J/LX0tj7VKsiiLJVQjhdIw2Wi3gSJJchRBOp2Sda2msXedaUSS5CiGcjuz+KoQQdmCkmvmaq5PvRCDJVQjhdCwrCzj3TgSSXIUQTsdowVIsqbk6yPnz52/q83jx4kWio6OJiopyXFBCCIsoMHsRgbPvoeWyydXf35+EhAQADAYD3bp1IyQkxMFRCSEsYVQahcbSa6oGo8xcHW7//v00a9aMpk2bOjoUIYQFLOmKZck2L45UJZLrpk2bbtr9UQjh3IpPaFXu3gLOnfptoKCggJ07dxIWFuboUIQQFjJatPurLMVyqKSkJFq3bk3Dhg0dHYoQwkKWzFxlKZaDbdq0ifDwcEeHIYQoA0XlX+fq0mWB3Nxc9u3bR2hoqKNDEUKUgZHiy19LO2QplgPVrl2bgwcPOjoMIUQZKaWZranKagEhhCgjS3YikJmrEEKUkRHMblAoyVUIIcrIosYtUhYQQoiyUWhmt3Ex1+/V0SS5CiGcjkVXaNljM0wbkuRaCdhll1Yg+twZm4+5pMV9Nh9TVD0K8y0FpeYqhBBlZFSWlAWcu+bq3NEJIaqk4iu0zB+W+PzzzwkPD6dfv35MmjSJ/Px8Ll68SEREBCEhIUyYMIGCggKguBfJhAkTCAkJISIigkuXLln9GSS5CiGcjlKYTazKguSq1+tZuXIla9euZePGjRgMBjZt2sTChQuJiopi+/bteHt7s2bNGgDi4uLw9vZm+/btREVFsXDhQqs/gyRXIYTTsWjmauFYBoOBvLw8ioqKyMvLo1GjRhw4cIDevXsDMHjwYBITEwHYuXMngwcPBqB3797s378fpaxrbig1VyGE07Go5mrBHlo6nY5nnnmGnj174u7uTpcuXWjdujXe3t64uRWnPz8/P/R6PVA8023cuDEAbm5ueHl5kZmZiY+PT5k/gyRXIYTTKV4tYL7lYHJy8k175UVGRhIZGWm6nZWVRWJiIomJiXh5efHSSy/xww8/2Cvsm7hscs3Pz2f48OEUFBRgMBjo3bs30dHRjg5LCGGBkrJAqc9RGgEBAaxbt+6Oz9m3bx933XWXaeYZGhrKsWPHyM7OpqioCDc3N9LS0tDpdEDxTDc1NRU/Pz+KiorIycmhfv36Vn0Gl6251qxZkxUrVrBhwwbi4+P54YcfOHHihKPDEkJYwoITWkYLSqFNmjTh5MmT3LhxA6UU+/fvp0WLFnTs2JGtW7cCsH79eoKDgwEIDg5m/fr1AGzdupVOnTqhWXmxgsvOXDVNo06dOgAUFRVRVFRk9Q9JCFGxjEozu7urUTM/N2zXrh29e/dm8ODBuLm50apVKyIjI+nRowcTJ05k8eLFtGrVioiICACGDh3KlClTCAkJoW7durzzzjtWfwaXTa5QfJZwyJAh/PHHH/ztb3+jXbt2jg5JCGEBhfkrsCy9Qis6OvqWkmCzZs1My6/+yt3dnSVLllgcZ2lctiwAUL16dRISEti9ezenTp3i119/dXRIQggLWLIUy5J1ro7k0sm1hLe3Nx07dqyws4RCiPJR/ykLlH5IcnWIK1eukJ2dDUBeXh779u3D39/fwVEJISyiihNsqYc0bnGM9PR0pk2bhsFgQClFWFgYPXv2dHRYQggLWLoUy5m5bHK97777iI+Pd3QYQggrKFV8mHuOM7tjcn3wwQdNS5dKrq3VNA2lFJqmcezYsYqJUAhR5Sg0s5e3WtoVy1HumFyPHz9ekXEIIYSJpZe/OjOLTmgdOXKEtWvXAsUnii5evGjXoIQQVVtJWaDUw9FBmmE2uS5dupRly5YRGxsLQGFhIVOmTLF7YEKIqs3cagEq+8x1+/btfPjhh3h4eADFjQ2uX79u98CEEFWXK6xzNbtaoEaNGmiaZjq5lZuba/egxH+xU08Ee2wm+GryKZuPGRPwgM3HFM7NotUCFROK1cwm1z59+jBz5kyys7NZvXo1a9euZdiwYRURmxCiyrLg8lYnLwuYTa7PPvsse/fupU6dOqSkpBAdHU2XLl0qIjYhRBVVsodWaZx9tYBFFxEEBgaSl5eHpmkEBgbaOyYhRBWnLJi5OvsVWmZPaMXFxREREcH27dvZunUrkZGRt23VJYQQNqMsPJyY2ZnrsmXLWL9+vWmrg8zMTJ544gmGDh1q9+CEEFWX2ZlrZW/cUr9+fVNHf4A6depYvaeMEEJYQikNo5mlVsrSvbUd5I7Jdfny5QDcfffdDBs2jF69eqFpGomJibRs2bLCAhRCVFGuulqg5EKBu+++m7vvvtt0f69evewfVTmlpqbyyiuvkJGRgaZpDBs2jJEjRzo6LCGEpVy5K9aLL75YkXHYVPXq1Zk2bRqtW7fm2rVrPP7443Tp0oUWLVo4OjQhhKWcPHmaY7bmeuXKFT755BPOnTtHfn6+6f6VK1faNbDy8PX1xdfXFwBPT0/8/f3R6/WSXIWoJJTSUGZrrs5dFjC7FGvy5Mn4+/tz6dIlXnzxRZo2bUrbtm0rIjabuHTpEr/88ovs/CpEZWLJNi9OXnM1m1yvXr1KREQEbm5uPPLII8TExHDgwIGKiK3crl0WQkcAABE5SURBVF+/TnR0NK+99hqenp6ODkcIURauvs7Vza34Kb6+vuzatQtfX1+ysrLsHlh5FRYWEh0dTf/+/QkNDXV0OEKIslBYsBrAuWeuZpPr2LFjycnJYerUqcydO5fr16/z6quvVkRsVlNKMX36dPz9/Rk1apSjwxFCWMPczLSyz1xLdkz18vLiiy++sHtAtnD06FESEhIIDAxk4MCBAEyaNInu3bs7ODIhhGUsaIZdWZPr3LlzTT1cb2fGjBl2CcgWOnTowNmzZx0dhhDCSpb0c620ybVNmzYVGYcQQvwfBZhbamXhaoHs7GxmzJjBr7/+iqZpLFiwgHvuuYeJEyfy559/0rRpUxYvXkzdunVRSjF//nx2795NrVq1ePPNN2ndurVVH+GOyXXw4MFWDSiEEOWlAZqNZq7z58+na9euLFmyhIKCAvLy8vjoo4/o3LkzY8aMITY2ltjYWKZMmUJSUhIXLlxg27ZtnDx5ktmzZxMXF2fVZ7Bo91chhKhQNmo5mJOTw+HDh01d/GrWrIm3tzeJiYkMGjQIgEGDBrFjxw4A0/2aptG+fXuys7NJT0+36iNY1CxbCCEqliUntDSSk5OZOHGi6a7IyEgiIyNNty9duoSPjw+vvvoqZ86coXXr1kyfPp2MjAzTVZyNGjUiIyMDAL1ej5+fn+n1fn5+6PV603PLQpKrsCl7bCY45tfzNh8zNtDf5mMKG1KAuZaCCgICAli3bt0dn1JUVMTp06d5/fXXadeuHfPmzSM2Nvam5/x1A1ZbcsnVAkKISs6Sr/0WlAX8/Pzw8/MzXf4eFhZGbGwsDRo0ID09HV9fX9LT0/Hx8QFAp9ORlpZmen1aWho6nc6qjyCrBYQQzskG/VwbNWqEn58f58+fx9/fn/379xMQEEBAQADx8fGMGTOG+Ph4UyvV4OBgvvzyS8LDwzl58iReXl5WlQRAVgsIIZyRAs1MWcDsaoL/eP3115k8eTKFhYU0a9aMmJgYjEYjEyZMYM2aNTRp0oTFixcD0L17d3bv3k1ISAgeHh4sWLDA6o/gki0HhRCiRKtWrW5bl12xYsUt92maxqxZs2zyvi7fclAIUfmUrHM1dzgzl245KISopJRm2eHEXLbloBCiErNkKVZl3f21RGVsOQjF9ZS4uDiUUkRERBAVFeXokIQQZWDua79zz1tdtOXgr7/+SlxcHHFxcdSoUYPRo0fTs2dPmjdv7ujQhBCWsNE6V0cym1zvNEuNiYmxeTC2kpyczAMPPICHhwcADz/8MNu2beO5555zcGRCCIu5enLt0aOH6b/z8/PZsWOH1YtqK0pgYCCLFy8mMzOTWrVqkZSUJBdFCFGZKNDMtBw0tw7W0cwm1969e990u1+/fvztb3+zW0C2EBAQwOjRo3n22Wfx8PDgvvvuo1o1aQAmRKVRCTYgNKfMjVsuXLhg6iDjzCIiIoiIiADg7bfftvr6YCFExbNlP1dHMZtcH3zwwZsauDRq1IjJkyfbNShbyMjIoEGDBly+fJlt27axevVqR4ckhLCUJZe/VvaywPHjxysiDpsbP348V69exc3NjVmzZuHt7e3okIQQZeHqM9eRI0fecg3u7e5zNv/6178cHYIQwlquXHPNz8/nxo0bZGZmkpWVhfrPVozXrl1Dr9dXWIBCiKrHkpqrs/cWuGNyXbVqFStWrCA9PZ0hQ4aYkqunpydPPfVUhQUohKiCXPkigpEjRzJy5Ei++OILRowYUZExCSFEpZ+5ml38Wa1aNbKzs023s7Ky+Oqrr+walBCiirPR7q+OZPaE1urVqxk+fLjpdt26dYmLi7vpPmFnysl/i+zMHpsJPn32os3HXNmymc3HrNIq+a+92eRqNBpRSpnWuhoMBgoLC+0emBCi6tKqwjrXoKAgJkyYwBNPPAEUn+jq2rWr3QMTQlRtLn+F1pQpU/jmm2/4+uuvAXj00UcZNmyY3QMTQlRhlaCmao5FJ7SefPJJlixZwpIlS2jRogVz586tiNiEEFVUSVnA3OHMLGrccvr0aTZu3MiWLVto2rQpoaGh9o5LCFHVuWpZICUlhU2bNrFx40bq169P3759UUpVmt0IhBCVmCtfRNCnTx86dOjAxx9/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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 2 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"# Confustion matrix by model\\n\",\n    \"confusion_matrix(\\n\",\n    \"  test_stats_list,\\n\",\n    \"  train_metadata_json,\\n\",\n    \"  'label',\\n\",\n    \"  [10,10,10],\\n\",\n    \"  False,\\n\",\n    \"  model_names=models_list,\\n\",\n    \"  output_directory='./viz2',\\n\",\n    \"  file_format='png'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.7.8\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/ray/kubernetes/README.md",
    "content": "## Running on Kubernetes\n\n### Connect to k8s cluster with a Ray operator\n\nYou should now be pointing to your cluster with `kubectl`. Check the nodes to make sure you're connected correctly:\n\n```\nkubectl get nodes\n```\n\nWe recommend using the [Kuberay](https://github.com/ray-project/kuberay) implementation of the Ray Operator to launch Ray clusters.\n\n### Configure the Ray cluster\n\nFirst choose your preferred cluster template from `clusters`, for example:\n\n```\nexport CLUSTER_NAME=ludwig-ray-cpu-cluster\n```\n\n### Start the cluster\n\n```\n./utils/ray_up.sh $CLUSTER_NAME\n```\n\n### Submit a script for execution\n\n```\n./utils/submit.sh $CLUSTER_NAME scripts/train.py\n```\n\n### SSH into the head node\n\n```\n./utils/attach.sh $CLUSTER_NAME\n```\n\n### Run the Ray Dashboard\n\n```\n./utils/dashboard.sh $CLUSTER_NAME\n```\n\nNavigate to http://localhost:8267\n\n### (For Ludwig Developers) Sync local Ludwig repo\n\n```\n./utils/rsync_up.sh $CLUSTER_NAME ~/repos/ludwig\n```\n\n### Shutdown the cluster\n\n```\n./utils/ray_down.sh $CLUSTER_NAME\n```\n\n### Connecting to remote filesystems (S3, GCS, etc.)\n\nBuild a custom Docker image deriving from `ludwig-ray` or `ludwig-ray-gpu` containing the library needed for your\ndata:\n\n- `s3fs`\n- `adlfs`\n- `gcsfs`\n\nSet 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).\n\nYou could also include the credentials directly into the Docker image if they don't need to be configured at runtime.\n"
  },
  {
    "path": "examples/ray/kubernetes/clusters/ludwig-ray-cpu-cluster.yaml",
    "content": "apiVersion: ray.io/v1alpha1\nkind: RayCluster\nmetadata:\n  labels:\n    controller-tools.k8s.io: \"1.0\"\n  name: ludwig-ray-cpu-cluster\nspec:\n  rayVersion: \"2.3.1\"\n  headGroupSpec:\n    serviceType: ClusterIP\n    replicas: 1\n    rayStartParams:\n      port: \"6379\"\n      metrics-export-port: \"8080\"\n      node-manager-port: \"22346\"\n      object-manager-port: \"22345\"\n      object-store-memory: \"200000000\"\n      redis-password: \"LetMeInRay\"\n      dashboard-host: \"0.0.0.0\"\n      node-ip-address: $MY_POD_IP\n      block: \"true\"\n    template:\n      metadata:\n        labels:\n          rayCluster: ludwig-ray-cpu-cluster\n          rayNodeType: head\n          groupName: headgroup\n        annotations:\n          key: value\n      spec:\n        volumes:\n          - emptyDir:\n              medium: Memory\n            name: dshm\n        containers:\n          - name: ray-head\n            image: ludwigai/ludwig-ray:master\n            lifecycle:\n              preStop:\n                exec:\n                  command:\n                    - /bin/sh\n                    - -c\n                    - ray stop\n            env:\n              - name: MY_POD_IP\n                valueFrom:\n                  fieldRef:\n                    fieldPath: status.podIP\n            ports:\n              - containerPort: 6379\n                name: redis\n                protocol: TCP\n              - containerPort: 10001\n                name: client\n                protocol: TCP\n              - containerPort: 8265\n                name: dashboard\n                protocol: TCP\n              - containerPort: 8000\n                name: ray-serve\n                protocol: TCP\n              - containerPort: 8080\n                name: metrics\n                protocol: TCP\n            resources:\n              limits:\n                cpu: \"8\"\n                memory: 16Gi\n              requests:\n                cpu: \"4\"\n                memory: 8Gi\n            securityContext:\n              capabilities:\n                add:\n                  - SYS_PTRACE\n  workerGroupSpecs:\n    - replicas: 1\n      minReplicas: 1\n      maxReplicas: 1\n      groupName: worker-cpu\n      rayStartParams:\n        redis-password: \"LetMeInRay\"\n        node-ip-address: $MY_POD_IP\n        block: \"true\"\n      template:\n        metadata:\n          labels:\n            rayCluster: ludwig-ray-cpu-cluster\n            rayNodeType: worker\n            groupName: worker-cpu\n          annotations:\n            key: value\n        spec:\n          volumes:\n            - emptyDir:\n                medium: Memory\n              name: dshm\n          initContainers:\n            - name: init-myservice\n              image: busybox:1.28\n              command:\n                [\n                  \"sh\",\n                  \"-c\",\n                  \"until nslookup $RAY_IP.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done\",\n                ]\n          containers:\n            - name: machine-learning\n              image: ludwigai/ludwig-ray:master\n              lifecycle:\n                preStop:\n                  exec:\n                    command:\n                      - /bin/sh\n                      - -c\n                      - ray stop\n              env:\n                - name: MY_POD_NAME\n                  valueFrom:\n                    fieldRef:\n                      fieldPath: metadata.name\n                - name: MY_POD_IP\n                  valueFrom:\n                    fieldRef:\n                      fieldPath: status.podIP\n              ports:\n                - containerPort: 80\n                  protocol: TCP\n              resources:\n                limits:\n                  cpu: \"8\"\n                  memory: 16Gi\n                requests:\n                  cpu: \"4\"\n                  memory: 8Gi\n"
  },
  {
    "path": "examples/ray/kubernetes/clusters/ludwig-ray-gpu-cluster.yaml",
    "content": "apiVersion: ray.io/v1alpha1\nkind: RayCluster\nmetadata:\n  labels:\n    controller-tools.k8s.io: \"1.0\"\n  name: ludwig-ray-gpu-cluster\nspec:\n  rayVersion: \"2.3.1\"\n  headGroupSpec:\n    serviceType: ClusterIP\n    replicas: 1\n    rayStartParams:\n      port: \"6379\"\n      metrics-export-port: \"8080\"\n      node-manager-port: \"22346\"\n      object-manager-port: \"22345\"\n      object-store-memory: \"200000000\"\n      redis-password: \"LetMeInRay\"\n      dashboard-host: \"0.0.0.0\"\n      node-ip-address: $MY_POD_IP\n      block: \"true\"\n    template:\n      metadata:\n        labels:\n          rayCluster: ludwig-ray-gpu-cluster\n          rayNodeType: head\n          groupName: headgroup\n        annotations:\n          key: value\n      spec:\n        volumes:\n          - emptyDir:\n              medium: Memory\n            name: dshm\n        containers:\n          - name: ray-head\n            image: ludwigai/ludwig-ray-gpu:master\n            lifecycle:\n              preStop:\n                exec:\n                  command:\n                    - /bin/sh\n                    - -c\n                    - ray stop\n            env:\n              - name: MY_POD_IP\n                valueFrom:\n                  fieldRef:\n                    fieldPath: status.podIP\n            ports:\n              - containerPort: 6379\n                name: redis\n                protocol: TCP\n              - containerPort: 10001\n                name: client\n                protocol: TCP\n              - containerPort: 8265\n                name: dashboard\n                protocol: TCP\n              - containerPort: 8000\n                name: ray-serve\n                protocol: TCP\n              - containerPort: 8080\n                name: metrics\n                protocol: TCP\n            resources:\n              limits:\n                cpu: \"8\"\n                memory: 16Gi\n                nvidia.com/gpu: \"1\"\n              requests:\n                cpu: \"4\"\n                memory: 8Gi\n            securityContext:\n              capabilities:\n                add:\n                  - SYS_PTRACE\n  workerGroupSpecs:\n    - replicas: 1\n      minReplicas: 1\n      maxReplicas: 1\n      groupName: worker-gpu\n      rayStartParams:\n        redis-password: \"LetMeInRay\"\n        node-ip-address: $MY_POD_IP\n        block: \"true\"\n      template:\n        metadata:\n          labels:\n            rayCluster: ludwig-ray-gpu-cluster\n            rayNodeType: worker\n            groupName: worker-gpu\n          annotations:\n            key: value\n        spec:\n          volumes:\n            - emptyDir:\n                medium: Memory\n              name: dshm\n          initContainers:\n            - name: init-myservice\n              image: busybox:1.28\n              command:\n                [\n                  \"sh\",\n                  \"-c\",\n                  \"until nslookup $RAY_IP.$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace).svc.cluster.local; do echo waiting for myservice; sleep 2; done\",\n                ]\n          containers:\n            - name: machine-learning\n              image: ludwigai/ludwig-ray-gpu:master\n              lifecycle:\n                preStop:\n                  exec:\n                    command:\n                      - /bin/sh\n                      - -c\n                      - ray stop\n              env:\n                - name: MY_POD_NAME\n                  valueFrom:\n                    fieldRef:\n                      fieldPath: metadata.name\n                - name: MY_POD_IP\n                  valueFrom:\n                    fieldRef:\n                      fieldPath: status.podIP\n              ports:\n                - containerPort: 80\n                  protocol: TCP\n              resources:\n                limits:\n                  cpu: \"8\"\n                  memory: 16Gi\n                  nvidia.com/gpu: \"1\"\n                requests:\n                  cpu: \"4\"\n                  memory: 8Gi\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/attach.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\nhead_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1)\nkubectl exec -it $head_pod -- /bin/bash\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/dashboard.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\nhead_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1)\nkubectl port-forward ${head_pod} 8267:8265\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/krsync.sh",
    "content": "#!/bin/bash\n\n# https://serverfault.com/a/887402\n\nif [ -z \"$KRSYNC_STARTED\" ]; then\n    export KRSYNC_STARTED=true\n    exec rsync --blocking-io --rsh \"$0\" $@\nfi\n\n# Running as --rsh\nnamespace=''\npod=$1\nshift\n\n# If use uses pod@namespace rsync passes as: {us} -l pod namespace ...\nif [ \"X$pod\" = \"X-l\" ]; then\n    pod=$1\n    shift\n    namespace=\"-n $1\"\n    shift\nfi\n\nexec kubectl $namespace exec -i $pod -- \"$@\"\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/ray_down.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\nkubectl delete -f clusters/$cluster_name.yaml\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/ray_up.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\nkubectl apply -f clusters/$cluster_name.yaml\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/rsync_up.sh",
    "content": "#!/bin/bash\n\n# Example: ./rsync_up.sh cluster-name ~/repos/ludwig\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\nludwig_local_dir=$2\n\npods=$(kubectl get pods --no-headers -o custom-columns=\":metadata.name\" | grep ${cluster_name}-)\nscript_full_path=$(dirname \"$0\")\n\necho \"Rsync to head and workers...\"\nfor pod in $pods\ndo\n    echo \"Rsync to pod: $pod\"\n    ${script_full_path}/krsync.sh -a --progress --stats ${ludwig_local_dir}/ludwig/ ${pod}:/home/ray/anaconda3/lib/python3.7/site-packages/ludwig\ndone\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/submit.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\npy_script=$2\n\nhead_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1)\nfname=$(basename $py_script)\n\nkubectl cp $py_script $head_pod:/home/ray/. && kubectl exec -it $head_pod -- python /home/ray/$fname\n"
  },
  {
    "path": "examples/ray/kubernetes/utils/upload.sh",
    "content": "#!/bin/bash\n\ncluster_name=\"${1:-$CLUSTER_NAME}\"\npy_script=$2\n\nhead_pod=$(kubectl get pods | grep $cluster_name-head | cut -d' ' -f1)\nfname=$(basename $py_script)\n\nkubectl cp $py_script $head_pod:/home/ray/.\necho /home/ray/$fname\n"
  },
  {
    "path": "examples/regex_freezing/ecd_freezing_with_regex_training.py",
    "content": "import logging\nimport os\nimport shutil\n\nimport pandas as pd\nimport yaml\nfrom datasets import load_dataset\n\nfrom ludwig.api import LudwigModel\n\n\"\"\"\nTo inspect model layers in the terminal, type: \"ludwig collect_summary -pm resnet18\"\n\nFor some models, a HuggingFace Token will be necessary.\nOnce you obtain one, use \"export HUGGING_FACE_HUB_TOKEN=\"<api_token>\"\" in the terminal.\n\"\"\"\n\ndataset = load_dataset(\"beans\")\ntrain_df = pd.DataFrame(\n    {\"image_path\": [f\"train_{i}.jpg\" for i in range(len(dataset[\"train\"]))], \"label\": dataset[\"train\"][\"labels\"]}\n)\ntest_df = pd.DataFrame(\n    {\"image_path\": [f\"test_{i}.jpg\" for i in range(len(dataset[\"test\"]))], \"label\": dataset[\"test\"][\"labels\"]}\n)\n\nos.makedirs(\"train_images\", exist_ok=True)\nos.makedirs(\"test_images\", exist_ok=True)\n\nfor i, img in enumerate(dataset[\"train\"][\"image\"]):\n    img.save(f\"train_images/train_{i}.jpg\")\nfor i, img in enumerate(dataset[\"test\"][\"image\"]):\n    img.save(f\"test_images/test_{i}.jpg\")\n\ntrain_df[\"image_path\"] = train_df[\"image_path\"].apply(lambda x: os.path.join(\"train_images\", x))\ntest_df[\"image_path\"] = test_df[\"image_path\"].apply(lambda x: os.path.join(\"test_images\", x))\n\ntrain_df.to_csv(\"beans_train.csv\", index=False)\ntest_df.to_csv(\"beans_test.csv\", index=False)\n\n\nconfig = yaml.safe_load(r\"\"\"\ninput_features:\n  - name: image_path\n    type: image\n    encoder:\n      type: resnet\n      use_pretrained: true\n      trainable: true\noutput_features:\n  - name: label\n    type: category\ntrainer:\n  epochs: 1\n  batch_size: 5\n  layers_to_freeze_regex: '(layer1\\.0\\.*|layer2\\.0\\.*)'\n\n    \"\"\")\n\nmodel = LudwigModel(config, logging_level=logging.INFO)\ntrain_stats = model.train(dataset=\"beans_train.csv\", skip_save_model=True)\neval_stats, predictions, output_directory = model.evaluate(dataset=\"beans_test.csv\")\n\nprint(\"Training Statistics: \", train_stats)\nprint(\"Evaluation Statistics: \", eval_stats)\n\nshutil.rmtree(\"train_images\")\nshutil.rmtree(\"test_images\")\nos.remove(\"beans_train.csv\")\nos.remove(\"beans_test.csv\")\n"
  },
  {
    "path": "examples/regex_freezing/llm_freezing_with_regex_training.py",
    "content": "import logging\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\n\"\"\"\nTo inspect model layers in the terminal, type: \"ludwig collect_summary -pm resnet18\"\n\nFor some models, a HuggingFace Token will be necessary.\nOnce you obtain one, use \"export HUGGING_FACE_HUB_TOKEN=\"<api_token>\"\" in the terminal.\n\"\"\"\n\nconfig_str = yaml.safe_load(r\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\n\nadapter:\n  type: lora\n\nprompt:\n  template: |\n    ### Instruction:\n    Generate a concise summary of the following text, capturing the main points and conclusions.\n\n    ### Input:\n    {input}\n\n    ### Response:\n\ninput_features:\n  - name: prompt\n    type: text\n    preprocessing:\n      max_sequence_length: 256\n\n\noutput_features:\n  - name: output\n    type: text\n    preprocessing:\n      max_sequence_length: 256\n\ntrainer:\n  type: finetune\n  layers_to_freeze_regex: (decoder\\.layers\\.22\\.final_layer_norm\\.*)\n  learning_rate: 0.0001\n  batch_size: 5\n  gradient_accumulation_steps: 16\n  epochs: 1\n  learning_rate_scheduler:\n    warmup_fraction: 0.01\n\npreprocessing:\n  sample_ratio: 0.1\n\ngeneration:\n  pad_token_id : 0\n\"\"\")\n\nmodel = LudwigModel(config=config_str, logging_level=logging.INFO)\nresults = model.train(dataset=\"ludwig://alpaca\")\n"
  },
  {
    "path": "examples/semantic_segmentation/camseq.py",
    "content": "import logging\nimport os\nimport shutil\n\nimport pandas as pd\nimport torch\nimport yaml\nfrom torchvision.utils import save_image\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import camseq\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\n# set up Python dictionary to hold model training parameters\nwith open(\"./config_camseq.yaml\") as f:\n    config = yaml.safe_load(f.read())\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config, logging_level=logging.INFO)\n\n# load Camseq dataset\ndf = camseq.load(split=False)\n\npred_set = df[0:1]  # prediction hold-out 1 image\ndata_set = df[1:]  # train,test,validate on remaining images\n\n# initiate model training\ntrain_stats, _, output_directory = model.train(  # training statistics  # location for training results saved to disk\n    dataset=data_set,\n    experiment_name=\"simple_image_experiment\",\n    model_name=\"single_model\",\n    skip_save_processed_input=True,\n)\n\n# print(\"{}\".format(model.model))\n\n# predict\npred_set.reset_index(inplace=True)\npred_out_df, results = model.predict(pred_set)\n\nif not isinstance(pred_out_df, pd.DataFrame):\n    pred_out_df = pred_out_df.compute()\npred_out_df[\"image_path\"] = pred_set[\"image_path\"]\npred_out_df[\"mask_path\"] = pred_set[\"mask_path\"]\n\nfor index, row in pred_out_df.iterrows():\n    pred_mask = torch.from_numpy(row[\"mask_path_predictions\"])\n    pred_mask_path = os.path.dirname(os.path.realpath(__file__)) + \"/predicted_\" + os.path.basename(row[\"mask_path\"])\n    print(f\"\\nSaving predicted mask to {pred_mask_path}\")\n    if torch.any(pred_mask.gt(1)):\n        pred_mask = pred_mask.float() / 255\n    save_image(pred_mask, pred_mask_path)\n    print(\"Input image_path:    {}\".format(row[\"image_path\"]))\n    print(\"Label mask_path:     {}\".format(row[\"mask_path\"]))\n    print(f\"Predicted mask_path: {pred_mask_path}\")\n"
  },
  {
    "path": "examples/semantic_segmentation/config_camseq.yaml",
    "content": "input_features:\n  - name: image_path\n    type: image\n    preprocessing:\n      num_processes: 6\n      infer_image_max_height: 1024\n      infer_image_max_width: 1024\n    encoder: unet\n\noutput_features:\n  - name: mask_path\n    type: image\n    preprocessing:\n      num_processes: 6\n      infer_image_max_height: 1024\n      infer_image_max_width: 1024\n      infer_image_num_classes: true\n      num_classes: 32\n    decoder:\n      type: unet\n      num_fc_layers: 0\n    loss:\n      type: softmax_cross_entropy\n\ncombiner:\n  type: concat\n  num_fc_layers: 0\n\ntrainer:\n  epochs: 100\n  early_stop: -1\n  batch_size: 1\n  max_batch_size: 1\n"
  },
  {
    "path": "examples/serve/README.md",
    "content": "# Ludwig Model Serve Example\n\nThis 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.\nA 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:\n\n| REST API         | Description                     |\n| ---------------- | ------------------------------- |\n| `/predict`       | Single record prediction        |\n| `/batch_predict` | Prediction for batch of records |\n\n### Preparatory Steps\n\n- Run the `simple_model_training.py` example in `examples/titanic`. This should result the following file structures:\n\n```\nexamples/\n    titantic/\n        results/\n            simple_experiment_simple_model/\n                model/\n                description.json\n                training_statistics.json\n```\n\n### Run Model Server Example\n\n- Open two terminal windows\n- In first terminal window:\n  - Ensure current working directory is `examples/serve`\n  - 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`).\n\n```\nludwig serve --model_path ../titanic/results/simple_experiment_simple_model/model\n```\n\nSample start up messages for ludwig model server\n\n```\n███████████████████████\n█ █ █ █  ▜█ █ █ █ █   █\n█ █ █ █ █ █ █ █ █ █ ███\n█ █   █ █ █ █ █ █ █ ▌ █\n█ █████ █ █ █ █ █ █ █ █\n█     █  ▟█     █ █   █\n███████████████████████\nludwig v0.3 - Serve\n\nINFO:     Started server process [4429]\nINFO:     Waiting for application startup.\nINFO:     Application startup complete.\nINFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)\n\n```\n\n- In the second terminal window:\n  - Ensure current working director is `examples/serve`\n  - Run the sample client program\n\n```\npython client_program.py\n```\n\nOutput should look like this\n\n````\nretrieved 1309 records for predictions\nsingle record for prediction:\n {'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}\n\ninvoking REST API /predict for single record...\n\nReceived 1 predictions\nSample predictions:\n   Survived_predictions  Survived_probabilities_False  Survived_probabilities_True  Survived_probability\n0                 False                      0.906132                     0.093868              0.906132\n\ninvoking REST API /batch_predict for entire dataframe...\n\nReceived 1309 predictions\nSample predictions:\n   Survived_predictions  Survived_probabilities_False  Survived_probabilities_True  Survived_probability\n0                 False                      0.906132                     0.093868              0.906132\n1                  True                      0.165714                     0.834286              0.834286\n2                  True                      0.441169                     0.558831              0.558831\n3                  True                      0.228311                     0.771689              0.771689\n4                 False                      0.878072                     0.121928              0.878072```\n````\n"
  },
  {
    "path": "examples/serve/client_program.py",
    "content": "import sys\n\nimport pandas as pd\nimport requests\n\nfrom ludwig.datasets import titanic\n\n# Ludwig model server default values\nLUDWIG_HOST = \"0.0.0.0\"\nLUDWIG_PORT = \"8000\"\n\n\n#\n# retrieve data to make predictions\n#\ntest_df = titanic.load()\nprint(f\"retrieved {test_df.shape[0]:d} records for predictions\")\n\n\n#\n# execute REST API /predict for a single record\n#\n\n# get a single record from dataframe and convert to list of dictionaries\nprediction_request_dict_list = test_df.head(1).to_dict(orient=\"records\")\n\n# extract dictionary for the single record only\nprediction_request_dict = prediction_request_dict_list[0]\n\nprint(\"single record for prediction:\\n\", prediction_request_dict)\n\n# construct URL\npredict_url = \"\".join([\"http://\", LUDWIG_HOST, \":\", LUDWIG_PORT, \"/predict\"])\n\nprint(\"\\ninvoking REST API /predict for single record...\")\n# connect using the default host address and port number\ntry:\n    response = requests.post(predict_url, data=prediction_request_dict)\nexcept requests.exceptions.ConnectionError as e:\n    print(e)\n    print(\"REST API /predict failed\")\n    sys.exit(1)\n\n\n# check if REST API worked\nif response.status_code == 200:\n    # REST API successful\n    # convert JSON response to panda dataframe\n    pred_df = pd.read_json(\"[\" + response.text + \"]\", orient=\"records\")\n\n    print(f\"\\nReceived {pred_df.shape[0]:d} predictions\")\n    print(\"Sample predictions:\")\n    print(pred_df.head())\n\nelse:\n    # Error encountered during REST API processing\n    print(\"\\nError during predictions, error code: \", response.status_code, \"reason code: \", response.text)\n\n#\n# execute REST API /batch_predict on a pandas dataframe\n#\n\n# create json representation of dataset for REST API\nprediction_request_json = test_df.to_json(orient=\"split\")\n\nprint(\"\\ninvoking REST API /batch_predict for entire dataframe...\")\n\n# construct URL\nbatch_predict_url = \"\".join([\"http://\", LUDWIG_HOST, \":\", LUDWIG_PORT, \"/batch_predict\"])\n\n# connect using the default host address and port number\nresponse = requests.post(batch_predict_url, data={\"dataset\": prediction_request_json})\ntry:\n    response = requests.post(batch_predict_url, data={\"dataset\": prediction_request_json})\nexcept requests.exceptions.ConnectionError as e:\n    print(e)\n    print(\"REST API /batch_predict failed\")\n    sys.exit(1)\n\n\n# check if REST API worked\nif response.status_code == 200:\n    # REST API successful\n    # convert JSON response to panda dataframe\n    pred_df = pd.read_json(response.text, orient=\"split\")\n\n    print(f\"\\nReceived {pred_df.shape[0]:d} predictions\")\n    print(\"Sample predictions:\")\n    print(pred_df.head())\n\nelse:\n    # Error encountered during REST API processing\n    print(\"\\nError during predictions, error code: \", response.status_code, \"reason code: \", response.text)\n"
  },
  {
    "path": "examples/synthetic/train.py",
    "content": "\"\"\"Train a model from entirely synthetic data.\"\"\"\n\nimport logging\nimport tempfile\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\n\nconfig = yaml.safe_load(\"\"\"\ninput_features:\n    - name: Pclass (new)\n      type: category\n\noutput_features:\n    - name: Survived\n      type: binary\n\n\"\"\")\n\ndf = build_synthetic_dataset_df(120, config)\nmodel = LudwigModel(config, logging_level=logging.INFO)\n\nwith tempfile.TemporaryDirectory() as tmpdir:\n    model.train(dataset=df, output_directory=tmpdir)\n"
  },
  {
    "path": "examples/tabnet/higgs/medium_config.yaml",
    "content": "input_features:\n  - name: lepton_pT\n    type: number\n  - name: lepton_eta\n    type: number\n  - name: lepton_phi\n    type: number\n  - name: missing_energy_magnitude\n    type: number\n  - name: missing_energy_phi\n    type: number\n  - name: jet_1_pt\n    type: number\n  - name: jet_1_eta\n    type: number\n  - name: jet_1_phi\n    type: number\n  - name: jet_1_b-tag\n    type: number\n  - name: jet_2_pt\n    type: number\n  - name: jet_2_eta\n    type: number\n  - name: jet_2_phi\n    type: number\n  - name: jet_2_b-tag\n    type: number\n  - name: jet_3_pt\n    type: number\n  - name: jet_3_eta\n    type: number\n  - name: jet_3_phi\n    type: number\n  - name: jet_3_b-tag\n    type: number\n  - name: jet_4_pt\n    type: number\n  - name: jet_4_eta\n    type: number\n  - name: jet_4_phi\n    type: number\n  - name: jet_4_b-tag\n    type: number\n  - name: m_jj\n    type: number\n  - name: m_jjj\n    type: number\n  - name: m_lv\n    type: number\n  - name: m_jlv\n    type: number\n  - name: m_bb\n    type: number\n  - name: m_wbb\n    type: number\n  - name: m_wwbb\n    type: number\noutput_features:\n  - name: label\n    type: binary\n    weight_regularization: null\ncombiner:\n  type: tabnet\n  size: 32 # N_a\n  output_size: 96 # N_d\n  sparsity: 0.000001 # lambda_sparse\n  bn_virtual_divider: 32 # factor to divide batch_size B to get B_v from the paper\n  bn_momentum: 0.1 # m_B\n  num_steps: 8 # N_steps\n  relaxation_factor: 2 # gamma\n  bn_virtual_bs: 256 # B_v\ntrainer:\n  batch_size: 8192 # B\n  eval_batch_size: 500000 # 65536 131072 262144 524288\n  epochs: 1000\n  early_stop: 20\n  learning_rate: 0.025\n  optimizer:\n    type: adam\n  learning_rate_scheduler:\n    decay: exponential\n    decay_steps: 10000\n    decay_rate: 0.9\n    staircase: true\n  validation_field: label\n"
  },
  {
    "path": "examples/tabnet/higgs/small_config.yaml",
    "content": "input_features:\n  - name: lepton_pT\n    type: number\n  - name: lepton_eta\n    type: number\n  - name: lepton_phi\n    type: number\n  - name: missing_energy_magnitude\n    type: number\n  - name: missing_energy_phi\n    type: number\n  - name: jet_1_pt\n    type: number\n  - name: jet_1_eta\n    type: number\n  - name: jet_1_phi\n    type: number\n  - name: jet_1_b-tag\n    type: number\n  - name: jet_2_pt\n    type: number\n  - name: jet_2_eta\n    type: number\n  - name: jet_2_phi\n    type: number\n  - name: jet_2_b-tag\n    type: number\n  - name: jet_3_pt\n    type: number\n  - name: jet_3_eta\n    type: number\n  - name: jet_3_phi\n    type: number\n  - name: jet_3_b-tag\n    type: number\n  - name: jet_4_pt\n    type: number\n  - name: jet_4_eta\n    type: number\n  - name: jet_4_phi\n    type: number\n  - name: jet_4_b-tag\n    type: number\n  - name: m_jj\n    type: number\n  - name: m_jjj\n    type: number\n  - name: m_lv\n    type: number\n  - name: m_jlv\n    type: number\n  - name: m_bb\n    type: number\n  - name: m_wbb\n    type: number\n  - name: m_wwbb\n    type: number\noutput_features:\n  - name: label\n    type: binary\n    weight_regularization: null\ncombiner:\n  type: tabnet\n  size: 24 # N_a\n  output_size: 26 # N_d\n  sparsity: 0.000001 # lambda_sparse\n  bn_virtual_divider: 32 # factor to divide batch_size B to get B_v from the paper\n  bn_momentum: 0.4 # m_B\n  num_steps: 5 # N_steps\n  relaxation_factor: 1.5 # gamma\n  bn_virtual_bs: 512 # B_v\ntrainer:\n  batch_size: 16384 # B\n  eval_batch_size: 500000 # 65536 131072 262144 524288\n  epochs: 1000\n  early_stop: 20\n  learning_rate: 0.02\n  optimizer:\n    type: adam\n  learning_rate_scheduler:\n    decay: exponential\n    decay_steps: 20000\n    decay_rate: 0.9\n    staircase: true\n  validation_field: label\n"
  },
  {
    "path": "examples/tabnet/higgs/train_higgs_medium.py",
    "content": "import logging\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import higgs\n\nmodel = LudwigModel(\n    config=\"medium_config.yaml\",\n    logging_level=logging.INFO,\n)\n\nhiggs_df = higgs.load()\nmodel.train(dataset=higgs_df, experiment_name=\"higgs_medium\", model_name=\"higgs_tabnet_medium\")\n"
  },
  {
    "path": "examples/tabnet/higgs/train_higgs_small.py",
    "content": "import logging\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import higgs\n\nmodel = LudwigModel(\n    config=\"small_config.yaml\",\n    logging_level=logging.INFO,\n)\n\nhiggs_df = higgs.load()\nmodel.train(dataset=higgs_df, experiment_name=\"higgs_small\", model_name=\"higgs_tabnet_small\")\n"
  },
  {
    "path": "examples/titanic/README.md",
    "content": "# Kaggle Titanic Survivor Prediction\n\nThis 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.\n\n### Preparatory Steps\n\nCreate and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials).\n\nThe 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.\n\n### Examples\n\n| File                         | Description                                                                    |\n| ---------------------------- | ------------------------------------------------------------------------------ |\n| simple_model_training.py     | Demonstrates using Ludwig api for training a model.                            |\n| multiple_model_training.py   | Trains two models and generates a visualization for results of training.       |\n| model_training_results.ipynb | Example for extracting training statistics and generate custom visualizations. |\n\nEnter `python simple_model_training.py` will train a single model. Results of model training will be stored in this location.\n\n```\n./results/\n    simple_experiment_simple_model/\n```\n\nEnter `python multiple_model_training.py` will train two models and generate standard Ludwig visualizations comparing the\ntwo models. Results will in the following directories:\n\n```\n./results/\n    multiple_model_experiment_model1/\n    multiple_model_experiment_model2/\n./visualizations/\n    learning_curves_Survived_accuracy.png\n    learning_curves_Survived_loss.png\n```\n\nThis is the standard Ludwig learning curve plot from training the two models\n![](../images/learning_curves_Survived_accuracy.png)\n\nThis is the custom visualization created by the Jupyter notebook `model_training_results.ipynb`.\n![](../images/custom_learning_curve.png)\n"
  },
  {
    "path": "examples/titanic/model1_config.yaml",
    "content": "input_features:\n    - name: Pclass\n      type: category\n    - name: Sex\n      type: category\n    - name: Age\n      type: number\n      preprocessing:\n          missing_value_strategy: fill_with_mean\n    - name: SibSp\n      type: number\n    - name: Parch\n      type: number\n    - name: Fare\n      type: number\n      preprocessing:\n          missing_value_strategy: fill_with_mean\n    - name: Embarked\n      type: category\n\noutput_features:\n    - name: Survived\n      type: binary\n"
  },
  {
    "path": "examples/titanic/model2_config.yaml",
    "content": "input_features:\n  - name: Pclass\n    type: category\n  - name: Sex\n    type: category\n  - name: Age\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: SibSp\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: Parch\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: Fare\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: zscore\n  - name: Embarked\n    type: category\n\noutput_features:\n  - name: Survived\n    type: binary\n    fc_layers: [{ output_size: 50 }]\n"
  },
  {
    "path": "examples/titanic/model_training_results.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# Custom Analysis of Training Results\\n\",\n    \"\\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    \"\\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    \"``` \\n\",\n    \"python multiple_model_training.py\\n\",\n    \"```\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Import required libraries\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 1,\n   \"metadata\": {\n    \"pycharm\": {\n     \"is_executing\": false\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from ludwig.utils.data_utils import load_json\\n\",\n    \"from ludwig.visualize import learning_curves\\n\",\n    \"import pandas as pd\\n\",\n    \"import numpy as np\\n\",\n    \"import os.path\\n\",\n    \"import matplotlib.pyplot as plt\\n\",\n    \"import seaborn as sns\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Generate Annotated Learning Curves Using Ludwig Visualization API\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 2,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {\n      \"needs_background\": \"light\"\n     },\n     \"output_type\": \"display_data\"\n    },\n    {\n     \"data\": {\n      \"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\",\n      \"text/plain\": [\n       \"<Figure size 432x288 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"# retrieve training statistics\\n\",\n    \"list_of_stats = []\\n\",\n    \"list_of_models = []\\n\",\n    \"\\n\",\n    \"for model in ['model1', 'model2']:\\n\",\n    \"    experiment_model_dir = './results/multiple_experiment_' + model \\n\",\n    \"    train_stats = load_json(os.path.join(experiment_model_dir,'training_statistics.json'))\\n\",\n    \"    list_of_stats.append(train_stats)\\n\",\n    \"    list_of_models.append(model)\\n\",\n    \"    \\n\",\n    \"\\n\",\n    \"# generating learning curves from training\\n\",\n    \"learning_curves(list_of_stats, 'Survived',\\n\",\n    \"                model_names=list_of_models,\\n\",\n    \"                output_directory='./visualizations',\\n\",\n    \"                file_format='png')\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Generate Annotated Learning Curves Using seaborn package\\n\",\n    \"\\n\",\n    \"### Helper function to collect training statistics\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 3,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# function to generate pandas data frame from training statistcs\\n\",\n    \"# Parameter:\\n\",\n    \"#   experiment_model_dir: directory containing the training statistics for a specific model training experiment\\n\",\n    \"#\\n\",\n    \"# Returns: pandas dataframe containing the performance metric and loss\\n\",\n    \"#\\n\",\n    \"\\n\",\n    \"def extract_training_stats(experiment_model_dir):\\n\",\n    \"    list_of_splits = ['training', 'validation', 'test']\\n\",\n    \"    list_of_df = []\\n\",\n    \"    for split in list_of_splits:\\n\",\n    \"        train_stats = load_json(os.path.join(experiment_model_dir,'training_statistics.json'))\\n\",\n    \"        df = pd.DataFrame(train_stats[split]['combined'])\\n\",\n    \"        df.columns = [split + '_' + c for c in df.columns]\\n\",\n    \"        list_of_df.append(df)\\n\",\n    \"        \\n\",\n    \"    df = pd.concat(list_of_df, axis=1)\\n\",\n    \"    df['epoch'] = df.index + 1\\n\",\n    \"        \\n\",\n    \"    return df\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Retrieve training results\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 4,\n   \"metadata\": {\n    \"pycharm\": {\n     \"is_executing\": false\n    }\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"model1 = extract_training_stats('./results/multiple_experiment_model1')\\n\",\n    \"model1.name = 'model1'\\n\",\n    \"model2 = extract_training_stats('./results/multiple_experiment_model2')\\n\",\n    \"model2.name = 'model2'\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 5,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/html\": [\n       \"<div>\\n\",\n       \"<style scoped>\\n\",\n       \"    .dataframe tbody tr th:only-of-type {\\n\",\n       \"        vertical-align: middle;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe tbody tr th {\\n\",\n       \"        vertical-align: top;\\n\",\n       \"    }\\n\",\n       \"\\n\",\n       \"    .dataframe thead th {\\n\",\n       \"        text-align: right;\\n\",\n       \"    }\\n\",\n       \"</style>\\n\",\n       \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n       \"  <thead>\\n\",\n       \"    <tr style=\\\"text-align: right;\\\">\\n\",\n       \"      <th></th>\\n\",\n       \"      <th>training_loss</th>\\n\",\n       \"      <th>validation_loss</th>\\n\",\n       \"      <th>test_loss</th>\\n\",\n       \"      <th>epoch</th>\\n\",\n       \"    </tr>\\n\",\n       \"  </thead>\\n\",\n       \"  <tbody>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>0</th>\\n\",\n       \"      <td>6.646716</td>\\n\",\n       \"      <td>7.069627</td>\\n\",\n       \"      <td>7.541213</td>\\n\",\n       \"      <td>1</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>1</th>\\n\",\n       \"      <td>6.495552</td>\\n\",\n       \"      <td>6.909096</td>\\n\",\n       \"      <td>7.370474</td>\\n\",\n       \"      <td>2</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>2</th>\\n\",\n       \"      <td>6.315707</td>\\n\",\n       \"      <td>6.718051</td>\\n\",\n       \"      <td>7.167351</td>\\n\",\n       \"      <td>3</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>3</th>\\n\",\n       \"      <td>6.135415</td>\\n\",\n       \"      <td>6.526596</td>\\n\",\n       \"      <td>6.963694</td>\\n\",\n       \"      <td>4</td>\\n\",\n       \"    </tr>\\n\",\n       \"    <tr>\\n\",\n       \"      <th>4</th>\\n\",\n       \"      <td>5.954975</td>\\n\",\n       \"      <td>6.334948</td>\\n\",\n       \"      <td>6.759850</td>\\n\",\n       \"      <td>5</td>\\n\",\n       \"    </tr>\\n\",\n       \"  </tbody>\\n\",\n       \"</table>\\n\",\n       \"</div>\"\n      ],\n      \"text/plain\": [\n       \"   training_loss  validation_loss  test_loss  epoch\\n\",\n       \"0       6.646716         7.069627   7.541213      1\\n\",\n       \"1       6.495552         6.909096   7.370474      2\\n\",\n       \"2       6.315707         6.718051   7.167351      3\\n\",\n       \"3       6.135415         6.526596   6.963694      4\\n\",\n       \"4       5.954975         6.334948   6.759850      5\"\n      ]\n     },\n     \"execution_count\": 5,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    }\n   ],\n   \"source\": [\n    \"model1.head()\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Helper function to generate plot ready data\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 6,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# create pandas dataframe suitable for plotting learning curves\\n\",\n    \"# Parameters\\n\",\n    \"#   train_df_list: list of pandas datatframe containing training statistics\\n\",\n    \"#\\n\",\n    \"# Returns: plot ready pandas dataframe\\n\",\n    \"\\n\",\n    \"def create_plot_ready_data(list_of_train_stats_df):\\n\",\n    \"    # holding ready for plot ready data\\n\",\n    \"    plot_ready_list = []\\n\",\n    \"    \\n\",\n    \"    # consolidate the multiple training statistics dataframes\\n\",\n    \"    for df in list_of_train_stats_df:\\n\",\n    \"        for col in ['training', 'validation']:\\n\",\n    \"            df2 = df[['epoch', col + '_loss']].copy()\\n\",\n    \"            df2.columns = ['epoch', 'loss']\\n\",\n    \"            df2['type'] = col\\n\",\n    \"            df2['model'] = df.name\\n\",\n    \"            plot_ready_list.append(df2)\\n\",\n    \"\\n\",\n    \"    return pd.concat(plot_ready_list, axis=0, ignore_index=True)\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Plot learning curves\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 7,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# create plot ready data\\n\",\n    \"learning_curves = create_plot_ready_data([model1, model2])\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 8,\n   \"metadata\": {},\n   \"outputs\": [\n    {\n     \"data\": {\n      \"text/plain\": [\n       \"Text(0.5, 1.0, 'Learning Curves')\"\n      ]\n     },\n     \"execution_count\": 8,\n     \"metadata\": {},\n     \"output_type\": \"execute_result\"\n    },\n    {\n     \"data\": {\n      \"image/png\": \"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\\n\",\n      \"text/plain\": [\n       \"<Figure size 720x432 with 1 Axes>\"\n      ]\n     },\n     \"metadata\": {},\n     \"output_type\": \"display_data\"\n    }\n   ],\n   \"source\": [\n    \"# Plot learning curves for the different models\\n\",\n    \"fig = plt.figure(figsize=(10,6))\\n\",\n    \"sns.set_style(style='dark')\\n\",\n    \"ax = sns.lineplot(x='epoch', y='loss',\\n\",\n    \"             style='type',\\n\",\n    \"             hue='model',\\n\",\n    \"             data=learning_curves)\\n\",\n    \"ax.set_title('Learning Curves', fontdict={'fontsize': 16})\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": 9,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"fig.savefig('./visualizations/custom_learning_curve.png')\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3 (ipykernel)\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.9.7\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 4\n}\n"
  },
  {
    "path": "examples/titanic/multiple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Multiple Model Training Example\n#\n# This example trains multiple models and extracts training statistics\n\nimport logging\nimport shutil\n\n# ## Import required libraries\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import titanic\nfrom ludwig.visualize import learning_curves\n\n# clean out old results\nshutil.rmtree(\"./results\", ignore_errors=True)\nshutil.rmtree(\"./visualizations\", ignore_errors=True)\n\n# list models to train\nlist_of_model_ids = [\"model1\", \"model2\"]\nlist_of_train_stats = []\n\ntraining_set, _, _ = titanic.load(split=True)\n\n# ## Train models\nfor model_id in list_of_model_ids:\n    print(\">>>> training: \", model_id)\n\n    # Define Ludwig model object that drive model training\n    model = LudwigModel(config=\"./\" + model_id + \"_config.yaml\", logging_level=logging.WARN)\n\n    # initiate model training\n    train_stats, _, _ = model.train(\n        dataset=training_set, experiment_name=\"multiple_model_experiment\", model_name=model_id\n    )\n\n    # save training stats for later use\n    list_of_train_stats.append(train_stats)\n\n    print(\">>>>>>> completed: \", model_id, \"\\n\")\n\n# generating learning curves from training\nlearning_curves(\n    list_of_train_stats,\n    \"Survived\",\n    model_names=list_of_model_ids,\n    output_directory=\"./visualizations\",\n    file_format=\"png\",\n)\n"
  },
  {
    "path": "examples/titanic/simple_model_training.py",
    "content": "#!/usr/bin/env python\n\n# # Simple Model Training Example\n#\n# This example is the API example for this Ludwig command line example\n# (https://ludwig-ai.github.io/ludwig-docs/latest/examples/titanic/).\n\n# Import required libraries\nimport logging\nimport os\nimport shutil\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import titanic\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\n# Download and prepare the dataset\ntraining_set, test_set, _ = titanic.load(split=True)\n\nconfig = yaml.safe_load(\"\"\"\ninput_features:\n    - name: Pclass\n      type: category\n    - name: Sex\n      type: category\n    - name: Age\n      type: number\n      preprocessing:\n          missing_value_strategy: fill_with_mean\n    - name: SibSp\n      type: number\n    - name: Parch\n      type: number\n    - name: Fare\n      type: number\n      preprocessing:\n          missing_value_strategy: fill_with_mean\n    - name: Embarked\n      type: category\n\noutput_features:\n    - name: Survived\n      type: binary\n\n\"\"\")\n\n# Define Ludwig model object that drive model training\nmodel = LudwigModel(config=config, logging_level=logging.INFO)\n\n# initiate model training\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(\n    dataset=training_set, experiment_name=\"simple_experiment\", model_name=\"simple_model\", skip_save_processed_input=True\n)\n\n# list contents of output directory\nprint(\"contents of output directory:\", output_directory)\nfor item in os.listdir(output_directory):\n    print(\"\\t\", item)\n\n# batch prediction\nmodel.predict(test_set, skip_save_predictions=False)\n"
  },
  {
    "path": "examples/twitter_bots/README.md",
    "content": "# Twitter Bots Example\n\nWe'll be using the twitter human-bots dataset which is composed of 37438 rows each corresponding to a Twitter user\naccount. Each row contains 20 feature columns collected via the Twitter API. These features contain multiple data\nmodalities, including the account description and the profile image.\n\nThe target column account_type has two unique values: bot or human. 25013 user accounts were annotated as human\naccounts, the remaining 12425 are bots.\n\n### Preparatory Steps\n\nCreate and download your [Kaggle API Credentials](https://github.com/Kaggle/kaggle-api#api-credentials).\n\nThe Twitter Bots dataset is hosted by Kaggle, Ludwig will need to authenticate you through the Kaggle API to download\nthe dataset.\n\n### Examples\n\nRun `python train_twitter_bots.py` to train a single model.\n\nFor a faster, more lightweight model run `python train_twitter_bots_text_only.py`, which does not use image features.\n\nThis will download the Twitter Bots dataset into the current\ndirectory, train a model, and write results into the following directories:\n\n```\n./outputs/results/\n    api_experiment_run/\n./outputs/visualizations/\n    confusion_matrix__account_type_top2.png\n    confusion_matrix_entropy__account_type_top2.png\n    learning_curves_account_type_accuracy.png\n    learning_curves_account_type_loss.png\n```\n\nAfter training, the script will generate the following plots:\n\n![Account Type Accuracy](images/learning_curves_account_type_loss.png)\n![Account Type Loss](images/learning_curves_account_type_accuracy.png)\n![Account Type Confusion Matrix](images/confusion_matrix__account_type_top2.png)\n"
  },
  {
    "path": "examples/twitter_bots/train_twitter_bots.py",
    "content": "#!/usr/bin/env python\n\"\"\"Trains model on Twitter Bots dataset using default settings.\"\"\"\n\nimport logging\nimport os\nimport shutil\n\nimport yaml\n\nfrom ludwig import datasets\nfrom ludwig.api import LudwigModel\nfrom ludwig.utils.fs_utils import rename\nfrom ludwig.visualize import confusion_matrix, learning_curves\n\nif __name__ == \"__main__\":\n    # Cleans out prior results\n    results_dir = os.path.join(\"outputs\", \"results\")\n    visualizations_dir = os.path.join(\"outputs\", \"visualizations\")\n    shutil.rmtree(results_dir, ignore_errors=True)\n    shutil.rmtree(visualizations_dir, ignore_errors=True)\n\n    # Loads the dataset\n    twitter_bots_dataset = datasets.get_dataset(\"twitter_bots\", cache_dir=\"downloads\")\n    training_set, val_set, test_set = twitter_bots_dataset.load(split=True)\n\n    # Moves profile images into local directory, so relative paths in the dataset will be resolved.\n    if not os.path.exists(\"profile_images\"):\n        rename(os.path.join(twitter_bots_dataset.processed_dataset_dir, \"profile_images\"), \"profile_images\")\n\n    config = yaml.safe_load(\"\"\"\n    input_features:\n      - name: default_profile\n        type: binary\n      - name: default_profile_image\n        type: binary\n      - name: description\n        type: text\n      - name: favourites_count\n        type: number\n      - name: followers_count\n        type: number\n      - name: friends_count\n        type: number\n      - name: geo_enabled\n        type: binary\n      - name: lang\n        type: category\n      - name: location\n        type: category\n      - name: profile_background_image_path\n        type: category\n      - name: profile_image_path\n        type: image\n        preprocessing:\n          num_channels: 3\n      - name: statuses_count\n        type: number\n      - name: verified\n        type: binary\n      - name: average_tweets_per_day\n        type: number\n      - name: account_age_days\n        type: number\n    output_features:\n      - name: account_type\n        type: binary\n        \"\"\")\n\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    train_stats, preprocessed_data, output_directory = model.train(dataset=training_set, output_directory=results_dir)\n\n    # Generates predictions and performance statistics for the test set.\n    test_stats, predictions, output_directory = model.evaluate(\n        test_set, collect_predictions=True, collect_overall_stats=True, output_directory=results_dir\n    )\n\n    confusion_matrix(\n        [test_stats],\n        model.training_set_metadata,\n        \"account_type\",\n        top_n_classes=[2],\n        model_names=[\"\"],\n        normalize=True,\n        output_directory=visualizations_dir,\n        file_format=\"png\",\n    )\n\n    # Visualizes learning curves, which show how performance metrics changed over time during training.\n    learning_curves(\n        train_stats, output_feature_name=\"account_type\", output_directory=visualizations_dir, file_format=\"png\"\n    )\n"
  },
  {
    "path": "examples/twitter_bots/train_twitter_bots_text_only.py",
    "content": "#!/usr/bin/env python\n\"\"\"Trains twitter bots using tabular and text features only, no images.\"\"\"\n\nimport logging\nimport os\nimport shutil\n\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import twitter_bots\nfrom ludwig.visualize import confusion_matrix, learning_curves\n\nif __name__ == \"__main__\":\n    # Cleans out prior results\n    results_dir = os.path.join(\"outputs\", \"results\")\n    visualizations_dir = os.path.join(\"outputs\", \"visualizations\")\n    shutil.rmtree(results_dir, ignore_errors=True)\n    shutil.rmtree(visualizations_dir, ignore_errors=True)\n\n    # Loads the dataset\n    training_set, val_set, test_set = twitter_bots.load(split=True)\n\n    config = yaml.safe_load(\"\"\"\ninput_features:\n  - name: created_at\n    type: date\n    column: created_at\n  - name: default_profile\n    type: binary\n    column: default_profile\n  - name: description\n    type: text\n    column: description\n  - name: favourites_count\n    type: number\n    column: favourites_count\n  - name: followers_count\n    type: number\n    column: followers_count\n  - name: friends_count\n    type: number\n    column: friends_count\n  - name: geo_enabled\n    type: binary\n    column: geo_enabled\n  - name: lang\n    type: category\n    column: lang\n  - name: location\n    type: text\n    column: location\n  - name: screen_name\n    type: text\n    column: screen_name\n  - name: statuses_count\n    type: number\n    column: statuses_count\n  - name: verified\n    type: binary\n    column: verified\n  - name: average_tweets_per_day\n    type: number\n    column: average_tweets_per_day\n  - name: account_age_days\n    type: number\n    column: account_age_days\noutput_features:\n  - name: account_type\n    type: category\n    column: account_type\ntrainer:\n  batch_size: 16\ndefaults:\n  text:\n    preprocessing:\n      tokenizer: space_punct\n      max_sequence_length: 16\nmodel_type: ecd\n        \"\"\")\n\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    train_stats, preprocessed_data, output_directory = model.train(dataset=training_set, output_directory=results_dir)\n\n    # Generates predictions and performance statistics for the test set.\n    test_stats, predictions, output_directory = model.evaluate(\n        test_set, collect_predictions=True, collect_overall_stats=True, output_directory=results_dir\n    )\n\n    confusion_matrix(\n        [test_stats],\n        model.training_set_metadata,\n        \"account_type\",\n        top_n_classes=[2],\n        model_names=[\"\"],\n        normalize=True,\n        output_directory=visualizations_dir,\n        file_format=\"png\",\n    )\n\n    # Visualizes learning curves, which show how performance metrics changed over time during training.\n    learning_curves(\n        train_stats, output_feature_name=\"account_type\", output_directory=visualizations_dir, file_format=\"png\"\n    )\n"
  },
  {
    "path": "examples/wine_quality/README.md",
    "content": "# Ludwig Defaults Config Section Example\n\nDemonstrates how to use Ludwig's defaults section introduced in v0.6.\n\n### Preparatory Steps\n\n- Create `data` directory\n- Download [Kaggle wine quality data set](https://www.kaggle.com/rajyellow46/wine-quality) into the `data` directory. Directory should\n  appear as follows:\n\n```\nwine_quality/\n    data/\n        winequalityN.csv\n```\n\n### Description\n\nJupyter notebook `model_defaults_example.ipynb` demonstrates how to use the defaults section of Ludwig.\nKey features demonstrated in the notebook:\n\n- Training data is prepared for use\n- Programmatically create Ludwig config dictionary from the training data dataframe\n- How to define preprocessing, encoder, decoder and loss sub-sections under the defaults section\n"
  },
  {
    "path": "examples/wine_quality/model_defaults_example.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"import pandas as pd \\n\",\n    \"import numpy as np\\n\",\n    \"\\n\",\n    \"import os\\n\",\n    \"\\n\",\n    \"import shutil\\n\",\n    \"from pprint import pprint\\n\",\n    \"import logging\\n\",\n    \"\\n\",\n    \"from ludwig.api import LudwigModel\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Receive data for training\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"train_df = pd.read_csv('./data/winequalityN.csv')\\n\",\n    \"train_df['quality'] = train_df['quality'].apply(str)\\n\",\n    \"train_df.shape\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# Replace white space in column names with underscore\\n\",\n    \"new_col = []\\n\",\n    \"for i in range(len(train_df.columns)):\\n\",\n    \"    new_col.append(train_df.columns[i].replace(' ', '_'))\\n\",\n    \"    \\n\",\n    \"train_df.columns = new_col\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"train_df.head()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"train_df.describe().T\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"train_df.dtypes\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"train_df['quality'].value_counts().sort_index()\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"cols = list(set(train_df.columns) - set(['quality']))\\n\",\n    \"features = train_df[cols]\\n\",\n    \"\\n\",\n    \"#extract categorical features\\n\",\n    \"categorical_features = []\\n\",\n    \"for p in features:\\n\",\n    \"    if train_df[p].dtype == 'object':\\n\",\n    \"        categorical_features.append(p)\\n\",\n    \"        \\n\",\n    \"print(\\\"categorical features:\\\", categorical_features, '\\\\n')\\n\",\n    \"\\n\",\n    \"# get numerical features\\n\",\n    \"numerical_features = list(set(features) - set(categorical_features))\\n\",\n    \"\\n\",\n    \"print(\\\"numerical features:\\\", numerical_features, \\\"\\\\n\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"for feature in categorical_features:\\n\",\n    \"    print(f\\\"# of distinct values in categorical feature '{feature}' : {train_df[feature].nunique()}\\\")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Create Ludwig Config\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"# template for config\\n\",\n    \"config = {'input_features':[], 'output_features': [], 'trainer':{}}\\n\",\n    \"\\n\",\n    \"# setup input features for categorical features\\n\",\n    \"for p in categorical_features:\\n\",\n    \"    a_feature = {\\n\",\n    \"        'name': p.replace(' ','_'), \\n\",\n    \"        'type': 'category'\\n\",\n    \"    }\\n\",\n    \"    config['input_features'].append(a_feature)\\n\",\n    \"\\n\",\n    \"# setup input features for numerical features\\n\",\n    \"for p in numerical_features:\\n\",\n    \"    a_feature = {\\n\",\n    \"        'name': p.replace(' ', '_'), \\n\",\n    \"        'type': 'number'\\n\",\n    \"    }\\n\",\n    \"    config['input_features'].append(a_feature)\\n\",\n    \"\\n\",\n    \"# set up output variable\\n\",\n    \"config['output_features'].append({'name': 'quality', 'type':'category'})\\n\",\n    \"\\n\",\n    \"# set default preprocessing and encoder for numerical features\\n\",\n    \"config['defaults'] = {\\n\",\n    \"    'number': {\\n\",\n    \"        'preprocessing': {\\n\",\n    \"            'missing_value_strategy': 'fill_with_mean', \\n\",\n    \"            'normalization': 'zscore'\\n\",\n    \"        },\\n\",\n    \"        'encoder': {\\n\",\n    \"            'type': 'dense',\\n\",\n    \"            'num_layers': 2\\n\",\n    \"        },\\n\",\n    \"    },\\n\",\n    \"    'category': {\\n\",\n    \"        'encoder': {\\n\",\n    \"            'type': 'sparse'\\n\",\n    \"        },\\n\",\n    \"        'decoder': {\\n\",\n    \"            'top_k': 2\\n\",\n    \"        },\\n\",\n    \"        'loss': {\\n\",\n    \"            'confidence_penalty': 0.1  \\n\",\n    \"        }\\n\",\n    \"    }\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"# set up trainer\\n\",\n    \"config['trainer'] = {'epochs': 5}\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"pprint(config, indent=2)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Initialize and Train LudwigModel\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"model = LudwigModel(config, backend = 'local', logging_level = logging.INFO)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"### Inspecting Config After Model Initialization\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"pprint(model.config['input_features'], indent=2)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"pprint(model.config['output_features'], indent=2)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"eval_stats, train_stats, _, _ = model.experiment(\\n\",\n    \"    dataset = train_df,\\n\",\n    \"    experiment_name = 'wine_quality'\\n\",\n    \")\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"## Cleanup\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {},\n   \"outputs\": [],\n   \"source\": [\n    \"try:\\n\",\n    \"    shutil.rmtree('./results')\\n\",\n    \"    items = os.listdir('./')\\n\",\n    \"    for item in items:\\n\",\n    \"        if item.endswith(\\\".hdf5\\\") or item.endswith(\\\".json\\\") or item == '.lock_preprocessing':\\n\",\n    \"            os.remove(os.path.join('./', item))\\n\",\n    \"except Exception as e:\\n\",\n    \"    pass \"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3.8.13 64-bit\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.8.13\"\n  },\n  \"orig_nbformat\": 4,\n  \"vscode\": {\n   \"interpreter\": {\n    \"hash\": \"949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1\"\n   }\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 2\n}\n"
  },
  {
    "path": "examples/wmt15/config_large.yaml",
    "content": "input_features:\n  - name: en\n    type: text\n    encoder: bert\n    pretrained_model_name_or_path: bert-base-uncased\n\noutput_features:\n  - name: fr\n    type: text\n    tokenizer: french_tokenize\n"
  },
  {
    "path": "examples/wmt15/config_small.yaml",
    "content": "input_features:\n  - name: en\n    type: text\n    encoder: embed\n\noutput_features:\n  - name: fr\n    type: text\n"
  },
  {
    "path": "examples/wmt15/train_nmt.py",
    "content": "\"\"\"Sample ludwig training code for training an NMT model (en -> fr) on WMT15 (https://www.statmt.org/wmt15/).\n\nThe dataset is rather large (8GB), which can take several minutes to preprocess.\n\"\"\"\n\nimport logging\nimport shutil\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets import wmt15\n\n# clean out prior results\nshutil.rmtree(\"./results\", ignore_errors=True)\n\n# Download and prepare the dataset\ntraining_set = wmt15.load()\n\nmodel = LudwigModel(config=\"./config_small.yaml\", logging_level=logging.INFO)\n\n(\n    train_stats,  # dictionary containing training statistics\n    preprocessed_data,  # tuple Ludwig Dataset objects of pre-processed training data\n    output_directory,  # location of training results stored on disk\n) = model.train(dataset=training_set, experiment_name=\"simple_experiment\", model_name=\"simple_model\")\n"
  },
  {
    "path": "ludwig/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport sys\n\nfrom ludwig.globals import LUDWIG_VERSION as __version__  # noqa\n\nlogging.basicConfig(level=logging.INFO, stream=sys.stdout, format=\"%(message)s\")\n\n# Disable annoying message about NUMEXPR_MAX_THREADS\nlogging.getLogger(\"numexpr\").setLevel(logging.WARNING)\n"
  },
  {
    "path": "ludwig/accounting/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/accounting/used_tokens.py",
    "content": "import torch\n\n\ndef get_used_tokens_for_ecd(inputs: dict[str, torch.Tensor], targets: dict[str, torch.Tensor]) -> int:\n    \"\"\"Returns the number of used tokens for an ECD model.\n\n    The number of used tokens is the total size of the input and output tensors, which corresponds to 1 token for\n    binary, category, and number features, and variable number of tokens for text features, for each example in the\n    batch.\n\n    Args:\n        inputs: The input tensors for one forward pass through ecd.\n        targets: The target tensors for one forward pass through ecd.\n    \"\"\"\n    used_tokens = 0\n    for input_feature_tensor in inputs.values():\n        used_tokens += torch.flatten(input_feature_tensor).shape[0]\n    if targets is not None:\n        # targets may be None for evaluation.\n        for output_feature_tensor in targets.values():\n            used_tokens += torch.flatten(output_feature_tensor).shape[0]\n    return used_tokens\n\n\ndef get_used_tokens_for_llm(model_inputs: torch.Tensor, tokenizer) -> int:\n    \"\"\"Returns the number of used tokens for an LLM model.\n\n    Args:\n        model_inputs: torch.Tensor with the merged input and target IDs.\n        tokenizer: The tokenizer used to encode the inputs.\n\n    Returns:\n        The total number of non-pad tokens, for all examples in the batch.\n    \"\"\"\n    return torch.sum(model_inputs != tokenizer.pad_token_id).item()\n"
  },
  {
    "path": "ludwig/api.py",
    "content": "# !/usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\"\"\"\nFile name: LudwigModel.py\nAuthor: Piero Molino\nDate created: 5/21/2019\nPython Version: 3+\n\"\"\"\n\nimport copy\nimport dataclasses\nimport logging\nimport os\nimport sys\nimport tempfile\nimport time\nimport traceback\nfrom collections import OrderedDict\nfrom dataclasses import dataclass\nfrom pprint import pformat\nfrom typing import Any, ClassVar\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom tabulate import tabulate\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.backend import Backend, initialize_backend, provision_preprocessing_workers\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import (\n    AUTO,\n    BATCH_SIZE,\n    EVAL_BATCH_SIZE,\n    FALLBACK_BATCH_SIZE,\n    FULL,\n    HYPEROPT,\n    HYPEROPT_WARNING,\n    MIN_DATASET_SPLIT_ROWS,\n    MODEL_ECD,\n    MODEL_LLM,\n    TEST,\n    TIMESERIES,\n    TRAINING,\n    VALIDATION,\n)\nfrom ludwig.data.cache.types import CacheableDataset\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.data.postprocessing import convert_predictions, postprocess\nfrom ludwig.data.preprocessing import load_metadata, preprocess_for_prediction, preprocess_for_training\nfrom ludwig.datasets import load_dataset_uris\nfrom ludwig.features.feature_registries import update_config_with_metadata, update_config_with_model\nfrom ludwig.globals import (\n    LUDWIG_VERSION,\n    MODEL_FILE_NAME,\n    MODEL_HYPERPARAMETERS_FILE_NAME,\n    MODEL_WEIGHTS_FILE_NAME,\n    set_disable_progressbar,\n    TRAIN_SET_METADATA_FILE_NAME,\n    TRAINING_CHECKPOINTS_DIR_PATH,\n)\nfrom ludwig.models.base import BaseModel\nfrom ludwig.models.calibrator import Calibrator\nfrom ludwig.models.inference import InferenceModule, save_ludwig_model_for_inference\nfrom ludwig.models.predictor import (\n    calculate_overall_stats,\n    print_evaluation_stats,\n    save_evaluation_stats,\n    save_prediction_outputs,\n)\nfrom ludwig.models.registry import model_type_registry\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.types import ModelConfigDict, TrainingSetMetadataDict\nfrom ludwig.upload import get_upload_registry\nfrom ludwig.utils import metric_utils\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom ludwig.utils.config_utils import get_preprocessing_params\nfrom ludwig.utils.data_utils import (\n    figure_data_format,\n    generate_kfold_splits,\n    load_dataset,\n    load_json,\n    load_yaml,\n    save_json,\n)\nfrom ludwig.utils.dataset_utils import generate_dataset_statistics\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import makedirs, path_exists, upload_output_directory\nfrom ludwig.utils.heuristics import get_auto_learning_rate\nfrom ludwig.utils.llm_utils import create_text_streamer, TextStreamer\nfrom ludwig.utils.misc_utils import (\n    get_commit_hash,\n    get_file_names,\n    get_from_registry,\n    get_output_directory,\n    set_saved_weights_in_checkpoint_flag,\n)\nfrom ludwig.utils.print_utils import print_boxed\nfrom ludwig.utils.tokenizers import HFTokenizer\nfrom ludwig.utils.torch_utils import DEVICE\nfrom ludwig.utils.trainer_utils import get_training_report\nfrom ludwig.utils.types import DataFrame, TorchDevice\nfrom ludwig.utils.upload_utils import HuggingFaceHub\n\nlogger = logging.getLogger(__name__)\n\n\n@PublicAPI\n@dataclass\nclass EvaluationFrequency:  # noqa F821\n    \"\"\"Represents the frequency of periodic evaluation of a metric during training. For example:\n\n    \"every epoch\"\n    frequency: 1, period: EPOCH\n\n    \"every 50 steps\".\n    frequency: 50, period: STEP\n    \"\"\"\n\n    frequency: float = 1.0\n    period: str = \"epoch\"  # One of \"epoch\" or \"step\".\n\n    EPOCH: ClassVar[str] = \"epoch\"  # One epoch is a single pass through the training set.\n    STEP: ClassVar[str] = \"step\"  # One step is training on one mini-batch.\n\n\n@PublicAPI\n@dataclass\nclass TrainingStats:  # noqa F821\n    \"\"\"Training stats were previously represented as a tuple or a dict.\n\n    This class replaces those while preserving dict and tuple-like behavior (unpacking, [] access).\n    \"\"\"\n\n    training: dict[str, Any]\n    validation: dict[str, Any]\n    test: dict[str, Any]\n    evaluation_frequency: EvaluationFrequency = dataclasses.field(default_factory=EvaluationFrequency)\n\n    # TODO(daniel): deprecate multiple return value unpacking and dictionary-style element access\n    def __iter__(self):\n        return iter((self.training, self.test, self.validation))\n\n    def __contains__(self, key):\n        return (\n            (key == TRAINING and self.training)\n            or (key == VALIDATION and self.validation)\n            or (key == TEST and self.test)\n        )\n\n    def __getitem__(self, key):\n        # Supports dict-style [] element access for compatibility.\n        return {TRAINING: self.training, VALIDATION: self.validation, TEST: self.test}[key]\n\n\n@PublicAPI\n@dataclass\nclass PreprocessedDataset:  # noqa F821\n    training_set: Dataset\n    validation_set: Dataset\n    test_set: Dataset\n    training_set_metadata: TrainingSetMetadataDict\n\n    # TODO(daniel): deprecate multiple return value unpacking and indexed access\n    def __iter__(self):\n        return iter((self.training_set, self.validation_set, self.test_set, self.training_set_metadata))\n\n    def __getitem__(self, index):\n        return (self.training_set, self.validation_set, self.test_set, self.training_set_metadata)[index]\n\n\n@PublicAPI\n@dataclass\nclass TrainingResults:  # noqa F821\n    train_stats: TrainingStats\n    preprocessed_data: PreprocessedDataset\n    output_directory: str\n\n    def __iter__(self):\n        \"\"\"Supports tuple-style return value unpacking ex.\n\n        train_stats, training_set, output_dir = model.train(...)\n        \"\"\"\n        return iter((self.train_stats, self.preprocessed_data, self.output_directory))\n\n    def __getitem__(self, index):\n        \"\"\"Provides indexed getter ex.\n\n        train_stats = model.train(...)[0]\n        \"\"\"\n        return (self.train_stats, self.preprocessed_data, self.output_directory)[index]\n\n\n@PublicAPI\nclass LudwigModel:\n    \"\"\"Class that allows access to high level Ludwig functionalities.\n\n    # Inputs\n\n    :param config: (Union[str, dict]) in-memory representation of\n            config or string path to a YAML config file.\n    :param logging_level: (int) Log level that will be sent to stderr.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param gpus: (Union[str, int, List[int]], default: `None`) GPUs\n        to use (it uses the same syntax of CUDA_VISIBLE_DEVICES)\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow Torch\n        to use multithreading parallelism to improve performance at the\n        cost of determinism.\n\n    # Example usage:\n\n    ```python\n    from ludwig.api import LudwigModel\n    ```\n\n    Train a model:\n\n    ```python\n    config = {...}\n    ludwig_model = LudwigModel(config)\n    train_stats, _, _ = ludwig_model.train(dataset=file_path)\n    ```\n\n    or\n\n    ```python\n    train_stats, _, _ = ludwig_model.train(dataset=dataframe)\n    ```\n\n    If you have already trained a model you can load it and use it to predict\n\n    ```python\n    ludwig_model = LudwigModel.load(model_dir)\n    ```\n\n    Predict:\n\n    ```python\n    predictions, _ = ludwig_model.predict(dataset=file_path)\n    ```\n\n    or\n\n    ```python\n    predictions, _ = ludwig_model.predict(dataset=dataframe)\n    ```\n\n    Evaluation:\n\n    ```python\n    eval_stats, _, _ = ludwig_model.evaluate(dataset=file_path)\n    ```\n\n    or\n\n    ```python\n    eval_stats, _, _ = ludwig_model.evaluate(dataset=dataframe)\n    ```\n    \"\"\"\n\n    def __init__(\n        self,\n        config: str | dict,\n        logging_level: int = logging.ERROR,\n        backend: Backend | str | None = None,\n        gpus: str | int | list[int] | None = None,\n        gpu_memory_limit: float | None = None,\n        allow_parallel_threads: bool = True,\n        callbacks: list[Callback] | None = None,\n    ) -> None:\n        \"\"\"Constructor for the Ludwig Model class.\n\n        # Inputs\n\n        :param config: (Union[str, dict]) in-memory representation of\n            config or string path to a YAML config file.\n        :param logging_level: (int) Log level that will be sent to stderr.\n        :param backend: (Union[Backend, str]) `Backend` or string name\n            of backend to use to execute preprocessing / training steps.\n        :param gpus: (Union[str, int, List[int]], default: `None`) GPUs\n            to use (it uses the same syntax of CUDA_VISIBLE_DEVICES)\n        :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n            [0, 1] allowed to allocate per GPU device.\n        :param allow_parallel_threads: (bool, default: `True`) allow Torch\n            to use multithreading parallelism to improve performance at the\n            cost of determinism.\n        :param callbacks: (list, default: `None`) a list of\n              `ludwig.callbacks.Callback` objects that provide hooks into the\n               Ludwig pipeline.\n\n        # Return\n\n        :return: (None) `None`\n        \"\"\"\n        # check if config is a path or a dict\n        if isinstance(config, str):  # assume path\n            config_dict = load_yaml(config)\n            self.config_fp = config\n        else:\n            config_dict = copy.deepcopy(config)\n            self.config_fp = None  # type: ignore [assignment]\n\n        self._user_config = upgrade_config_dict_to_latest_version(config_dict)\n\n        # Initialize the config object\n        self.config_obj = ModelConfig.from_dict(self._user_config)\n\n        # setup logging\n        self.set_logging_level(logging_level)\n\n        # setup Backend\n        self.backend = initialize_backend(backend or self._user_config.get(\"backend\"))\n        self.callbacks = callbacks if callbacks is not None else []\n\n        # setup PyTorch env (GPU allocation, etc.)\n        self.backend.initialize_pytorch(\n            gpus=gpus, gpu_memory_limit=gpu_memory_limit, allow_parallel_threads=allow_parallel_threads\n        )\n\n        # setup model\n        self.model = None\n        self.training_set_metadata: dict[str, dict] | None = None\n\n        # online training state\n        self._online_trainer = None\n\n        # Zero-shot LLM usage.\n        if (\n            self.config_obj.model_type == MODEL_LLM\n            and self.config_obj.trainer.type == \"none\"\n            # Category output features require a vocabulary. The LLM LudwigModel should be initialized with\n            # model.train(dataset).\n            and self.config_obj.output_features[0].type == \"text\"\n        ):\n            self._initialize_llm()\n\n    def _initialize_llm(self, random_seed: int = default_random_seed):\n        \"\"\"Initialize the LLM model.\n\n        Should only be used in a zero-shot (NoneTrainer) setting.\n        \"\"\"\n        self.model = LudwigModel.create_model(self.config_obj, random_seed=random_seed)\n\n        if self.model.model.device.type == \"cpu\" and torch.cuda.is_available():\n            logger.warning(f\"LLM was initialized on {self.model.model.device}. Moving to GPU for inference.\")\n            self.model.model.to(torch.device(\"cuda\"))\n\n    def train(\n        self,\n        dataset: str | dict | pd.DataFrame | None = None,\n        training_set: str | dict | pd.DataFrame | Dataset | None = None,\n        validation_set: str | dict | pd.DataFrame | Dataset | None = None,\n        test_set: str | dict | pd.DataFrame | Dataset | None = None,\n        training_set_metadata: str | dict | None = None,\n        data_format: str | None = None,\n        experiment_name: str = \"api_experiment\",\n        model_name: str = \"run\",\n        model_resume_path: str | None = None,\n        skip_save_training_description: bool = False,\n        skip_save_training_statistics: bool = False,\n        skip_save_model: bool = False,\n        skip_save_progress: bool = False,\n        skip_save_log: bool = False,\n        skip_save_processed_input: bool = False,\n        output_directory: str | None = \"results\",\n        random_seed: int = default_random_seed,\n        **kwargs,\n    ) -> TrainingResults:\n        \"\"\"This function is used to perform a full training of the model on the specified dataset.\n\n        During training if the skip parameters are False\n        the model and statistics will be saved in a directory\n        `[output_dir]/[experiment_name]_[model_name]_n` where all variables are\n        resolved to user specified ones and `n` is an increasing number\n        starting from 0 used to differentiate among repeated runs.\n\n        # Inputs\n\n        :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing the entire dataset to be used in the experiment.\n            If it has a split column, it will be used for splitting\n            (0 for train, 1 for validation, 2 for test),\n            otherwise the dataset will be randomly split.\n        :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing training data.\n        :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing validation data.\n        :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing test data.\n        :param training_set_metadata: (Union[str, dict], default: `None`)\n            metadata JSON file or loaded metadata. Intermediate preprocessed\n            structure containing the mappings of the input dataset created the\n            first time an input file is used in the same directory with the\n            same name and a '.meta.json' extension.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`,\n            `'feather'`, `'fwf'`,\n            `'hdf5'` (cache file produced during previous training),\n            `'html'` (file containing a single HTML `<table>`),\n            `'json'`, `'jsonl'`, `'parquet'`,\n            `'pickle'` (pickled Pandas DataFrame),\n            `'sas'`, `'spss'`, `'stata'`, `'tsv'`.\n        :param experiment_name: (str, default: `'experiment'`) name for\n            the experiment.\n        :param model_name: (str, default: `'run'`) name of the model that is\n            being used.\n        :param model_resume_path: (str, default: `None`) resumes training of\n            the model from the path specified. The config is restored.\n            In addition to config, training statistics, loss for each\n            epoch and the state of the optimizer are restored such that\n            training can be effectively continued from a previously interrupted\n            training process.\n        :param skip_save_training_description: (bool, default: `False`)\n            disables saving the description JSON file.\n        :param skip_save_training_statistics: (bool, default: `False`)\n            disables saving training statistics JSON file.\n        :param skip_save_model: (bool, default: `False`) disables\n            saving model weights and hyperparameters each time the model\n            improves. By default Ludwig saves model weights after each epoch\n            the validation metric improves, but if the model is really big\n            that can be time consuming. If you do not want to keep\n            the weights and just find out what performance a model can get\n            with a set of hyperparameters, use this parameter to skip it,\n            but the model will not be loadable later on and the returned model\n            will have the weights obtained at the end of training, instead of\n            the weights of the epoch with the best validation performance.\n        :param skip_save_progress: (bool, default: `False`) disables saving\n            progress each epoch. By default Ludwig saves weights and stats\n            after each epoch for enabling resuming of training, but if\n            the model is really big that can be time consuming and will uses\n            twice as much space, use this parameter to skip it, but training\n            cannot be resumed later on.\n        :param skip_save_log: (bool, default: `False`) disables saving\n            TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n            but if it is not needed turning it off can slightly increase the\n            overall speed.\n        :param skip_save_processed_input: (bool, default: `False`) if input\n            dataset is provided it is preprocessed and cached by saving an HDF5\n            and JSON files to avoid running the preprocessing again. If this\n            parameter is `False`, the HDF5 and JSON file are not saved.\n        :param output_directory: (str, default: `'results'`) the directory that\n            will contain the training statistics, TensorBoard logs, the saved\n            model and the training progress files.\n        :param random_seed: (int, default: `42`) a random seed that will be\n            used anywhere there is a call to a random number generator: data\n            splitting, parameter initialization and training set shuffling\n        :param kwargs: (dict, default: {}) a dictionary of optional parameters.\n\n        # Return\n\n        :return: (Tuple[Dict, Union[Dict, pd.DataFrame], str]) tuple containing\n            `(training_statistics, preprocessed_data, output_directory)`.\n            `training_statistics` is a nested dictionary of dataset -> feature_name -> metric_name -> List of metrics.\n                Each metric corresponds to each training checkpoint.\n            `preprocessed_data` is the tuple containing these three data sets\n            `(training_set, validation_set, test_set)`.\n            `output_directory` filepath to where training results are stored.\n        \"\"\"\n        # Only reset the metadata if the model has not been trained before\n        if self.training_set_metadata:\n            logger.warning(\n                \"This model has been trained before. Its architecture has been defined by the original training set \"\n                \"(for example, the number of possible categorical outputs). The current training data will be mapped \"\n                \"to this architecture. If you want to change the architecture of the model, please concatenate your \"\n                \"new training data with the original and train a new model from scratch.\"\n            )\n            training_set_metadata = self.training_set_metadata\n\n        if self._user_config.get(HYPEROPT):\n            print_boxed(\"WARNING\")\n            logger.warning(HYPEROPT_WARNING)\n\n        # setup directories and file names\n        if model_resume_path is not None:\n            if path_exists(model_resume_path):\n                output_directory = model_resume_path\n                if self.backend.is_coordinator():\n                    logger.info(f\"Model resume path '{model_resume_path}' exists, trying to resume training.\")\n            else:\n                if self.backend.is_coordinator():\n                    logger.info(\n                        f\"Model resume path '{model_resume_path}' does not exist, starting training from scratch\"\n                    )\n                model_resume_path = None\n\n        if model_resume_path is None:\n            if self.backend.is_coordinator():\n                output_directory = get_output_directory(output_directory, experiment_name, model_name)\n            else:\n                output_directory = None\n\n        # if we are skipping all saving,\n        # there is no need to create a directory that will remain empty\n        should_create_output_directory = not (\n            skip_save_training_description\n            and skip_save_training_statistics\n            and skip_save_model\n            and skip_save_progress\n            and skip_save_log\n            and skip_save_processed_input\n        )\n\n        output_url = output_directory\n        with upload_output_directory(output_directory) as (output_directory, upload_fn):\n            train_callbacks = self.callbacks\n            if upload_fn is not None:\n                # Upload output files (checkpoints, etc.) to remote storage at the end of\n                # each epoch and evaluation, in case of failure in the middle of training.\n                class UploadOnEpochEndCallback(Callback):\n                    def on_eval_end(self, trainer, progress_tracker, save_path):\n                        upload_fn()\n\n                    def on_epoch_end(self, trainer, progress_tracker, save_path):\n                        upload_fn()\n\n                train_callbacks = train_callbacks + [UploadOnEpochEndCallback()]\n\n            description_fn = training_stats_fn = model_dir = None\n            if self.backend.is_coordinator():\n                if should_create_output_directory:\n                    makedirs(output_directory, exist_ok=True)\n                description_fn, training_stats_fn, model_dir = get_file_names(output_directory)\n\n            if isinstance(training_set, Dataset) and training_set_metadata is not None:\n                preprocessed_data = (training_set, validation_set, test_set, training_set_metadata)\n            else:\n                # save description\n                if self.backend.is_coordinator():\n                    description = get_experiment_description(\n                        self.config_obj.to_dict(),\n                        dataset=dataset,\n                        training_set=training_set,\n                        validation_set=validation_set,\n                        test_set=test_set,\n                        training_set_metadata=training_set_metadata,\n                        data_format=data_format,\n                        backend=self.backend,\n                        random_seed=random_seed,\n                    )\n\n                    if not skip_save_training_description:\n                        save_json(description_fn, description)\n\n                    # print description\n                    experiment_description = [\n                        [\"Experiment name\", experiment_name],\n                        [\"Model name\", model_name],\n                        [\"Output directory\", output_directory],\n                    ]\n                    for key, value in description.items():\n                        if key != \"config\":  # Config is printed separately.\n                            experiment_description.append([key, pformat(value, indent=4)])\n\n                    if self.backend.is_coordinator():\n                        print_boxed(\"EXPERIMENT DESCRIPTION\")\n                        logger.info(tabulate(experiment_description, tablefmt=\"fancy_grid\"))\n\n                        print_boxed(\"LUDWIG CONFIG\")\n                        logger.info(\"User-specified config (with upgrades):\\n\")\n                        logger.info(pformat(self._user_config, indent=4))\n                        logger.info(\n                            \"\\nFull config saved to:\\n\"\n                            f\"{output_directory}/{experiment_name}/model/model_hyperparameters.json\"\n                        )\n\n                preprocessed_data = self.preprocess(  # type: ignore[assignment]\n                    dataset=dataset,\n                    training_set=training_set,\n                    validation_set=validation_set,\n                    test_set=test_set,\n                    training_set_metadata=training_set_metadata,\n                    data_format=data_format,\n                    experiment_name=experiment_name,\n                    model_name=model_name,\n                    model_resume_path=model_resume_path,\n                    skip_save_training_description=skip_save_training_description,\n                    skip_save_training_statistics=skip_save_training_statistics,\n                    skip_save_model=skip_save_model,\n                    skip_save_progress=skip_save_progress,\n                    skip_save_log=skip_save_log,\n                    skip_save_processed_input=skip_save_processed_input,\n                    output_directory=output_directory,\n                    random_seed=random_seed,\n                    **kwargs,\n                )\n                training_set, validation_set, test_set, training_set_metadata = preprocessed_data\n\n            self.training_set_metadata = training_set_metadata\n\n            if self.backend.is_coordinator():\n                dataset_statistics = generate_dataset_statistics(training_set, validation_set, test_set)\n\n                if not skip_save_model:\n                    # save train set metadata\n                    os.makedirs(model_dir, exist_ok=True)  # type: ignore[arg-type]\n                    save_json(  # type: ignore[arg-type]\n                        os.path.join(model_dir, TRAIN_SET_METADATA_FILE_NAME), training_set_metadata\n                    )\n\n                logger.info(\"\\nDataset Statistics\")\n                logger.info(tabulate(dataset_statistics, headers=\"firstrow\", tablefmt=\"fancy_grid\"))\n\n            for callback in self.callbacks:\n                callback.on_train_init(\n                    base_config=self._user_config,\n                    experiment_directory=output_directory,\n                    experiment_name=experiment_name,\n                    model_name=model_name,\n                    output_directory=output_directory,\n                    resume_directory=model_resume_path,\n                )\n\n            # Build model if not provided\n            # if it was provided it means it was already loaded\n            if not self.model:\n                if self.backend.is_coordinator():\n                    print_boxed(\"MODEL\")\n                # update model config with metadata properties derived from training set\n                update_config_with_metadata(self.config_obj, training_set_metadata)\n                logger.info(\"Warnings and other logs:\")\n                self.model = LudwigModel.create_model(self.config_obj, random_seed=random_seed)\n                # update config with properties determined during model instantiation\n                update_config_with_model(self.config_obj, self.model)\n                set_saved_weights_in_checkpoint_flag(self.config_obj)\n\n            # auto tune learning rate\n            if hasattr(self.config_obj.trainer, \"learning_rate\") and self.config_obj.trainer.learning_rate == AUTO:\n                detected_learning_rate = get_auto_learning_rate(self.config_obj)\n                self.config_obj.trainer.learning_rate = detected_learning_rate\n\n            with self.backend.create_trainer(\n                model=self.model,\n                config=self.config_obj.trainer,\n                resume=model_resume_path is not None,\n                skip_save_model=skip_save_model,\n                skip_save_progress=skip_save_progress,\n                skip_save_log=skip_save_log,\n                callbacks=train_callbacks,\n                random_seed=random_seed,\n            ) as trainer:\n                # auto tune batch size\n                self._tune_batch_size(trainer, training_set, random_seed=random_seed)\n\n                if (\n                    self.config_obj.model_type == \"LLM\"\n                    and trainer.config.type == \"none\"\n                    and self.config_obj.adapter is not None\n                    and self.config_obj.adapter.pretrained_adapter_weights is not None\n                ):\n                    trainer.model.initialize_adapter()  # Load pre-trained adapter weights for inference only\n\n                # train model\n                if self.backend.is_coordinator():\n                    print_boxed(\"TRAINING\")\n                    if not skip_save_model:\n                        self.save_config(model_dir)\n\n                for callback in self.callbacks:\n                    callback.on_train_start(\n                        model=self.model,\n                        config=self.config_obj.to_dict(),\n                        config_fp=self.config_fp,\n                    )\n\n                try:\n                    train_stats = trainer.train(\n                        training_set,\n                        validation_set=validation_set,\n                        test_set=test_set,\n                        save_path=model_dir,\n                    )\n                    self.model, train_trainset_stats, train_valiset_stats, train_testset_stats = train_stats\n\n                    # Calibrates output feature probabilities on validation set if calibration is enabled.\n                    # Must be done after training, and before final model parameters are saved.\n                    if self.backend.is_coordinator():\n                        calibrator = Calibrator(\n                            self.model,\n                            self.backend,\n                            batch_size=trainer.eval_batch_size,\n                        )\n                        if calibrator.calibration_enabled():\n                            if validation_set is None:\n                                logger.warning(\n                                    \"Calibration uses validation set, but no validation split specified.\"\n                                    \"Will use training set for calibration.\"\n                                    \"Recommend providing a validation set when using calibration.\"\n                                )\n                                calibrator.train_calibration(training_set, TRAINING)\n                            elif len(validation_set) < MIN_DATASET_SPLIT_ROWS:\n                                logger.warning(\n                                    f\"Validation set size ({len(validation_set)} rows) is too small for calibration.\"\n                                    \"Will use training set for calibration.\"\n                                    f\"Validation set much have at least {MIN_DATASET_SPLIT_ROWS} rows.\"\n                                )\n                                calibrator.train_calibration(training_set, TRAINING)\n                            else:\n                                calibrator.train_calibration(validation_set, VALIDATION)\n                        if not skip_save_model:\n                            self.model.save(model_dir)\n\n                    # Evaluation Frequency\n                    if self.config_obj.model_type == MODEL_ECD and self.config_obj.trainer.steps_per_checkpoint:\n                        evaluation_frequency = EvaluationFrequency(\n                            self.config_obj.trainer.steps_per_checkpoint, EvaluationFrequency.STEP\n                        )\n                    elif self.config_obj.model_type == MODEL_ECD and self.config_obj.trainer.checkpoints_per_epoch:\n                        evaluation_frequency = EvaluationFrequency(\n                            1.0 / self.config_obj.trainer.checkpoints_per_epoch, EvaluationFrequency.EPOCH\n                        )\n                    else:\n                        evaluation_frequency = EvaluationFrequency(1, EvaluationFrequency.EPOCH)\n\n                    # Unpack train()'s return.\n                    # The statistics are all nested dictionaries of TrainerMetrics: feature_name -> metric_name ->\n                    # List[TrainerMetric], with one entry per training checkpoint, according to steps_per_checkpoint.\n                    # We reduce the dictionary of TrainerMetrics to a simple list of floats for interfacing with Ray\n                    # Tune.\n                    train_stats = TrainingStats(\n                        metric_utils.reduce_trainer_metrics_dict(train_trainset_stats),\n                        metric_utils.reduce_trainer_metrics_dict(train_valiset_stats),\n                        metric_utils.reduce_trainer_metrics_dict(train_testset_stats),\n                        evaluation_frequency,\n                    )\n\n                    # save training statistics\n                    if self.backend.is_coordinator():\n                        if not skip_save_training_statistics:\n                            save_json(training_stats_fn, train_stats)\n\n                    # results of the model with highest validation test performance\n                    if (\n                        self.backend.is_coordinator()\n                        and validation_set is not None\n                        and not self.config_obj.trainer.skip_all_evaluation\n                    ):\n                        print_boxed(\"TRAINING REPORT\")\n                        training_report = get_training_report(\n                            trainer.validation_field,\n                            trainer.validation_metric,\n                            test_set is not None,\n                            train_valiset_stats,\n                            train_testset_stats,\n                        )\n                        logger.info(tabulate(training_report, tablefmt=\"fancy_grid\"))\n                        logger.info(f\"\\nFinished: {experiment_name}_{model_name}\")\n                        logger.info(f\"Saved to: {output_directory}\")\n                finally:\n                    for callback in self.callbacks:\n                        callback.on_train_end(output_directory)\n\n                self.training_set_metadata = training_set_metadata\n\n                if self.is_merge_and_unload_set():\n                    # For an LLM model trained with a LoRA adapter, merge first, then save the full model.\n                    self.model.merge_and_unload(progressbar=self.config_obj.adapter.postprocessor.progressbar)\n\n                    if self.backend.is_coordinator() and not skip_save_model:\n                        self.model.save_base_model(model_dir)\n                elif self.backend.is_coordinator() and not skip_save_model:\n                    self.model.save(model_dir)\n\n                # Synchronize model weights between workers\n                self.backend.sync_model(self.model)\n\n                print_boxed(\"FINISHED\")\n                return TrainingResults(train_stats, preprocessed_data, output_url)\n\n    def train_online(\n        self,\n        dataset: str | dict | pd.DataFrame,\n        training_set_metadata: str | dict | None = None,\n        data_format: str = \"auto\",\n        random_seed: int = default_random_seed,\n    ) -> None:\n        \"\"\"Performs one epoch of training of the model on `dataset`.\n\n        # Inputs\n\n        :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing the entire dataset to be used in the experiment.\n            If it has a split column, it will be used for splitting (0 for train,\n            1 for validation, 2 for test), otherwise the dataset will be\n            randomly split.\n        :param training_set_metadata: (Union[str, dict], default: `None`)\n            metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n            dataset created the first time an input file is used in the same\n            directory with the same name and a '.meta.json' extension.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`, `'hdf5'` (cache file produced during previous training),\n            `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n            `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n            `'stata'`, `'tsv'`.\n        :param random_seed: (int, default: `42`) a random seed that is going to be\n               used anywhere there is a call to a random number generator: data\n               splitting, parameter initialization and training set shuffling\n\n        # Return\n\n        :return: (None) `None`\n        \"\"\"\n        training_set_metadata = training_set_metadata or self.training_set_metadata\n        preprocessing_params = get_preprocessing_params(self.config_obj)\n\n        with provision_preprocessing_workers(self.backend):\n            # TODO (Connor): Refactor to use self.config_obj\n            training_dataset, _, _, training_set_metadata = preprocess_for_training(\n                self.config_obj.to_dict(),\n                training_set=dataset,\n                training_set_metadata=training_set_metadata,\n                data_format=data_format,\n                skip_save_processed_input=True,\n                preprocessing_params=preprocessing_params,\n                backend=self.backend,\n                random_seed=random_seed,\n                callbacks=self.callbacks,\n            )\n\n        if not self.training_set_metadata:\n            self.training_set_metadata = training_set_metadata\n\n        if not self.model:\n            update_config_with_metadata(self.config_obj, training_set_metadata)\n            self.model = LudwigModel.create_model(self.config_obj, random_seed=random_seed)\n            # update config with properties determined during model instantiation\n            update_config_with_model(self.config_obj, self.model)\n            set_saved_weights_in_checkpoint_flag(self.config_obj)\n\n        if not self._online_trainer:\n            self._online_trainer = self.backend.create_trainer(\n                config=self.config_obj.trainer, model=self.model, random_seed=random_seed\n            )\n\n            self._tune_batch_size(self._online_trainer, dataset, random_seed=random_seed)\n\n        self.model = self._online_trainer.train_online(training_dataset)\n\n    def _tune_batch_size(self, trainer, dataset, random_seed: int = default_random_seed):\n        \"\"\"Sets AUTO batch-size-related parameters based on the trainer, backend type, and number of workers.\n\n        Batch-size related parameters that are set:\n        - trainer.batch_size\n        - trainer.eval_batch_size\n        - trainer.gradient_accumulation_steps\n        - trainer.effective_batch_size\n\n        The final batch size selected may be non-deterministic even with a fixed random seed since throughput-based\n        heuristics may be affected by resources used by other processes running on the machine.\n        \"\"\"\n        if not self.config_obj.trainer.can_tune_batch_size():\n            # Some model types don't have batch sizes to be tuned\n            return\n\n        # Render the batch size and gradient accumulation steps prior to batch size tuning. This is needed in the event\n        # the effective_batch_size and gradient_accumulation_steps are set explicitly, but batch_size is AUTO. In this\n        # case, we can infer the batch_size directly without tuning.\n        num_workers = self.backend.num_training_workers\n        self.config_obj.trainer.update_batch_size_grad_accum(num_workers)\n\n        # TODO (ASN): add support for substitute_with_max parameter\n        # TODO(travis): detect train and eval batch sizes separately (enable / disable gradients)\n        if self.config_obj.trainer.batch_size == AUTO:\n            if self.backend.supports_batch_size_tuning():\n                tuned_batch_size = trainer.tune_batch_size(\n                    self.config_obj.to_dict(), dataset, random_seed=random_seed, tune_for_training=True\n                )\n            else:\n                logger.warning(\n                    f\"Backend {self.backend.BACKEND_TYPE} does not support batch size tuning, \"\n                    f\"using fallback training batch size {FALLBACK_BATCH_SIZE}.\"\n                )\n                tuned_batch_size = FALLBACK_BATCH_SIZE\n\n            # TODO(travis): pass these in as args to trainer when we call train,\n            #  to avoid setting state on possibly remote trainer\n            self.config_obj.trainer.batch_size = tuned_batch_size\n\n            # Re-render the gradient_accumulation_steps to account for the explicit batch size.\n            self.config_obj.trainer.update_batch_size_grad_accum(num_workers)\n\n        if self.config_obj.trainer.eval_batch_size in {AUTO, None}:\n            if self.backend.supports_batch_size_tuning():\n                tuned_batch_size = trainer.tune_batch_size(\n                    self.config_obj.to_dict(), dataset, random_seed=random_seed, tune_for_training=False\n                )\n            else:\n                logger.warning(\n                    f\"Backend {self.backend.BACKEND_TYPE} does not support batch size tuning, \"\n                    f\"using fallback eval batch size {FALLBACK_BATCH_SIZE}.\"\n                )\n                tuned_batch_size = FALLBACK_BATCH_SIZE\n\n            self.config_obj.trainer.eval_batch_size = tuned_batch_size\n\n        # Update trainer params separate to config params for backends with stateful trainers\n        trainer.batch_size = self.config_obj.trainer.batch_size\n        trainer.eval_batch_size = self.config_obj.trainer.eval_batch_size\n        trainer.gradient_accumulation_steps = self.config_obj.trainer.gradient_accumulation_steps\n\n    def save_dequantized_base_model(self, save_path: str) -> None:\n        \"\"\"Upscales quantized weights of a model to fp16 and saves the result in a specified folder.\n\n        Args:\n            save_path (str): The path to the folder where the upscaled model weights will be saved.\n\n        Raises:\n            ValueError:\n                If the model type is not 'llm' or if quantization is not enabled or the number of bits is not 4 or 8.\n            RuntimeError:\n                If no GPU is available, as GPU is required for quantized models.\n\n        Returns:\n            None\n        \"\"\"\n        if self.config_obj.model_type != MODEL_LLM:\n            raise ValueError(\n                f\"Model type {self.config_obj.model_type} is not supported by this method. Only `llm` model type is \"\n                \"supported.\"\n            )\n\n        if not self.config_obj.quantization:\n            raise ValueError(\n                \"Quantization is not enabled in your Ludwig model config. \"\n                \"To enable quantization, set `quantization` to `{'bits': 4}` or `{'bits': 8}` in your model config.\"\n            )\n\n        if self.config_obj.quantization.bits != 4:\n            raise ValueError(\n                \"This method only works with quantized models with 4 bits. \"\n                \"Support for 8-bit quantized models will be added in a future release.\"\n            )\n\n        if not torch.cuda.is_available():\n            raise RuntimeError(\"GPU is required for quantized models but no GPU found.\")\n\n        # Create the LLM model class instance with the loaded LLM if it hasn't been initialized yet.\n        if not self.model:\n            self.model = LudwigModel.create_model(self.config_obj)\n\n        self.model.save_dequantized_base_model(save_path)\n\n        logger.info(\n            \"If you want to upload this model to huggingface.co, run the following Python commands: \\n\"\n            \"from ludwig.utils.hf_utils import upload_folder_to_hfhub; \\n\"\n            f\"upload_folder_to_hfhub(repo_id='desired/huggingface/repo/name', folder_path='{save_path}')\"\n        )\n\n    def generate(\n        self,\n        input_strings: str | list[str],\n        generation_config: dict | None = None,\n        streaming: bool | None = False,\n    ) -> str | list[str]:\n        \"\"\"A simple generate() method that directly uses the underlying transformers library to generate text.\n\n        Args:\n            input_strings (Union[str, List[str]]): Input text or list of texts to generate from.\n            generation_config (Optional[dict]): Configuration for text generation.\n            streaming (Optional[bool]): If True, enable streaming output.\n\n        Returns:\n            Union[str, List[str]]: Generated text or list of generated texts.\n        \"\"\"\n        if self.config_obj.model_type != MODEL_LLM:\n            raise ValueError(\n                f\"Model type {self.config_obj.model_type} is not supported by this method. Only `llm` model type is \"\n                \"supported.\"\n            )\n        if not torch.cuda.is_available():\n            # GPU is generally well-advised for working with LLMs and is required for loading quantized models, see\n            # https://github.com/ludwig-ai/ludwig/issues/3695.\n            raise ValueError(\"GPU is not available.\")\n\n        # TODO(Justin): Decide if it's worth folding padding_side handling into llm.py's tokenizer initialization.\n        # For batch inference with models like facebook/opt-350m, if the tokenizer padding side is off, HF prints a\n        # warning, e.g.:\n        # \"A decoder-only architecture is being used, but right-padding was detected! For correct generation results, \"\n        # \"please set `padding_side='left'` when initializing the tokenizer.\n        padding_side = \"left\" if not self.model.model.config.is_encoder_decoder else \"right\"\n        tokenizer = HFTokenizer(self.config_obj.base_model, padding_side=padding_side)\n\n        with self.model.use_generation_config(generation_config):\n            start_time = time.time()\n            tokenized_inputs = tokenizer.tokenizer(input_strings, return_tensors=\"pt\", padding=True)\n            input_ids = tokenized_inputs[\"input_ids\"].to(\"cuda\")\n            attention_mask = tokenized_inputs[\"attention_mask\"].to(\"cuda\")\n\n            if streaming:\n                streamer = create_text_streamer(tokenizer.tokenizer)\n                outputs = self._generate_streaming_outputs(input_strings, input_ids, attention_mask, streamer)\n            else:\n                outputs = self._generate_non_streaming_outputs(input_strings, input_ids, attention_mask)\n\n            decoded_outputs = tokenizer.tokenizer.batch_decode(outputs, skip_special_tokens=True)\n            logger.info(f\"Finished generating in: {(time.time() - start_time):.2f}s.\")\n\n            return decoded_outputs[0] if len(decoded_outputs) == 1 else decoded_outputs\n\n    def _generate_streaming_outputs(\n        self,\n        input_strings: str | list[str],\n        input_ids: torch.Tensor,\n        attention_mask: torch.Tensor,\n        streamer: TextStreamer,\n    ) -> torch.Tensor:\n        \"\"\"Generate streaming outputs for the given input.\n\n        Args:\n            input_strings (Union[str, List[str]]): Input text or list of texts to generate from.\n            input_ids (torch.Tensor): Tensor containing input IDs.\n            attention_mask (torch.Tensor): Tensor containing attention masks.\n            streamer (Union[TextStreamer, None]): Text streamer instance for streaming output.\n\n        Returns:\n            torch.Tensor: Concatenated tensor of generated outputs.\n        \"\"\"\n        outputs = []\n        input_strings = input_strings if isinstance(input_strings, list) else [input_strings]\n        for i in range(len(input_ids)):\n            with torch.no_grad():\n                logger.info(f\"Input: {input_strings[i]}\\n\")\n                # NOTE: self.model.model.generation_config is not used here because it is the default\n                # generation config that the CausalLM was initialized with, rather than the one set within the\n                # context manager.\n                generated_output = self.model.model.generate(\n                    input_ids=input_ids[i].unsqueeze(0),\n                    attention_mask=attention_mask[i].unsqueeze(0),\n                    generation_config=self.model.generation,\n                    streamer=streamer,\n                )\n                logger.info(\"----------------------\")\n                outputs.append(generated_output)\n        return torch.cat(outputs, dim=0)\n\n    def _generate_non_streaming_outputs(\n        self,\n        _input_strings: str | list[str],\n        input_ids: torch.Tensor,\n        attention_mask: torch.Tensor,\n    ) -> torch.Tensor:\n        \"\"\"Generate non-streaming outputs for the given input.\n\n        Args:\n            _input_strings (Union[str, List[str]]): Unused input parameter.\n            input_ids (torch.Tensor): Tensor containing input IDs.\n            attention_mask (torch.Tensor): Tensor containing attention masks.\n            streamer (Union[TextStreamer, None]): Text streamer instance for streaming output.\n\n        Returns:\n            torch.Tensor: Tensor of generated outputs.\n        \"\"\"\n        with torch.no_grad():\n            # NOTE: self.model.model.generation_config is not used here because it is the default\n            # generation config that the CausalLM was initialized with, rather than the one set within the\n            # context manager.\n            return self.model.model.generate(\n                input_ids=input_ids,\n                attention_mask=attention_mask,\n                generation_config=self.model.generation,\n            )\n\n    def predict(\n        self,\n        dataset: str | dict | pd.DataFrame | None = None,\n        data_format: str = None,\n        split: str = FULL,\n        batch_size: int = 128,\n        generation_config: dict | None = None,\n        skip_save_unprocessed_output: bool = True,\n        skip_save_predictions: bool = True,\n        output_directory: str = \"results\",\n        return_type: str | dict | pd.DataFrame = pd.DataFrame,\n        callbacks: list[Callback] | None = None,\n        **kwargs,\n    ) -> tuple[dict | pd.DataFrame, str]:\n        \"\"\"Using a trained model, make predictions from the provided dataset.\n\n        # Inputs\n\n        :param dataset: (Union[str, dict, pandas.DataFrame]): source containing the entire dataset to be evaluated.\n        :param data_format: (str, default: `None`) format to interpret data sources. Will be inferred automatically\n            if not specified.  Valid formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`, `'hdf5'` (cache file produced during previous training), `'html'` (file containing a single\n            HTML `<table>`), `'json'`, `'jsonl'`, `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`,\n            `'spss'`, `'stata'`, `'tsv'`.\n        :param split: (str, default= `'full'`):  if the input dataset contains a split column, this parameter\n            indicates which split of the data to use. Possible values are `'full'`, `'training'`, `'validation'`,\n            `'test'`.\n        :param batch_size: (int, default: 128) size of batch to use when making predictions.\n        :param generation_config: (Dict, default: `None`) config for the generation of the\n            predictions. If `None`, the config that was used during model training is\n            used. This is only used if the model type is LLM. Otherwise, this parameter is\n            ignored. See\n            [Large Language Models](https://ludwig.ai/latest/configuration/large_language_model/#generation) under\n            \"Generation\" for an example generation config.\n        :param skip_save_unprocessed_output: (bool, default: `True`) if this parameter is `False`, predictions and\n            their probabilities are saved in both raw unprocessed numpy files containing tensors and as\n            postprocessed CSV files (one for each output feature). If this parameter is `True`, only the CSV ones\n            are saved and the numpy ones are skipped.\n        :param skip_save_predictions: (bool, default: `True`) skips saving test predictions CSV files.\n        :param output_directory: (str, default: `'results'`) the directory that will contain the training\n            statistics, TensorBoard logs, the saved model and the training progress files.\n        :param return_type: (Union[str, dict, pandas.DataFrame], default: pd.DataFrame) indicates the format of the\n            returned predictions.\n        :param callbacks: (Optional[List[Callback]], default: None) optional list of callbacks to use during this\n            predict operation. Any callbacks already registered to the model will be preserved.\n\n        # Return\n\n        :return `(predictions, output_directory)`: (Tuple[Union[dict, pd.DataFrame], str])\n            `predictions` predictions from the provided dataset,\n            `output_directory` filepath string to where data was stored.\n        \"\"\"\n        self._check_initialization()\n\n        # preprocessing\n        start_time = time.time()\n        logger.debug(\"Preprocessing\")\n        dataset, _ = preprocess_for_prediction(  # TODO (Connor): Refactor to use self.config_obj\n            self.config_obj.to_dict(),\n            dataset=dataset,\n            training_set_metadata=self.training_set_metadata,\n            data_format=data_format,\n            split=split,\n            include_outputs=False,\n            backend=self.backend,\n            callbacks=self.callbacks + (callbacks or []),\n        )\n\n        logger.debug(\"Predicting\")\n        with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor:\n            with self.model.use_generation_config(generation_config):\n                predictions = predictor.batch_predict(\n                    dataset,\n                )\n\n            if self.backend.is_coordinator():\n                # if we are skipping all saving,\n                # there is no need to create a directory that will remain empty\n                should_create_exp_dir = not (skip_save_unprocessed_output and skip_save_predictions)\n                if should_create_exp_dir:\n                    makedirs(output_directory, exist_ok=True)\n\n            logger.debug(\"Postprocessing\")\n            postproc_predictions = postprocess(\n                predictions,\n                self.model.output_features,\n                self.training_set_metadata,\n                output_directory=output_directory,\n                backend=self.backend,\n                skip_save_unprocessed_output=skip_save_unprocessed_output or not self.backend.is_coordinator(),\n            )\n            converted_postproc_predictions = convert_predictions(\n                postproc_predictions, self.model.output_features, return_type=return_type, backend=self.backend\n            )\n            if self.backend.is_coordinator():\n                if not skip_save_predictions:\n                    save_prediction_outputs(\n                        postproc_predictions, self.model.output_features, output_directory, self.backend\n                    )\n\n                    logger.info(f\"Saved to: {output_directory}\")\n\n            logger.info(f\"Finished predicting in: {(time.time() - start_time):.2f}s.\")\n            return converted_postproc_predictions, output_directory\n\n    def evaluate(\n        self,\n        dataset: str | dict | pd.DataFrame | None = None,\n        data_format: str | None = None,\n        split: str = FULL,\n        batch_size: int | None = None,\n        skip_save_unprocessed_output: bool = True,\n        skip_save_predictions: bool = True,\n        skip_save_eval_stats: bool = True,\n        collect_predictions: bool = False,\n        collect_overall_stats: bool = False,\n        output_directory: str = \"results\",\n        return_type: str | dict | pd.DataFrame = pd.DataFrame,\n        **kwargs,\n    ) -> tuple[dict, dict | pd.DataFrame, str]:\n        \"\"\"This function is used to predict the output variables given the input variables using the trained model\n        and compute test statistics like performance measures, confusion matrices and the like.\n\n        # Inputs\n        :param dataset: (Union[str, dict, pandas.DataFrame]) source containing\n            the entire dataset to be evaluated.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`, `'hdf5'` (cache file produced during previous training),\n            `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n            `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n            `'stata'`, `'tsv'`.\n        :param split: (str, default=`'full'`): if the input dataset contains\n            a split column, this parameter indicates which split of the data\n            to use. Possible values are `'full'`, `'training'`, `'validation'`, `'test'`.\n        :param batch_size: (int, default: None) size of batch to use when making\n            predictions. Defaults to model config eval_batch_size\n        :param skip_save_unprocessed_output: (bool, default: `True`) if this\n            parameter is `False`, predictions and their probabilities are saved\n            in both raw unprocessed numpy files containing tensors and as\n            postprocessed CSV files (one for each output feature).\n            If this parameter is `True`, only the CSV ones are saved and the\n            numpy ones are skipped.\n        :param skip_save_predictions: (bool, default: `True`) skips saving\n            test predictions CSV files.\n        :param skip_save_eval_stats: (bool, default: `True`) skips saving\n            test statistics JSON file.\n        :param collect_predictions: (bool, default: `False`) if `True`\n            collects post-processed predictions during eval.\n        :param collect_overall_stats: (bool, default: False) if `True`\n            collects overall stats during eval.\n        :param output_directory: (str, default: `'results'`) the directory that\n            will contain the training statistics, TensorBoard logs, the saved\n            model and the training progress files.\n        :param return_type: (Union[str, dict, pd.DataFrame], default: pandas.DataFrame) indicates\n            the format to of the returned predictions.\n\n        # Return\n        :return: (`evaluation_statistics`, `predictions`, `output_directory`)\n            `evaluation_statistics` dictionary containing evaluation performance\n                statistics,\n            `postprocess_predictions` contains predicted values,\n            `output_directory` is location where results are stored.\n        \"\"\"\n        self._check_initialization()\n\n        for callback in self.callbacks:\n            callback.on_evaluation_start()\n\n        # preprocessing\n        logger.debug(\"Preprocessing\")\n        dataset, training_set_metadata = preprocess_for_prediction(  # TODO (Connor): Refactor to use self.config_obj\n            self.config_obj.to_dict(),\n            dataset=dataset,\n            training_set_metadata=self.training_set_metadata,\n            data_format=data_format,\n            split=split,\n            include_outputs=True,\n            backend=self.backend,\n            callbacks=self.callbacks,\n        )\n\n        # Fallback to use eval_batch_size or batch_size if not provided\n        if batch_size is None:\n            # Requires dictionary getter since some trainer configs may not have a batch_size param\n            batch_size = self.config_obj.trainer.to_dict().get(\n                EVAL_BATCH_SIZE, None\n            ) or self.config_obj.trainer.to_dict().get(BATCH_SIZE, None)\n\n        logger.debug(\"Predicting\")\n        with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor:\n            eval_stats, predictions = predictor.batch_evaluation(\n                dataset,\n                collect_predictions=collect_predictions or collect_overall_stats,\n            )\n\n            # calculate the overall metrics\n            if collect_overall_stats:\n                dataset = dataset.to_df()\n\n                overall_stats = calculate_overall_stats(\n                    self.model.output_features, predictions, dataset, training_set_metadata\n                )\n                eval_stats = {\n                    of_name: (\n                        {**eval_stats[of_name], **overall_stats[of_name]}\n                        # account for presence of 'combined' key\n                        if of_name in overall_stats\n                        else {**eval_stats[of_name]}\n                    )\n                    for of_name in eval_stats\n                }\n\n            if self.backend.is_coordinator():\n                # if we are skipping all saving,\n                # there is no need to create a directory that will remain empty\n                should_create_exp_dir = not (\n                    skip_save_unprocessed_output and skip_save_predictions and skip_save_eval_stats\n                )\n                if should_create_exp_dir:\n                    makedirs(output_directory, exist_ok=True)\n\n            if collect_predictions:\n                logger.debug(\"Postprocessing\")\n                postproc_predictions = postprocess(\n                    predictions,\n                    self.model.output_features,\n                    self.training_set_metadata,\n                    output_directory=output_directory,\n                    backend=self.backend,\n                    skip_save_unprocessed_output=skip_save_unprocessed_output or not self.backend.is_coordinator(),\n                )\n            else:\n                postproc_predictions = predictions  # = {}\n\n            if self.backend.is_coordinator():\n                should_save_predictions = (\n                    collect_predictions and postproc_predictions is not None and not skip_save_predictions\n                )\n                if should_save_predictions:\n                    save_prediction_outputs(\n                        postproc_predictions, self.model.output_features, output_directory, self.backend\n                    )\n\n                print_evaluation_stats(eval_stats)\n                if not skip_save_eval_stats:\n                    save_evaluation_stats(eval_stats, output_directory)\n\n                if should_save_predictions or not skip_save_eval_stats:\n                    logger.info(f\"Saved to: {output_directory}\")\n\n            if collect_predictions:\n                postproc_predictions = convert_predictions(\n                    postproc_predictions, self.model.output_features, return_type=return_type, backend=self.backend\n                )\n\n            for callback in self.callbacks:\n                callback.on_evaluation_end()\n\n            return eval_stats, postproc_predictions, output_directory\n\n    def forecast(\n        self,\n        dataset: DataFrame,\n        data_format: str | None = None,\n        horizon: int = 1,\n        output_directory: str | None = None,\n        output_format: str = \"parquet\",\n    ) -> DataFrame:\n        # TODO(travis): WIP\n        dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, self.backend)\n        if isinstance(dataset, CacheableDataset):\n            dataset = dataset.unwrap()\n        dataset = load_dataset(dataset, data_format=data_format, df_lib=self.backend.df_engine.df_lib)\n\n        window_sizes = [\n            feature.preprocessing.window_size\n            for feature in self.config_obj.input_features\n            if feature.type == TIMESERIES\n        ]\n        if not window_sizes:\n            raise ValueError(\"Forecasting requires at least one input feature of type `timeseries`.\")\n\n        # TODO(travis): there's a lot of redundancy in this approach, since we are preprocessing the same DataFrame\n        # multiple times with only a small number of features (the horizon) being appended each time.\n        # A much better approach would be to only preprocess a single row, but incorporating the row-level embedding\n        # over the window_size of rows precending it, then performing the model forward pass on only that row of\n        # data.\n        max_lookback_window_size = max(window_sizes)\n        total_forecasted = 0\n        while total_forecasted < horizon:\n            # We only need the last `window_size` worth of rows to forecast the next value\n            dataset = dataset.tail(max_lookback_window_size)\n\n            # Run through preprocessing and prediction to obtain row-wise next values\n            # TODO(travis): can optimize the preprocessing part here, since we only need to preprocess / predict\n            # the last row, not the last `window_size` rows.\n            preds, _ = self.predict(dataset, skip_save_predictions=True, skip_save_unprocessed_output=True)\n\n            next_series = {}\n            for feature in self.config_obj.output_features:\n                if feature.type == TIMESERIES:\n                    key = f\"{feature.name}_predictions\"\n                    next_series[feature.column] = pd.Series(preds[key].iloc[-1])\n\n            next_preds = pd.DataFrame(next_series)\n            dataset = pd.concat([dataset, next_preds], axis=0).reset_index(drop=True)\n            total_forecasted += len(next_preds)\n\n        horizon_df = dataset.tail(total_forecasted).head(horizon)\n        return_cols = [feature.column for feature in self.config_obj.output_features if feature.type == TIMESERIES]\n        results_df = horizon_df[return_cols]\n\n        if output_directory is not None:\n            if self.backend.is_coordinator():\n                # TODO(travis): generalize this to support any pandas output format\n                if output_format == \"parquet\":\n                    output_path = os.path.join(output_directory, \"forecast.parquet\")\n                    results_df.to_parquet(output_path)\n                elif output_format == \"csv\":\n                    output_path = os.path.join(output_directory, \"forecast.csv\")\n                    results_df.to_csv(output_path)\n                else:\n                    raise ValueError(f\"`output_format` {output_format} not supported. Must be one of [parquet, csv]\")\n                logger.info(f\"Saved to: {output_path}\")\n\n        return results_df\n\n    def experiment(\n        self,\n        dataset: str | dict | pd.DataFrame | None = None,\n        training_set: str | dict | pd.DataFrame | None = None,\n        validation_set: str | dict | pd.DataFrame | None = None,\n        test_set: str | dict | pd.DataFrame | None = None,\n        training_set_metadata: str | dict | None = None,\n        data_format: str | None = None,\n        experiment_name: str = \"experiment\",\n        model_name: str = \"run\",\n        model_resume_path: str | None = None,\n        eval_split: str = TEST,\n        skip_save_training_description: bool = False,\n        skip_save_training_statistics: bool = False,\n        skip_save_model: bool = False,\n        skip_save_progress: bool = False,\n        skip_save_log: bool = False,\n        skip_save_processed_input: bool = False,\n        skip_save_unprocessed_output: bool = False,\n        skip_save_predictions: bool = False,\n        skip_save_eval_stats: bool = False,\n        skip_collect_predictions: bool = False,\n        skip_collect_overall_stats: bool = False,\n        output_directory: str = \"results\",\n        random_seed: int = default_random_seed,\n        **kwargs,\n    ) -> tuple[dict | None, TrainingStats, PreprocessedDataset, str]:\n        \"\"\"Trains a model on a dataset's training and validation splits and uses it to predict on the test split.\n        It saves the trained model and the statistics of training and testing.\n\n        # Inputs\n        :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing the entire dataset to be used in the experiment.\n            If it has a split column, it will be used for splitting (0 for train,\n            1 for validation, 2 for test), otherwise the dataset will be\n            randomly split.\n        :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing training data.\n        :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing validation data.\n        :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n            source containing test data.\n        :param training_set_metadata: (Union[str, dict], default: `None`)\n            metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n            dataset created the first time an input file is used in the same\n            directory with the same name and a '.meta.json' extension.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`, `'hdf5'` (cache file produced during previous training),\n            `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n            `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n            `'stata'`, `'tsv'`.\n        :param experiment_name: (str, default: `'experiment'`) name for\n            the experiment.\n        :param model_name: (str, default: `'run'`) name of the model that is\n            being used.\n        :param model_resume_path: (str, default: `None`) resumes training of\n            the model from the path specified. The config is restored.\n            In addition to config, training statistics and loss for\n            epoch and the state of the optimizer are restored such that\n            training can be effectively continued from a previously interrupted\n            training process.\n        :param eval_split: (str, default: `test`) split on which\n            to perform evaluation. Valid values are `training`, `validation`\n            and `test`.\n        :param skip_save_training_description: (bool, default: `False`) disables\n            saving the description JSON file.\n        :param skip_save_training_statistics: (bool, default: `False`) disables\n            saving training statistics JSON file.\n        :param skip_save_model: (bool, default: `False`) disables\n            saving model weights and hyperparameters each time the model\n            improves. By default Ludwig saves model weights after each epoch\n            the validation metric improves, but if the model is really big\n            that can be time consuming. If you do not want to keep\n            the weights and just find out what performance a model can get\n            with a set of hyperparameters, use this parameter to skip it,\n            but the model will not be loadable later on and the returned model\n            will have the weights obtained at the end of training, instead of\n            the weights of the epoch with the best validation performance.\n        :param skip_save_progress: (bool, default: `False`) disables saving\n            progress each epoch. By default Ludwig saves weights and stats\n            after each epoch for enabling resuming of training, but if\n            the model is really big that can be time consuming and will uses\n            twice as much space, use this parameter to skip it, but training\n            cannot be resumed later on.\n        :param skip_save_log: (bool, default: `False`) disables saving\n            TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n            but if it is not needed turning it off can slightly increase the\n            overall speed.\n        :param skip_save_processed_input: (bool, default: `False`) if input\n            dataset is provided it is preprocessed and cached by saving an HDF5\n            and JSON files to avoid running the preprocessing again. If this\n            parameter is `False`, the HDF5 and JSON file are not saved.\n        :param skip_save_unprocessed_output: (bool, default: `False`) by default\n            predictions and their probabilities are saved in both raw\n            unprocessed numpy files containing tensors and as postprocessed\n            CSV files (one for each output feature). If this parameter is True,\n            only the CSV ones are saved and the numpy ones are skipped.\n        :param skip_save_predictions: (bool, default: `False`) skips saving test\n            predictions CSV files\n        :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n            statistics JSON file\n        :param skip_collect_predictions: (bool, default: `False`) skips\n            collecting post-processed predictions during eval.\n        :param skip_collect_overall_stats: (bool, default: `False`) skips\n            collecting overall stats during eval.\n        :param output_directory: (str, default: `'results'`) the directory that\n            will contain the training statistics, TensorBoard logs, the saved\n            model and the training progress files.\n        :param random_seed: (int: default: 42) random seed used for weights\n            initialization, splits and any other random function.\n\n        # Return\n        :return: (Tuple[dict, dict, tuple, str))\n            `(evaluation_statistics, training_statistics, preprocessed_data, output_directory)`\n            `evaluation_statistics` dictionary with evaluation performance\n                statistics on the test_set,\n            `training_statistics` is a nested dictionary of dataset -> feature_name -> metric_name -> List of metrics.\n                Each metric corresponds to each training checkpoint.\n            `preprocessed_data` tuple containing preprocessed\n            `(training_set, validation_set, test_set)`, `output_directory`\n            filepath string to where results are stored.\n        \"\"\"\n        if self._user_config.get(HYPEROPT):\n            print_boxed(\"WARNING\")\n            logger.warning(HYPEROPT_WARNING)\n\n        train_stats, preprocessed_data, output_directory = self.train(\n            dataset=dataset,\n            training_set=training_set,\n            validation_set=validation_set,\n            test_set=test_set,\n            training_set_metadata=training_set_metadata,\n            data_format=data_format,\n            experiment_name=experiment_name,\n            model_name=model_name,\n            model_resume_path=model_resume_path,\n            skip_save_training_description=skip_save_training_description,\n            skip_save_training_statistics=skip_save_training_statistics,\n            skip_save_model=skip_save_model,\n            skip_save_progress=skip_save_progress,\n            skip_save_log=skip_save_log,\n            skip_save_processed_input=skip_save_processed_input,\n            skip_save_unprocessed_output=skip_save_unprocessed_output,\n            output_directory=output_directory,\n            random_seed=random_seed,\n        )\n\n        training_set, validation_set, test_set, training_set_metadata = preprocessed_data\n\n        eval_set = validation_set\n        if eval_split == TRAINING:\n            eval_set = training_set\n        elif eval_split == VALIDATION:\n            eval_set = validation_set\n        elif eval_split == TEST:\n            eval_set = test_set\n        else:\n            logger.warning(f\"Eval split {eval_split} not supported. \" f\"Using validation set instead\")\n\n        if eval_set is not None:\n            trainer_dict = self.config_obj.trainer.to_dict()\n            batch_size = trainer_dict.get(EVAL_BATCH_SIZE, trainer_dict.get(BATCH_SIZE, None))\n\n            # predict\n            try:\n                eval_stats, _, _ = self.evaluate(\n                    eval_set,\n                    data_format=data_format,\n                    batch_size=batch_size,\n                    output_directory=output_directory,\n                    skip_save_unprocessed_output=skip_save_unprocessed_output,\n                    skip_save_predictions=skip_save_predictions,\n                    skip_save_eval_stats=skip_save_eval_stats,\n                    collect_predictions=not skip_collect_predictions,\n                    collect_overall_stats=not skip_collect_overall_stats,\n                    return_type=\"dict\",\n                )\n            except NotImplementedError:\n                logger.warning(\n                    \"Skipping evaluation as the necessary methods are not \"\n                    \"supported. Full exception below:\\n\"\n                    f\"{traceback.format_exc()}\"\n                )\n                eval_stats = None\n        else:\n            logger.warning(f\"The evaluation set {eval_set} was not provided. \" f\"Skipping evaluation\")\n            eval_stats = None\n\n        return eval_stats, train_stats, preprocessed_data, output_directory\n\n    def collect_weights(self, tensor_names: list[str] = None, **kwargs) -> list:\n        \"\"\"Load a pre-trained model and collect the tensors with a specific name.\n\n        # Inputs\n        :param tensor_names: (list, default: `None`) List of tensor names to collect\n            weights\n\n        # Return\n        :return: (list) List of tensors\n        \"\"\"\n        self._check_initialization()\n        collected_tensors = self.model.collect_weights(tensor_names)\n        return collected_tensors\n\n    def collect_activations(\n        self,\n        layer_names: list[str],\n        dataset: str | dict[str, list] | pd.DataFrame,\n        data_format: str | None = None,\n        split: str = FULL,\n        batch_size: int = 128,\n        **kwargs,\n    ) -> list:\n        \"\"\"Loads a pre-trained model model and input data to collect the values of the activations contained in the\n        tensors.\n\n        # Inputs\n        :param layer_names: (list) list of strings for layer names in the model\n            to collect activations.\n        :param dataset: (Union[str, Dict[str, list], pandas.DataFrame]) source\n            containing the data to make predictions.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`, `'hdf5'` (cache file produced during previous training),\n            `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n            `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n            `'stata'`, `'tsv'`.\n        :param split: (str, default= `'full'`): if the input dataset contains\n            a split column, this parameter indicates which split of the data\n            to use. Possible values are `'full'`, `'training'`, `'validation'`, `'test'`.\n        :param batch_size: (int, default: 128) size of batch to use when making\n            predictions.\n\n        # Return\n        :return: (list) list of collected tensors.\n        \"\"\"\n        self._check_initialization()\n\n        # preprocessing\n        logger.debug(\"Preprocessing\")\n        dataset, training_set_metadata = preprocess_for_prediction(  # TODO (Connor): Refactor to use self.config_obj\n            self.config_obj.to_dict(),\n            dataset=dataset,\n            training_set_metadata=self.training_set_metadata,\n            data_format=data_format,\n            split=split,\n            include_outputs=False,\n        )\n\n        logger.debug(\"Predicting\")\n        with self.backend.create_predictor(self.model, batch_size=batch_size) as predictor:\n            activations = predictor.batch_collect_activations(\n                layer_names,\n                dataset,\n            )\n\n            return activations\n\n    def preprocess(\n        self,\n        dataset: str | dict | pd.DataFrame | None = None,\n        training_set: str | dict | pd.DataFrame | None = None,\n        validation_set: str | dict | pd.DataFrame | None = None,\n        test_set: str | dict | pd.DataFrame | None = None,\n        training_set_metadata: str | dict | None = None,\n        data_format: str | None = None,\n        skip_save_processed_input: bool = True,\n        random_seed: int = default_random_seed,\n        **kwargs,\n    ) -> PreprocessedDataset:\n        \"\"\"This function is used to preprocess data.\n\n        # Args:\n            :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n                source containing the entire dataset to be used in the experiment.\n                If it has a split column, it will be used for splitting\n                (0 for train, 1 for validation, 2 for test),\n                otherwise the dataset will be randomly split.\n            :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n                source containing training data.\n            :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n                source containing validation data.\n            :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n                source containing test data.\n            :param training_set_metadata: (Union[str, dict], default: `None`)\n                metadata JSON file or loaded metadata. Intermediate preprocessed\n            structure containing the mappings of the input\n                dataset created the first time an input file is used in the same\n                directory with the same name and a '.meta.json' extension.\n            :param data_format: (str, default: `None`) format to interpret data\n                sources. Will be inferred automatically if not specified.  Valid\n                formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`,\n                `'feather'`, `'fwf'`,\n                `'hdf5'` (cache file produced during previous training),\n                `'html'` (file containing a single HTML `<table>`),\n                `'json'`, `'jsonl'`, `'parquet'`,\n                `'pickle'` (pickled Pandas DataFrame),\n                `'sas'`, `'spss'`, `'stata'`, `'tsv'`.\n            :param skip_save_processed_input: (bool, default: `False`) if input\n                dataset is provided it is preprocessed and cached by saving an HDF5\n                and JSON files to avoid running the preprocessing again. If this\n                parameter is `False`, the HDF5 and JSON file are not saved.\n            :param random_seed: (int, default: `42`) a random seed that will be\n                used anywhere there is a call to a random number generator: data\n                splitting, parameter initialization and training set shuffling\n\n        # Returns:\n            :return: (PreprocessedDataset) data structure containing\n                `(proc_training_set, proc_validation_set, proc_test_set, training_set_metadata)`.\n\n        # Raises:\n            RuntimeError: An error occurred while preprocessing the data. Examples include training dataset\n                being empty after preprocessing, lazy loading not being supported with RayBackend, etc.\n        \"\"\"\n        print_boxed(\"PREPROCESSING\")\n\n        for callback in self.callbacks:\n            callback.on_preprocess_start(self.config_obj.to_dict())\n\n        preprocessing_params = get_preprocessing_params(self.config_obj)\n\n        proc_training_set = proc_validation_set = proc_test_set = None\n        try:\n            with provision_preprocessing_workers(self.backend):\n                # TODO (Connor): Refactor to use self.config_obj\n                preprocessed_data = preprocess_for_training(\n                    self.config_obj.to_dict(),\n                    dataset=dataset,\n                    training_set=training_set,\n                    validation_set=validation_set,\n                    test_set=test_set,\n                    training_set_metadata=training_set_metadata,\n                    data_format=data_format,\n                    skip_save_processed_input=skip_save_processed_input,\n                    preprocessing_params=preprocessing_params,\n                    backend=self.backend,\n                    random_seed=random_seed,\n                    callbacks=self.callbacks,\n                )\n\n            proc_training_set, proc_validation_set, proc_test_set, training_set_metadata = preprocessed_data\n\n            return PreprocessedDataset(proc_training_set, proc_validation_set, proc_test_set, training_set_metadata)\n        except Exception as e:\n            raise RuntimeError(f\"Caught exception during model preprocessing: {str(e)}\") from e\n        finally:\n            for callback in self.callbacks:\n                callback.on_preprocess_end(proc_training_set, proc_validation_set, proc_test_set, training_set_metadata)\n\n    @staticmethod\n    def load(\n        model_dir: str,\n        logging_level: int = logging.ERROR,\n        backend: Backend | str | None = None,\n        gpus: str | int | list[int] | None = None,\n        gpu_memory_limit: float | None = None,\n        allow_parallel_threads: bool = True,\n        callbacks: list[Callback] = None,\n        from_checkpoint: bool = False,\n    ) -> \"LudwigModel\":  # return is an instance of ludwig.api.LudwigModel class\n        \"\"\"This function allows for loading pretrained models.\n\n        # Inputs\n\n        :param model_dir: (str) path to the directory containing the model.\n               If the model was trained by the `train` or `experiment` command,\n               the model is in `results_dir/experiment_dir/model`.\n        :param logging_level: (int, default: 40) log level that will be sent to\n            stderr.\n        :param backend: (Union[Backend, str]) `Backend` or string name\n            of backend to use to execute preprocessing / training steps.\n        :param gpus: (Union[str, int, List[int]], default: `None`) GPUs\n            to use (it uses the same syntax of CUDA_VISIBLE_DEVICES)\n        :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n            [0, 1] allowed to allocate per GPU device.\n        :param allow_parallel_threads: (bool, default: `True`) allow Torch\n            to use\n            multithreading parallelism to improve performance at the cost of\n            determinism.\n        :param callbacks: (list, default: `None`) a list of\n            `ludwig.callbacks.Callback` objects that provide hooks into the\n            Ludwig pipeline.\n        :param from_checkpoint: (bool, default: `False`) if `True`, the model\n            will be loaded from the latest checkpoint (training_checkpoints/)\n            instead of the final model weights.\n\n        # Return\n\n        :return: (LudwigModel) a LudwigModel object\n\n\n        # Example usage\n\n        ```python\n        ludwig_model = LudwigModel.load(model_dir)\n        ```\n        \"\"\"\n        # Initialize PyTorch before calling `broadcast()` to prevent initializing\n        # Torch with default parameters\n        backend_param = backend\n        backend = initialize_backend(backend)\n        backend.initialize_pytorch(\n            gpus=gpus, gpu_memory_limit=gpu_memory_limit, allow_parallel_threads=allow_parallel_threads\n        )\n\n        config = backend.broadcast_return(lambda: load_json(os.path.join(model_dir, MODEL_HYPERPARAMETERS_FILE_NAME)))\n\n        # Upgrades deprecated fields and adds new required fields in case the config loaded from disk is old.\n        config_obj = ModelConfig.from_dict(config)\n\n        # Ensure that the original backend is used if it was specified in the config and user requests it\n        if backend_param is None and \"backend\" in config:\n            # Reset backend from config\n            backend = initialize_backend(config.get(\"backend\"))\n\n        # initialize model\n        ludwig_model = LudwigModel(\n            config_obj.to_dict(),\n            logging_level=logging_level,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n\n        # generate model from config\n        set_saved_weights_in_checkpoint_flag(config_obj)\n        ludwig_model.model = LudwigModel.create_model(config_obj)\n\n        # load model weights\n        ludwig_model.load_weights(model_dir, from_checkpoint)\n\n        # If merge_and_unload was NOT performed before saving (i.e., adapter weights exist),\n        # we need to merge them now for inference.\n        if ludwig_model.is_merge_and_unload_set():\n            weights_save_path = os.path.join(model_dir, MODEL_WEIGHTS_FILE_NAME)\n            adapter_config_path = os.path.join(weights_save_path, \"adapter_config.json\")\n            if os.path.exists(adapter_config_path):\n                ludwig_model.model.merge_and_unload(progressbar=config_obj.adapter.postprocessor.progressbar)\n\n        # load train set metadata\n        ludwig_model.training_set_metadata = backend.broadcast_return(\n            lambda: load_metadata(os.path.join(model_dir, TRAIN_SET_METADATA_FILE_NAME))\n        )\n\n        return ludwig_model\n\n    def load_weights(\n        self,\n        model_dir: str,\n        from_checkpoint: bool = False,\n    ) -> None:\n        \"\"\"Loads weights from a pre-trained model.\n\n        # Inputs\n        :param model_dir: (str) filepath string to location of a pre-trained\n            model\n        :param from_checkpoint: (bool, default: `False`) if `True`, the model\n            will be loaded from the latest checkpoint (training_checkpoints/)\n            instead of the final model weights.\n\n        # Return\n        :return: `None`\n\n        # Example usage\n\n        ```python\n        ludwig_model.load_weights(model_dir)\n        ```\n        \"\"\"\n        if self.backend.is_coordinator():\n            if from_checkpoint:\n                with self.backend.create_trainer(\n                    model=self.model,\n                    config=self.config_obj.trainer,\n                ) as trainer:\n                    checkpoint = trainer.create_checkpoint_handle()\n                    training_checkpoints_path = os.path.join(model_dir, TRAINING_CHECKPOINTS_DIR_PATH)\n                    trainer.resume_weights_and_optimizer(training_checkpoints_path, checkpoint)\n            else:\n                self.model.load(model_dir)\n\n        self.backend.sync_model(self.model)\n\n    def save(self, save_path: str) -> None:\n        \"\"\"This function allows to save models on disk.\n\n        # Inputs\n\n        :param  save_path: (str) path to the directory where the model is\n                going to be saved. Both a JSON file containing the model\n                architecture hyperparameters and checkpoints files containing\n                model weights will be saved.\n\n        # Return\n\n        :return: (None) `None`\n\n        # Example usage\n\n        ```python\n        ludwig_model.save(save_path)\n        ```\n        \"\"\"\n        self._check_initialization()\n\n        # save config\n        self.save_config(save_path)\n\n        # save model weights\n        self.model.save(save_path)\n\n        # save training set metadata\n        training_set_metadata_path = os.path.join(save_path, TRAIN_SET_METADATA_FILE_NAME)\n        save_json(training_set_metadata_path, self.training_set_metadata)\n\n    @staticmethod\n    def upload_to_hf_hub(\n        repo_id: str,\n        model_path: str,\n        repo_type: str = \"model\",\n        private: bool = False,\n        commit_message: str = \"Upload trained [Ludwig](https://ludwig.ai/latest/) model weights\",\n        commit_description: str | None = None,\n    ) -> bool:\n        \"\"\"Uploads trained model artifacts to the HuggingFace Hub.\n\n        # Inputs\n\n        :param repo_id: (`str`)\n            A namespace (user or an organization) and a repo name separated\n            by a `/`.\n        :param model_path: (`str`)\n            The path of the saved model. This is either (a) the folder where\n            the 'model_weights' folder and the 'model_hyperparameters.json' file\n            are stored, or (b) the parent of that folder.\n        :param private: (`bool`, *optional*, defaults to `False`)\n            Whether the model repo should be private.\n        :param repo_type: (`str`, *optional*)\n            Set to `\"dataset\"` or `\"space\"` if uploading to a dataset or\n            space, `None` or `\"model\"` if uploading to a model. Default is\n            `None`.\n        :param commit_message: (`str`, *optional*)\n            The summary / title / first line of the generated commit. Defaults to:\n            `f\"Upload {path_in_repo} with huggingface_hub\"`\n        :param commit_description: (`str` *optional*)\n            The description of the generated commit\n\n        # Returns\n\n        :return: (bool) True for success, False for failure.\n        \"\"\"\n        if os.path.exists(os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)) and os.path.exists(\n            os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)\n        ):\n            experiment_path = model_path\n        elif os.path.exists(os.path.join(model_path, MODEL_WEIGHTS_FILE_NAME)) and os.path.exists(\n            os.path.join(model_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n        ):\n            experiment_path = os.path.dirname(model_path)\n        else:\n            raise ValueError(\n                f\"Can't find 'model_weights' and '{MODEL_HYPERPARAMETERS_FILE_NAME}' either at \"\n                f\"'{model_path}' or at '{model_path}/model'\"\n            )\n        model_service = get_upload_registry()[\"hf_hub\"]\n        hub: HuggingFaceHub = model_service()\n        hub.login()\n        upload_status: bool = hub.upload(\n            repo_id=repo_id,\n            model_path=experiment_path,\n            repo_type=repo_type,\n            private=private,\n            commit_message=commit_message,\n            commit_description=commit_description,\n        )\n        return upload_status\n\n    def save_config(self, save_path: str) -> None:\n        \"\"\"Save config to specified location.\n\n        # Inputs\n\n        :param save_path: (str) filepath string to save config as a\n            JSON file.\n\n        # Return\n        :return: `None`\n        \"\"\"\n        os.makedirs(save_path, exist_ok=True)\n        model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n        save_json(model_hyperparameters_path, self.config_obj.to_dict())\n\n    def to_torchscript(\n        self,\n        model_only: bool = False,\n        device: TorchDevice | None = None,\n    ):\n        \"\"\"Converts the trained model to Torchscript.\n\n        # Inputs\n\n        :param  model_only (bool, optional): If True, only the ECD model will be converted to Torchscript. Else,\n        preprocessing and postprocessing steps will also be converted to Torchscript. :param device (TorchDevice,\n        optional): If None, the model will be converted to Torchscript on the same device to     ensure maximum model\n        parity.\n\n        # Returns\n\n        :return: A torch.jit.ScriptModule that can be used to predict on a dictionary of inputs.\n        \"\"\"\n        if device is None:\n            device = DEVICE\n\n        self._check_initialization()\n        if model_only:\n            return self.model.to_torchscript(device)\n        else:\n            inference_module = InferenceModule.from_ludwig_model(\n                self.model, self.config_obj.to_dict(), self.training_set_metadata, device=device\n            )\n            return torch.jit.script(inference_module)\n\n    def save_torchscript(\n        self,\n        save_path: str,\n        model_only: bool = False,\n        device: TorchDevice | None = None,\n    ):\n        \"\"\"Saves the Torchscript model to disk.\n\n        # Inputs\n\n        :param save_path (str): The path to the directory where the model will be saved.\n        :param model_only (bool, optional): If True, only the ECD model will be converted to Torchscript. Else, the\n            preprocessing and postprocessing steps will also be converted to Torchscript.\n        :param device (TorchDevice, optional): If None, the model will be converted to Torchscript on the same device to\n            ensure maximum model parity.\n\n        # Return\n\n        :return: `None`\n        \"\"\"\n        if device is None:\n            device = DEVICE\n\n        save_ludwig_model_for_inference(\n            save_path,\n            self.model,\n            self.config_obj.to_dict(),\n            self.training_set_metadata,\n            model_only=model_only,\n            device=device,\n        )\n\n    def _check_initialization(self):\n        if self.model is None or self._user_config is None or self.training_set_metadata is None:\n            raise ValueError(\"Model has not been trained or loaded\")\n\n    def free_gpu_memory(self):\n        \"\"\"Manually moves the model to CPU to force GPU memory to be freed.\n\n        For more context: https://discuss.pytorch.org/t/how-can-we-release-gpu-memory-cache/14530/35\n        \"\"\"\n        if torch.cuda.is_available():\n            self.model.model.to(torch.device(\"cpu\"))\n            torch.cuda.empty_cache()\n\n    @staticmethod\n    def create_model(config_obj: ModelConfig | dict, random_seed: int = default_random_seed) -> BaseModel:\n        \"\"\"Instantiates BaseModel object.\n\n        # Inputs\n        :param config_obj: (Union[Config, dict]) Ludwig config object\n        :param random_seed: (int, default: ludwig default random seed) Random seed used for weights initialization,\n            splits and any other random function. # Return\n        :return: (ludwig.models.BaseModel) Instance of the Ludwig model object.\n        \"\"\"\n        if isinstance(config_obj, dict):\n            config_obj = ModelConfig.from_dict(config_obj)\n        model_type = get_from_registry(config_obj.model_type, model_type_registry)\n        return model_type(config_obj, random_seed=random_seed)\n\n    @staticmethod\n    def set_logging_level(logging_level: int) -> None:\n        \"\"\"Sets level for log messages.\n\n        # Inputs\n\n        :param logging_level: (int) Set/Update the logging level. Use logging\n        constants like `logging.DEBUG` , `logging.INFO` and `logging.ERROR`.\n\n        # Return\n\n        :return: `None`\n        \"\"\"\n        logging.getLogger(\"ludwig\").setLevel(logging_level)\n        if logging_level in {logging.WARNING, logging.ERROR, logging.CRITICAL}:\n            set_disable_progressbar(True)\n        else:\n            set_disable_progressbar(False)\n\n    @property\n    def config(self) -> ModelConfigDict:\n        \"\"\"Returns the fully-rendered config of this model including default values.\"\"\"\n        return self.config_obj.to_dict()\n\n    @config.setter\n    def config(self, user_config: ModelConfigDict):\n        \"\"\"Updates the config of this model.\n\n        WARNING: this can have unexpected results on an already trained model.\n        \"\"\"\n        self._user_config = user_config\n        self.config_obj = ModelConfig.from_dict(self._user_config)\n\n    def is_merge_and_unload_set(self) -> bool:\n        \"\"\"Check whether the encapsulated model is of type LLM and is configured to merge_and_unload QLoRA weights.\n\n        # Return\n\n        :return (bool): whether merge_and_unload should be done.\n        \"\"\"\n        # TODO: In the future, it may be possible to move up the model type check into the BaseModel class.\n        return self.config_obj.model_type == MODEL_LLM and self.model.is_merge_and_unload_set()\n\n\n@PublicAPI\ndef kfold_cross_validate(\n    num_folds: int,\n    config: dict | str,\n    dataset: str = None,\n    data_format: str = None,\n    skip_save_training_description: bool = False,\n    skip_save_training_statistics: bool = False,\n    skip_save_model: bool = False,\n    skip_save_progress: bool = False,\n    skip_save_log: bool = False,\n    skip_save_processed_input: bool = False,\n    skip_save_predictions: bool = False,\n    skip_save_eval_stats: bool = False,\n    skip_collect_predictions: bool = False,\n    skip_collect_overall_stats: bool = False,\n    output_directory: str = \"results\",\n    random_seed: int = default_random_seed,\n    gpus: str | int | list[int] | None = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    backend: Backend | str | None = None,\n    logging_level: int = logging.INFO,\n    **kwargs,\n) -> tuple[dict, dict]:\n    \"\"\"Performs k-fold cross validation and returns result data structures.\n\n    # Inputs\n\n    :param num_folds: (int) number of folds to create for the cross-validation\n    :param config: (Union[dict, str]) model specification\n           required to build a model. Parameter may be a dictionary or string\n           specifying the file path to a yaml configuration file.  Refer to the\n           [User Guide](http://ludwig.ai/user_guide/#model-config)\n           for details.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used for k_fold processing.\n        :param data_format: (str, default: `None`) format to interpret data\n            sources. Will be inferred automatically if not specified.  Valid\n            formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n            `'fwf'`,\n            `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n            `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n            `'stata'`, `'tsv'`.  Currently `hdf5` format is not supported for\n            k_fold cross validation.\n    :param skip_save_training_description: (bool, default: `False`) disables\n            saving the description JSON file.\n    :param skip_save_training_statistics: (bool, default: `False`) disables\n            saving training statistics JSON file.\n    :param skip_save_model: (bool, default: `False`) disables\n        saving model weights and hyperparameters each time the model\n        improves. By default Ludwig saves model weights after each epoch\n        the validation metric improves, but if the model is really big\n        that can be time consuming. If you do not want to keep\n        the weights and just find out what performance a model can get\n        with a set of hyperparameters, use this parameter to skip it,\n        but the model will not be loadable later on and the returned model\n        will have the weights obtained at the end of training, instead of\n        the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n           progress each epoch. By default Ludwig saves weights and stats\n           after each epoch for enabling resuming of training, but if\n           the model is really big that can be time consuming and will uses\n           twice as much space, use this parameter to skip it, but training\n           cannot be resumed later on.\n    :param skip_save_log: (bool, default: `False`) disables saving TensorBoard\n           logs. By default Ludwig saves logs for the TensorBoard, but if it\n           is not needed turning it off can slightly increase the\n           overall speed.\n    :param skip_save_processed_input: (bool, default: `False`) if input\n        dataset is provided it is preprocessed and cached by saving an HDF5\n        and JSON files to avoid running the preprocessing again. If this\n        parameter is `False`, the HDF5 and JSON file are not saved.\n    :param skip_save_predictions: (bool, default: `False`) skips saving test\n            predictions CSV files.\n    :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n            statistics JSON file.\n    :param skip_collect_predictions: (bool, default: `False`) skips collecting\n            post-processed predictions during eval.\n    :param skip_collect_overall_stats: (bool, default: `False`) skips collecting\n            overall stats during eval.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param random_seed: (int, default: `42`) Random seed\n            used for weights initialization,\n           splits and any other random function.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n            for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n            [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow Torch to\n            use multithreading parallelism\n           to improve performance at the cost of determinism.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n            of backend to use to execute preprocessing / training steps.\n    :param logging_level: (int, default: INFO) log level to send to stderr.\n\n\n    # Return\n\n    :return: (tuple(kfold_cv_statistics, kfold_split_indices), dict) a tuple of\n            dictionaries `kfold_cv_statistics`: contains metrics from cv run.\n             `kfold_split_indices`: indices to split training data into\n             training fold and test fold.\n    \"\"\"\n    # if config is a path, convert to dictionary\n    if isinstance(config, str):  # assume path\n        config = load_yaml(config)\n    backend = initialize_backend(backend or config.get(\"backend\"))\n\n    # check for k_fold\n    if num_folds is None:\n        raise ValueError(\"k_fold parameter must be specified\")\n\n    logger.info(f\"starting {num_folds:d}-fold cross validation\")\n\n    # create output_directory if not available\n    if not os.path.isdir(output_directory):\n        os.mkdir(output_directory)\n\n    # prepare data for k-fold processing\n    # use Ludwig's utility to facilitate creating a dataframe\n    # that is used as the basis for creating folds\n\n    dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend)\n\n    # determine data format of provided dataset\n    if not data_format or data_format == \"auto\":\n        data_format = figure_data_format(dataset)\n\n    data_df = load_dataset(dataset, data_format=data_format, df_lib=backend.df_engine.df_lib)\n\n    kfold_cv_stats = {}\n    kfold_split_indices = {}\n\n    for train_indices, test_indices, fold_num in generate_kfold_splits(data_df, num_folds, random_seed):\n        with tempfile.TemporaryDirectory() as temp_dir_name:\n            curr_train_df = data_df.iloc[train_indices]\n            curr_test_df = data_df.iloc[test_indices]\n\n            kfold_split_indices[\"fold_\" + str(fold_num)] = {\n                \"training_indices\": train_indices,\n                \"test_indices\": test_indices,\n            }\n\n            # train and validate model on this fold\n            logger.info(f\"training on fold {fold_num:d}\")\n\n            model = LudwigModel(\n                config=config,\n                logging_level=logging_level,\n                backend=backend,\n                gpus=gpus,\n                gpu_memory_limit=gpu_memory_limit,\n                allow_parallel_threads=allow_parallel_threads,\n            )\n            eval_stats, train_stats, preprocessed_data, output_directory = model.experiment(\n                training_set=curr_train_df,\n                test_set=curr_test_df,\n                experiment_name=\"cross_validation\",\n                model_name=\"fold_\" + str(fold_num),\n                skip_save_training_description=skip_save_training_description,\n                skip_save_training_statistics=skip_save_training_statistics,\n                skip_save_model=skip_save_model,\n                skip_save_progress=skip_save_progress,\n                skip_save_log=skip_save_log,\n                skip_save_processed_input=skip_save_processed_input,\n                skip_save_predictions=skip_save_predictions,\n                skip_save_eval_stats=skip_save_eval_stats,\n                skip_collect_predictions=skip_collect_predictions,\n                skip_collect_overall_stats=skip_collect_overall_stats,\n                output_directory=os.path.join(temp_dir_name, \"results\"),\n                random_seed=random_seed,\n            )\n\n            # augment the training statistics with scoring metric from\n            # the hold out fold\n            if dataclasses.is_dataclass(train_stats):\n                train_stats_dict = dataclasses.asdict(train_stats)\n            elif hasattr(train_stats, \"to_dict\"):\n                train_stats_dict = train_stats.to_dict()\n            else:\n                train_stats_dict = vars(train_stats)\n            train_stats_dict[\"fold_eval_stats\"] = eval_stats\n\n            # collect training statistics for this fold\n            kfold_cv_stats[\"fold_\" + str(fold_num)] = train_stats_dict\n\n    # consolidate raw fold metrics across all folds\n    raw_kfold_stats = {}\n    for fold_name in kfold_cv_stats:\n        curr_fold_eval_stats = kfold_cv_stats[fold_name][\"fold_eval_stats\"]\n        for of_name in curr_fold_eval_stats:\n            if of_name not in raw_kfold_stats:\n                raw_kfold_stats[of_name] = {}\n            fold_eval_stats_of = curr_fold_eval_stats[of_name]\n\n            for metric in fold_eval_stats_of:\n                if metric not in {\n                    \"predictions\",\n                    \"probabilities\",\n                    \"confusion_matrix\",\n                    \"overall_stats\",\n                    \"per_class_stats\",\n                    \"roc_curve\",\n                    \"precision_recall_curve\",\n                }:\n                    if metric not in raw_kfold_stats[of_name]:\n                        raw_kfold_stats[of_name][metric] = []\n                    raw_kfold_stats[of_name][metric].append(fold_eval_stats_of[metric])\n\n    # calculate overall kfold statistics\n    overall_kfold_stats = {}\n    for of_name in raw_kfold_stats:\n        overall_kfold_stats[of_name] = {}\n        for metric in raw_kfold_stats[of_name]:\n            mean = np.mean(raw_kfold_stats[of_name][metric])\n            std = np.std(raw_kfold_stats[of_name][metric])\n            overall_kfold_stats[of_name][metric + \"_mean\"] = mean\n            overall_kfold_stats[of_name][metric + \"_std\"] = std\n\n    kfold_cv_stats[\"overall\"] = overall_kfold_stats\n\n    logger.info(f\"completed {num_folds:d}-fold cross validation\")\n\n    return kfold_cv_stats, kfold_split_indices\n\n\ndef _get_compute_description(backend) -> dict:\n    \"\"\"Returns the compute description for the backend.\"\"\"\n    compute_description = {\"num_nodes\": backend.num_nodes}\n\n    if torch.cuda.is_available():\n        # Assumption: All nodes are of the same instance type.\n        # TODO: fix for Ray where workers may be of different skus\n        compute_description.update(\n            {\n                \"gpus_per_node\": torch.cuda.device_count(),\n                \"arch_list\": torch.cuda.get_arch_list(),\n                \"gencode_flags\": torch.cuda.get_gencode_flags(),\n                \"devices\": {},\n            }\n        )\n        for i in range(torch.cuda.device_count()):\n            compute_description[\"devices\"][i] = {\n                \"gpu_type\": torch.cuda.get_device_name(i),\n                \"device_capability\": torch.cuda.get_device_capability(i),\n                \"device_properties\": str(torch.cuda.get_device_properties(i)),\n            }\n\n    return compute_description\n\n\n@PublicAPI\ndef get_experiment_description(\n    config,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    training_set_metadata=None,\n    data_format=None,\n    backend=None,\n    random_seed=None,\n):\n    description = OrderedDict()\n    description[\"ludwig_version\"] = LUDWIG_VERSION\n    description[\"command\"] = \" \".join(sys.argv)\n\n    commit_hash = get_commit_hash()\n    if commit_hash is not None:\n        description[\"commit_hash\"] = commit_hash[:12]\n\n    if random_seed is not None:\n        description[\"random_seed\"] = random_seed\n\n    if isinstance(dataset, str):\n        description[\"dataset\"] = dataset\n    if isinstance(training_set, str):\n        description[\"training_set\"] = training_set\n    if isinstance(validation_set, str):\n        description[\"validation_set\"] = validation_set\n    if isinstance(test_set, str):\n        description[\"test_set\"] = test_set\n    if training_set_metadata is not None:\n        description[\"training_set_metadata\"] = training_set_metadata\n\n    # determine data format if not provided or auto\n    if not data_format or data_format == \"auto\":\n        data_format = figure_data_format(dataset, training_set, validation_set, test_set)\n\n    if data_format:\n        description[\"data_format\"] = str(data_format)\n\n    description[\"config\"] = config\n    description[\"torch_version\"] = torch.__version__\n    description[\"compute\"] = _get_compute_description(backend)\n\n    return description\n"
  },
  {
    "path": "ludwig/api_annotations.py",
    "content": "def PublicAPI(*args, **kwargs):\n    \"\"\"Annotation for documenting public APIs. Public APIs are classes and methods exposed to end users of Ludwig.\n\n    If stability=\"stable\", the APIs will remain backwards compatible across minor Ludwig releases\n    (e.g., Ludwig 0.6 -> Ludwig 0.7).\n\n    If stability=\"experimental\", the APIs can be used by advanced users who are tolerant to and expect\n    breaking changes. This will likely be seen in the case of incremental new feature development.\n\n    Args:\n        stability: One of {\"stable\", \"experimental\"}\n\n    Examples:\n        >>> from api_annotations import PublicAPI\n        >>> @PublicAPI\n        ... def func1(x):\n        ...     return x\n        >>> @PublicAPI(stability=\"experimental\")\n        ... def func2(y):\n        ...     return y\n    \"\"\"\n    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):\n        return PublicAPI(stability=\"stable\")(args[0])\n\n    if \"stability\" in kwargs:\n        stability = kwargs[\"stability\"]\n        assert stability in [\"stable\", \"experimental\"], stability\n    elif kwargs:\n        raise ValueError(f\"Unknown kwargs: {kwargs.keys()}\")\n    else:\n        stability = \"stable\"\n\n    def wrap(obj):\n        if stability == \"experimental\":\n            message = f\"PublicAPI ({stability}): This API is {stability} and may change before becoming stable.\"\n        else:\n            message = \"PublicAPI: This API is stable across Ludwig releases.\"\n\n        _append_doc(obj, message=message)\n        _mark_annotated(obj)\n        return obj\n\n    return wrap\n\n\ndef DeveloperAPI(*args, **kwargs):\n    \"\"\"Annotation for documenting developer APIs. Developer APIs are lower-level methods explicitly exposed to\n    advanced Ludwig users and library developers. Their interfaces may change across minor Ludwig releases (for\n    e.g., Ludwig 0.6.1 and Ludwig 0.6.2).\n\n    Examples:\n        >>> from api_annotations import DeveloperAPI\n        >>> @DeveloperAPI\n        ... def func(x):\n        ...     return x\n    \"\"\"\n    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):\n        return DeveloperAPI()(args[0])\n\n    def wrap(obj):\n        _append_doc(obj, message=\"DeveloperAPI: This API may change across minor Ludwig releases.\")\n        _mark_annotated(obj)\n        return obj\n\n    return wrap\n\n\ndef Deprecated(*args, **kwargs):\n    \"\"\"Annotation for documenting a deprecated API. Deprecated APIs may be removed in future releases of Ludwig\n    (e.g., Ludwig 0.7 to Ludwig 0.8).\n\n    Args:\n        message: A message to help users understand the reason for the deprecation, and provide a migration path.\n\n    Examples:\n        >>> from api_annotations import Deprecated\n        >>> @Deprecated\n        ... def func(x):\n        ...     return x\n        >>> @Deprecated(message=\"g() is deprecated because the API is error prone. Please call h() instead.\")\n        ... def g(y):\n        ...     return y\n    \"\"\"\n    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):\n        return Deprecated()(args[0])\n\n    message = \"**DEPRECATED:** This API is deprecated and may be removed in a future Ludwig release.\"\n\n    if \"message\" in kwargs:\n        message += \" \" + kwargs[\"message\"]\n        del kwargs[\"message\"]\n\n    if kwargs:\n        raise ValueError(f\"Unknown kwargs: {kwargs.keys()}\")\n\n    def inner(obj):\n        _append_doc(obj, message=message, directive=\"warning\")\n        _mark_annotated(obj)\n        return obj\n\n    return inner\n\n\ndef _append_doc(obj, message: str, directive: str | None = None) -> str:\n    \"\"\"\n    Args:\n        message: An additional message to append to the end of docstring for a class\n                 or method that uses one of the API annotations\n        directive: A shorter message that provides contexts for the message and indents it.\n                For example, this could be something like 'warning' or 'info'.\n    \"\"\"\n    if not obj.__doc__:\n        obj.__doc__ = \"\"\n\n    obj.__doc__ = obj.__doc__.rstrip()\n\n    indent = _get_indent(obj.__doc__)\n    obj.__doc__ += \"\\n\\n\"\n    if directive is not None:\n        obj.__doc__ += f\"{' ' * indent}.. {directive}::\\n\"\n        obj.__doc__ += f\"{' ' * (indent + 4)}{message}\"\n    else:\n        obj.__doc__ += f\"{' ' * indent}{message}\"\n    obj.__doc__ += f\"\\n{' ' * indent}\"\n\n\ndef _mark_annotated(obj) -> None:\n    # Set magic token for check_api_annotations linter.\n    if hasattr(obj, \"__name__\"):\n        obj._annotated = obj.__name__\n\n\ndef _is_annotated(obj) -> bool:\n    # Check the magic token exists and applies to this class (not a subclass).\n    return hasattr(obj, \"_annotated\") and obj._annotated == obj.__name__\n\n\ndef _get_indent(docstring: str) -> int:\n    \"\"\"\n    Example:\n        >>> def f():\n        ...     '''Docstring summary.'''\n        >>> f.__doc__\n        'Docstring summary.'\n        >>> _get_indent(f.__doc__)\n        0\n        >>> def g(foo):\n        ...     '''Docstring summary.\n        ...\n        ...     Args:\n        ...         foo: Does bar.\n        ...     '''\n        >>> g.__doc__\n        'Docstring summary.\\\\n\\\\n    Args:\\\\n        foo: Does bar.\\\\n    '\n        >>> _get_indent(g.__doc__)\n        4\n        >>> class A:\n        ...     def h():\n        ...         '''Docstring summary.\n        ...\n        ...         Returns:\n        ...             None.\n        ...         '''\n        >>> A.h.__doc__\n        'Docstring summary.\\\\n\\\\n        Returns:\\\\n            None.\\\\n        '\n        >>> _get_indent(A.h.__doc__)\n        8\n    \"\"\"\n    if not docstring:\n        return 0\n\n    non_empty_lines = list(filter(bool, docstring.splitlines()))\n    if len(non_empty_lines) == 1:\n        # Docstring contains summary only.\n        return 0\n\n    # The docstring summary isn't indented, so check the indentation of the second non-empty line.\n    return len(non_empty_lines[1]) - len(non_empty_lines[1].lstrip())\n"
  },
  {
    "path": "ludwig/automl/__init__.py",
    "content": "from ludwig.automl.automl import auto_train  # noqa\nfrom ludwig.automl.automl import cli_init_config  # noqa\nfrom ludwig.automl.automl import create_auto_config  # noqa\nfrom ludwig.automl.automl import train_with_config  # noqa; noqa\n"
  },
  {
    "path": "ludwig/automl/auto_tune_config.py",
    "content": "import copy\nimport logging\nimport math\nfrom collections import OrderedDict\n\nimport psutil\n\ntry:\n    import GPUtil\nexcept ImportError:\n    raise ImportError(\"GPUtil is not installed. In order to use auto_train please run pip install ludwig[ray]\")\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import initialize_backend\nfrom ludwig.constants import (\n    AUTO,\n    AUTOML_DEFAULT_TEXT_ENCODER,\n    AUTOML_LARGE_TEXT_DATASET,\n    AUTOML_MAX_ROWS_PER_CHECKPOINT,\n    AUTOML_SMALLER_TEXT_ENCODER,\n    AUTOML_SMALLER_TEXT_LENGTH,\n    AUTOML_TEXT_ENCODER_MAX_TOKEN_LEN,\n    HYPEROPT,\n    MINIMUM_BATCH_SIZE,\n    PREPROCESSING,\n    SPACE,\n    TEXT,\n    TRAINER,\n)\nfrom ludwig.data.preprocessing import preprocess_for_training\nfrom ludwig.features.feature_registries import update_config_with_metadata\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.automl.utils import get_model_type\nfrom ludwig.utils.torch_utils import initialize_pytorch\n\nlogger = logging.getLogger(__name__)\n\n# maps variable search space that can be modified to minimum permissible value for the range\nRANKED_MODIFIABLE_PARAM_LIST = {\n    \"tabnet\": OrderedDict(\n        {\n            \"trainer.batch_size\": 32,\n            \"combiner.size\": 8,\n            \"combiner.output_size\": 8,\n        }\n    ),\n    \"concat\": OrderedDict(\n        {\n            \"trainer.batch_size\": 32,\n            \"combiner.output_size\": 64,\n            \"combiner.num_fc_layers\": 1,\n        }\n    ),\n    \"tabtransformer\": OrderedDict(\n        {\n            \"trainer.batch_size\": 32,\n            \"combiner.num_heads:\": 4,\n            \"combiner.output_size\": 8,\n            \"combiner.num_layers\": 4,\n            \"combiner.num_fc_layers\": 1,\n        }\n    ),\n    \"text\": OrderedDict(  # for single input feature text models e.g. bert and its variants\n        {\n            \"trainer.batch_size\": 16,\n        }\n    ),\n}\n\n\nBYTES_PER_MiB = 1048576\nBYTES_PER_WEIGHT = 4  # assumes 32-bit precision = 4 bytes\nBYTES_OPTIMIZER_PER_WEIGHT = 8  # for optimizer m and v vectors\n\n\ndef get_trainingset_metadata(config, dataset, backend):\n    _, _, _, training_set_metadata = preprocess_for_training(\n        config, dataset=dataset, preprocessing_params=config[PREPROCESSING], backend=backend\n    )\n    return training_set_metadata\n\n\n# Note: if run in Ray Cluster, this method is run remote with gpu resources requested if available\ndef _get_machine_memory():\n    if GPUtil.getGPUs():\n        machine_mem = GPUtil.getGPUs()[0].memoryTotal * BYTES_PER_MiB\n    else:\n        machine_mem = psutil.virtual_memory().total\n    return machine_mem\n\n\ndef _get_text_feature_max_length(config, training_set_metadata) -> int:\n    \"\"\"Returns max sequence length over text features, subject to preprocessing limit.\"\"\"\n    max_length = 0\n    for feature in config[\"input_features\"]:\n        if feature[\"type\"] == TEXT:\n            feature_max_len = training_set_metadata[feature[\"name\"]][\"max_sequence_length\"]\n            if feature_max_len > max_length:\n                max_length = feature_max_len\n    if (\n        (\"preprocessing\" in config)\n        and (TEXT in config[\"preprocessing\"])\n        and (\"max_sequence_length\" in config[\"preprocessing\"][TEXT])\n    ):\n        limit = config[\"preprocessing\"][TEXT][\"max_sequence_length\"]\n    else:\n        limit = 256  # Preprocessing default max_sequence_length = 256\n    if max_length > limit + 2:  # For start and stop symbols.\n        max_length = limit + 2\n    return max_length\n\n\ndef _get_text_model_memory_usage(config, training_set_metadata, memory_usage) -> int:\n    max_feature_token_length = _get_text_feature_max_length(config, training_set_metadata)\n    memory_usage = (memory_usage / AUTOML_TEXT_ENCODER_MAX_TOKEN_LEN) * max_feature_token_length\n    return memory_usage\n\n\ndef compute_memory_usage(config_obj, training_set_metadata, model_category) -> int:\n    update_config_with_metadata(config_obj, training_set_metadata)\n    lm = LudwigModel.create_model(config_obj)\n    model_size = lm.get_model_size()  # number of parameters in model\n    batch_size = config_obj.trainer.batch_size\n    if batch_size == AUTO:\n        # Smallest valid batch size that will allow training to complete\n        batch_size = MINIMUM_BATCH_SIZE\n    memory_usage = model_size * (BYTES_PER_WEIGHT + BYTES_OPTIMIZER_PER_WEIGHT) * batch_size\n    if model_category == TEXT:\n        return _get_text_model_memory_usage(config_obj.to_dict(), training_set_metadata, memory_usage)\n    else:\n        return memory_usage\n\n\ndef sub_new_params(config: dict, new_param_vals: dict):\n    new_config = copy.deepcopy(config)\n    for param, val in new_param_vals.items():\n        config_section = param.split(\".\")[0]\n        param_name = param.split(\".\")[1]\n        new_config[config_section][param_name] = val\n    return new_config\n\n\ndef get_new_params(current_param_values, hyperparam_search_space, params_to_modify):\n    for param, _ in params_to_modify.items():\n        if param in hyperparam_search_space:\n            if hyperparam_search_space[param][SPACE] == \"choice\":\n                current_param_values[param] = hyperparam_search_space[param][\"categories\"][-1]\n            else:\n                current_param_values[param] = hyperparam_search_space[param][\"upper\"]\n    return current_param_values\n\n\ndef _update_text_encoder(input_features: list, old_text_encoder: str, new_text_encoder: str) -> None:\n    for feature in input_features:\n        if feature[\"type\"] == TEXT and feature[\"encoder\"] == old_text_encoder:\n            feature[\"encoder\"] = new_text_encoder\n\n\ndef _get_text_feature_min_usable_length(input_features: list, training_set_metadata) -> int:\n    \"\"\"Returns min of AUTOML_SMALLER_TEXT_LENGTH and lowest 99th percentile sequence length over text features.\"\"\"\n    min_usable_length = AUTOML_SMALLER_TEXT_LENGTH\n    for feature in input_features:\n        if feature[\"type\"] == TEXT:\n            feature_99ptile_len = training_set_metadata[feature[\"name\"]][\"max_sequence_length_99ptile\"]\n            if feature_99ptile_len < min_usable_length:\n                min_usable_length = feature_99ptile_len\n    return round(min_usable_length)\n\n\ndef reduce_text_feature_max_length(config, training_set_metadata) -> bool:\n    \"\"\"Reduce max sequence length, when viable, to control its quadratic impact.\"\"\"\n    input_features = config[\"input_features\"]\n    min_usable_length = _get_text_feature_min_usable_length(input_features, training_set_metadata)\n    seq_len_limit = {\"max_sequence_length\": min_usable_length}\n    if \"preprocessing\" not in config:\n        config[\"preprocessing\"] = {TEXT: seq_len_limit}\n    elif (\n        (TEXT not in config[\"preprocessing\"])\n        or (\"max_sequence_length\" not in config[\"preprocessing\"][TEXT])\n        or (min_usable_length < float(config[\"preprocessing\"][TEXT][\"max_sequence_length\"]))\n    ):\n        config[\"preprocessing\"][TEXT] = seq_len_limit\n    else:\n        return False\n    return True\n\n\n# For hyperparam_search_space comprised solely of choice spaces, compute maximum number of\n# combinations and return that value if it is less than num_samples; else return num_samples.\ndef _update_num_samples(num_samples, hyperparam_search_space):\n    max_num_samples = 1\n    for param in hyperparam_search_space.keys():\n        if hyperparam_search_space[param][SPACE] == \"choice\":\n            max_num_samples *= len(hyperparam_search_space[param][\"categories\"])\n        else:\n            return num_samples\n    if max_num_samples < num_samples:\n        return max_num_samples\n    return num_samples\n\n\n# Note: if run in Ray Cluster, this method is run remote with gpu resources requested if available\ndef memory_tune_config(config, dataset, model_category, row_count, backend):\n    backend = initialize_backend(backend)\n\n    fits_in_memory = False\n    tried_reduce_seq_len = False\n    config_obj = ModelConfig.from_dict(config)\n    raw_config = config_obj.to_dict()\n    training_set_metadata = get_trainingset_metadata(raw_config, dataset, backend)\n    modified_hyperparam_search_space = copy.deepcopy(raw_config[HYPEROPT][\"parameters\"])\n    current_param_values = {}\n    param_list = []\n    model_type = get_model_type(raw_config)\n    if model_type in RANKED_MODIFIABLE_PARAM_LIST:\n        params_to_modify = RANKED_MODIFIABLE_PARAM_LIST[model_type]\n        if len(params_to_modify.keys()) > 0:\n            param_list = list(params_to_modify.keys())\n            max_memory = _get_machine_memory()\n            initialize_pytorch()\n\n    while param_list:\n        # compute memory utilization\n        current_param_values = get_new_params(current_param_values, modified_hyperparam_search_space, params_to_modify)\n        temp_config = sub_new_params(raw_config, current_param_values)\n        config_obj = ModelConfig.from_dict(temp_config)\n        mem_use = compute_memory_usage(config_obj, training_set_metadata, model_category)\n        if mem_use > max_memory and model_category == TEXT and not tried_reduce_seq_len:\n            tried_reduce_seq_len = True\n            if reduce_text_feature_max_length(config, training_set_metadata):\n                reduce_text_feature_max_length(temp_config, training_set_metadata)\n                config_obj = ModelConfig.from_dict(temp_config)\n                mem_use = compute_memory_usage(config_obj, training_set_metadata, model_category)\n        logger.info(f\"Checking model estimated mem use {mem_use} against memory size {max_memory}\")\n        if mem_use <= max_memory:\n            fits_in_memory = True\n            break\n        # check if we have exhausted tuning of current param (e.g. we can no longer reduce the param value)\n        param, min_value = param_list[0], params_to_modify[param_list[0]]\n\n        if param in modified_hyperparam_search_space.keys():\n            param_space = modified_hyperparam_search_space[param][\"space\"]\n            if param_space == \"choice\":\n                if (\n                    len(modified_hyperparam_search_space[param][\"categories\"]) >= 2\n                    and modified_hyperparam_search_space[param][\"categories\"][-2] >= min_value\n                ):\n                    modified_hyperparam_search_space[param][\"categories\"] = modified_hyperparam_search_space[param][\n                        \"categories\"\n                    ][:-1]\n                else:\n                    param_list.pop(0)  # exhausted reduction of this parameter\n            else:\n                # reduce by 10%\n                upper_bound, lower_bound = (\n                    modified_hyperparam_search_space[param][\"upper\"],\n                    modified_hyperparam_search_space[param][\"lower\"],\n                )\n                reduction_val = (upper_bound - lower_bound) * 0.1\n                new_upper_bound = upper_bound - reduction_val\n                if (new_upper_bound) > lower_bound and new_upper_bound > min_value:\n                    modified_hyperparam_search_space[param][\"upper\"] = new_upper_bound\n                else:\n                    param_list.pop(0)  # exhausted reduction of this parameter\n        else:\n            param_list.pop(0)  # param not in hyperopt search space\n\n    if model_category == TEXT and row_count > AUTOML_LARGE_TEXT_DATASET:\n        if \"checkpoints_per_epoch\" not in config[TRAINER] and \"steps_per_checkpoint\" not in config[TRAINER]:\n            checkpoints_per_epoch = max(2, math.floor(row_count / AUTOML_MAX_ROWS_PER_CHECKPOINT))\n            config[TRAINER][\n                \"checkpoints_per_epoch\"\n            ] = checkpoints_per_epoch  # decrease latency to get model accuracy signal\n        if \"evaluate_training_set\" not in config[TRAINER]:\n            config[TRAINER][\"evaluate_training_set\"] = False  # reduce overhead for increased evaluation frequency\n        if not fits_in_memory:\n            # Switch to smaller pre-trained model encoder for large datasets.\n            _update_text_encoder(config[\"input_features\"], AUTOML_DEFAULT_TEXT_ENCODER, AUTOML_SMALLER_TEXT_ENCODER)\n\n    modified_config = copy.deepcopy(config)\n\n    modified_config[HYPEROPT][\"parameters\"] = modified_hyperparam_search_space\n    modified_config[HYPEROPT][\"executor\"][\"num_samples\"] = _update_num_samples(\n        modified_config[HYPEROPT][\"executor\"][\"num_samples\"], modified_hyperparam_search_space\n    )\n    return modified_config, fits_in_memory\n"
  },
  {
    "path": "ludwig/automl/automl.py",
    "content": "\"\"\"automl.py.\n\nDriver script which:\n\n(1) Builds a base config by performing type inference and populating config\n    w/default combiner parameters, training parameters, and hyperopt search space\n(2) Tunes config based on resource constraints\n(3) Runs hyperparameter optimization experiment\n\"\"\"\n\nimport argparse\nimport copy\nimport logging\nimport os\nimport warnings\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.automl.base_config import (\n    create_default_config,\n    DatasetInfo,\n    get_dataset_info,\n    get_features_config,\n    get_reference_configs,\n)\nfrom ludwig.backend import Backend, initialize_backend\nfrom ludwig.constants import (\n    AUTO,\n    AUTOML_DEFAULT_IMAGE_ENCODER,\n    AUTOML_DEFAULT_TABULAR_MODEL,\n    AUTOML_DEFAULT_TEXT_ENCODER,\n    BINARY,\n    CATEGORY,\n    ENCODER,\n    HYPEROPT,\n    IMAGE,\n    INPUT_FEATURES,\n    NAME,\n    NUMBER,\n    OUTPUT_FEATURES,\n    TABULAR,\n    TEXT,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.data.cache.types import CacheableDataset\nfrom ludwig.datasets import load_dataset_uris\nfrom ludwig.globals import LUDWIG_VERSION, MODEL_FILE_NAME\nfrom ludwig.hyperopt.run import hyperopt\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.automl.ray_utils import _ray_init\nfrom ludwig.utils.automl.utils import _add_transfer_config, get_model_type, set_output_feature_metric\nfrom ludwig.utils.data_utils import load_dataset, use_credentials\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import open_file\nfrom ludwig.utils.heuristics import get_auto_learning_rate\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.print_utils import print_ludwig\n\ntry:\n    import dask.dataframe as dd\n    from ray.tune import ExperimentAnalysis\nexcept ImportError as e:\n    raise RuntimeError(\"ray is not installed. In order to use auto_train please run pip install ludwig[ray]\") from e\n\n\nlogger = logging.getLogger(__name__)\n\nOUTPUT_DIR = \".\"\nTABULAR_TYPES = {CATEGORY, NUMBER, BINARY}\n\n\nclass AutoTrainResults:\n    def __init__(self, experiment_analysis: ExperimentAnalysis, creds: dict[str, Any] = None):\n        self._experiment_analysis = experiment_analysis\n        self._creds = creds\n\n    @property\n    def experiment_analysis(self):\n        return self._experiment_analysis\n\n    @property\n    def best_trial_id(self) -> str:\n        return self._experiment_analysis.best_trial.trial_id\n\n    @property\n    def best_model(self) -> LudwigModel | None:\n        checkpoint = self._experiment_analysis.best_checkpoint\n        if checkpoint is None:\n            logger.warning(\"No best model found\")\n            return None\n\n        # Use credentials context for remote checkpoints that may need custom auth\n        with use_credentials(self._creds):\n            with checkpoint.as_directory() as ckpt_path:\n                model_dir = os.path.join(ckpt_path, MODEL_FILE_NAME)\n                if not os.path.isdir(model_dir):\n                    logger.warning(\n                        f\"Best checkpoint does not contain model files at {model_dir}. \"\n                        \"The trial may not have completed a full training epoch.\"\n                    )\n                    return None\n                # Ray Tune checkpoints contain training_checkpoints/ (from\n                # mid-training saves) but not model_weights (only saved after\n                # training completes). Load from the training checkpoint.\n                return LudwigModel.load(model_dir, from_checkpoint=True)\n\n\n@PublicAPI\ndef auto_train(\n    dataset: str | pd.DataFrame | dd.DataFrame,\n    target: str,\n    time_limit_s: int | float,\n    output_directory: str = OUTPUT_DIR,\n    tune_for_memory: bool = False,\n    user_config: dict = None,\n    random_seed: int = default_random_seed,\n    use_reference_config: bool = False,\n    **kwargs,\n) -> AutoTrainResults:\n    \"\"\"Main auto train API that first builds configs for each model type (e.g. concat, tabnet, transformer). Then\n    selects model based on dataset attributes. And finally runs a hyperparameter optimization experiment.\n\n    All batch and learning rate tuning is done @ training time.\n\n    # Inputs\n    :param dataset: (str, pd.DataFrame, dd.DataFrame) data source to train over.\n    :param target: (str) name of target feature\n    :param time_limit_s: (int, float) total time allocated to auto_train. acts\n                        as the stopping parameter\n    :param output_directory: (str) directory into which to write results, defaults to\n                             current working directory.\n    :param tune_for_memory: (bool) refine hyperopt search space for available\n                            host / GPU memory\n    :param user_config: (dict) override automatic selection of specified config items\n    :param random_seed: (int, default: `42`) a random seed that will be used anywhere\n                        there is a call to a random number generator, including\n                        hyperparameter search sampling, as well as data splitting,\n                        parameter initialization and training set shuffling\n    :param use_reference_config: (bool) refine hyperopt search space by setting first\n                                 search point from reference model config, if any\n    :param kwargs: additional keyword args passed down to `ludwig.hyperopt.run.hyperopt`.\n\n    # Returns\n    :return: (AutoTrainResults) results containing hyperopt experiments and best model\n    \"\"\"\n    config = create_auto_config(\n        dataset,\n        target,\n        time_limit_s,\n        tune_for_memory,\n        user_config,\n        random_seed,\n        use_reference_config=use_reference_config,\n    )\n    return train_with_config(dataset, config, output_directory=output_directory, random_seed=random_seed, **kwargs)\n\n\n@PublicAPI\ndef create_auto_config(\n    dataset: str | pd.DataFrame | dd.DataFrame | DatasetInfo,\n    target: str | list[str],\n    time_limit_s: int | float,\n    tune_for_memory: bool = False,\n    user_config: dict = None,\n    random_seed: int = default_random_seed,\n    imbalance_threshold: float = 0.9,\n    use_reference_config: bool = False,\n    backend: Backend | str = None,\n) -> ModelConfigDict:\n    \"\"\"Returns an auto-generated Ludwig config with the intent of training the best model on given given dataset /\n    target in the given time limit.\n\n    # Inputs\n    :param dataset: (str, pd.DataFrame, dd.DataFrame, DatasetInfo) data source to train over.\n    :param target: (str, List[str]) name of target feature\n    :param time_limit_s: (int, float) total time allocated to auto_train. acts\n                         as the stopping parameter\n    :param tune_for_memory: (bool) DEPRECATED refine hyperopt search space for available\n                            host / GPU memory\n    :param user_config: (dict) override automatic selection of specified config items\n    :param random_seed: (int, default: `42`) a random seed that will be used anywhere\n                        there is a call to a random number generator, including\n                        hyperparameter search sampling, as well as data splitting,\n                        parameter initialization and training set shuffling\n    :param imbalance_threshold: (float) maximum imbalance ratio (minority / majority) to perform stratified sampling\n    :param use_reference_config: (bool) refine hyperopt search space by setting first\n                                 search point from reference model config, if any\n\n    # Return\n    :return: (dict) selected model configuration\n    \"\"\"\n    backend = initialize_backend(backend)\n\n    if not isinstance(dataset, DatasetInfo):\n        # preload ludwig datasets\n        dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend)\n        if isinstance(dataset, CacheableDataset):\n            dataset = dataset.unwrap()\n        dataset = load_dataset(dataset, df_lib=backend.df_engine.df_lib)\n\n    dataset_info = get_dataset_info(dataset) if not isinstance(dataset, DatasetInfo) else dataset\n    features_config = create_features_config(dataset_info, target)\n    return create_automl_config_for_features(\n        features_config,\n        dataset_info,\n        target,\n        time_limit_s=time_limit_s,\n        user_config=user_config,\n        random_seed=random_seed,\n        imbalance_threshold=imbalance_threshold,\n        use_reference_config=use_reference_config,\n        backend=backend,\n    )\n\n\n@PublicAPI\ndef create_automl_config_for_features(\n    features_config: ModelConfigDict,\n    dataset_info: DatasetInfo,\n    target: str | list[str],\n    time_limit_s: int | float,\n    tune_for_memory: bool = False,\n    user_config: dict = None,\n    random_seed: int = default_random_seed,\n    imbalance_threshold: float = 0.9,\n    use_reference_config: bool = False,\n    backend: Backend | str = None,\n) -> ModelConfigDict:\n    default_configs = create_default_config(\n        features_config, dataset_info, target, time_limit_s, random_seed, imbalance_threshold, backend\n    )\n    model_config, _, _ = _model_select(dataset_info, default_configs, user_config, use_reference_config)\n\n    if tune_for_memory:\n        warnings.warn(\"`tune_for_memory=True` is deprecated, `batch_size=auto` will be used instead\")\n\n    return model_config\n\n\n@PublicAPI\ndef create_features_config(\n    dataset_info: DatasetInfo,\n    target_name: str | list[str] = None,\n) -> ModelConfigDict:\n    return get_features_config(dataset_info.fields, dataset_info.row_count, target_name)\n\n\n@PublicAPI\ndef train_with_config(\n    dataset: str | pd.DataFrame | dd.DataFrame,\n    config: dict,\n    output_directory: str = OUTPUT_DIR,\n    random_seed: int = default_random_seed,\n    **kwargs,\n) -> AutoTrainResults:\n    \"\"\"Performs hyperparameter optimization with respect to the given config and selects the best model.\n\n    # Inputs\n    :param dataset: (str) filepath to dataset.\n    :param config: (dict) optional Ludwig configuration to use for training, defaults\n                   to `create_auto_config`.\n    :param output_directory: (str) directory into which to write results, defaults to\n        current working directory.\n    :param random_seed: (int, default: `42`) a random seed that will be used anywhere\n                        there is a call to a random number generator, including\n                        hyperparameter search sampling, as well as data splitting,\n                        parameter initialization and training set shuffling\n    :param kwargs: additional keyword args passed down to `ludwig.hyperopt.run.hyperopt`.\n\n    # Returns\n    :return: (AutoTrainResults) results containing hyperopt experiments and best model\n    \"\"\"\n    _ray_init()\n\n    model_type = get_model_type(config)\n    hyperopt_results = _train(\n        config, dataset, output_directory=output_directory, model_name=model_type, random_seed=random_seed, **kwargs\n    )\n    # catch edge case where metric_score is nan\n    # TODO (ASN): Decide how we want to proceed if at least one trial has\n    # completed\n    for trial in hyperopt_results.ordered_trials:\n        if isinstance(trial.metric_score, str) or np.isnan(trial.metric_score):\n            warnings.warn(\n                \"There was an error running the experiment. \"\n                \"A trial failed to start. \"\n                \"Consider increasing the time budget for experiment. \"\n            )\n\n    # Extract credentials needed to pull artifacts, if provided\n    creds = None\n    backend: Backend = initialize_backend(kwargs.get(\"backend\"))\n    if backend is not None:\n        creds = backend.storage.artifacts.credentials\n\n    experiment_analysis = hyperopt_results.experiment_analysis\n    return AutoTrainResults(experiment_analysis, creds)\n\n\ndef _model_select(\n    dataset_info: DatasetInfo,\n    default_configs,\n    user_config,\n    use_reference_config: bool,\n):\n    \"\"\"Performs model selection based on dataset or user specified model.\n\n    Note: Current implementation returns tabnet by default for tabular datasets.\n    \"\"\"\n    fields = dataset_info.fields\n\n    base_config = copy.deepcopy(default_configs[\"base_config\"])\n    model_category = None\n\n    input_features = default_configs[\"base_config\"][\"input_features\"]\n\n    # tabular dataset heuristics\n    if len(fields) > 3 and all(f[TYPE] in TABULAR_TYPES for f in input_features):\n        model_category = TABULAR\n        base_config = merge_dict(base_config, default_configs[\"combiner\"][AUTOML_DEFAULT_TABULAR_MODEL])\n\n        # override combiner heuristic if explicitly provided by user\n        if user_config is not None:\n            if \"combiner\" in user_config.keys():\n                model_type = user_config[\"combiner\"][\"type\"]\n                base_config = merge_dict(base_config, default_configs[\"combiner\"][model_type])\n    else:\n        # text heuristics\n        for i, input_feature in enumerate(input_features):\n            base_config_input_feature = base_config[\"input_features\"][i]\n            # default text encoder is bert\n            if input_feature[TYPE] == TEXT:\n                model_category = TEXT\n                if ENCODER in input_feature:\n                    base_config_input_feature[ENCODER][TYPE] = AUTOML_DEFAULT_TEXT_ENCODER\n                else:\n                    base_config_input_feature[ENCODER] = {TYPE: AUTOML_DEFAULT_TEXT_ENCODER}\n                # TODO(shreya): Should this hyperopt config param be set here?\n                base_config[HYPEROPT][\"executor\"][\"num_samples\"] = 5  # set for small hyperparameter search space\n                base_config = merge_dict(base_config, default_configs[TEXT][AUTOML_DEFAULT_TEXT_ENCODER])\n\n            # TODO (ASN): add image heuristics\n            if input_feature[TYPE] == IMAGE:\n                model_category = IMAGE\n                if ENCODER in input_feature:\n                    base_config_input_feature[ENCODER][TYPE] = AUTOML_DEFAULT_IMAGE_ENCODER\n                else:\n                    base_config_input_feature[ENCODER] = {TYPE: AUTOML_DEFAULT_IMAGE_ENCODER}\n\n        # Merge combiner config\n        base_config = merge_dict(base_config, default_configs[\"combiner\"][\"concat\"])\n\n    # Adjust learning rate based on other config settings\n    if base_config[TRAINER][\"learning_rate\"] == AUTO:\n        # Add a fake output feature to ensure we can load the ModelConfig, as we expect there to be at least\n        # one output feature in all cases\n        # TODO(travis): less hacky way to do this, we should probably allow ModelConfig to be created without output\n        # features\n        load_config = copy.deepcopy(base_config)\n        if not load_config.get(OUTPUT_FEATURES):\n            load_config[OUTPUT_FEATURES] = [{\"name\": \"fake\", \"type\": \"binary\"}]\n        base_config[TRAINER][\"learning_rate\"] = get_auto_learning_rate(ModelConfig.from_dict(load_config))\n\n    # override and constrain automl config based on user specified values\n    if user_config is not None:\n        base_config = merge_dict(base_config, user_config)\n\n        # remove all parameters from hyperparameter search that user has\n        # provided explicit values for\n        hyperopt_params = copy.deepcopy(base_config[\"hyperopt\"][\"parameters\"])\n        for hyperopt_params in hyperopt_params.keys():\n            config_section, param = hyperopt_params.split(\".\")[0], hyperopt_params.split(\".\")[1]\n            if config_section in user_config.keys():\n                if param in user_config[config_section]:\n                    del base_config[\"hyperopt\"][\"parameters\"][hyperopt_params]\n\n    # if single output feature, set relevant metric and goal if not already set\n    base_config = set_output_feature_metric(base_config)\n\n    # add as initial trial in the automl search the hyperparameter settings from\n    # the best model for a similar dataset and matching model type, if any.\n    if use_reference_config:\n        ref_configs = get_reference_configs()\n        base_config = _add_transfer_config(base_config, ref_configs)\n\n    return base_config, model_category, dataset_info.row_count\n\n\ndef _train(\n    config: dict,\n    dataset: str | pd.DataFrame | dd.DataFrame,\n    output_directory: str,\n    model_name: str,\n    random_seed: int,\n    **kwargs,\n):\n    hyperopt_results = hyperopt(\n        config,\n        dataset=dataset,\n        output_directory=output_directory,\n        model_name=model_name,\n        random_seed=random_seed,\n        skip_save_log=True,  # avoid per-step log overhead by default\n        **kwargs,\n    )\n    return hyperopt_results\n\n\ndef init_config(\n    dataset: str,\n    target: str | list[str],\n    time_limit_s: int | float,\n    tune_for_memory: bool = False,\n    suggested: bool = False,\n    hyperopt: bool = False,\n    output: str = None,\n    random_seed: int = default_random_seed,\n    use_reference_config: bool = False,\n    **kwargs,\n):\n    config = create_auto_config(\n        dataset=dataset,\n        target=target,\n        time_limit_s=time_limit_s,\n        random_seed=random_seed,\n        use_reference_config=use_reference_config,\n        tune_for_memory=tune_for_memory,\n    )\n\n    if HYPEROPT in config and not hyperopt:\n        del config[HYPEROPT]\n\n    if not suggested:\n        # Only use inputs and outputs\n        minimal_config = {\n            INPUT_FEATURES: [{\"name\": f[NAME], \"type\": f[TYPE]} for f in config[INPUT_FEATURES]],\n            OUTPUT_FEATURES: [{\"name\": f[NAME], \"type\": f[TYPE]} for f in config[OUTPUT_FEATURES]],\n        }\n        if hyperopt:\n            minimal_config[HYPEROPT] = config[HYPEROPT]\n        config = minimal_config\n\n    if output is None:\n        print(yaml.safe_dump(config, None, sort_keys=False))\n    else:\n        with open_file(output, \"w\") as f:\n            yaml.safe_dump(config, f, sort_keys=False)\n\n\ndef cli_init_config(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script initializes a valid config from a dataset.\",\n        prog=\"ludwig init_config\",\n        usage=\"%(prog)s [options]\",\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--dataset\",\n        type=str,\n        help=\"input data file path\",\n    )\n    parser.add_argument(\n        \"-t\",\n        \"--target\",\n        type=str,\n        help=\"target(s) to predict as output features of the model\",\n        action=\"append\",\n        required=False,\n    )\n    parser.add_argument(\n        \"--time_limit_s\",\n        type=int,\n        help=\"time limit to train the model in seconds when using hyperopt\",\n        required=False,\n    )\n    parser.add_argument(\n        \"--suggested\",\n        type=bool,\n        help=\"use suggested config from automl, otherwise only use inferred types and return a minimal config\",\n        default=False,\n        required=False,\n    )\n    parser.add_argument(\n        \"--hyperopt\",\n        type=bool,\n        help=\"include automl hyperopt config\",\n        default=False,\n        required=False,\n    )\n    parser.add_argument(\n        \"--random_seed\",\n        type=int,\n        help=\"seed for random number generators used in hyperopt to improve repeatability\",\n        required=False,\n    )\n    parser.add_argument(\n        \"--use_reference_config\",\n        type=bool,\n        help=\"refine hyperopt search space by setting first search point from stored reference model config\",\n        default=False,\n        required=False,\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        type=str,\n        help=\"output initialized YAML config path\",\n        required=False,\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"init_config\", *sys_argv)\n\n    print_ludwig(\"Init Config\", LUDWIG_VERSION)\n    init_config(**vars(args))\n"
  },
  {
    "path": "ludwig/automl/base_config.py",
    "content": "\"\"\"Uses heuristics to build ludwig configuration file:\n\n(1) infer types based on dataset\n(2) populate with\n    - default combiner parameters,\n    - preprocessing parameters,\n    - combiner specific default training parameters,\n    - combiner specific hyperopt space\n    - feature parameters\n(3) add machineresources\n    (base implementation -- # CPU, # GPU)\n\"\"\"\n\nimport logging\nimport os\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport dask.dataframe as dd\nimport numpy as np\nimport pandas as pd\nimport yaml\nfrom dataclasses_json import dataclass_json, LetterCase\nfrom tqdm import tqdm\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.backend import Backend\nfrom ludwig.constants import (\n    COLUMN,\n    COMBINER,\n    ENCODER,\n    EXECUTOR,\n    HYPEROPT,\n    INPUT_FEATURES,\n    PREPROCESSING,\n    SCHEDULER,\n    SEARCH_ALG,\n    SPLIT,\n    TEXT,\n    TYPE,\n)\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.automl.data_source import DataSource, wrap_data_source\nfrom ludwig.utils.automl.field_info import FieldConfig, FieldInfo, FieldMetadata\nfrom ludwig.utils.automl.type_inference import infer_type, should_exclude\nfrom ludwig.utils.data_utils import load_yaml\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.system_utils import Resources\n\nlogger = logging.getLogger(__name__)\n\nPATH_HERE = os.path.abspath(os.path.dirname(__file__))\nCONFIG_DIR = os.path.join(PATH_HERE, \"defaults\")\n\nBASE_AUTOML_CONFIG = os.path.join(CONFIG_DIR, \"base_automl_config.yaml\")\nREFERENCE_CONFIGS = os.path.join(CONFIG_DIR, \"reference_configs.yaml\")\n\ncombiner_defaults = {\n    \"concat\": os.path.join(CONFIG_DIR, \"combiner/concat_config.yaml\"),\n    \"tabnet\": os.path.join(CONFIG_DIR, \"combiner/tabnet_config.yaml\"),\n    \"transformer\": os.path.join(CONFIG_DIR, \"combiner/transformer_config.yaml\"),\n}\n\nencoder_defaults = {\"text\": {\"bert\": os.path.join(CONFIG_DIR, \"text/bert_config.yaml\")}}\n\n# Cap for number of distinct values to return.\nMAX_DISTINCT_VALUES_TO_RETURN = 10\n\n\n@DeveloperAPI\n@dataclass_json(letter_case=LetterCase.CAMEL)\n@dataclass\nclass DatasetInfo:\n    fields: list[FieldInfo]\n    row_count: int\n    size_bytes: int = -1\n\n\ndef allocate_experiment_resources(resources: Resources) -> dict:\n    \"\"\"Allocates ray trial resources based on available resources.\n\n    # Inputs :param resources (dict) specifies all available GPUs, CPUs and associated     metadata of the machines\n    (i.e. memory)\n\n    # Return\n    :return: (dict) gpu and cpu resources per trial\n    \"\"\"\n    # TODO (ASN):\n    # (1) expand logic to support multiple GPUs per trial (multi-gpu training)\n    # (2) add support for kubernetes namespace (if applicable)\n    # (3) add support for smarter allocation based on size of GPU memory\n    experiment_resources = {\"cpu_resources_per_trial\": 1}\n    gpu_count, cpu_count = resources.gpus, resources.cpus\n    if gpu_count > 0:\n        experiment_resources.update({\"gpu_resources_per_trial\": 1})\n        if cpu_count > 1:\n            cpus_per_trial = max(int(cpu_count / gpu_count), 1)\n            experiment_resources[\"cpu_resources_per_trial\"] = cpus_per_trial\n\n    return experiment_resources\n\n\ndef get_resource_aware_hyperopt_config(\n    experiment_resources: dict[str, Any], time_limit_s: int | float, random_seed: int\n) -> dict[str, Any]:\n    \"\"\"Returns a Ludwig config with the hyperopt section populated with appropriate parameters.\n\n    Hyperopt parameters are intended to be appropriate for the given resources and time limit.\n    \"\"\"\n    executor = experiment_resources\n    executor.update({\"time_budget_s\": time_limit_s})\n    if time_limit_s is not None:\n        executor.update({SCHEDULER: {\"max_t\": time_limit_s}})\n\n    return {\n        HYPEROPT: {\n            SEARCH_ALG: {\"random_state_seed\": random_seed},\n            EXECUTOR: executor,\n        },\n    }\n\n\ndef _get_stratify_split_config(field_meta: FieldMetadata) -> dict:\n    return {\n        PREPROCESSING: {\n            SPLIT: {\n                TYPE: \"stratify\",\n                COLUMN: field_meta.name,\n            }\n        }\n    }\n\n\ndef get_default_automl_hyperopt() -> dict[str, Any]:\n    \"\"\"Returns general, default settings for hyperopt.\n\n    For example:\n    - We set a random_state_seed for sample sequence repeatability\n    - We use an increased reduction_factor to get more pruning/exploration.\n\n    TODO: If settings seem reasonable, consider building this into the hyperopt schema, directly.\n    \"\"\"\n    return yaml.safe_load(\"\"\"\n  search_alg:\n    type: variant_generator\n  executor:\n    type: ray\n    num_samples: 10\n    time_budget_s: 3600\n    scheduler:\n      type: async_hyperband\n      time_attr: time_total_s\n      max_t: 3600\n      grace_period: 72\n      reduction_factor: 5\n\"\"\")\n\n\ndef create_default_config(\n    features_config: ModelConfigDict,\n    dataset_info: DatasetInfo,\n    target_name: str | list[str],\n    time_limit_s: int | float,\n    random_seed: int,\n    imbalance_threshold: float = 0.9,\n    backend: Backend = None,\n) -> dict:\n    \"\"\"Returns auto_train configs for three available combiner models. Coordinates the following tasks:\n\n    - extracts fields and generates list of FieldInfo objects\n    - gets field metadata (i.e avg. words, total non-null entries)\n    - builds input_features and output_features section of config\n    - for imbalanced datasets, a preprocessing section is added to perform stratified sampling if the imbalance ratio\n      is smaller than imbalance_threshold\n    - for each combiner, adds default training, hyperopt\n    - infers resource constraints and adds gpu and cpu resource allocation per\n      trial\n\n    # Inputs\n    :param dataset_info: (str) filepath Dataset Info object.\n    :param target_name: (str, List[str]) name of target feature\n    :param time_limit_s: (int, float) total time allocated to auto_train. acts\n                                    as the stopping parameter\n    :param random_seed: (int, default: `42`) a random seed that will be used anywhere\n                        there is a call to a random number generator, including\n                        hyperparameter search sampling, as well as data splitting,\n                        parameter initialization and training set shuffling\n    :param imbalance_threshold: (float) maximum imbalance ratio (minority / majority) to perform stratified sampling\n    :param backend: (Backend) backend to use for training.\n\n    # Return\n    :return: (dict) dictionaries contain auto train config files for all available\n    combiner types\n    \"\"\"\n    base_automl_config = load_yaml(BASE_AUTOML_CONFIG)\n    base_automl_config.update(features_config)\n\n    targets = convert_targets(target_name)\n    features_metadata = get_field_metadata(dataset_info.fields, dataset_info.row_count, targets)\n\n    # Handle expensive features for CPU\n    resources = backend.get_available_resources()\n    for ifeature in base_automl_config[INPUT_FEATURES]:\n        if resources.gpus == 0:\n            if ifeature[TYPE] == TEXT:\n                # When no GPUs are available, default to the embed encoder, which is fast enough for CPU\n                ifeature[ENCODER] = {\"type\": \"embed\"}\n\n    # create set of all feature types appearing in the dataset\n    feature_types = [[feat[TYPE] for feat in features] for features in features_config.values()]\n    feature_types = set(sum(feature_types, []))\n\n    model_configs = {}\n\n    # update hyperopt config\n    experiment_resources = allocate_experiment_resources(resources)\n    base_automl_config = merge_dict(\n        base_automl_config, get_resource_aware_hyperopt_config(experiment_resources, time_limit_s, random_seed)\n    )\n\n    # add preprocessing section if single output feature is imbalanced\n    outputs_metadata = [f for f in features_metadata if f.mode == \"output\"]\n    if len(outputs_metadata) == 1:\n        of_meta = outputs_metadata[0]\n        is_categorical = of_meta.config.type in [\"category\", \"binary\"]\n        is_imbalanced = of_meta.imbalance_ratio < imbalance_threshold\n        if is_categorical and is_imbalanced:\n            base_automl_config.update(_get_stratify_split_config(of_meta))\n\n    model_configs[\"base_config\"] = base_automl_config\n\n    # read in all encoder configs\n    for feat_type, default_configs in encoder_defaults.items():\n        if feat_type in feature_types:\n            if feat_type not in model_configs.keys():\n                model_configs[feat_type] = {}\n            for encoder_name, encoder_config_path in default_configs.items():\n                model_configs[feat_type][encoder_name] = load_yaml(encoder_config_path)\n\n    # read in all combiner configs\n    model_configs[COMBINER] = {}\n    for combiner_type, default_config in combiner_defaults.items():\n        combiner_config = load_yaml(default_config)\n        model_configs[COMBINER][combiner_type] = combiner_config\n\n    return model_configs\n\n\n# Read in the score and configuration of a reference model trained by Ludwig for each dataset in a list.\ndef get_reference_configs() -> dict:\n    reference_configs = load_yaml(REFERENCE_CONFIGS)\n    return reference_configs\n\n\ndef get_dataset_info(df: pd.DataFrame | dd.DataFrame) -> DatasetInfo:\n    \"\"\"Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type\n    inference.\n\n    # Inputs\n    :param df: (Union[pd.DataFrame, dd.DataFrame]) Pandas or Dask dataframe.  # Return\n    :return: (DatasetInfo) Structure containing list of FieldInfo objects.\n    \"\"\"\n    source = wrap_data_source(df)\n    return get_dataset_info_from_source(source)\n\n\ndef is_field_boolean(source: DataSource, field: str) -> bool:\n    \"\"\"Returns a boolean indicating whether the object field should have a bool dtype.\n\n    Columns with object dtype that have 3 distinct values of which one is Nan/None is a bool type column.\n    \"\"\"\n    unique_values = source.df[field].unique()\n    if len(unique_values) <= 3:\n        for entry in unique_values:\n            try:\n                if np.isnan(entry):\n                    continue\n            except TypeError:\n                # For some field types such as object arrays, np.isnan throws a TypeError\n                # In this case, do nothing and proceed to checking if the entry is a bool object\n                pass\n            if isinstance(entry, bool):\n                continue\n            return False\n        return True\n    return False\n\n\n@DeveloperAPI\ndef get_dataset_info_from_source(source: DataSource) -> DatasetInfo:\n    \"\"\"Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type\n    inference.\n\n    # Inputs\n    :param source: (DataSource) A wrapper around a data source, which may represent a pandas or Dask dataframe. # Return\n    :return: (DatasetInfo) Structure containing list of FieldInfo objects.\n    \"\"\"\n    row_count = len(source)\n    fields = []\n    for field in tqdm(source.columns, desc=\"Analyzing fields\", total=len(source.columns)):\n        logger.info(f\"Analyzing field: {field}\")\n        dtype = source.get_dtype(field)\n        num_distinct_values, distinct_values, distinct_values_balance = source.get_distinct_values(\n            field, MAX_DISTINCT_VALUES_TO_RETURN\n        )\n        nonnull_values = source.get_nonnull_values(field)\n        image_values = source.get_image_values(field)\n        audio_values = source.get_audio_values(field)\n\n        if dtype == \"object\":\n            # Check if it is a nullboolean field. We do this since if you read a csv with\n            # pandas that has a column of booleans and some missing values, the column is\n            # interpreted as object dtype instead of bool\n            if is_field_boolean(source, field):\n                dtype = \"bool\"\n\n        avg_words = None\n        if source.is_string_type(dtype):\n            try:\n                avg_words = source.get_avg_num_tokens(field)\n            except AttributeError:\n                # Series is not actually a string type despite being an object, e.g., Decimal, Datetime, etc.\n                avg_words = None\n\n        fields.append(\n            FieldInfo(\n                name=field,\n                dtype=dtype,\n                distinct_values=distinct_values,\n                num_distinct_values=num_distinct_values,\n                distinct_values_balance=distinct_values_balance,\n                nonnull_values=nonnull_values,\n                image_values=image_values,\n                audio_values=audio_values,\n                avg_words=avg_words,\n            )\n        )\n    return DatasetInfo(fields=fields, row_count=row_count, size_bytes=source.size_bytes())\n\n\ndef get_features_config(\n    fields: list[FieldInfo],\n    row_count: int,\n    target_name: str | list[str] = None,\n) -> dict:\n    \"\"\"Constructs FieldInfo objects for each feature in dataset. These objects are used for downstream type\n    inference.\n\n    # Inputs\n    :param fields: (List[FieldInfo]) FieldInfo objects for all fields in dataset\n    :param row_count: (int) total number of entries in original dataset :param target_name (str, List[str]) name of\n        target feature # Return\n    :return: (dict) section of auto_train config for input_features and output_features\n    \"\"\"\n    targets = convert_targets(target_name)\n    metadata = get_field_metadata(fields, row_count, targets)\n    return get_config_from_metadata(metadata, targets)\n\n\ndef convert_targets(target_name: str | list[str] = None) -> set[str]:\n    targets = target_name\n    if isinstance(targets, str):\n        targets = [targets]\n    if targets is None:\n        targets = []\n    return set(targets)\n\n\ndef get_config_from_metadata(metadata: list[FieldMetadata], targets: set[str] = None) -> dict:\n    \"\"\"Builds input/output feature sections of auto-train config using field metadata.\n\n    # Inputs\n    :param metadata: (List[FieldMetadata]) field descriptions :param targets (Set[str]) names of target features #\n        Return\n    :return: (dict) section of auto_train config for input_features and output_features\n    \"\"\"\n    config = {\n        \"input_features\": [],\n        \"output_features\": [],\n    }\n\n    for field_meta in metadata:\n        if field_meta.name in targets:\n            config[\"output_features\"].append(field_meta.config.to_dict())\n        elif not field_meta.excluded and field_meta.mode == \"input\":\n            config[\"input_features\"].append(field_meta.config.to_dict())\n\n    return config\n\n\n@DeveloperAPI\ndef get_field_metadata(fields: list[FieldInfo], row_count: int, targets: set[str] = None) -> list[FieldMetadata]:\n    \"\"\"Computes metadata for each field in dataset.\n\n    # Inputs\n    :param fields: (List[FieldInfo]) FieldInfo objects for all fields in dataset\n    :param row_count: (int) total number of entries in original dataset :param targets (Set[str]) names of target\n        features # Return\n    :return: (List[FieldMetadata]) list of objects containing metadata for each field\n    \"\"\"\n\n    metadata = []\n    column_count = len(fields)\n    for idx, field in enumerate(fields):\n        missing_value_percent = 1 - float(field.nonnull_values) / row_count\n        dtype = infer_type(field, missing_value_percent, row_count)\n        metadata.append(\n            FieldMetadata(\n                name=field.name,\n                config=FieldConfig(\n                    name=field.name,\n                    column=field.name,\n                    type=dtype,\n                ),\n                excluded=should_exclude(idx, field, dtype, column_count, row_count, targets),\n                mode=infer_mode(field, targets),\n                missing_values=missing_value_percent,\n                imbalance_ratio=field.distinct_values_balance,\n            )\n        )\n\n    return metadata\n\n\ndef infer_mode(field: FieldInfo, targets: set[str] = None) -> str:\n    if field.name in targets:\n        return \"output\"\n    if field.name.lower() == \"split\":\n        return \"split\"\n    return \"input\"\n"
  },
  {
    "path": "ludwig/automl/defaults/base_automl_config.yaml",
    "content": "trainer:\n  batch_size: auto #256\n  learning_rate: auto #.001\n  # validation_metric: accuracy\n\nhyperopt:\n  search_alg:\n    # Gives results like default + supports random_state_seed for sample sequence repeatability\n    type: variant_generator\n  executor:\n    type: ray\n    num_samples: 10\n    time_budget_s: 7200\n    scheduler:\n      type: async_hyperband\n      time_attr: time_total_s\n      max_t: 7200\n      grace_period: 72\n      # Increased over default to get more pruning/exploration\n      reduction_factor: 5\n"
  },
  {
    "path": "ludwig/automl/defaults/combiner/concat_config.yaml",
    "content": "combiner:\n  type: concat\n\nhyperopt:\n  # goal: maximize\n  parameters:\n    combiner.num_fc_layers:\n      space: randint\n      lower: 1\n      upper: 4\n    combiner.output_size:\n      space: choice\n      categories: [128, 256]\n    combiner.dropout:\n      space: uniform\n      lower: 0.0\n      upper: 0.1\n    # This needs to be loguniform due to invalid schemas created by merging with a choice parameter space. See the\n    # comment in ludwig/automl/defaults/text/bert_config.yaml for more information.\n    trainer.learning_rate:\n      space: loguniform\n      lower: 0.00002\n      upper: 0.001\n    trainer.batch_size:\n      space: choice\n      categories: [64, 128, 256, 512, 1024]\n"
  },
  {
    "path": "ludwig/automl/defaults/combiner/tabnet_config.yaml",
    "content": "combiner:\n  type: tabnet\n\ntrainer:\n  batch_size: auto\n  learning_rate_scaling: sqrt\n  learning_rate_scheduler:\n    decay: exponential\n    decay_steps: 20000\n    decay_rate: 0.8\n  optimizer:\n    type: adam\n\nhyperopt:\n  parameters:\n    trainer.learning_rate:\n      space: loguniform\n      lower: 0.00002\n      upper: 0.001\n    trainer.learning_rate_scheduler.decay_rate:\n      space: choice\n      categories: [0.8, 0.9, 0.95]\n    trainer.learning_rate_scheduler.decay_steps:\n      space: choice\n      categories: [500, 2000, 8000, 10000, 20000]\n    combiner.size:\n      space: choice\n      categories: [8, 16, 24, 32, 64]\n    combiner.output_size:\n      space: choice\n      categories: [8, 16, 24, 32, 64, 128]\n    combiner.num_steps:\n      space: choice\n      categories: [3, 4, 5, 6, 7, 8, 9, 10]\n    combiner.relaxation_factor:\n      space: choice\n      categories: [1.0, 1.2, 1.5, 2.0]\n    combiner.sparsity:\n      space: choice\n      categories: [0.0, 0.000001, 0.0001, 0.001, 0.01, 0.1]\n    combiner.bn_virtual_bs:\n      space: choice\n      categories: [256, 512, 1024, 2048, 4096]\n    combiner.bn_momentum:\n      space: choice\n      categories: [0.4, 0.3, 0.2, 0.1, 0.05, 0.02]\n"
  },
  {
    "path": "ludwig/automl/defaults/combiner/transformer_config.yaml",
    "content": "combiner:\n  type: transformer\n\ntrainer:\n  batch_size: auto #256\n  learning_rate: auto #0.0001\n  # validation_metric: accuracy\n\nhyperopt:\n  # goal: maximize\n  parameters:\n    trainer.learning_rate:\n      space: loguniform\n      lower: 0.00002\n      upper: 0.001\n    trainer.batch_size:\n      space: choice\n      categories: [64, 128, 256]\n    combiner.num_heads:\n      space: choice\n      categories: [4]\n    combiner.dropout:\n      space: uniform\n      lower: 0.1\n      upper: 0.3\n    combiner.num_layers:\n      space: randint\n      lower: 3\n      upper: 4\n    combiner.num_fc_layers:\n      space: choice\n      categories: [1, 2]\n    combiner.fc_dropout:\n      space: uniform\n      lower: 0.1\n      upper: 0.5\n"
  },
  {
    "path": "ludwig/automl/defaults/reference_configs.yaml",
    "content": "# Record the score and configuration of a reference model trained by Ludwig for specified datasets.\n# This information is useful for Ludwig AutoML hyperparameter transfer learning or for manual experimentation.\ndatasets:\n  - name: adult_census_income\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.8682432174682617\n    training_rows: 29305\n    test_rows: 16281\n    validation_rows: 3256\n    config:\n      output_features:\n        - name: income\n          type: category\n      input_features:\n        - name: age\n          type: number\n        - name: workclass\n          type: category\n        - name: fnlwgt\n          type: number\n        - name: education\n          type: category\n        - name: education-num\n          type: number\n        - name: marital-status\n          type: category\n        - name: occupation\n          type: category\n        - name: relationship\n          type: category\n        - name: race\n          type: category\n        - name: sex\n          type: category\n        - name: capital-gain\n          type: number\n        - name: capital-loss\n          type: number\n        - name: hours-per-week\n          type: number\n        - name: native-country\n          type: category\n      combiner:\n        type: tabnet\n        size: 8 # N_a\n        output_size: 128 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.4 # m_B\n        num_steps: 3 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 4096 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 500\n          decay_rate: 0.95\n        validation_metric: accuracy\n  - name: allstate_claims_severity\n    goal: minimize\n    metric: root_mean_squared_error\n    validation_metric_score: 1915.5531005859375\n    training_rows: 131726\n    test_rows: 37750\n    validation_rows: 18842\n    config:\n      output_features:\n        - name: loss\n          type: number\n      input_features:\n        - column: cat1\n          name: cat1\n          type: category\n        - column: cat2\n          name: cat2\n          type: category\n        - column: cat3\n          name: cat3\n          type: category\n        - column: cat4\n          name: cat4\n          type: category\n        - column: cat5\n          name: cat5\n          type: category\n        - column: cat6\n          name: cat6\n          type: category\n        - column: cat7\n          name: cat7\n          type: category\n        - column: cat8\n          name: cat8\n          type: category\n        - column: cat9\n          name: cat9\n          type: category\n        - column: cat10\n          name: cat10\n          type: category\n        - column: cat11\n          name: cat11\n          type: category\n        - column: cat12\n          name: cat12\n          type: category\n        - column: cat13\n          name: cat13\n          type: category\n        - column: cat14\n          name: cat14\n          type: category\n        - column: cat15\n          name: cat15\n          type: category\n        - column: cat16\n          name: cat16\n          type: category\n        - column: cat17\n          name: cat17\n          type: category\n        - column: cat18\n          name: cat18\n          type: category\n        - column: cat19\n          name: cat19\n          type: category\n        - column: cat20\n          name: cat20\n          type: category\n        - column: cat21\n          name: cat21\n          type: category\n        - column: cat22\n          name: cat22\n          type: category\n        - column: cat23\n          name: cat23\n          type: category\n        - column: cat24\n          name: cat24\n          type: category\n        - column: cat25\n          name: cat25\n          type: category\n        - column: cat26\n          name: cat26\n          type: category\n        - column: cat27\n          name: cat27\n          type: category\n        - column: cat28\n          name: cat28\n          type: category\n        - column: cat29\n          name: cat29\n          type: category\n        - column: cat30\n          name: cat30\n          type: category\n        - column: cat31\n          name: cat31\n          type: category\n        - column: cat32\n          name: cat32\n          type: category\n        - column: cat33\n          name: cat33\n          type: category\n        - column: cat34\n          name: cat34\n          type: category\n        - column: cat35\n          name: cat35\n          type: category\n        - column: cat36\n          name: cat36\n          type: category\n        - column: cat37\n          name: cat37\n          type: category\n        - column: cat38\n          name: cat38\n          type: category\n        - column: cat39\n          name: cat39\n          type: category\n        - column: cat40\n          name: cat40\n          type: category\n        - column: cat41\n          name: cat41\n          type: category\n        - column: cat42\n          name: cat42\n          type: category\n        - column: cat43\n          name: cat43\n          type: category\n        - column: cat44\n          name: cat44\n          type: category\n        - column: cat45\n          name: cat45\n          type: category\n        - column: cat46\n          name: cat46\n          type: category\n        - column: cat47\n          name: cat47\n          type: category\n        - column: cat48\n          name: cat48\n          type: category\n        - column: cat49\n          name: cat49\n          type: category\n        - column: cat50\n          name: cat50\n          type: category\n        - column: cat51\n          name: cat51\n          type: category\n        - column: cat52\n          name: cat52\n          type: category\n        - column: cat53\n          name: cat53\n          type: category\n        - column: cat54\n          name: cat54\n          type: category\n        - column: cat55\n          name: cat55\n          type: category\n        - column: cat56\n          name: cat56\n          type: category\n        - column: cat57\n          name: cat57\n          type: category\n        - column: cat58\n          name: cat58\n          type: category\n        - column: cat59\n          name: cat59\n          type: category\n        - column: cat60\n          name: cat60\n          type: category\n        - column: cat61\n          name: cat61\n          type: category\n        - column: cat62\n          name: cat62\n          type: category\n        - column: cat63\n          name: cat63\n          type: category\n        - column: cat64\n          name: cat64\n          type: category\n        - column: cat65\n          name: cat65\n          type: category\n        - column: cat66\n          name: cat66\n          type: category\n        - column: cat67\n          name: cat67\n          type: category\n        - column: cat68\n          name: cat68\n          type: category\n        - column: cat69\n          name: cat69\n          type: category\n        - column: cat70\n          name: cat70\n          type: category\n        - column: cat71\n          name: cat71\n          type: category\n        - column: cat72\n          name: cat72\n          type: category\n        - column: cat73\n          name: cat73\n          type: category\n        - column: cat74\n          name: cat74\n          type: category\n        - column: cat75\n          name: cat75\n          type: category\n        - column: cat76\n          name: cat76\n          type: category\n        - column: cat77\n          name: cat77\n          type: category\n        - column: cat78\n          name: cat78\n          type: category\n        - column: cat79\n          name: cat79\n          type: category\n        - column: cat80\n          name: cat80\n          type: category\n        - column: cat81\n          name: cat81\n          type: category\n        - column: cat82\n          name: cat82\n          type: category\n        - column: cat83\n          name: cat83\n          type: category\n        - column: cat84\n          name: cat84\n          type: category\n        - column: cat85\n          name: cat85\n          type: category\n        - column: cat86\n          name: cat86\n          type: category\n        - column: cat87\n          name: cat87\n          type: category\n        - column: cat88\n          name: cat88\n          type: category\n        - column: cat89\n          name: cat89\n          type: category\n        - column: cat90\n          name: cat90\n          type: category\n        - column: cat91\n          name: cat91\n          type: category\n        - column: cat92\n          name: cat92\n          type: category\n        - column: cat93\n          name: cat93\n          type: category\n        - column: cat94\n          name: cat94\n          type: category\n        - column: cat95\n          name: cat95\n          type: category\n        - column: cat96\n          name: cat96\n          type: category\n        - column: cat97\n          name: cat97\n          type: category\n        - column: cat98\n          name: cat98\n          type: category\n        - column: cat99\n          name: cat99\n          type: category\n        - column: cat100\n          name: cat100\n          type: category\n        - column: cat101\n          name: cat101\n          type: category\n        - column: cat102\n          name: cat102\n          type: category\n        - column: cat103\n          name: cat103\n          type: category\n        - column: cat104\n          name: cat104\n          type: category\n        - column: cat105\n          name: cat105\n          type: category\n        - column: cat106\n          name: cat106\n          type: category\n        - column: cat107\n          name: cat107\n          type: category\n        - column: cat108\n          name: cat108\n          type: category\n        - column: cat109\n          name: cat109\n          type: category\n        - column: cat110\n          name: cat110\n          type: category\n        - column: cat111\n          name: cat111\n          type: category\n        - column: cat112\n          name: cat112\n          type: category\n        - column: cat113\n          name: cat113\n          type: category\n        - column: cat114\n          name: cat114\n          type: category\n        - column: cat115\n          name: cat115\n          type: category\n        - column: cat116\n          name: cat116\n          type: category\n        - column: cont1\n          name: cont1\n          type: number\n        - column: cont2\n          name: cont2\n          type: number\n        - column: cont3\n          name: cont3\n          type: number\n        - column: cont4\n          name: cont4\n          type: number\n        - column: cont5\n          name: cont5\n          type: number\n        - column: cont6\n          name: cont6\n          type: number\n        - column: cont7\n          name: cont7\n          type: number\n        - column: cont8\n          name: cont8\n          type: number\n        - column: cont9\n          name: cont9\n          type: number\n        - column: cont10\n          name: cont10\n          type: number\n        - column: cont11\n          name: cont11\n          type: number\n        - column: cont12\n          name: cont12\n          type: number\n        - column: cont13\n          name: cont13\n          type: number\n        - column: cont14\n          name: cont14\n          type: number\n      combiner:\n        type: tabnet\n        size: 128 # N_a\n        output_size: 8 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.02 # m_B\n        num_steps: 10 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 4096 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 10000\n          decay_rate: 0.9\n        validation_metric: root_mean_squared_error\n  - name: bnp_claims_management\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.7761691808700562\n    training_rows: 80101\n    test_rows: 22823\n    validation_rows: 11397\n    config:\n      output_features:\n        - name: target\n          type: binary\n      input_features:\n        - name: v1\n          type: number\n        - name: v2\n          type: number\n        - name: v3\n          type: category\n        - name: v4\n          type: number\n        - name: v5\n          type: number\n        - name: v6\n          type: number\n        - name: v7\n          type: number\n        - name: v8\n          type: number\n        - name: v9\n          type: number\n        - name: v10\n          type: number\n        - name: v11\n          type: number\n        - name: v12\n          type: number\n        - name: v13\n          type: number\n        - name: v14\n          type: number\n        - name: v15\n          type: number\n        - name: v16\n          type: number\n        - name: v17\n          type: number\n        - name: v18\n          type: number\n        - name: v19\n          type: number\n        - name: v20\n          type: number\n        - name: v21\n          type: number\n        - name: v22\n          type: category\n        - name: v23\n          type: number\n        - name: v24\n          type: category\n        - name: v25\n          type: number\n        - name: v26\n          type: number\n        - name: v27\n          type: number\n        - name: v28\n          type: number\n        - name: v29\n          type: number\n        - name: v30\n          type: category\n        - name: v31\n          type: category\n        - name: v32\n          type: number\n        - name: v33\n          type: number\n        - name: v34\n          type: number\n        - name: v35\n          type: number\n        - name: v36\n          type: number\n        - name: v37\n          type: number\n        - name: v38\n          type: number\n        - name: v39\n          type: number\n        - name: v40\n          type: number\n        - name: v41\n          type: number\n        - name: v42\n          type: number\n        - name: v43\n          type: number\n        - name: v44\n          type: number\n        - name: v45\n          type: number\n        - name: v46\n          type: number\n        - name: v47\n          type: category\n        - name: v48\n          type: number\n        - name: v49\n          type: number\n        - name: v50\n          type: number\n        - name: v51\n          type: number\n        - name: v52\n          type: category\n        - name: v53\n          type: number\n        - name: v54\n          type: number\n        - name: v55\n          type: number\n        - name: v56\n          type: category\n        - name: v57\n          type: number\n        - name: v58\n          type: number\n        - name: v59\n          type: number\n        - name: v60\n          type: number\n        - name: v61\n          type: number\n        - name: v62\n          type: category\n        - name: v63\n          type: number\n        - name: v64\n          type: number\n        - name: v65\n          type: number\n        - name: v66\n          type: category\n        - name: v67\n          type: number\n        - name: v68\n          type: number\n        - name: v69\n          type: number\n        - name: v70\n          type: number\n        - name: v71\n          type: category\n        - name: v72\n          type: category\n        - name: v73\n          type: number\n        - name: v74\n          type: category\n        - name: v75\n          type: category\n        - name: v76\n          type: number\n        - name: v77\n          type: number\n        - name: v78\n          type: number\n        - name: v79\n          type: category\n        - name: v80\n          type: number\n        - name: v81\n          type: number\n        - name: v82\n          type: number\n        - name: v83\n          type: number\n        - name: v84\n          type: number\n        - name: v85\n          type: number\n        - name: v86\n          type: number\n        - name: v87\n          type: number\n        - name: v88\n          type: number\n        - name: v89\n          type: number\n        - name: v90\n          type: number\n        - name: v91\n          type: category\n        - name: v92\n          type: number\n        - name: v93\n          type: number\n        - name: v94\n          type: number\n        - name: v95\n          type: number\n        - name: v96\n          type: number\n        - name: v97\n          type: number\n        - name: v98\n          type: number\n        - name: v99\n          type: number\n        - name: v100\n          type: number\n        - name: v101\n          type: number\n        - name: v102\n          type: number\n        - name: v103\n          type: number\n        - name: v104\n          type: number\n        - name: v105\n          type: number\n        - name: v106\n          type: number\n        - name: v107\n          type: category\n        - name: v108\n          type: number\n        - name: v109\n          type: number\n        - name: v110\n          type: category\n        - name: v111\n          type: number\n        - name: v112\n          type: category\n        - name: v113\n          type: category\n        - name: v114\n          type: number\n        - name: v115\n          type: number\n        - name: v116\n          type: number\n        - name: v117\n          type: number\n        - name: v118\n          type: number\n        - name: v119\n          type: number\n        - name: v120\n          type: number\n        - name: v121\n          type: number\n        - name: v122\n          type: number\n        - name: v123\n          type: number\n        - name: v124\n          type: number\n        - name: v125\n          type: category\n        - name: v126\n          type: number\n        - name: v127\n          type: number\n        - name: v128\n          type: number\n        - name: v129\n          type: number\n        - name: v130\n          type: number\n        - name: v131\n          type: number\n      combiner:\n        type: tabnet\n        size: 32 # N_a\n        output_size: 8 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.02 # m_B\n        num_steps: 3 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 256 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 2000\n          decay_rate: 0.4\n        validation_metric: accuracy\n  - name: ieee_fraud\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.9836957454681396\n    training_rows: 413498\n    test_rows: 118039\n    validation_rows: 59003\n    config:\n      output_features:\n        - name: isFraud\n          type: binary\n      input_features:\n        - name: TransactionDT\n          type: number\n        - name: TransactionAmt\n          type: number\n        - name: ProductCD\n          type: category\n        - name: card1\n          type: number\n        - name: card2\n          type: number\n        - name: card3\n          type: number\n        - name: card4\n          type: category\n        - name: card5\n          type: number\n        - name: card6\n          type: category\n        - name: addr1\n          type: number\n        - name: addr2\n          type: number\n        - name: dist1\n          type: number\n        - name: dist2\n          type: number\n        - name: P_emaildomain\n          type: category\n        - name: R_emaildomain\n          type: category\n        - name: C1\n          type: number\n        - name: C2\n          type: number\n        - name: C3\n          type: number\n        - name: C4\n          type: number\n        - name: C5\n          type: number\n        - name: C6\n          type: number\n        - name: C7\n          type: number\n        - name: C8\n          type: number\n        - name: C9\n          type: number\n        - name: C10\n          type: number\n        - name: C11\n          type: number\n        - name: C12\n          type: number\n        - name: C13\n          type: number\n        - name: C14\n          type: number\n        - name: D1\n          type: number\n        - name: D2\n          type: number\n        - name: D3\n          type: number\n        - name: D4\n          type: number\n        - name: D5\n          type: number\n        - name: D6\n          type: number\n        - name: D7\n          type: number\n        - name: D8\n          type: number\n        - name: D9\n          type: number\n        - name: D10\n          type: number\n        - name: D11\n          type: number\n        - name: D12\n          type: number\n        - name: D13\n          type: number\n        - name: D14\n          type: number\n        - name: D15\n          type: number\n        - name: M1\n          type: category\n        - name: M2\n          type: category\n        - name: M3\n          type: category\n        - name: M4\n          type: category\n        - name: M5\n          type: category\n        - name: M6\n          type: category\n        - name: M7\n          type: category\n        - name: M8\n          type: category\n        - name: M9\n          type: category\n        - name: V1\n          type: number\n        - name: V2\n          type: number\n        - name: V3\n          type: number\n        - name: V4\n          type: number\n        - name: V5\n          type: number\n        - name: V6\n          type: number\n        - name: V7\n          type: number\n        - name: V8\n          type: number\n        - name: V9\n          type: number\n        - name: V10\n          type: number\n        - name: V11\n          type: number\n        - name: V12\n          type: number\n        - name: V13\n          type: number\n        - name: V14\n          type: number\n        - name: V15\n          type: number\n        - name: V16\n          type: number\n        - name: V17\n          type: number\n        - name: V18\n          type: number\n        - name: V19\n          type: number\n        - name: V20\n          type: number\n        - name: V21\n          type: number\n        - name: V22\n          type: number\n        - name: V23\n          type: number\n        - name: V24\n          type: number\n        - name: V25\n          type: number\n        - name: V26\n          type: number\n        - name: V27\n          type: number\n        - name: V28\n          type: number\n        - name: V29\n          type: number\n        - name: V30\n          type: number\n        - name: V31\n          type: number\n        - name: V32\n          type: number\n        - name: V33\n          type: number\n        - name: V34\n          type: number\n        - name: V35\n          type: number\n        - name: V36\n          type: number\n        - name: V37\n          type: number\n        - name: V38\n          type: number\n        - name: V39\n          type: number\n        - name: V40\n          type: number\n        - name: V41\n          type: number\n        - name: V42\n          type: number\n        - name: V43\n          type: number\n        - name: V44\n          type: number\n        - name: V45\n          type: number\n        - name: V46\n          type: number\n        - name: V47\n          type: number\n        - name: V48\n          type: number\n        - name: V49\n          type: number\n        - name: V50\n          type: number\n        - name: V51\n          type: number\n        - name: V52\n          type: number\n        - name: V53\n          type: number\n        - name: V54\n          type: number\n        - name: V55\n          type: number\n        - name: V56\n          type: number\n        - name: V57\n          type: number\n        - name: V58\n          type: number\n        - name: V59\n          type: number\n        - name: V60\n          type: number\n        - name: V61\n          type: number\n        - name: V62\n          type: number\n        - name: V63\n          type: number\n        - name: V64\n          type: number\n        - name: V65\n          type: number\n        - name: V66\n          type: number\n        - name: V67\n          type: number\n        - name: V68\n          type: number\n        - name: V69\n          type: number\n        - name: V70\n          type: number\n        - name: V71\n          type: number\n        - name: V72\n          type: number\n        - name: V73\n          type: number\n        - name: V74\n          type: number\n        - name: V75\n          type: number\n        - name: V76\n          type: number\n        - name: V77\n          type: number\n        - name: V78\n          type: number\n        - name: V79\n          type: number\n        - name: V80\n          type: number\n        - name: V81\n          type: number\n        - name: V82\n          type: number\n        - name: V83\n          type: number\n        - name: V84\n          type: number\n        - name: V85\n          type: number\n        - name: V86\n          type: number\n        - name: V87\n          type: number\n        - name: V88\n          type: number\n        - name: V89\n          type: number\n        - name: V90\n          type: number\n        - name: V91\n          type: number\n        - name: V92\n          type: number\n        - name: V93\n          type: number\n        - name: V94\n          type: number\n        - name: V95\n          type: number\n        - name: V96\n          type: number\n        - name: V97\n          type: number\n        - name: V98\n          type: number\n        - name: V99\n          type: number\n        - name: V100\n          type: number\n        - name: V101\n          type: number\n        - name: V102\n          type: number\n        - name: V103\n          type: number\n        - name: V104\n          type: number\n        - name: V105\n          type: number\n        - name: V106\n          type: number\n        - name: V107\n          type: number\n        - name: V108\n          type: number\n        - name: V109\n          type: number\n        - name: V110\n          type: number\n        - name: V111\n          type: number\n        - name: V112\n          type: number\n        - name: V113\n          type: number\n        - name: V114\n          type: number\n        - name: V115\n          type: number\n        - name: V116\n          type: number\n        - name: V117\n          type: number\n        - name: V118\n          type: number\n        - name: V119\n          type: number\n        - name: V120\n          type: number\n        - name: V121\n          type: number\n        - name: V122\n          type: number\n        - name: V123\n          type: number\n        - name: V124\n          type: number\n        - name: V125\n          type: number\n        - name: V126\n          type: number\n        - name: V127\n          type: number\n        - name: V128\n          type: number\n        - name: V129\n          type: number\n        - name: V130\n          type: number\n        - name: V131\n          type: number\n        - name: V132\n          type: number\n        - name: V133\n          type: number\n        - name: V134\n          type: number\n        - name: V135\n          type: number\n        - name: V136\n          type: number\n        - name: V137\n          type: number\n        - name: V138\n          type: number\n        - name: V139\n          type: number\n        - name: V140\n          type: number\n        - name: V141\n          type: number\n        - name: V142\n          type: number\n        - name: V143\n          type: number\n        - name: V144\n          type: number\n        - name: V145\n          type: number\n        - name: V146\n          type: number\n        - name: V147\n          type: number\n        - name: V148\n          type: number\n        - name: V149\n          type: number\n        - name: V150\n          type: number\n        - name: V151\n          type: number\n        - name: V152\n          type: number\n        - name: V153\n          type: number\n        - name: V154\n          type: number\n        - name: V155\n          type: number\n        - name: V156\n          type: number\n        - name: V157\n          type: number\n        - name: V158\n          type: number\n        - name: V159\n          type: number\n        - name: V160\n          type: number\n        - name: V161\n          type: number\n        - name: V162\n          type: number\n        - name: V163\n          type: number\n        - name: V164\n          type: number\n        - name: V165\n          type: number\n        - name: V166\n          type: number\n        - name: V167\n          type: number\n        - name: V168\n          type: number\n        - name: V169\n          type: number\n        - name: V170\n          type: number\n        - name: V171\n          type: number\n        - name: V172\n          type: number\n        - name: V173\n          type: number\n        - name: V174\n          type: number\n        - name: V175\n          type: number\n        - name: V176\n          type: number\n        - name: V177\n          type: number\n        - name: V178\n          type: number\n        - name: V179\n          type: number\n        - name: V180\n          type: number\n        - name: V181\n          type: number\n        - name: V182\n          type: number\n        - name: V183\n          type: number\n        - name: V184\n          type: number\n        - name: V185\n          type: number\n        - name: V186\n          type: number\n        - name: V187\n          type: number\n        - name: V188\n          type: number\n        - name: V189\n          type: number\n        - name: V190\n          type: number\n        - name: V191\n          type: number\n        - name: V192\n          type: number\n        - name: V193\n          type: number\n        - name: V194\n          type: number\n        - name: V195\n          type: number\n        - name: V196\n          type: number\n        - name: V197\n          type: number\n        - name: V198\n          type: number\n        - name: V199\n          type: number\n        - name: V200\n          type: number\n        - name: V201\n          type: number\n        - name: V202\n          type: number\n        - name: V203\n          type: number\n        - name: V204\n          type: number\n        - name: V205\n          type: number\n        - name: V206\n          type: number\n        - name: V207\n          type: number\n        - name: V208\n          type: number\n        - name: V209\n          type: number\n        - name: V210\n          type: number\n        - name: V211\n          type: number\n        - name: V212\n          type: number\n        - name: V213\n          type: number\n        - name: V214\n          type: number\n        - name: V215\n          type: number\n        - name: V216\n          type: number\n        - name: V217\n          type: number\n        - name: V218\n          type: number\n        - name: V219\n          type: number\n        - name: V220\n          type: number\n        - name: V221\n          type: number\n        - name: V222\n          type: number\n        - name: V223\n          type: number\n        - name: V224\n          type: number\n        - name: V225\n          type: number\n        - name: V226\n          type: number\n        - name: V227\n          type: number\n        - name: V228\n          type: number\n        - name: V229\n          type: number\n        - name: V230\n          type: number\n        - name: V231\n          type: number\n        - name: V232\n          type: number\n        - name: V233\n          type: number\n        - name: V234\n          type: number\n        - name: V235\n          type: number\n        - name: V236\n          type: number\n        - name: V237\n          type: number\n        - name: V238\n          type: number\n        - name: V239\n          type: number\n        - name: V240\n          type: number\n        - name: V241\n          type: number\n        - name: V242\n          type: number\n        - name: V243\n          type: number\n        - name: V244\n          type: number\n        - name: V245\n          type: number\n        - name: V246\n          type: number\n        - name: V247\n          type: number\n        - name: V248\n          type: number\n        - name: V249\n          type: number\n        - name: V250\n          type: number\n        - name: V251\n          type: number\n        - name: V252\n          type: number\n        - name: V253\n          type: number\n        - name: V254\n          type: number\n        - name: V255\n          type: number\n        - name: V256\n          type: number\n        - name: V257\n          type: number\n        - name: V258\n          type: number\n        - name: V259\n          type: number\n        - name: V260\n          type: number\n        - name: V261\n          type: number\n        - name: V262\n          type: number\n        - name: V263\n          type: number\n        - name: V264\n          type: number\n        - name: V265\n          type: number\n        - name: V266\n          type: number\n        - name: V267\n          type: number\n        - name: V268\n          type: number\n        - name: V269\n          type: number\n        - name: V270\n          type: number\n        - name: V271\n          type: number\n        - name: V272\n          type: number\n        - name: V273\n          type: number\n        - name: V274\n          type: number\n        - name: V275\n          type: number\n        - name: V276\n          type: number\n        - name: V277\n          type: number\n        - name: V278\n          type: number\n        - name: V279\n          type: number\n        - name: V280\n          type: number\n        - name: V281\n          type: number\n        - name: V282\n          type: number\n        - name: V283\n          type: number\n        - name: V284\n          type: number\n        - name: V285\n          type: number\n        - name: V286\n          type: number\n        - name: V287\n          type: number\n        - name: V288\n          type: number\n        - name: V289\n          type: number\n        - name: V290\n          type: number\n        - name: V291\n          type: number\n        - name: V292\n          type: number\n        - name: V293\n          type: number\n        - name: V294\n          type: number\n        - name: V295\n          type: number\n        - name: V296\n          type: number\n        - name: V297\n          type: number\n        - name: V298\n          type: number\n        - name: V299\n          type: number\n        - name: V300\n          type: number\n        - name: V301\n          type: number\n        - name: V302\n          type: number\n        - name: V303\n          type: number\n        - name: V304\n          type: number\n        - name: V305\n          type: number\n        - name: V306\n          type: number\n        - name: V307\n          type: number\n        - name: V308\n          type: number\n        - name: V309\n          type: number\n        - name: V310\n          type: number\n        - name: V311\n          type: number\n        - name: V312\n          type: number\n        - name: V313\n          type: number\n        - name: V314\n          type: number\n        - name: V315\n          type: number\n        - name: V316\n          type: number\n        - name: V317\n          type: number\n        - name: V318\n          type: number\n        - name: V319\n          type: number\n        - name: V320\n          type: number\n        - name: V321\n          type: number\n        - name: V322\n          type: number\n        - name: V323\n          type: number\n        - name: V324\n          type: number\n        - name: V325\n          type: number\n        - name: V326\n          type: number\n        - name: V327\n          type: number\n        - name: V328\n          type: number\n        - name: V329\n          type: number\n        - name: V330\n          type: number\n        - name: V331\n          type: number\n        - name: V332\n          type: number\n        - name: V333\n          type: number\n        - name: V334\n          type: number\n        - name: V335\n          type: number\n        - name: V336\n          type: number\n        - name: V337\n          type: number\n        - name: V338\n          type: number\n        - name: V339\n          type: number\n        - name: id_01\n          type: number\n        - name: id_02\n          type: number\n        - name: id_03\n          type: number\n        - name: id_04\n          type: number\n        - name: id_05\n          type: number\n        - name: id_06\n          type: number\n        - name: id_07\n          type: number\n        - name: id_08\n          type: number\n        - name: id_09\n          type: number\n        - name: id_10\n          type: number\n        - name: id_11\n          type: number\n        - name: id_12\n          type: category\n        - name: id_13\n          type: number\n        - name: id_14\n          type: number\n        - name: id_15\n          type: category\n        - name: id_16\n          type: category\n        - name: id_17\n          type: number\n        - name: id_18\n          type: number\n        - name: id_19\n          type: number\n        - name: id_20\n          type: number\n        - name: id_21\n          type: number\n        - name: id_22\n          type: number\n        - name: id_23\n          type: category\n        - name: id_24\n          type: number\n        - name: id_25\n          type: number\n        - name: id_26\n          type: number\n        - name: id_27\n          type: category\n        - name: id_28\n          type: category\n        - name: id_29\n          type: category\n        - name: id_30\n          type: category\n        - name: id_31\n          type: text\n        - name: id_32\n          type: number\n        - name: id_33\n          type: category\n        - name: id_34\n          type: category\n        - name: id_35\n          type: category\n        - name: id_36\n          type: category\n        - name: id_37\n          type: category\n        - name: id_38\n          type: category\n        - name: DeviceType\n          type: category\n        - name: DeviceInfo\n          type: category\n      combiner:\n        type: tabnet\n        size: 128 # N_a\n        output_size: 24 # N_d\n        sparsity: 0.000001 # lambda_sparse\n        bn_momentum: 0.02 # m_B\n        num_steps: 10 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 2048 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 10000\n          decay_rate: 0.95\n        validation_metric: accuracy\n  - name: mercedes_benz_greener\n    goal: minimize\n    metric: root_mean_squared_error\n    validation_metric_score: 7.685836315155029\n    training_rows: 2969\n    test_rows: 840\n    validation_rows: 400\n    config:\n      output_features:\n        - name: y\n          type: number\n      input_features:\n        - name: X0\n          type: category\n        - name: X1\n          type: category\n        - name: X2\n          type: category\n        - name: X3\n          type: category\n        - name: X4\n          type: category\n        - name: X5\n          type: category\n        - name: X6\n          type: category\n        - name: X8\n          type: category\n        - name: X10\n          type: binary\n        - name: X11\n          type: binary\n        - name: X12\n          type: binary\n        - name: X13\n          type: binary\n        - name: X14\n          type: binary\n        - name: X15\n          type: binary\n        - name: X16\n          type: binary\n        - name: X17\n          type: binary\n        - name: X18\n          type: binary\n        - name: X19\n          type: binary\n        - name: X20\n          type: binary\n        - name: X21\n          type: binary\n        - name: X22\n          type: binary\n        - name: X23\n          type: binary\n        - name: X24\n          type: binary\n        - name: X26\n          type: binary\n        - name: X27\n          type: binary\n        - name: X28\n          type: binary\n        - name: X29\n          type: binary\n        - name: X30\n          type: binary\n        - name: X31\n          type: binary\n        - name: X32\n          type: binary\n        - name: X33\n          type: binary\n        - name: X34\n          type: binary\n        - name: X35\n          type: binary\n        - name: X36\n          type: binary\n        - name: X37\n          type: binary\n        - name: X38\n          type: binary\n        - name: X39\n          type: binary\n        - name: X40\n          type: binary\n        - name: X41\n          type: binary\n        - name: X42\n          type: binary\n        - name: X43\n          type: binary\n        - name: X44\n          type: binary\n        - name: X45\n          type: binary\n        - name: X46\n          type: binary\n        - name: X47\n          type: binary\n        - name: X48\n          type: binary\n        - name: X49\n          type: binary\n        - name: X50\n          type: binary\n        - name: X51\n          type: binary\n        - name: X52\n          type: binary\n        - name: X53\n          type: binary\n        - name: X54\n          type: binary\n        - name: X55\n          type: binary\n        - name: X56\n          type: binary\n        - name: X57\n          type: binary\n        - name: X58\n          type: binary\n        - name: X59\n          type: binary\n        - name: X60\n          type: binary\n        - name: X61\n          type: binary\n        - name: X62\n          type: binary\n        - name: X63\n          type: binary\n        - name: X64\n          type: binary\n        - name: X65\n          type: binary\n        - name: X66\n          type: binary\n        - name: X67\n          type: binary\n        - name: X68\n          type: binary\n        - name: X69\n          type: binary\n        - name: X70\n          type: binary\n        - name: X71\n          type: binary\n        - name: X73\n          type: binary\n        - name: X74\n          type: binary\n        - name: X75\n          type: binary\n        - name: X76\n          type: binary\n        - name: X77\n          type: binary\n        - name: X78\n          type: binary\n        - name: X79\n          type: binary\n        - name: X80\n          type: binary\n        - name: X81\n          type: binary\n        - name: X82\n          type: binary\n        - name: X83\n          type: binary\n        - name: X84\n          type: binary\n        - name: X85\n          type: binary\n        - name: X86\n          type: binary\n        - name: X87\n          type: binary\n        - name: X88\n          type: binary\n        - name: X89\n          type: binary\n        - name: X90\n          type: binary\n        - name: X91\n          type: binary\n        - name: X92\n          type: binary\n        - name: X93\n          type: binary\n        - name: X94\n          type: binary\n        - name: X95\n          type: binary\n        - name: X96\n          type: binary\n        - name: X97\n          type: binary\n        - name: X98\n          type: binary\n        - name: X99\n          type: binary\n        - name: X100\n          type: binary\n        - name: X101\n          type: binary\n        - name: X102\n          type: binary\n        - name: X103\n          type: binary\n        - name: X104\n          type: binary\n        - name: X105\n          type: binary\n        - name: X106\n          type: binary\n        - name: X107\n          type: binary\n        - name: X108\n          type: binary\n        - name: X109\n          type: binary\n        - name: X110\n          type: binary\n        - name: X111\n          type: binary\n        - name: X112\n          type: binary\n        - name: X113\n          type: binary\n        - name: X114\n          type: binary\n        - name: X115\n          type: binary\n        - name: X116\n          type: binary\n        - name: X117\n          type: binary\n        - name: X118\n          type: binary\n        - name: X119\n          type: binary\n        - name: X120\n          type: binary\n        - name: X122\n          type: binary\n        - name: X123\n          type: binary\n        - name: X124\n          type: binary\n        - name: X125\n          type: binary\n        - name: X126\n          type: binary\n        - name: X127\n          type: binary\n        - name: X128\n          type: binary\n        - name: X129\n          type: binary\n        - name: X130\n          type: binary\n        - name: X131\n          type: binary\n        - name: X132\n          type: binary\n        - name: X133\n          type: binary\n        - name: X134\n          type: binary\n        - name: X135\n          type: binary\n        - name: X136\n          type: binary\n        - name: X137\n          type: binary\n        - name: X138\n          type: binary\n        - name: X139\n          type: binary\n        - name: X140\n          type: binary\n        - name: X141\n          type: binary\n        - name: X142\n          type: binary\n        - name: X143\n          type: binary\n        - name: X144\n          type: binary\n        - name: X145\n          type: binary\n        - name: X146\n          type: binary\n        - name: X147\n          type: binary\n        - name: X148\n          type: binary\n        - name: X150\n          type: binary\n        - name: X151\n          type: binary\n        - name: X152\n          type: binary\n        - name: X153\n          type: binary\n        - name: X154\n          type: binary\n        - name: X155\n          type: binary\n        - name: X156\n          type: binary\n        - name: X157\n          type: binary\n        - name: X158\n          type: binary\n        - name: X159\n          type: binary\n        - name: X160\n          type: binary\n        - name: X161\n          type: binary\n        - name: X162\n          type: binary\n        - name: X163\n          type: binary\n        - name: X164\n          type: binary\n        - name: X165\n          type: binary\n        - name: X166\n          type: binary\n        - name: X167\n          type: binary\n        - name: X168\n          type: binary\n        - name: X169\n          type: binary\n        - name: X170\n          type: binary\n        - name: X171\n          type: binary\n        - name: X172\n          type: binary\n        - name: X173\n          type: binary\n        - name: X174\n          type: binary\n        - name: X175\n          type: binary\n        - name: X176\n          type: binary\n        - name: X177\n          type: binary\n        - name: X178\n          type: binary\n        - name: X179\n          type: binary\n        - name: X180\n          type: binary\n        - name: X181\n          type: binary\n        - name: X182\n          type: binary\n        - name: X183\n          type: binary\n        - name: X184\n          type: binary\n        - name: X185\n          type: binary\n        - name: X186\n          type: binary\n        - name: X187\n          type: binary\n        - name: X189\n          type: binary\n        - name: X190\n          type: binary\n        - name: X191\n          type: binary\n        - name: X192\n          type: binary\n        - name: X194\n          type: binary\n        - name: X195\n          type: binary\n        - name: X196\n          type: binary\n        - name: X197\n          type: binary\n        - name: X198\n          type: binary\n        - name: X199\n          type: binary\n        - name: X200\n          type: binary\n        - name: X201\n          type: binary\n        - name: X202\n          type: binary\n        - name: X203\n          type: binary\n        - name: X204\n          type: binary\n        - name: X205\n          type: binary\n        - name: X206\n          type: binary\n        - name: X207\n          type: binary\n        - name: X208\n          type: binary\n        - name: X209\n          type: binary\n        - name: X210\n          type: binary\n        - name: X211\n          type: binary\n        - name: X212\n          type: binary\n        - name: X213\n          type: binary\n        - name: X214\n          type: binary\n        - name: X215\n          type: binary\n        - name: X216\n          type: binary\n        - name: X217\n          type: binary\n        - name: X218\n          type: binary\n        - name: X219\n          type: binary\n        - name: X220\n          type: binary\n        - name: X221\n          type: binary\n        - name: X222\n          type: binary\n        - name: X223\n          type: binary\n        - name: X224\n          type: binary\n        - name: X225\n          type: binary\n        - name: X226\n          type: binary\n        - name: X227\n          type: binary\n        - name: X228\n          type: binary\n        - name: X229\n          type: binary\n        - name: X230\n          type: binary\n        - name: X231\n          type: binary\n        - name: X232\n          type: binary\n        - name: X233\n          type: binary\n        - name: X234\n          type: binary\n        - name: X235\n          type: binary\n        - name: X236\n          type: binary\n        - name: X237\n          type: binary\n        - name: X238\n          type: binary\n        - name: X239\n          type: binary\n        - name: X240\n          type: binary\n        - name: X241\n          type: binary\n        - name: X242\n          type: binary\n        - name: X243\n          type: binary\n        - name: X244\n          type: binary\n        - name: X245\n          type: binary\n        - name: X246\n          type: binary\n        - name: X247\n          type: binary\n        - name: X248\n          type: binary\n        - name: X249\n          type: binary\n        - name: X250\n          type: binary\n        - name: X251\n          type: binary\n        - name: X252\n          type: binary\n        - name: X253\n          type: binary\n        - name: X254\n          type: binary\n        - name: X255\n          type: binary\n        - name: X256\n          type: binary\n        - name: X257\n          type: binary\n        - name: X258\n          type: binary\n        - name: X259\n          type: binary\n        - name: X260\n          type: binary\n        - name: X261\n          type: binary\n        - name: X262\n          type: binary\n        - name: X263\n          type: binary\n        - name: X264\n          type: binary\n        - name: X265\n          type: binary\n        - name: X266\n          type: binary\n        - name: X267\n          type: binary\n        - name: X268\n          type: binary\n        - name: X269\n          type: binary\n        - name: X270\n          type: binary\n        - name: X271\n          type: binary\n        - name: X272\n          type: binary\n        - name: X273\n          type: binary\n        - name: X274\n          type: binary\n        - name: X275\n          type: binary\n        - name: X276\n          type: binary\n        - name: X277\n          type: binary\n        - name: X278\n          type: binary\n        - name: X279\n          type: binary\n        - name: X280\n          type: binary\n        - name: X281\n          type: binary\n        - name: X282\n          type: binary\n        - name: X283\n          type: binary\n        - name: X284\n          type: binary\n        - name: X285\n          type: binary\n        - name: X286\n          type: binary\n        - name: X287\n          type: binary\n        - name: X288\n          type: binary\n        - name: X289\n          type: binary\n        - name: X290\n          type: binary\n        - name: X291\n          type: binary\n        - name: X292\n          type: binary\n        - name: X293\n          type: binary\n        - name: X294\n          type: binary\n        - name: X295\n          type: binary\n        - name: X296\n          type: binary\n        - name: X297\n          type: binary\n        - name: X298\n          type: binary\n        - name: X299\n          type: binary\n        - name: X300\n          type: binary\n        - name: X301\n          type: binary\n        - name: X302\n          type: binary\n        - name: X304\n          type: binary\n        - name: X305\n          type: binary\n        - name: X306\n          type: binary\n        - name: X307\n          type: binary\n        - name: X308\n          type: binary\n        - name: X309\n          type: binary\n        - name: X310\n          type: binary\n        - name: X311\n          type: binary\n        - name: X312\n          type: binary\n        - name: X313\n          type: binary\n        - name: X314\n          type: binary\n        - name: X315\n          type: binary\n        - name: X316\n          type: binary\n        - name: X317\n          type: binary\n        - name: X318\n          type: binary\n        - name: X319\n          type: binary\n        - name: X320\n          type: binary\n        - name: X321\n          type: binary\n        - name: X322\n          type: binary\n        - name: X323\n          type: binary\n        - name: X324\n          type: binary\n        - name: X325\n          type: binary\n        - name: X326\n          type: binary\n        - name: X327\n          type: binary\n        - name: X328\n          type: binary\n        - name: X329\n          type: binary\n        - name: X330\n          type: binary\n        - name: X331\n          type: binary\n        - name: X332\n          type: binary\n        - name: X333\n          type: binary\n        - name: X334\n          type: binary\n        - name: X335\n          type: binary\n        - name: X336\n          type: binary\n        - name: X337\n          type: binary\n        - name: X338\n          type: binary\n        - name: X339\n          type: binary\n        - name: X340\n          type: binary\n        - name: X341\n          type: binary\n        - name: X342\n          type: binary\n        - name: X343\n          type: binary\n        - name: X344\n          type: binary\n        - name: X345\n          type: binary\n        - name: X346\n          type: binary\n        - name: X347\n          type: binary\n        - name: X348\n          type: binary\n        - name: X349\n          type: binary\n        - name: X350\n          type: binary\n        - name: X351\n          type: binary\n        - name: X352\n          type: binary\n        - name: X353\n          type: binary\n        - name: X354\n          type: binary\n        - name: X355\n          type: binary\n        - name: X356\n          type: binary\n        - name: X357\n          type: binary\n        - name: X358\n          type: binary\n        - name: X359\n          type: binary\n        - name: X360\n          type: binary\n        - name: X361\n          type: binary\n        - name: X362\n          type: binary\n        - name: X363\n          type: binary\n        - name: X364\n          type: binary\n        - name: X365\n          type: binary\n        - name: X366\n          type: binary\n        - name: X367\n          type: binary\n        - name: X368\n          type: binary\n        - name: X369\n          type: binary\n        - name: X370\n          type: binary\n        - name: X371\n          type: binary\n        - name: X372\n          type: binary\n        - name: X373\n          type: binary\n        - name: X374\n          type: binary\n        - name: X375\n          type: binary\n        - name: X376\n          type: binary\n        - name: X377\n          type: binary\n        - name: X378\n          type: binary\n        - name: X379\n          type: binary\n        - name: X380\n          type: binary\n        - name: X382\n          type: binary\n        - name: X383\n          type: binary\n        - name: X384\n          type: binary\n        - name: X385\n          type: binary\n      combiner:\n        type: tabnet\n        size: 128 # N_a\n        output_size: 8 # N_d\n        sparsity: 0.1 # lambda_sparse\n        bn_momentum: 0.1 # m_B\n        num_steps: 9 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 256 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 500\n          decay_rate: 0.95\n        validation_metric: root_mean_squared_error\n  - name: otto_group_product\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.7956883907318115\n    training_rows: 43459\n    test_rows: 12296\n    validation_rows: 6123\n    config:\n      output_features:\n        - name: target\n          type: category\n      input_features:\n        - name: feat_1\n          type: number\n        - name: feat_2\n          type: number\n        - name: feat_3\n          type: number\n        - name: feat_4\n          type: number\n        - name: feat_5\n          type: number\n        - name: feat_6\n          type: number\n        - name: feat_7\n          type: number\n        - name: feat_8\n          type: number\n        - name: feat_9\n          type: number\n        - name: feat_10\n          type: number\n        - name: feat_11\n          type: number\n        - name: feat_12\n          type: number\n        - name: feat_13\n          type: number\n        - name: feat_14\n          type: number\n        - name: feat_15\n          type: number\n        - name: feat_16\n          type: number\n        - name: feat_17\n          type: number\n        - name: feat_18\n          type: number\n        - name: feat_19\n          type: number\n        - name: feat_20\n          type: number\n        - name: feat_21\n          type: category\n        - name: feat_22\n          type: number\n        - name: feat_23\n          type: number\n        - name: feat_24\n          type: number\n        - name: feat_25\n          type: number\n        - name: feat_26\n          type: number\n        - name: feat_27\n          type: number\n        - name: feat_28\n          type: number\n        - name: feat_29\n          type: number\n        - name: feat_30\n          type: number\n        - name: feat_31\n          type: number\n        - name: feat_32\n          type: number\n        - name: feat_33\n          type: number\n        - name: feat_34\n          type: number\n        - name: feat_35\n          type: number\n        - name: feat_36\n          type: number\n        - name: feat_37\n          type: number\n        - name: feat_38\n          type: number\n        - name: feat_39\n          type: number\n        - name: feat_40\n          type: number\n        - name: feat_41\n          type: number\n        - name: feat_42\n          type: number\n        - name: feat_43\n          type: number\n        - name: feat_44\n          type: number\n        - name: feat_45\n          type: number\n        - name: feat_46\n          type: number\n        - name: feat_47\n          type: number\n        - name: feat_48\n          type: number\n        - name: feat_49\n          type: number\n        - name: feat_50\n          type: number\n        - name: feat_51\n          type: number\n        - name: feat_52\n          type: number\n        - name: feat_53\n          type: number\n        - name: feat_54\n          type: number\n        - name: feat_55\n          type: number\n        - name: feat_56\n          type: number\n        - name: feat_57\n          type: number\n        - name: feat_58\n          type: number\n        - name: feat_59\n          type: number\n        - name: feat_60\n          type: number\n        - name: feat_61\n          type: number\n        - name: feat_62\n          type: number\n        - name: feat_63\n          type: number\n        - name: feat_64\n          type: number\n        - name: feat_65\n          type: number\n        - name: feat_66\n          type: number\n        - name: feat_67\n          type: number\n        - name: feat_68\n          type: number\n        - name: feat_69\n          type: number\n        - name: feat_70\n          type: number\n        - name: feat_71\n          type: number\n        - name: feat_72\n          type: number\n        - name: feat_73\n          type: number\n        - name: feat_74\n          type: number\n        - name: feat_75\n          type: number\n        - name: feat_76\n          type: number\n        - name: feat_77\n          type: number\n        - name: feat_78\n          type: number\n        - name: feat_79\n          type: number\n        - name: feat_80\n          type: number\n        - name: feat_81\n          type: number\n        - name: feat_82\n          type: number\n        - name: feat_83\n          type: number\n        - name: feat_84\n          type: number\n        - name: feat_85\n          type: number\n        - name: feat_86\n          type: number\n        - name: feat_87\n          type: number\n        - name: feat_88\n          type: number\n        - name: feat_89\n          type: number\n        - name: feat_90\n          type: number\n        - name: feat_91\n          type: number\n        - name: feat_92\n          type: number\n        - name: feat_93\n          type: number\n      combiner:\n        type: tabnet\n        size: 128 # N_a\n        output_size: 128 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.2 # m_B\n        num_steps: 3 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 512 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 20000\n          decay_rate: 0.4\n        validation_metric: accuracy\n  - name: poker_hand\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.9804078340530396\n    training_rows: 22509\n    test_rows: 0\n    validation_rows: 2501\n    config:\n      output_features:\n        - name: hand\n          type: category\n      input_features:\n        - name: S1\n          type: number\n        - name: C1\n          type: number\n        - name: S2\n          type: number\n        - name: C2\n          type: number\n        - name: S3\n          type: number\n        - name: C3\n          type: number\n        - name: S4\n          type: number\n        - name: C4\n          type: number\n        - name: S5\n          type: number\n        - name: C5\n          type: number\n      combiner:\n        type: tabnet\n        size: 16 # N_a\n        output_size: 128 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.02 # m_B\n        num_steps: 6 # N_steps\n        relaxation_factor: 1.0 # gamma\n        bn_virtual_bs: 512 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 8000\n          decay_rate: 0.8\n        validation_metric: accuracy\n  - name: porto_seguro_safe_driver\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.9630663394927979\n    training_rows: 416779\n    test_rows: 118948\n    validation_rows: 59485\n    config:\n      output_features:\n        - name: target\n          type: binary\n      input_features:\n        - name: ps_ind_01\n          type: category\n        - name: ps_ind_02_cat\n          type: number\n        - name: ps_ind_03\n          type: category\n        - name: ps_ind_04_cat\n          type: category\n        - name: ps_ind_05_cat\n          type: category\n        - name: ps_ind_06_bin\n          type: binary\n        - name: ps_ind_07_bin\n          type: binary\n        - name: ps_ind_08_bin\n          type: binary\n        - name: ps_ind_09_bin\n          type: binary\n        - name: ps_ind_10_bin\n          type: binary\n        - name: ps_ind_11_bin\n          type: binary\n        - name: ps_ind_12_bin\n          type: binary\n        - name: ps_ind_13_bin\n          type: binary\n        - name: ps_ind_14\n          type: category\n        - name: ps_ind_15\n          type: category\n        - name: ps_ind_16_bin\n          type: binary\n        - name: ps_ind_17_bin\n          type: binary\n        - name: ps_ind_18_bin\n          type: binary\n        - name: ps_reg_01\n          type: number\n        - name: ps_reg_02\n          type: number\n        - name: ps_reg_03\n          type: number\n        - name: ps_car_01_cat\n          type: category\n        - name: ps_car_02_cat\n          type: category\n        - name: ps_car_03_cat\n          type: category\n        - name: ps_car_04_cat\n          type: category\n        - name: ps_car_05_cat\n          type: category\n        - name: ps_car_06_cat\n          type: category\n        - name: ps_car_07_cat\n          type: category\n        - name: ps_car_08_cat\n          type: binary\n        - name: ps_car_09_cat\n          type: category\n        - name: ps_car_10_cat\n          type: category\n        - name: ps_car_11_cat\n          type: number\n        - name: ps_car_11\n          type: category\n        - name: ps_car_12\n          type: number\n        - name: ps_car_13\n          type: number\n        - name: ps_car_14\n          type: number\n        - name: ps_car_15\n          type: number\n        - name: ps_calc_01\n          type: number\n        - name: ps_calc_02\n          type: number\n        - name: ps_calc_03\n          type: number\n        - name: ps_calc_04\n          type: category\n        - name: ps_calc_05\n          type: category\n        - name: ps_calc_06\n          type: category\n        - name: ps_calc_07\n          type: category\n        - name: ps_calc_08\n          type: category\n        - name: ps_calc_09\n          type: category\n        - name: ps_calc_10\n          type: number\n        - name: ps_calc_11\n          type: number\n        - name: ps_calc_12\n          type: category\n        - name: ps_calc_13\n          type: category\n        - name: ps_calc_14\n          type: number\n        - name: ps_calc_15_bin\n          type: binary\n        - name: ps_calc_16_bin\n          type: binary\n        - name: ps_calc_17_bin\n          type: binary\n        - name: ps_calc_18_bin\n          type: binary\n        - name: ps_calc_19_bin\n          type: binary\n        - name: ps_calc_20_bin\n          type: binary\n      combiner:\n        type: tabnet\n        size: 32 # N_a\n        output_size: 32 # N_d\n        sparsity: 0.0001 # lambda_sparse\n        bn_momentum: 0.4 # m_B\n        num_steps: 5 # N_steps\n        relaxation_factor: 1.2 # gamma\n        bn_virtual_bs: 1024 # B_v\n      trainer:\n        batch_size: 1024 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 10000\n          decay_rate: 0.9\n        validation_metric: accuracy\n  - name: santander_customer_satisfaction\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.9611535668373108\n    training_rows: 53298\n    test_rows: 15128\n    validation_rows: 7594\n    config:\n      output_features:\n        - name: TARGET\n          type: binary\n      input_features:\n        - name: var3\n          type: number\n        - name: var15\n          type: number\n        - name: imp_ent_var16_ult1\n          type: number\n        - name: imp_op_var39_comer_ult1\n          type: number\n        - name: imp_op_var39_comer_ult3\n          type: number\n        - name: imp_op_var40_comer_ult1\n          type: number\n        - name: imp_op_var40_comer_ult3\n          type: number\n        - name: imp_op_var40_efect_ult1\n          type: number\n        - name: imp_op_var40_efect_ult3\n          type: number\n        - name: imp_op_var40_ult1\n          type: number\n        - name: imp_op_var41_comer_ult1\n          type: number\n        - name: imp_op_var41_comer_ult3\n          type: number\n        - name: imp_op_var41_efect_ult1\n          type: number\n        - name: imp_op_var41_efect_ult3\n          type: number\n        - name: imp_op_var41_ult1\n          type: number\n        - name: imp_op_var39_efect_ult1\n          type: number\n        - name: imp_op_var39_efect_ult3\n          type: number\n        - name: imp_op_var39_ult1\n          type: number\n        - name: imp_sal_var16_ult1\n          type: number\n        - name: ind_var1_0\n          type: binary\n        - name: ind_var1\n          type: binary\n        - name: ind_var2_0\n          type: binary\n        - name: ind_var2\n          type: binary\n        - name: ind_var5_0\n          type: binary\n        - name: ind_var5\n          type: binary\n        - name: ind_var6_0\n          type: binary\n        - name: ind_var6\n          type: binary\n        - name: ind_var8_0\n          type: binary\n        - name: ind_var8\n          type: binary\n        - name: ind_var12_0\n          type: binary\n        - name: ind_var12\n          type: binary\n        - name: ind_var13_0\n          type: binary\n        - name: ind_var13_corto_0\n          type: binary\n        - name: ind_var13_corto\n          type: binary\n        - name: ind_var13_largo_0\n          type: binary\n        - name: ind_var13_largo\n          type: binary\n        - name: ind_var13_medio_0\n          type: binary\n        - name: ind_var13_medio\n          type: binary\n        - name: ind_var13\n          type: binary\n        - name: ind_var14_0\n          type: binary\n        - name: ind_var14\n          type: binary\n        - name: ind_var17_0\n          type: binary\n        - name: ind_var17\n          type: binary\n        - name: ind_var18_0\n          type: binary\n        - name: ind_var18\n          type: binary\n        - name: ind_var19\n          type: binary\n        - name: ind_var20_0\n          type: binary\n        - name: ind_var20\n          type: binary\n        - name: ind_var24_0\n          type: binary\n        - name: ind_var24\n          type: binary\n        - name: ind_var25_cte\n          type: binary\n        - name: ind_var26_0\n          type: binary\n        - name: ind_var26_cte\n          type: binary\n        - name: ind_var26\n          type: binary\n        - name: ind_var25_0\n          type: binary\n        - name: ind_var25\n          type: binary\n        - name: ind_var27_0\n          type: binary\n        - name: ind_var28_0\n          type: binary\n        - name: ind_var28\n          type: binary\n        - name: ind_var27\n          type: binary\n        - name: ind_var29_0\n          type: binary\n        - name: ind_var29\n          type: binary\n        - name: ind_var30_0\n          type: binary\n        - name: ind_var30\n          type: binary\n        - name: ind_var31_0\n          type: binary\n        - name: ind_var31\n          type: binary\n        - name: ind_var32_cte\n          type: binary\n        - name: ind_var32_0\n          type: binary\n        - name: ind_var32\n          type: binary\n        - name: ind_var33_0\n          type: binary\n        - name: ind_var33\n          type: binary\n        - name: ind_var34_0\n          type: binary\n        - name: ind_var34\n          type: binary\n        - name: ind_var37_cte\n          type: binary\n        - name: ind_var37_0\n          type: binary\n        - name: ind_var37\n          type: binary\n        - name: ind_var39_0\n          type: binary\n        - name: ind_var40_0\n          type: binary\n        - name: ind_var40\n          type: binary\n        - name: ind_var41_0\n          type: binary\n        - name: ind_var41\n          type: binary\n        - name: ind_var39\n          type: binary\n        - name: ind_var44_0\n          type: binary\n        - name: ind_var44\n          type: binary\n        - name: ind_var46_0\n          type: binary\n        - name: ind_var46\n          type: binary\n        - name: num_var1_0\n          type: number\n        - name: num_var1\n          type: number\n        - name: num_var4\n          type: category\n        - name: num_var5_0\n          type: number\n        - name: num_var5\n          type: number\n        - name: num_var6_0\n          type: number\n        - name: num_var6\n          type: number\n        - name: num_var8_0\n          type: number\n        - name: num_var8\n          type: number\n        - name: num_var12_0\n          type: number\n        - name: num_var12\n          type: number\n        - name: num_var13_0\n          type: number\n        - name: num_var13_corto_0\n          type: number\n        - name: num_var13_corto\n          type: number\n        - name: num_var13_largo_0\n          type: number\n        - name: num_var13_largo\n          type: number\n        - name: num_var13_medio_0\n          type: number\n        - name: num_var13_medio\n          type: number\n        - name: num_var13\n          type: number\n        - name: num_var14_0\n          type: number\n        - name: num_var14\n          type: number\n        - name: num_var17_0\n          type: number\n        - name: num_var17\n          type: number\n        - name: num_var18_0\n          type: number\n        - name: num_var18\n          type: number\n        - name: num_var20_0\n          type: number\n        - name: num_var20\n          type: number\n        - name: num_var24_0\n          type: number\n        - name: num_var24\n          type: number\n        - name: num_var26_0\n          type: number\n        - name: num_var26\n          type: number\n        - name: num_var25_0\n          type: number\n        - name: num_var25\n          type: number\n        - name: num_op_var40_hace2\n          type: number\n        - name: num_op_var40_hace3\n          type: number\n        - name: num_op_var40_ult1\n          type: number\n        - name: num_op_var40_ult3\n          type: number\n        - name: num_op_var41_hace2\n          type: number\n        - name: num_op_var41_hace3\n          type: number\n        - name: num_op_var41_ult1\n          type: number\n        - name: num_op_var41_ult3\n          type: number\n        - name: num_op_var39_hace2\n          type: number\n        - name: num_op_var39_hace3\n          type: number\n        - name: num_op_var39_ult1\n          type: number\n        - name: num_op_var39_ult3\n          type: number\n        - name: num_var27_0\n          type: binary\n        - name: num_var28_0\n          type: binary\n        - name: num_var28\n          type: binary\n        - name: num_var27\n          type: binary\n        - name: num_var29_0\n          type: number\n        - name: num_var29\n          type: number\n        - name: num_var30_0\n          type: number\n        - name: num_var30\n          type: number\n        - name: num_var31_0\n          type: number\n        - name: num_var31\n          type: number\n        - name: num_var32_0\n          type: number\n        - name: num_var32\n          type: number\n        - name: num_var33_0\n          type: number\n        - name: num_var33\n          type: number\n        - name: num_var34_0\n          type: number\n        - name: num_var34\n          type: number\n        - name: num_var35\n          type: number\n        - name: num_var37_med_ult2\n          type: number\n        - name: num_var37_0\n          type: number\n        - name: num_var37\n          type: number\n        - name: num_var39_0\n          type: number\n        - name: num_var40_0\n          type: number\n        - name: num_var40\n          type: number\n        - name: num_var41_0\n          type: number\n        - name: num_var41\n          type: binary\n        - name: num_var39\n          type: number\n        - name: num_var42_0\n          type: number\n        - name: num_var42\n          type: number\n        - name: num_var44_0\n          type: number\n        - name: num_var44\n          type: number\n        - name: num_var46_0\n          type: binary\n        - name: num_var46\n          type: binary\n        - name: saldo_var1\n          type: number\n        - name: saldo_var5\n          type: number\n        - name: saldo_var6\n          type: number\n        - name: saldo_var8\n          type: number\n        - name: saldo_var12\n          type: number\n        - name: saldo_var13_corto\n          type: number\n        - name: saldo_var13_largo\n          type: number\n        - name: saldo_var13_medio\n          type: number\n        - name: saldo_var13\n          type: number\n        - name: saldo_var14\n          type: number\n        - name: saldo_var17\n          type: number\n        - name: saldo_var18\n          type: number\n        - name: saldo_var20\n          type: number\n        - name: saldo_var24\n          type: number\n        - name: saldo_var26\n          type: number\n        - name: saldo_var25\n          type: number\n        - name: saldo_var28\n          type: binary\n        - name: saldo_var27\n          type: binary\n        - name: saldo_var29\n          type: number\n        - name: saldo_var30\n          type: number\n        - name: saldo_var31\n          type: number\n        - name: saldo_var32\n          type: number\n        - name: saldo_var33\n          type: number\n        - name: saldo_var34\n          type: number\n        - name: saldo_var37\n          type: number\n        - name: saldo_var40\n          type: number\n        - name: saldo_var41\n          type: binary\n        - name: saldo_var42\n          type: number\n        - name: saldo_var44\n          type: number\n        - name: saldo_var46\n          type: binary\n        - name: var36\n          type: number\n        - name: delta_imp_amort_var18_1y3\n          type: number\n        - name: delta_imp_amort_var34_1y3\n          type: number\n        - name: delta_imp_aport_var13_1y3\n          type: number\n        - name: delta_imp_aport_var17_1y3\n          type: number\n        - name: delta_imp_aport_var33_1y3\n          type: number\n        - name: delta_imp_compra_var44_1y3\n          type: number\n        - name: delta_imp_reemb_var13_1y3\n          type: number\n        - name: delta_imp_reemb_var17_1y3\n          type: number\n        - name: delta_imp_reemb_var33_1y3\n          type: number\n        - name: delta_imp_trasp_var17_in_1y3\n          type: number\n        - name: delta_imp_trasp_var17_out_1y3\n          type: number\n        - name: delta_imp_trasp_var33_in_1y3\n          type: number\n        - name: delta_imp_trasp_var33_out_1y3\n          type: number\n        - name: delta_imp_venta_var44_1y3\n          type: number\n        - name: delta_num_aport_var13_1y3\n          type: number\n        - name: delta_num_aport_var17_1y3\n          type: number\n        - name: delta_num_aport_var33_1y3\n          type: number\n        - name: delta_num_compra_var44_1y3\n          type: number\n        - name: delta_num_reemb_var13_1y3\n          type: number\n        - name: delta_num_reemb_var17_1y3\n          type: number\n        - name: delta_num_reemb_var33_1y3\n          type: number\n        - name: delta_num_trasp_var17_in_1y3\n          type: number\n        - name: delta_num_trasp_var17_out_1y3\n          type: number\n        - name: delta_num_trasp_var33_in_1y3\n          type: number\n        - name: delta_num_trasp_var33_out_1y3\n          type: number\n        - name: delta_num_venta_var44_1y3\n          type: number\n        - name: imp_amort_var18_hace3\n          type: binary\n        - name: imp_amort_var18_ult1\n          type: number\n        - name: imp_amort_var34_hace3\n          type: binary\n        - name: imp_amort_var34_ult1\n          type: number\n        - name: imp_aport_var13_hace3\n          type: number\n        - name: imp_aport_var13_ult1\n          type: number\n        - name: imp_aport_var17_hace3\n          type: number\n        - name: imp_aport_var17_ult1\n          type: number\n        - name: imp_aport_var33_hace3\n          type: number\n        - name: imp_aport_var33_ult1\n          type: number\n        - name: imp_var7_emit_ult1\n          type: number\n        - name: imp_var7_recib_ult1\n          type: number\n        - name: imp_compra_var44_hace3\n          type: number\n        - name: imp_compra_var44_ult1\n          type: number\n        - name: imp_reemb_var13_hace3\n          type: binary\n        - name: imp_reemb_var13_ult1\n          type: number\n        - name: imp_reemb_var17_hace3\n          type: number\n        - name: imp_reemb_var17_ult1\n          type: number\n        - name: imp_reemb_var33_hace3\n          type: binary\n        - name: imp_reemb_var33_ult1\n          type: number\n        - name: imp_var43_emit_ult1\n          type: number\n        - name: imp_trans_var37_ult1\n          type: number\n        - name: imp_trasp_var17_in_hace3\n          type: number\n        - name: imp_trasp_var17_in_ult1\n          type: number\n        - name: imp_trasp_var17_out_hace3\n          type: binary\n        - name: imp_trasp_var17_out_ult1\n          type: number\n        - name: imp_trasp_var33_in_hace3\n          type: number\n        - name: imp_trasp_var33_in_ult1\n          type: number\n        - name: imp_trasp_var33_out_hace3\n          type: binary\n        - name: imp_trasp_var33_out_ult1\n          type: number\n        - name: imp_venta_var44_hace3\n          type: number\n        - name: imp_venta_var44_ult1\n          type: number\n        - name: ind_var7_emit_ult1\n          type: binary\n        - name: ind_var7_recib_ult1\n          type: binary\n        - name: ind_var10_ult1\n          type: binary\n        - name: ind_var10cte_ult1\n          type: binary\n        - name: ind_var9_cte_ult1\n          type: binary\n        - name: ind_var9_ult1\n          type: binary\n        - name: ind_var43_emit_ult1\n          type: binary\n        - name: ind_var43_recib_ult1\n          type: binary\n        - name: var21\n          type: number\n        - name: num_var2_0_ult1\n          type: binary\n        - name: num_var2_ult1\n          type: binary\n        - name: num_aport_var13_hace3\n          type: number\n        - name: num_aport_var13_ult1\n          type: number\n        - name: num_aport_var17_hace3\n          type: number\n        - name: num_aport_var17_ult1\n          type: number\n        - name: num_aport_var33_hace3\n          type: number\n        - name: num_aport_var33_ult1\n          type: number\n        - name: num_var7_emit_ult1\n          type: number\n        - name: num_var7_recib_ult1\n          type: number\n        - name: num_compra_var44_hace3\n          type: number\n        - name: num_compra_var44_ult1\n          type: number\n        - name: num_ent_var16_ult1\n          type: number\n        - name: num_var22_hace2\n          type: number\n        - name: num_var22_hace3\n          type: number\n        - name: num_var22_ult1\n          type: number\n        - name: num_var22_ult3\n          type: number\n        - name: num_med_var22_ult3\n          type: number\n        - name: num_med_var45_ult3\n          type: number\n        - name: num_meses_var5_ult3\n          type: category\n        - name: num_meses_var8_ult3\n          type: category\n        - name: num_meses_var12_ult3\n          type: category\n        - name: num_meses_var13_corto_ult3\n          type: category\n        - name: num_meses_var13_largo_ult3\n          type: category\n        - name: num_meses_var13_medio_ult3\n          type: number\n        - name: num_meses_var17_ult3\n          type: category\n        - name: num_meses_var29_ult3\n          type: category\n        - name: num_meses_var33_ult3\n          type: category\n        - name: num_meses_var39_vig_ult3\n          type: category\n        - name: num_meses_var44_ult3\n          type: category\n        - name: num_op_var39_comer_ult1\n          type: number\n        - name: num_op_var39_comer_ult3\n          type: number\n        - name: num_op_var40_comer_ult1\n          type: number\n        - name: num_op_var40_comer_ult3\n          type: number\n        - name: num_op_var40_efect_ult1\n          type: number\n        - name: num_op_var40_efect_ult3\n          type: number\n        - name: num_op_var41_comer_ult1\n          type: number\n        - name: num_op_var41_comer_ult3\n          type: number\n        - name: num_op_var41_efect_ult1\n          type: number\n        - name: num_op_var41_efect_ult3\n          type: number\n        - name: num_op_var39_efect_ult1\n          type: number\n        - name: num_op_var39_efect_ult3\n          type: number\n        - name: num_reemb_var13_hace3\n          type: binary\n        - name: num_reemb_var13_ult1\n          type: number\n        - name: num_reemb_var17_hace3\n          type: number\n        - name: num_reemb_var17_ult1\n          type: number\n        - name: num_reemb_var33_hace3\n          type: binary\n        - name: num_reemb_var33_ult1\n          type: number\n        - name: num_sal_var16_ult1\n          type: number\n        - name: num_var43_emit_ult1\n          type: number\n        - name: num_var43_recib_ult1\n          type: number\n        - name: num_trasp_var11_ult1\n          type: number\n        - name: num_trasp_var17_in_hace3\n          type: number\n        - name: num_trasp_var17_in_ult1\n          type: number\n        - name: num_trasp_var17_out_hace3\n          type: binary\n        - name: num_trasp_var17_out_ult1\n          type: number\n        - name: num_trasp_var33_in_hace3\n          type: number\n        - name: num_trasp_var33_in_ult1\n          type: number\n        - name: num_trasp_var33_out_hace3\n          type: binary\n        - name: num_trasp_var33_out_ult1\n          type: number\n        - name: num_venta_var44_hace3\n          type: number\n        - name: num_venta_var44_ult1\n          type: number\n        - name: num_var45_hace2\n          type: number\n        - name: num_var45_hace3\n          type: number\n        - name: num_var45_ult1\n          type: number\n        - name: num_var45_ult3\n          type: number\n        - name: saldo_var2_ult1\n          type: binary\n        - name: saldo_medio_var5_hace2\n          type: number\n        - name: saldo_medio_var5_hace3\n          type: number\n        - name: saldo_medio_var5_ult1\n          type: number\n        - name: saldo_medio_var5_ult3\n          type: number\n        - name: saldo_medio_var8_hace2\n          type: number\n        - name: saldo_medio_var8_hace3\n          type: number\n        - name: saldo_medio_var8_ult1\n          type: number\n        - name: saldo_medio_var8_ult3\n          type: number\n        - name: saldo_medio_var12_hace2\n          type: number\n        - name: saldo_medio_var12_hace3\n          type: number\n        - name: saldo_medio_var12_ult1\n          type: number\n        - name: saldo_medio_var12_ult3\n          type: number\n        - name: saldo_medio_var13_corto_hace2\n          type: number\n        - name: saldo_medio_var13_corto_hace3\n          type: number\n        - name: saldo_medio_var13_corto_ult1\n          type: number\n        - name: saldo_medio_var13_corto_ult3\n          type: number\n        - name: saldo_medio_var13_largo_hace2\n          type: number\n        - name: saldo_medio_var13_largo_hace3\n          type: number\n        - name: saldo_medio_var13_largo_ult1\n          type: number\n        - name: saldo_medio_var13_largo_ult3\n          type: number\n        - name: saldo_medio_var13_medio_hace2\n          type: number\n        - name: saldo_medio_var13_medio_hace3\n          type: binary\n        - name: saldo_medio_var13_medio_ult1\n          type: number\n        - name: saldo_medio_var13_medio_ult3\n          type: number\n        - name: saldo_medio_var17_hace2\n          type: number\n        - name: saldo_medio_var17_hace3\n          type: number\n        - name: saldo_medio_var17_ult1\n          type: number\n        - name: saldo_medio_var17_ult3\n          type: number\n        - name: saldo_medio_var29_hace2\n          type: number\n        - name: saldo_medio_var29_hace3\n          type: number\n        - name: saldo_medio_var29_ult1\n          type: number\n        - name: saldo_medio_var29_ult3\n          type: number\n        - name: saldo_medio_var33_hace2\n          type: number\n        - name: saldo_medio_var33_hace3\n          type: number\n        - name: saldo_medio_var33_ult1\n          type: number\n        - name: saldo_medio_var33_ult3\n          type: number\n        - name: saldo_medio_var44_hace2\n          type: number\n        - name: saldo_medio_var44_hace3\n          type: number\n        - name: saldo_medio_var44_ult1\n          type: number\n        - name: saldo_medio_var44_ult3\n          type: number\n        - name: var38\n          type: number\n      combiner:\n        type: tabnet\n        size: 24 # N_a\n        output_size: 128 # N_d\n        sparsity: 0.001 # lambda_sparse\n        bn_momentum: 0.2 # m_B\n        num_steps: 7 # N_steps\n        relaxation_factor: 1.2 # gamma\n        bn_virtual_bs: 256 # B_v\n      trainer:\n        batch_size: 4096 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 10000\n          decay_rate: 0.8\n        validation_metric: accuracy\n  - name: santander_customer_transaction\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.9150915145874023\n    training_rows: 139904\n    test_rows: 40098\n    validation_rows: 19998\n    config:\n      output_features:\n        - name: target\n          type: binary\n      input_features:\n        - name: var_0\n          type: number\n        - name: var_1\n          type: number\n        - name: var_2\n          type: number\n        - name: var_3\n          type: number\n        - name: var_4\n          type: number\n        - name: var_5\n          type: number\n        - name: var_6\n          type: number\n        - name: var_7\n          type: number\n        - name: var_8\n          type: number\n        - name: var_9\n          type: number\n        - name: var_10\n          type: number\n        - name: var_11\n          type: number\n        - name: var_12\n          type: number\n        - name: var_13\n          type: number\n        - name: var_14\n          type: number\n        - name: var_15\n          type: number\n        - name: var_16\n          type: number\n        - name: var_17\n          type: number\n        - name: var_18\n          type: number\n        - name: var_19\n          type: number\n        - name: var_20\n          type: number\n        - name: var_21\n          type: number\n        - name: var_22\n          type: number\n        - name: var_23\n          type: number\n        - name: var_24\n          type: number\n        - name: var_25\n          type: number\n        - name: var_26\n          type: number\n        - name: var_27\n          type: number\n        - name: var_28\n          type: number\n        - name: var_29\n          type: number\n        - name: var_30\n          type: number\n        - name: var_31\n          type: number\n        - name: var_32\n          type: number\n        - name: var_33\n          type: number\n        - name: var_34\n          type: number\n        - name: var_35\n          type: number\n        - name: var_36\n          type: number\n        - name: var_37\n          type: number\n        - name: var_38\n          type: number\n        - name: var_39\n          type: number\n        - name: var_40\n          type: number\n        - name: var_41\n          type: number\n        - name: var_42\n          type: number\n        - name: var_43\n          type: number\n        - name: var_44\n          type: number\n        - name: var_45\n          type: number\n        - name: var_46\n          type: number\n        - name: var_47\n          type: number\n        - name: var_48\n          type: number\n        - name: var_49\n          type: number\n        - name: var_50\n          type: number\n        - name: var_51\n          type: number\n        - name: var_52\n          type: number\n        - name: var_53\n          type: number\n        - name: var_54\n          type: number\n        - name: var_55\n          type: number\n        - name: var_56\n          type: number\n        - name: var_57\n          type: number\n        - name: var_58\n          type: number\n        - name: var_59\n          type: number\n        - name: var_60\n          type: number\n        - name: var_61\n          type: number\n        - name: var_62\n          type: number\n        - name: var_63\n          type: number\n        - name: var_64\n          type: number\n        - name: var_65\n          type: number\n        - name: var_66\n          type: number\n        - name: var_67\n          type: number\n        - name: var_68\n          type: number\n        - name: var_69\n          type: number\n        - name: var_70\n          type: number\n        - name: var_71\n          type: number\n        - name: var_72\n          type: number\n        - name: var_73\n          type: number\n        - name: var_74\n          type: number\n        - name: var_75\n          type: number\n        - name: var_76\n          type: number\n        - name: var_77\n          type: number\n        - name: var_78\n          type: number\n        - name: var_79\n          type: number\n        - name: var_80\n          type: number\n        - name: var_81\n          type: number\n        - name: var_82\n          type: number\n        - name: var_83\n          type: number\n        - name: var_84\n          type: number\n        - name: var_85\n          type: number\n        - name: var_86\n          type: number\n        - name: var_87\n          type: number\n        - name: var_88\n          type: number\n        - name: var_89\n          type: number\n        - name: var_90\n          type: number\n        - name: var_91\n          type: number\n        - name: var_92\n          type: number\n        - name: var_93\n          type: number\n        - name: var_94\n          type: number\n        - name: var_95\n          type: number\n        - name: var_96\n          type: number\n        - name: var_97\n          type: number\n        - name: var_98\n          type: number\n        - name: var_99\n          type: number\n        - name: var_100\n          type: number\n        - name: var_101\n          type: number\n        - name: var_102\n          type: number\n        - name: var_103\n          type: number\n        - name: var_104\n          type: number\n        - name: var_105\n          type: number\n        - name: var_106\n          type: number\n        - name: var_107\n          type: number\n        - name: var_108\n          type: number\n        - name: var_109\n          type: number\n        - name: var_110\n          type: number\n        - name: var_111\n          type: number\n        - name: var_112\n          type: number\n        - name: var_113\n          type: number\n        - name: var_114\n          type: number\n        - name: var_115\n          type: number\n        - name: var_116\n          type: number\n        - name: var_117\n          type: number\n        - name: var_118\n          type: number\n        - name: var_119\n          type: number\n        - name: var_120\n          type: number\n        - name: var_121\n          type: number\n        - name: var_122\n          type: number\n        - name: var_123\n          type: number\n        - name: var_124\n          type: number\n        - name: var_125\n          type: number\n        - name: var_126\n          type: number\n        - name: var_127\n          type: number\n        - name: var_128\n          type: number\n        - name: var_129\n          type: number\n        - name: var_130\n          type: number\n        - name: var_131\n          type: number\n        - name: var_132\n          type: number\n        - name: var_133\n          type: number\n        - name: var_134\n          type: number\n        - name: var_135\n          type: number\n        - name: var_136\n          type: number\n        - name: var_137\n          type: number\n        - name: var_138\n          type: number\n        - name: var_139\n          type: number\n        - name: var_140\n          type: number\n        - name: var_141\n          type: number\n        - name: var_142\n          type: number\n        - name: var_143\n          type: number\n        - name: var_144\n          type: number\n        - name: var_145\n          type: number\n        - name: var_146\n          type: number\n        - name: var_147\n          type: number\n        - name: var_148\n          type: number\n        - name: var_149\n          type: number\n        - name: var_150\n          type: number\n        - name: var_151\n          type: number\n        - name: var_152\n          type: number\n        - name: var_153\n          type: number\n        - name: var_154\n          type: number\n        - name: var_155\n          type: number\n        - name: var_156\n          type: number\n        - name: var_157\n          type: number\n        - name: var_158\n          type: number\n        - name: var_159\n          type: number\n        - name: var_160\n          type: number\n        - name: var_161\n          type: number\n        - name: var_162\n          type: number\n        - name: var_163\n          type: number\n        - name: var_164\n          type: number\n        - name: var_165\n          type: number\n        - name: var_166\n          type: number\n        - name: var_167\n          type: number\n        - name: var_168\n          type: number\n        - name: var_169\n          type: number\n        - name: var_170\n          type: number\n        - name: var_171\n          type: number\n        - name: var_172\n          type: number\n        - name: var_173\n          type: number\n        - name: var_174\n          type: number\n        - name: var_175\n          type: number\n        - name: var_176\n          type: number\n        - name: var_177\n          type: number\n        - name: var_178\n          type: number\n        - name: var_179\n          type: number\n        - name: var_180\n          type: number\n        - name: var_181\n          type: number\n        - name: var_182\n          type: number\n        - name: var_183\n          type: number\n        - name: var_184\n          type: number\n        - name: var_185\n          type: number\n        - name: var_186\n          type: number\n        - name: var_187\n          type: number\n        - name: var_188\n          type: number\n        - name: var_189\n          type: number\n        - name: var_190\n          type: number\n        - name: var_191\n          type: number\n        - name: var_192\n          type: number\n        - name: var_193\n          type: number\n        - name: var_194\n          type: number\n        - name: var_195\n          type: number\n        - name: var_196\n          type: number\n        - name: var_197\n          type: number\n        - name: var_198\n          type: number\n        - name: var_199\n          type: number\n      combiner:\n        type: tabnet\n        size: 8 # N_a\n        output_size: 8 # N_d\n        sparsity: 0.0 # lambda_sparse\n        bn_momentum: 0.4 # m_B\n        num_steps: 3 # N_steps\n        relaxation_factor: 2.0 # gamma\n        bn_virtual_bs: 256 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 20000\n          decay_rate: 0.95\n        validation_metric: accuracy\n  - name: sarcos\n    goal: minimize\n    metric: root_mean_squared_error\n    validation_metric_score: 2.0124664306640625\n    training_rows: 40036\n    test_rows: 0\n    validation_rows: 4448\n    config:\n      output_features:\n        - name: torque_1\n          type: number\n      input_features:\n        - name: position_1\n          type: number\n        - name: position_2\n          type: number\n        - name: position_3\n          type: number\n        - name: position_4\n          type: number\n        - name: position_5\n          type: number\n        - name: position_6\n          type: number\n        - name: position_7\n          type: number\n        - name: velocity_1\n          type: number\n        - name: velocity_2\n          type: number\n        - name: velocity_3\n          type: number\n        - name: velocity_4\n          type: number\n        - name: velocity_5\n          type: number\n        - name: velocity_6\n          type: number\n        - name: velocity_7\n          type: number\n        - name: acceleration_1\n          type: number\n        - name: acceleration_2\n          type: number\n        - name: acceleration_3\n          type: number\n        - name: acceleration_4\n          type: number\n        - name: acceleration_5\n          type: number\n        - name: acceleration_6\n          type: number\n        - name: acceleration_7\n          type: number\n      combiner:\n        type: tabnet\n        size: 128 # N_a\n        output_size: 8 # N_d\n        sparsity: 0.000001 # lambda_sparse\n        bn_momentum: 0.02 # m_B\n        num_steps: 4 # N_steps\n        relaxation_factor: 1.2 # gamma\n        bn_virtual_bs: 4096 # B_v\n      trainer:\n        batch_size: 256 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.005\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 20000\n          decay_rate: 0.4\n        validation_metric: root_mean_squared_error\n  - name: walmart_recruiting\n    goal: maximize\n    metric: accuracy\n    validation_metric_score: 0.31689465045928955\n    training_rows: 453154\n    test_rows: 129276\n    validation_rows: 64624\n    config:\n      output_features:\n        - name: TripType\n          type: category\n      input_features:\n        - name: VisitNumber\n          type: number\n        - name: Weekday\n          type: category\n        - name: Upc\n          type: number\n        - name: ScanCount\n          type: number\n        - name: FinelineNumber\n          type: number\n      combiner:\n        type: tabnet\n        size: 32 # N_a\n        output_size: 128 # N_d\n        sparsity: 0.000001 # lambda_sparse\n        bn_momentum: 0.4 # m_B\n        num_steps: 4 # N_steps\n        relaxation_factor: 1.2 # gamma\n        bn_virtual_bs: 4096 # B_v\n      trainer:\n        batch_size: 8192 # B\n        eval_batch_size: null # 65536 131072 262144 524288\n        epochs: 300\n        early_stop: 30\n        learning_rate: 0.01\n        optimizer:\n          type: adam\n        learning_rate_scheduler:\n          decay: exponential\n          decay_steps: 20000\n          decay_rate: 0.9\n        validation_metric: accuracy\n"
  },
  {
    "path": "ludwig/automl/defaults/text/bert_config.yaml",
    "content": "trainer:\n  epochs: 10\n  learning_rate_scheduler:\n    warmup_fraction: 0.1\n    decay: linear\n  optimizer:\n    type: adamw\n  use_mixed_precision: true\n\ndefaults:\n  text:\n    encoder:\n      type: bert\n      trainable: true\n\nhyperopt:\n  # goal: maximize\n  parameters:\n    # This parameter space was updated to be loguniform because of issues merging with the trainer.learning_rate\n    # parameter space in ludwig/automl/defaults/combiner/concat_config.yaml. Doing automl on a text feature would\n    # create an invalid combination of loguniform and choice paramters.\n    # TODO(jeffkinnison): Add a second pass `merge_dicts` to handle parameter spaces\n    trainer.learning_rate:\n      space: loguniform\n      lower: 0.00002\n      upper: 0.00003\n    trainer.batch_size:\n      space: choice\n      categories: [16, 32, 64, 128]\n"
  },
  {
    "path": "ludwig/backend/__init__.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport contextlib\nimport logging\nimport os\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.backend.base import Backend, LocalBackend\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO: remove LOCAL_BACKEND as a global constant, replace with singleton LocalBackend.shared_instance().\nLOCAL_BACKEND = LocalBackend.shared_instance()\n\n\nLOCAL = \"local\"\nDASK = \"dask\"\nDEEPSPEED = \"deepspeed\"\nRAY = \"ray\"\n\nALL_BACKENDS = [LOCAL, DASK, DEEPSPEED, RAY]\n\n\ndef _has_ray():\n    # Temporary workaround to prevent tests from automatically using the Ray backend. Taken from\n    # https://stackoverflow.com/questions/25188119/test-if-code-is-executed-from-within-a-py-test-session\n    if \"PYTEST_CURRENT_TEST\" in os.environ:\n        return False\n\n    try:\n        import ray\n    except ImportError:\n        return False\n\n    if ray.is_initialized():\n        return True\n\n    try:\n        ray.init(\"auto\", ignore_reinit_error=True)\n        return True\n    except Exception:\n        return False\n\n\ndef get_local_backend(**kwargs):\n    return LocalBackend(**kwargs)\n\n\ndef create_deepspeed_backend(**kwargs):\n    from ludwig.backend.deepspeed import DeepSpeedBackend\n\n    return DeepSpeedBackend(**kwargs)\n\n\ndef create_ray_backend(**kwargs):\n    from ludwig.backend.ray import RayBackend\n\n    return RayBackend(**kwargs)\n\n\nbackend_registry = {\n    LOCAL: get_local_backend,\n    DEEPSPEED: create_deepspeed_backend,\n    RAY: create_ray_backend,\n    None: get_local_backend,\n}\n\n\n@DeveloperAPI\ndef create_backend(type, **kwargs):\n    if isinstance(type, Backend):\n        return type\n\n    if type is None and _has_ray():\n        type = RAY\n\n    return backend_registry[type](**kwargs)\n\n\n@DeveloperAPI\ndef initialize_backend(backend):\n    if isinstance(backend, dict):\n        backend = create_backend(**backend)\n    else:\n        backend = create_backend(backend)\n    backend.initialize()\n    return backend\n\n\n@contextlib.contextmanager\ndef provision_preprocessing_workers(backend):\n    if backend.BACKEND_TYPE == RAY:\n        with backend.provision_preprocessing_workers():\n            yield\n    else:\n        yield\n"
  },
  {
    "path": "ludwig/backend/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom __future__ import annotations\n\nimport time\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable, Generator\nfrom concurrent.futures import ThreadPoolExecutor\nfrom contextlib import contextmanager\nfrom typing import Any, TYPE_CHECKING\n\nimport numpy as np\nimport pandas as pd\nimport psutil\nimport torch\nfrom tqdm import tqdm\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.backend.utils.storage import StorageManager\nfrom ludwig.constants import MODEL_LLM\nfrom ludwig.data.cache.manager import CacheManager\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.data.dataframe.pandas import PANDAS\nfrom ludwig.data.dataset.base import DatasetManager\nfrom ludwig.data.dataset.pandas import PandasDatasetManager\nfrom ludwig.distributed import init_dist_strategy\nfrom ludwig.distributed.base import DistributedStrategy\nfrom ludwig.models.base import BaseModel\nfrom ludwig.schema.trainer import BaseTrainerConfig\nfrom ludwig.types import HyperoptConfigDict\nfrom ludwig.utils.audio_utils import read_audio_from_path\nfrom ludwig.utils.batch_size_tuner import BatchSizeEvaluator\nfrom ludwig.utils.dataframe_utils import from_batches, to_batches\nfrom ludwig.utils.fs_utils import get_bytes_obj_from_path\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.system_utils import Resources\nfrom ludwig.utils.torch_utils import initialize_pytorch\nfrom ludwig.utils.types import DataFrame, Series\n\nif TYPE_CHECKING:\n    from ludwig.trainers.base import BaseTrainer\n\n\n@DeveloperAPI\nclass Backend(ABC):\n    def __init__(\n        self,\n        dataset_manager: DatasetManager,\n        cache_dir: str | None = None,\n        credentials: dict[str, dict[str, Any]] | None = None,\n    ):\n        credentials = credentials or {}\n        self._dataset_manager = dataset_manager\n        self._storage_manager = StorageManager(**credentials)\n        self._cache_manager = CacheManager(self._dataset_manager, cache_dir)\n\n    @property\n    def storage(self) -> StorageManager:\n        return self._storage_manager\n\n    @property\n    def cache(self) -> CacheManager:\n        return self._cache_manager\n\n    @property\n    def dataset_manager(self) -> DatasetManager:\n        return self._dataset_manager\n\n    @abstractmethod\n    def initialize(self):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def initialize_pytorch(self, *args, **kwargs):\n        raise NotImplementedError()\n\n    @contextmanager\n    @abstractmethod\n    def create_trainer(self, config: BaseTrainerConfig, model: BaseModel, **kwargs) -> Generator:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def sync_model(self, model):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def broadcast_return(self, fn):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def is_coordinator(self):\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def df_engine(self) -> DataFrameEngine:\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def supports_multiprocessing(self):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def read_binary_files(self, column: Series, map_fn: Callable | None = None) -> Series:\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def num_nodes(self) -> int:\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def num_training_workers(self) -> int:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_available_resources(self) -> Resources:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def max_concurrent_trials(self, hyperopt_config: HyperoptConfigDict) -> int | None:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def tune_batch_size(self, evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int:\n        \"\"\"Returns best batch size (measured in samples / s) on the given evaluator.\n\n        The evaluator class will need to be instantiated on each worker in the backend cluster, then call\n        `evaluator.select_best_batch_size(dataset_len)`.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def batch_transform(\n        self, df: DataFrame, batch_size: int, transform_fn: Callable, name: str | None = None\n    ) -> DataFrame:\n        \"\"\"Applies `transform_fn` to every `batch_size` length batch of `df` and returns the result.\"\"\"\n        raise NotImplementedError()\n\n    def supports_batch_size_tuning(self) -> bool:\n        return True\n\n\nclass LocalPreprocessingMixin:\n    @property\n    def df_engine(self):\n        return PANDAS\n\n    @property\n    def supports_multiprocessing(self):\n        return True\n\n    @staticmethod\n    def read_binary_files(column: pd.Series, map_fn: Callable | None = None, file_size: int | None = None) -> pd.Series:\n        column = column.fillna(np.nan).replace([np.nan], [None])  # normalize NaNs to None\n\n        sample_fname = column.head(1).values[0]\n        with ThreadPoolExecutor() as executor:  # number of threads is inferred\n            if isinstance(sample_fname, str):\n                if map_fn is read_audio_from_path:  # bypass torchaudio issue that no longer takes in file-like objects\n                    result = executor.map(  # type: ignore[misc]\n                        lambda path: map_fn(path) if path is not None else path, column.values\n                    )\n                else:\n                    result = executor.map(\n                        lambda path: get_bytes_obj_from_path(path) if path is not None else path, column.values\n                    )\n            else:\n                # If the sample path is not a string, assume the paths has already been read in\n                result = column.values\n\n            if map_fn is not None and map_fn is not read_audio_from_path:\n                result = executor.map(map_fn, result)\n\n        return pd.Series(result, index=column.index, name=column.name)\n\n    @staticmethod\n    def batch_transform(df: DataFrame, batch_size: int, transform_fn: Callable, name: str | None = None) -> DataFrame:\n        name = name or \"Batch Transform\"\n        batches = to_batches(df, batch_size)\n        transform = transform_fn()\n        out_batches = [transform(batch.reset_index(drop=True)) for batch in tqdm(batches, desc=name)]\n        out_df = from_batches(out_batches).reset_index(drop=True)\n        return out_df\n\n\nclass LocalTrainingMixin:\n    @staticmethod\n    def initialize():\n        init_dist_strategy(\"local\")\n\n    @staticmethod\n    def initialize_pytorch(*args, **kwargs):\n        initialize_pytorch(*args, **kwargs)\n\n    @staticmethod\n    def create_predictor(model: BaseModel, **kwargs):\n        from ludwig.models.predictor import get_predictor_cls\n\n        return get_predictor_cls(model.type())(model, **kwargs)  # type: ignore[call-arg]\n\n    def sync_model(self, model):\n        pass\n\n    @staticmethod\n    def broadcast_return(fn):\n        return fn()\n\n    @staticmethod\n    def is_coordinator() -> bool:\n        return True\n\n    @staticmethod\n    def tune_batch_size(evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int:\n        evaluator = evaluator_cls()\n        return evaluator.select_best_batch_size(dataset_len)\n\n\nclass RemoteTrainingMixin:\n    def sync_model(self, model):\n        pass\n\n    @staticmethod\n    def broadcast_return(fn):\n        return fn()\n\n    @staticmethod\n    def is_coordinator() -> bool:\n        return True\n\n\n@DeveloperAPI\nclass LocalBackend(LocalPreprocessingMixin, LocalTrainingMixin, Backend):\n    BACKEND_TYPE = \"local\"\n\n    _shared_instance: LocalBackend\n\n    @classmethod\n    def shared_instance(cls) -> LocalBackend:\n        \"\"\"Returns a shared singleton LocalBackend instance.\"\"\"\n        if not hasattr(cls, \"_shared_instance\"):\n            cls._shared_instance = cls()\n        return cls._shared_instance\n\n    def __init__(self, **kwargs) -> None:\n        super().__init__(dataset_manager=PandasDatasetManager(self), **kwargs)\n\n    @property\n    def num_nodes(self) -> int:\n        return 1\n\n    @property\n    def num_training_workers(self) -> int:\n        return 1\n\n    def get_available_resources(self) -> Resources:\n        return Resources(cpus=psutil.cpu_count(), gpus=torch.cuda.device_count())\n\n    def max_concurrent_trials(self, hyperopt_config: HyperoptConfigDict) -> int | None:\n        # Every trial will be run with Pandas and NO Ray Datasets. Allow Ray Tune to use all the\n        # trial resources it wants, because there is no Ray Datasets process to compete with it for CPUs.\n        return None\n\n    def create_trainer(\n        self,\n        config: BaseTrainerConfig,\n        model: BaseModel,\n        **kwargs,\n    ) -> BaseTrainer:  # type: ignore[override]\n        from ludwig.trainers.registry import get_llm_trainers_registry, get_trainers_registry\n\n        trainer_cls: type\n        if model.type() == MODEL_LLM:\n            trainer_cls = get_from_registry(config.type, get_llm_trainers_registry())\n        else:\n            trainer_cls = get_from_registry(model.type(), get_trainers_registry())\n\n        return trainer_cls(config=config, model=model, **kwargs)\n\n\n@DeveloperAPI\nclass DataParallelBackend(LocalPreprocessingMixin, Backend, ABC):\n    BACKEND_TYPE = \"deepspeed\"\n\n    def __init__(self, **kwargs):\n        super().__init__(dataset_manager=PandasDatasetManager(self), **kwargs)\n        self._distributed: DistributedStrategy | None = None\n\n    @abstractmethod\n    def initialize(self):\n        pass\n\n    def initialize_pytorch(self, *args, **kwargs):\n        initialize_pytorch(\n            *args, local_rank=self._distributed.local_rank(), local_size=self._distributed.local_size(), **kwargs\n        )\n\n    def create_trainer(\n        self,\n        config: BaseTrainerConfig,\n        model: BaseModel,\n        **kwargs,\n    ) -> BaseTrainer:  # type: ignore[override]\n        from ludwig.trainers.trainer import Trainer\n\n        return Trainer(config, model, distributed=self._distributed, **kwargs)\n\n    def create_predictor(self, model: BaseModel, **kwargs):\n        from ludwig.models.predictor import get_predictor_cls\n\n        return get_predictor_cls(model.type())(model, distributed=self._distributed, **kwargs)  # type: ignore[call-arg]\n\n    def sync_model(self, model):\n        # Model weights are only saved on the coordinator, so broadcast\n        # to all other ranks\n        self._distributed.sync_model(model)\n\n    def broadcast_return(self, fn):\n        \"\"\"Returns the result of calling `fn` on coordinator, broadcast to all other ranks.\n\n        Specifically, `fn` is only executed on coordinator, but its result is returned by every rank by broadcasting the\n        return value from coordinator.\n        \"\"\"\n        result = fn() if self.is_coordinator() else None\n        if self._distributed:\n            name = f\"broadcast_return_{int(time.time())}\"\n            result = self._distributed.broadcast_object(result, name=name)\n        return result\n\n    def is_coordinator(self):\n        return self._distributed.rank() == 0\n\n    @property\n    def num_nodes(self) -> int:\n        return self._distributed.size() // self._distributed.local_size()\n\n    @property\n    def num_training_workers(self) -> int:\n        return self._distributed.size()\n\n    def get_available_resources(self) -> Resources:\n        # TODO(travis): this double-counts on the same device, it should use a cross-communicator instead\n        cpus = torch.as_tensor([psutil.cpu_count()], dtype=torch.int)\n        cpus = self._distributed.allreduce(cpus).item()\n\n        gpus = torch.as_tensor([torch.cuda.device_count()], dtype=torch.int)\n        gpus = self._distributed.allreduce(gpus).item()\n\n        return Resources(cpus=cpus, gpus=gpus)\n\n    def max_concurrent_trials(self, hyperopt_config: HyperoptConfigDict) -> int | None:\n        # Return None since there is no Ray component\n        return None\n\n    def tune_batch_size(self, evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int:\n        evaluator = evaluator_cls()\n        return evaluator.select_best_batch_size(dataset_len)\n"
  },
  {
    "path": "ludwig/backend/datasource.py",
    "content": "\"\"\"Custom Ray datasource utilities for reading binary files with None handling.\"\"\"\n\nimport logging\nfrom typing import Optional, TYPE_CHECKING\n\nimport pandas as pd\nimport ray\nimport urllib3\n\nfrom ludwig.utils.fs_utils import get_bytes_obj_from_http_path, is_http\n\nif TYPE_CHECKING:\n    import pyarrow\n\nlogger = logging.getLogger(__name__)\n\n\ndef read_binary_files_with_index(\n    paths_and_idxs: list[tuple[str | None, int]],\n    filesystem: Optional[\"pyarrow.fs.FileSystem\"] = None,\n) -> \"ray.data.Dataset\":\n    \"\"\"Read binary files into a Ray Dataset, handling None paths and HTTP URLs.\n\n    Each row in the resulting dataset has columns:\n    - \"data\": the raw bytes of the file (or None if path was None/failed)\n    - \"idx\": the original index for reordering\n\n    Args:\n        paths_and_idxs: List of (path, index) tuples. Path can be None.\n        filesystem: PyArrow filesystem for reading non-HTTP files.\n\n    Returns:\n        A ray.data.Dataset with \"data\" and \"idx\" columns.\n    \"\"\"\n\n    def _read_file(path: str | None, idx: int) -> dict:\n        if path is None:\n            return {\"data\": None, \"idx\": idx}\n        elif is_http(path):\n            try:\n                data = get_bytes_obj_from_http_path(path)\n            except urllib3.exceptions.HTTPError as e:\n                logger.warning(e)\n                data = None\n            return {\"data\": data, \"idx\": idx}\n        else:\n            try:\n                with filesystem.open_input_stream(path) as f:\n                    data = f.read()\n            except Exception as e:\n                logger.warning(f\"Failed to read file {path}: {e}\")\n                data = None\n            return {\"data\": data, \"idx\": idx}\n\n    # Create a dataset from the paths and indices, then map to read files\n    records = [{\"path\": p, \"idx\": i} for p, i in paths_and_idxs]\n    ds = ray.data.from_items(records)\n\n    def read_batch(batch: pd.DataFrame) -> pd.DataFrame:\n        results = []\n        for _, row in batch.iterrows():\n            result = _read_file(row[\"path\"], row[\"idx\"])\n            results.append(result)\n        return pd.DataFrame(results)\n\n    ds = ds.map_batches(read_batch, batch_format=\"pandas\")\n    return ds\n"
  },
  {
    "path": "ludwig/backend/deepspeed.py",
    "content": "from typing import Any\n\nimport deepspeed\n\nfrom ludwig.backend.base import DataParallelBackend\nfrom ludwig.constants import FALLBACK_BATCH_SIZE\nfrom ludwig.distributed import init_dist_strategy\nfrom ludwig.utils.batch_size_tuner import BatchSizeEvaluator\n\n\nclass DeepSpeedBackend(DataParallelBackend):\n    BACKEND_TYPE = \"deepspeed\"\n\n    def __init__(\n        self,\n        zero_optimization: dict[str, Any] | None = None,\n        fp16: dict[str, Any] | None = None,\n        bf16: dict[str, Any] | None = None,\n        compression_training: dict[str, Any] | None = None,\n        **kwargs\n    ):\n        super().__init__(**kwargs)\n        self.zero_optimization = zero_optimization\n        self.fp16 = fp16\n        self.bf16 = bf16\n        self.compression_training = compression_training\n\n    def initialize(self):\n        # Unlike when we use the Ray backend, we need to initialize the `torch.distributed` context so we can\n        # broadcast, allgather, etc. before preparing the model within the trainer.\n        deepspeed.init_distributed()\n        self._distributed = init_dist_strategy(\n            self.BACKEND_TYPE,\n            zero_optimization=self.zero_optimization,\n            fp16=self.fp16,\n            bf16=self.bf16,\n            compression_training=self.compression_training,\n        )\n\n    def supports_batch_size_tuning(self) -> bool:\n        # TODO(travis): need to fix checkpoint saving/loading for DeepSpeed to enable tuning\n        return False\n\n    def tune_batch_size(self, evaluator_cls: type[BatchSizeEvaluator], dataset_len: int) -> int:\n        return FALLBACK_BATCH_SIZE\n"
  },
  {
    "path": "ludwig/backend/ray.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport contextlib\nimport copy\nimport logging\nimport os\nimport tempfile\nfrom collections.abc import Callable\nfrom functools import partial\nfrom typing import Any, TYPE_CHECKING\n\nimport dask\nimport numpy as np\nimport pandas as pd\nimport ray\nimport ray.train as rt\nimport torch\nimport tqdm\nfrom fsspec.config import conf\nfrom pyarrow.fs import FSSpecHandler, PyFileSystem\nfrom ray import ObjectRef\nfrom ray.train import Checkpoint, RunConfig, ScalingConfig\nfrom ray.train.constants import TRAIN_ENABLE_WORKER_SPREAD_ENV\nfrom ray.train.torch import TorchConfig, TorchTrainer\nfrom ray.util.dask import ray_dask_get\nfrom ray.util.placement_group import placement_group, remove_placement_group\n\nif TYPE_CHECKING:\n    from ludwig.api import LudwigModel\n\nfrom ludwig.backend.base import Backend, RemoteTrainingMixin\nfrom ludwig.backend.datasource import read_binary_files_with_index\nfrom ludwig.constants import MODEL_ECD, MODEL_LLM, NAME, PREPROCESSING, PROC_COLUMN, TYPE\nfrom ludwig.data.dataframe.base import DataFrameEngine\n\ntry:\n    from ludwig.data.dataset.ray import (\n        _SCALAR_TYPES,\n        cast_as_tensor_dtype,\n        RayDataset,\n        RayDatasetManager,\n        RayDatasetShard,\n    )\nexcept (ImportError, AttributeError):\n    _SCALAR_TYPES = cast_as_tensor_dtype = RayDataset = RayDatasetManager = RayDatasetShard = None\nfrom ludwig.models.base import BaseModel\nfrom ludwig.models.ecd import ECD\nfrom ludwig.models.predictor import BasePredictor, get_output_columns, get_predictor_cls\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.trainers.registry import get_ray_trainers_registry, register_ray_trainer\nfrom ludwig.trainers.trainer import BaseTrainer, RemoteTrainer\nfrom ludwig.utils.data_utils import use_credentials\nfrom ludwig.utils.fs_utils import get_fs_and_path\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.system_utils import Resources\nfrom ludwig.utils.torch_utils import get_torch_device, initialize_pytorch\nfrom ludwig.utils.types import Series\n\nlogger = logging.getLogger(__name__)\n\nFIFTEEN_MINS_IN_S = 15 * 60\n\n\ndef _num_nodes() -> int:\n    node_resources = [node[\"Resources\"] for node in ray.nodes()]\n    return len(node_resources)\n\n\ndef get_trainer_kwargs(**kwargs) -> dict[str, Any]:\n    kwargs = copy.deepcopy(kwargs)\n\n    # Our goal is to have a worker per resource used for training.\n    # The priority is GPUs, but can fall back to CPUs if there are no\n    # GPUs available.\n    use_gpu = kwargs.get(\"use_gpu\", int(ray.cluster_resources().get(\"GPU\", 0)) > 0)\n    if use_gpu:\n        num_workers = int(ray.cluster_resources().get(\"GPU\", 0))\n    else:\n        num_workers = _num_nodes()\n\n    # Remove nics if present (legacy option)\n    kwargs.pop(\"nics\", None)\n\n    defaults = dict(\n        backend=TorchConfig(),\n        num_workers=num_workers,\n        use_gpu=use_gpu,\n        resources_per_worker={\n            \"CPU\": 0 if use_gpu else 1,\n            \"GPU\": 1 if use_gpu else 0,\n        },\n    )\n    return {**defaults, **kwargs}\n\n\ndef _create_dask_engine(**kwargs):\n    from ludwig.data.dataframe.dask import DaskEngine\n\n    return DaskEngine(**kwargs)\n\n\ndef _create_modin_engine(**kwargs):\n    from ludwig.data.dataframe.modin import ModinEngine\n\n    return ModinEngine(**kwargs)\n\n\ndef _create_pandas_engine(**kwargs):\n    from ludwig.data.dataframe.pandas import PandasEngine\n\n    return PandasEngine(**kwargs)\n\n\n_engine_registry = {\n    \"dask\": _create_dask_engine,\n    \"modin\": _create_modin_engine,\n    \"pandas\": _create_pandas_engine,\n}\n\n\ndef _get_df_engine(processor):\n    logger.info(f\"Ray processor params: {processor}\")\n    if processor is None:\n        # TODO ray: find an informed way to set the parallelism, in practice\n        #  it looks like Dask handles this well on its own most of the time\n        return _create_dask_engine()\n\n    processor_kwargs = processor.copy()\n\n    dtype = processor_kwargs.pop(\"type\", \"dask\")\n    engine_cls = _engine_registry.get(dtype)\n\n    return engine_cls(**processor_kwargs)\n\n\ndef _make_picklable(obj):\n    \"\"\"Recursively convert defaultdicts (which contain unpicklable lambdas) to regular dicts.\"\"\"\n    from collections import defaultdict\n\n    if isinstance(obj, defaultdict):\n        return {k: _make_picklable(v) for k, v in obj.items()}\n    elif isinstance(obj, dict):\n        return {k: _make_picklable(v) for k, v in obj.items()}\n    elif isinstance(obj, tuple) and hasattr(obj, \"_fields\"):\n        # NamedTuple: reconstruct with the same field names\n        return type(obj)(**{f: _make_picklable(getattr(obj, f)) for f in obj._fields})\n    elif isinstance(obj, list):\n        return [_make_picklable(item) for item in obj]\n    elif isinstance(obj, tuple):\n        return tuple(_make_picklable(item) for item in obj)\n    return obj\n\n\ndef train_fn(\n    executable_kwargs: dict[str, Any] = None,\n    model_ref: ObjectRef = None,  # noqa: F821\n    training_set_metadata: dict[str, Any] = None,\n    features: dict[str, dict] = None,\n    **kwargs,\n):\n    \"\"\"Ray Train worker function for distributed training.\n\n    Runs inside each Ray worker process. Loads the model from an object ref, wraps dataset shards, trains, and saves\n    results to a Ray checkpoint so the driver can retrieve them (Ray Train 2.x requires a checkpoint for metrics).\n    \"\"\"\n    # Pin GPU before loading the model to prevent memory leaking onto other devices\n    initialize_pytorch()\n\n    # Initialize a local distributed strategy so metric modules can sync.\n    from ludwig.distributed import init_dist_strategy\n\n    init_dist_strategy(\"local\")\n\n    train_shard = RayDatasetShard(\n        rt.get_dataset_shard(\"train\"),\n        features,\n        training_set_metadata,\n    )\n\n    try:\n        val_shard = rt.get_dataset_shard(\"val\")\n    except KeyError:\n        val_shard = None\n\n    if val_shard is not None:\n        val_shard = RayDatasetShard(\n            val_shard,\n            features,\n            training_set_metadata,\n        )\n\n    try:\n        test_shard = rt.get_dataset_shard(\"test\")\n    except KeyError:\n        test_shard = None\n\n    if test_shard is not None:\n        test_shard = RayDatasetShard(\n            test_shard,\n            features,\n            training_set_metadata,\n        )\n\n    model = ray.get(model_ref)\n    # Use Ray Train's device assignment which respects use_gpu setting,\n    # rather than get_torch_device() which always picks CUDA if available.\n    from ray.train.torch import get_device as ray_get_device\n\n    device = ray_get_device()\n    model = model.to(device)\n\n    trainer = RemoteTrainer(model=model, report_tqdm_to_ray=True, **executable_kwargs)\n    results = trainer.train(train_shard, val_shard, test_shard, **kwargs)\n\n    if results is not None:\n        # only return the model state dict back to the head node.\n        trained_model, *args = results\n        results = (trained_model.cpu().state_dict(), *args)\n\n    torch.cuda.empty_cache()\n\n    # Save results to a checkpoint so the driver can retrieve them.\n    # In Ray Train 2.x, result.metrics is only populated when a checkpoint is provided.\n    train_results = results, trainer.validation_field, trainer.validation_metric\n    # Convert defaultdicts to regular dicts so they can be pickled by torch.save.\n    train_results = _make_picklable(train_results)\n    with tempfile.TemporaryDirectory() as tmpdir:\n        torch.save(train_results, os.path.join(tmpdir, \"train_results.pt\"))\n        rt.report(metrics={}, checkpoint=Checkpoint.from_directory(tmpdir))\n\n\n@ray.remote\ndef tune_batch_size_fn(\n    dataset: RayDataset = None,\n    data_loader_kwargs: dict[str, Any] = None,\n    executable_kwargs: dict[str, Any] = None,\n    model: ECD = None,  # noqa: F821\n    ludwig_config: dict[str, Any] = None,\n    training_set_metadata: dict[str, Any] = None,\n    features: dict[str, dict] = None,\n    **kwargs,\n) -> int:\n    # Pin GPU before loading the model to prevent memory leaking onto other devices\n    initialize_pytorch()\n\n    try:\n        ds = dataset.to_ray_dataset(shuffle=False)\n        train_shard = RayDatasetShard(\n            ds,\n            features,\n            training_set_metadata,\n        )\n\n        device = get_torch_device()\n        model = model.to(device)\n\n        trainer = RemoteTrainer(model=model, **executable_kwargs)\n        return trainer.tune_batch_size(ludwig_config, train_shard, **kwargs)\n    finally:\n        torch.cuda.empty_cache()\n\n\n@ray.remote\ndef tune_learning_rate_fn(\n    dataset: RayDataset,\n    config: dict[str, Any],\n    data_loader_kwargs: dict[str, Any] = None,\n    executable_kwargs: dict[str, Any] = None,\n    model: ECD = None,  # noqa: F821\n    training_set_metadata: dict[str, Any] = None,\n    features: dict[str, dict] = None,\n    **kwargs,\n) -> float:\n    # Pin GPU before loading the model to prevent memory leaking onto other devices\n    initialize_pytorch()\n\n    try:\n        ds = dataset.to_ray_dataset(shuffle=False)\n        train_shard = RayDatasetShard(\n            ds,\n            features,\n            training_set_metadata,\n        )\n\n        device = get_torch_device()\n        model = model.to(device)\n\n        trainer = RemoteTrainer(model=model, **executable_kwargs)\n        return trainer.tune_learning_rate(config, train_shard, **kwargs)\n    finally:\n        torch.cuda.empty_cache()\n\n\nclass TqdmCallback(rt.UserCallback):\n    \"\"\"Class for a custom ray callback that updates tqdm progress bars in the driver process.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Constructor for TqdmCallback.\"\"\"\n        super().__init__()\n        self.progess_bars = {}\n\n    def after_report(self, run_context, metrics: list[dict], checkpoint=None) -> None:\n        \"\"\"Called every time ray.train.report is called from subprocesses.\n\n        In Ray 2.x, metrics is a list of metric dicts (one per worker). We look for progress_bar data from the\n        coordinator worker.\n        \"\"\"\n        for result in metrics:\n            progress_bar_opts = result.get(\"progress_bar\")\n            if not progress_bar_opts:\n                continue\n            # Skip commands received by non-coordinators\n            if not progress_bar_opts[\"is_coordinator\"]:\n                continue\n            _id = progress_bar_opts[\"id\"]\n            action = progress_bar_opts.get(\"action\")\n            if action == \"create\":\n                progress_bar_config = progress_bar_opts.get(\"config\")\n                self.progess_bars[_id] = tqdm.tqdm(**progress_bar_config)\n            elif action == \"close\":\n                if _id in self.progess_bars:\n                    self.progess_bars[_id].close()\n            elif action == \"update\":\n                update_by = progress_bar_opts.get(\"update_by\", 1)\n                if _id in self.progess_bars:\n                    self.progess_bars[_id].update(update_by)\n\n\n@contextlib.contextmanager\ndef spread_env(use_gpu: bool = False, num_workers: int = 1, **kwargs):\n    if TRAIN_ENABLE_WORKER_SPREAD_ENV in os.environ:\n        # User set this explicitly, so honor their selection\n        yield\n        return\n\n    try:\n        if not use_gpu and num_workers > 1:\n            # When doing CPU-only training, default to a SPREAD policy to avoid\n            # packing too many workers on a single machine\n            os.environ[TRAIN_ENABLE_WORKER_SPREAD_ENV] = \"1\"\n        yield\n    finally:\n        if TRAIN_ENABLE_WORKER_SPREAD_ENV in os.environ:\n            del os.environ[TRAIN_ENABLE_WORKER_SPREAD_ENV]\n\n\ndef _build_scaling_config(trainer_kwargs: dict[str, Any]) -> ScalingConfig:\n    \"\"\"Convert legacy trainer kwargs to a Ray ScalingConfig.\"\"\"\n    return ScalingConfig(\n        num_workers=trainer_kwargs.get(\"num_workers\", 1),\n        use_gpu=trainer_kwargs.get(\"use_gpu\", False),\n        resources_per_worker=trainer_kwargs.get(\"resources_per_worker\"),\n    )\n\n\ndef run_train_remote(train_loop, trainer_kwargs: dict[str, Any], callbacks=None, datasets=None, train_loop_config=None):\n    \"\"\"Run a distributed training function using Ray TorchTrainer.\"\"\"\n    resolved_kwargs = get_trainer_kwargs(**trainer_kwargs)\n\n    scaling_config = _build_scaling_config(resolved_kwargs)\n    torch_config = resolved_kwargs.get(\"backend\", TorchConfig())\n\n    run_config_kwargs = {}\n    if callbacks:\n        run_config_kwargs[\"callbacks\"] = callbacks\n\n    with spread_env(**resolved_kwargs):\n        torch_trainer = TorchTrainer(\n            train_loop_per_worker=train_loop,\n            train_loop_config=train_loop_config,\n            torch_config=torch_config,\n            scaling_config=scaling_config,\n            run_config=RunConfig(**run_config_kwargs),\n            datasets=datasets,\n        )\n        result = torch_trainer.fit()\n    return result\n\n\n@register_ray_trainer(MODEL_ECD, default=True)\nclass RayTrainerV2(BaseTrainer):\n    def __init__(\n        self,\n        model: BaseModel,\n        trainer_kwargs: dict[str, Any],\n        data_loader_kwargs: dict[str, Any],\n        executable_kwargs: dict[str, Any],\n        **kwargs,\n    ):\n        self.model = model.cpu()\n        self.data_loader_kwargs = data_loader_kwargs\n        self.executable_kwargs = executable_kwargs\n        self.trainer_kwargs = trainer_kwargs\n        self._validation_field = None\n        self._validation_metric = None\n\n    @staticmethod\n    def get_schema_cls():\n        return ECDTrainerConfig\n\n    def train(\n        self,\n        training_set: RayDataset,\n        validation_set: RayDataset | None = None,\n        test_set: RayDataset | None = None,\n        **kwargs,\n    ):\n        executable_kwargs = self.executable_kwargs\n\n        kwargs = {\n            \"training_set_metadata\": training_set.training_set_metadata,\n            \"features\": training_set.features,\n            **kwargs,\n        }\n\n        dataset = {\"train\": training_set.to_ray_dataset(shuffle=True)}\n        if validation_set is not None:\n            dataset[\"val\"] = validation_set.to_ray_dataset(shuffle=False)\n        if test_set is not None:\n            dataset[\"test\"] = test_set.to_ray_dataset(shuffle=False)\n\n        train_loop_config = {\"executable_kwargs\": executable_kwargs, \"model_ref\": ray.put(self.model), **kwargs}\n\n        def _train_loop(config):\n            train_fn(**config)\n\n        result = run_train_remote(\n            _train_loop,\n            trainer_kwargs=self.trainer_kwargs,\n            callbacks=[TqdmCallback()],\n            datasets=dataset,\n            train_loop_config=train_loop_config,\n        )\n\n        # Load training results from the checkpoint saved by train_fn\n        with result.checkpoint.as_directory() as tmpdir:\n            train_results = torch.load(os.path.join(tmpdir, \"train_results.pt\"), weights_only=False)\n        results, self._validation_field, self._validation_metric = train_results\n\n        # load state dict back into the model\n        state_dict, *args = results\n        self.model.load_state_dict(state_dict)\n        results = (self.model, *args)\n\n        return results\n\n    def train_online(self, *args, **kwargs):\n        # TODO: When this is implemented we also need to update the\n        # Tqdm flow to report back the callback\n        raise NotImplementedError()\n\n    def tune_batch_size(\n        self,\n        config: dict[str, Any],\n        training_set: RayDataset,\n        **kwargs,\n    ) -> int:\n        return ray.get(\n            tune_batch_size_fn.options(num_cpus=self.num_cpus, num_gpus=self.num_gpus).remote(\n                dataset=training_set,\n                data_loader_kwargs=self.data_loader_kwargs,\n                executable_kwargs=self.executable_kwargs,\n                model=ray.put(self.model),\n                ludwig_config=config,\n                training_set_metadata=training_set.training_set_metadata,\n                features=training_set.features,\n                **kwargs,\n            )\n        )\n\n    def tune_learning_rate(self, config, training_set: RayDataset, **kwargs) -> float:\n        return ray.get(\n            tune_learning_rate_fn.options(num_cpus=self.num_cpus, num_gpus=self.num_gpus).remote(\n                dataset=training_set,\n                config=config,\n                data_loader_kwargs=self.data_loader_kwargs,\n                executable_kwargs=self.executable_kwargs,\n                model=ray.put(self.model),\n                training_set_metadata=training_set.training_set_metadata,\n                features=training_set.features,\n                **kwargs,\n            )\n        )\n\n    @property\n    def validation_field(self):\n        return self._validation_field\n\n    @property\n    def validation_metric(self):\n        return self._validation_metric\n\n    @property\n    def config(self) -> ECDTrainerConfig:\n        return self.executable_kwargs[\"config\"]\n\n    @property\n    def batch_size(self) -> int:\n        return self.config.batch_size\n\n    @batch_size.setter\n    def batch_size(self, value: int):\n        self.config.batch_size = value\n\n    @property\n    def eval_batch_size(self) -> int:\n        return self.config.eval_batch_size if self.config.eval_batch_size is not None else self.config.batch_size\n\n    @eval_batch_size.setter\n    def eval_batch_size(self, value: int):\n        self.config.eval_batch_size = value\n\n    @property\n    def resources_per_worker(self) -> dict[str, Any]:\n        trainer_kwargs = get_trainer_kwargs(**self.trainer_kwargs)\n        return trainer_kwargs.get(\"resources_per_worker\", {})\n\n    @property\n    def num_cpus(self) -> int:\n        return self.resources_per_worker.get(\"CPU\", 1)\n\n    @property\n    def num_gpus(self) -> int:\n        return self.resources_per_worker.get(\"GPU\", 0)\n\n    def set_base_learning_rate(self, learning_rate: float):\n        self.config.learning_rate = learning_rate\n\n    def shutdown(self):\n        pass\n\n\ndef eval_fn(\n    predictor_kwargs: dict[str, Any] = None,\n    model_ref: ObjectRef = None,  # noqa: F821\n    training_set_metadata: dict[str, Any] = None,\n    features: dict[str, dict] = None,\n    **kwargs,\n):\n    \"\"\"Ray Train worker function for distributed evaluation.\n\n    Runs inside each Ray worker process. Loads the model from an object ref, wraps the eval dataset shard, runs\n    prediction and evaluation, and saves results to a Ray checkpoint for driver retrieval.\n    \"\"\"\n    # Pin GPU before loading the model to prevent memory leaking onto other devices\n    initialize_pytorch()\n\n    # Initialize a local distributed strategy so metric modules can sync.\n    from ludwig.distributed import init_dist_strategy\n\n    init_dist_strategy(\"local\")\n\n    try:\n        eval_shard = RayDatasetShard(\n            rt.get_dataset_shard(\"eval\"),\n            features,\n            training_set_metadata,\n        )\n\n        model = ray.get(model_ref)\n        # Use Ray Train's device assignment which respects use_gpu setting\n        from ray.train.torch import get_device as ray_get_device\n\n        device = ray_get_device()\n        model = model.to(device)\n\n        predictor_cls = get_predictor_cls(model.type())\n        predictor = predictor_cls(dist_model=model, model=model, report_tqdm_to_ray=True, **predictor_kwargs)\n        eval_results = predictor.batch_evaluation(eval_shard, **kwargs)\n\n        # Save results to a checkpoint so the driver can retrieve them.\n        # In Ray Train 2.x, result.metrics is only populated when a checkpoint is provided.\n        eval_results = _make_picklable(eval_results)\n        with tempfile.TemporaryDirectory() as tmpdir:\n            torch.save(eval_results, os.path.join(tmpdir, \"eval_results.pt\"))\n            rt.report(metrics={}, checkpoint=Checkpoint.from_directory(tmpdir))\n    finally:\n        torch.cuda.empty_cache()\n\n\nclass RayPredictor(BasePredictor):\n    def __init__(\n        self, model: BaseModel, df_engine: DataFrameEngine, trainer_kwargs, data_loader_kwargs, **predictor_kwargs\n    ):\n        self.batch_size = predictor_kwargs[\"batch_size\"]\n        self.trainer_kwargs = trainer_kwargs\n        self.data_loader_kwargs = data_loader_kwargs\n        self.predictor_kwargs = predictor_kwargs\n        self.actor_handles = []\n        self.model = model.cpu()\n        self.df_engine = df_engine\n\n    def get_trainer_kwargs(self) -> dict[str, Any]:\n        return get_trainer_kwargs(**self.trainer_kwargs)\n\n    def get_resources_per_worker(self) -> tuple[int, int]:\n        trainer_kwargs = self.get_trainer_kwargs()\n        resources_per_worker = trainer_kwargs.get(\"resources_per_worker\", {})\n        num_gpus = resources_per_worker.get(\"GPU\", 0)\n        num_cpus = resources_per_worker.get(\"CPU\", (1 if num_gpus == 0 else 0))\n        return num_cpus, num_gpus\n\n    def batch_predict(self, dataset: RayDataset, *args, collect_logits: bool = False, **kwargs):\n        self._check_dataset(dataset)\n\n        predictor_kwargs = self.predictor_kwargs\n        output_columns = get_output_columns(self.model.output_features, include_logits=collect_logits)\n        batch_predictor = self.get_batch_infer_model(\n            self.model,\n            predictor_kwargs,\n            output_columns,\n            dataset.features,\n            dataset.training_set_metadata,\n            *args,\n            collect_logits=collect_logits,\n            **kwargs,\n        )\n\n        columns = [f.proc_column for f in self.model.input_features.values()]\n\n        def to_tensors(df: pd.DataFrame) -> pd.DataFrame:\n            for c in columns:\n                df[c] = cast_as_tensor_dtype(df[c])\n            return df\n\n        num_cpus, num_gpus = self.get_resources_per_worker()\n\n        predictions = dataset.ds.map_batches(to_tensors, batch_format=\"pandas\").map_batches(\n            batch_predictor,\n            batch_size=self.batch_size,\n            compute=ray.data.ActorPoolStrategy(),\n            batch_format=\"pandas\",\n            num_cpus=num_cpus,\n            num_gpus=num_gpus,\n        )\n\n        predictions = self.df_engine.from_ray_dataset(predictions)\n\n        return predictions\n\n    def predict_single(self, batch):\n        raise NotImplementedError(\"predict_single can only be called on a local predictor\")\n\n    def batch_evaluation(\n        self,\n        dataset: RayDataset,\n        collect_predictions: bool = False,\n        collect_logits=False,\n        **kwargs,\n    ):\n        # We need to be in a distributed context to collect the aggregated metrics, since it relies on collective\n        # communication ops. However, distributed training is not suitable for transforming one big dataset to another.\n        # For that we will use Ray Datasets. Therefore, we break this up into two separate steps, and two passes over\n        # the dataset. In the future, we can explore ways to combine these into a single step to reduce IO.\n        # Collect eval metrics by distributing work across nodes / gpus\n        datasets = {\"eval\": dataset.to_ray_dataset(shuffle=False)}\n        predictor_kwargs = {\n            **self.predictor_kwargs,\n            \"collect_predictions\": False,\n        }\n        eval_loop_config = {\n            \"predictor_kwargs\": predictor_kwargs,\n            \"model_ref\": ray.put(self.model),\n            \"training_set_metadata\": dataset.training_set_metadata,\n            \"features\": dataset.features,\n            **kwargs,\n        }\n\n        def _eval_loop(config):\n            eval_fn(**config)\n\n        result = run_train_remote(\n            _eval_loop,\n            trainer_kwargs=self.trainer_kwargs,\n            datasets=datasets,\n            train_loop_config=eval_loop_config,\n        )\n\n        # Load eval results from the checkpoint saved by eval_fn\n        with result.checkpoint.as_directory() as tmpdir:\n            eval_stats, _ = torch.load(os.path.join(tmpdir, \"eval_results.pt\"), weights_only=False)\n\n        predictions = None\n        if collect_predictions:\n            # Collect eval predictions by using Ray Datasets to transform partitions of the data in parallel\n            predictions = self.batch_predict(dataset, collect_logits=collect_logits)\n\n        return eval_stats, predictions\n\n    def batch_collect_activations(self, model, *args, **kwargs):\n        raise NotImplementedError(\"Ray backend does not support collecting activations at this time.\")\n\n    def _check_dataset(self, dataset):\n        if not isinstance(dataset, RayDataset):\n            raise RuntimeError(f\"Ray backend requires RayDataset for inference, \" f\"found: {type(dataset)}\")\n\n    def shutdown(self):\n        for handle in self.actor_handles:\n            ray.kill(handle)\n        self.actor_handles.clear()\n\n    def get_batch_infer_model(\n        self,\n        model: \"LudwigModel\",  # noqa: F821\n        predictor_kwargs: dict[str, Any],\n        output_columns: list[str],\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n        *args,\n        **kwargs,\n    ):\n        model_ref = ray.put(model)\n        _, num_gpus = self.get_resources_per_worker()\n\n        class BatchInferModel:\n            def __init__(self):\n                model = ray.get(model_ref)\n                # Respect the GPU setting from resources_per_worker.\n                # When num_gpus=0, force CPU even if CUDA is available on the machine,\n                # to avoid device mismatches between model outputs and targets.\n                if num_gpus > 0:\n                    device = get_torch_device()\n                else:\n                    device = \"cpu\"\n                self.model = model.to(device)\n\n                self.output_columns = output_columns\n                self.features = features\n                self.training_set_metadata = training_set_metadata\n                self.reshape_map = {\n                    f[PROC_COLUMN]: training_set_metadata[f[NAME]].get(\"reshape\") for f in features.values()\n                }\n                predictor_cls = get_predictor_cls(self.model.type())\n                predictor = predictor_cls(dist_model=self.model, model=self.model, **predictor_kwargs)\n                self.predict = partial(predictor.predict_single, *args, **kwargs)\n\n            def __call__(self, df: pd.DataFrame) -> pd.DataFrame:\n                dataset = self._prepare_batch(df)\n                predictions = self.predict(batch=dataset).set_index(df.index)\n                ordered_predictions = predictions[self.output_columns]\n                return ordered_predictions\n\n            def _prepare_batch(self, batch: pd.DataFrame) -> dict[str, np.ndarray]:\n                res = {}\n                for c in self.features.keys():\n                    if self.features[c][TYPE] not in _SCALAR_TYPES:\n                        # Ensure columns stacked instead of turned into np.array([np.array, ...], dtype=object) objects\n                        res[c] = np.stack(batch[c].values)\n                    else:\n                        res[c] = batch[c].to_numpy()\n\n                for c in self.features.keys():\n                    reshape = self.reshape_map.get(c)\n                    if reshape is not None:\n                        res[c] = res[c].reshape((-1, *reshape))\n\n                return res\n\n        return BatchInferModel\n\n\nclass RayBackend(RemoteTrainingMixin, Backend):\n    BACKEND_TYPE = \"ray\"\n\n    def __init__(self, processor=None, trainer=None, loader=None, preprocessor_kwargs=None, **kwargs):\n        super().__init__(dataset_manager=RayDatasetManager(self), **kwargs)\n        self._preprocessor_kwargs = preprocessor_kwargs or {}\n        self._df_engine = _get_df_engine(processor)\n        self._distributed_kwargs = trainer or {}\n        self._pytorch_kwargs = {}\n        self._data_loader_kwargs = loader or {}\n        self._preprocessor_pg = None\n\n    def initialize(self):\n        initialize_ray()\n\n        dask.config.set(scheduler=ray_dask_get)\n        # Disable placement groups on dask\n        dask.config.set(annotations={\"ray_remote_args\": {\"placement_group\": None}})\n        # Prevent Dask from converting object-dtype columns to PyArrow strings,\n        # which corrupts binary data, numpy arrays, and complex Python objects.\n        dask.config.set({\"dataframe.convert-string\": False})\n\n    def generate_bundles(self, num_cpu):\n        # Ray requires that each bundle be scheduleable on a single node.\n        # So a bundle of 320 cpus would never get scheduled. For now a simple heuristic\n        # to be used is to just request 1 cpu at a time.\n        return [{\"CPU\": 1} for _ in range(int(num_cpu))]\n\n    @contextlib.contextmanager\n    def provision_preprocessing_workers(self):\n        num_cpu = self._preprocessor_kwargs.get(\"num_cpu\")\n        if not num_cpu:\n            logger.info(\n                \"Backend config has num_cpu not set.\" \" provision_preprocessing_workers() is a no-op in this case.\"\n            )\n            yield\n        else:\n            bundles = self.generate_bundles(num_cpu)\n            logger.info(\"Requesting bundles of %s for preprocessing\", bundles)\n            self._preprocessor_pg = placement_group(bundles)\n            ready = self._preprocessor_pg.wait(FIFTEEN_MINS_IN_S)\n\n            if not ready:\n                remove_placement_group(self._preprocessor_pg)\n                raise TimeoutError(\n                    \"Ray timed out in provisioning the placement group for preprocessing.\"\n                    f\" {num_cpu} CPUs were requested but were unable to be provisioned.\"\n                )\n\n            logger.info(\"%s CPUs were requested and successfully provisioned\", num_cpu)\n            try:\n                with dask.config.set(annotations={\"ray_remote_args\": {\"placement_group\": self._preprocessor_pg}}):\n                    yield\n            finally:\n                self._release_preprocessing_workers()\n\n    def _release_preprocessing_workers(self):\n        if self._preprocessor_pg is not None:\n            remove_placement_group(self._preprocessor_pg)\n        self._preprocessor_pg = None\n\n    def initialize_pytorch(self, **kwargs):\n        # Make sure we don't claim any GPU resources on the head node\n        initialize_pytorch(gpus=-1)\n        self._pytorch_kwargs = kwargs\n\n    def create_trainer(self, model: BaseModel, **kwargs) -> \"BaseTrainer\":  # noqa: F821\n        executable_kwargs = {**kwargs, **self._pytorch_kwargs}\n        if model.type() == MODEL_LLM:\n            from ludwig.trainers.registry import get_llm_ray_trainers_registry\n\n            trainer_config = kwargs.get(\"config\")\n            trainer_type = trainer_config.type if trainer_config else None\n            trainer_cls = get_from_registry(trainer_type, get_llm_ray_trainers_registry())\n        else:\n            trainer_cls = get_from_registry(model.type(), get_ray_trainers_registry())\n\n        # Deep copy to workaround https://github.com/ray-project/ray/issues/24139\n        all_kwargs = {\n            \"model\": model,\n            \"trainer_kwargs\": copy.deepcopy(self._distributed_kwargs),\n            \"data_loader_kwargs\": self._data_loader_kwargs,\n            \"executable_kwargs\": executable_kwargs,\n        }\n        all_kwargs.update(kwargs)\n        return trainer_cls(**all_kwargs)\n\n    def create_predictor(self, model: BaseModel, **kwargs):\n        executable_kwargs = {**kwargs, **self._pytorch_kwargs}\n        return RayPredictor(\n            model,\n            self.df_engine,\n            copy.deepcopy(self._distributed_kwargs),\n            self._data_loader_kwargs,\n            **executable_kwargs,\n        )\n\n    def set_distributed_kwargs(self, **kwargs):\n        self._distributed_kwargs = kwargs\n\n    @property\n    def df_engine(self):\n        return self._df_engine\n\n    @property\n    def supports_multiprocessing(self):\n        return False\n\n    def check_lazy_load_supported(self, feature):\n        if not feature[PREPROCESSING][\"in_memory\"]:\n            raise ValueError(\n                f\"RayBackend does not support lazy loading of data files at train time. \"\n                f\"Set preprocessing config `in_memory: True` for feature {feature[NAME]}\"\n            )\n\n    def read_binary_files(self, column: Series, map_fn: Callable | None = None, file_size: int | None = None) -> Series:\n        column = column.fillna(np.nan).replace([np.nan], [None])  # normalize NaNs to None\n\n        # Assume that the list of filenames is small enough to fit in memory. Should be true unless there\n        # are literally billions of filenames.\n        # TODO(travis): determine if there is a performance penalty to passing in individual files instead of\n        #  a directory. If so, we can do some preprocessing to determine if it makes sense to read the full directory\n        #  then filter out files as a postprocessing step (depending on the ratio of included to excluded files in\n        #  the directory). Based on a preliminary look at how Ray handles directory expansion to files, it looks like\n        #  there should not be any difference between providing a directory versus a list of files.\n        pd_column = self.df_engine.compute(column)\n        fnames = pd_column.values.tolist()\n        idxs = pd_column.index.tolist()\n\n        # Sample a filename to extract the filesystem info\n        sample_fname = fnames[0]\n        if isinstance(sample_fname, str):\n            fs, _ = get_fs_and_path(sample_fname)\n            filesystem = PyFileSystem(FSSpecHandler(fs))\n\n            paths_and_idxs = list(zip(fnames, idxs))\n            ds = read_binary_files_with_index(paths_and_idxs, filesystem=filesystem)\n            # Rename \"data\" column to \"value\" for downstream compatibility\n            ds = ds.rename_columns({\"data\": \"value\"})\n        else:\n            # Assume the path has already been read in, so just convert directly to a dataset\n            # Name the column \"value\" to match the behavior of the above\n            column_df = column.to_frame(name=\"value\")\n            column_df[\"idx\"] = column_df.index\n            ds = self.df_engine.to_ray_dataset(column_df)\n\n        # Collect the Ray Dataset to pandas to avoid Arrow's string coercion\n        # for binary/object columns (to_dask() converts bytes to string[pyarrow],\n        # corrupting binary data and complex Python objects).\n        pdf = ds.to_pandas()\n\n        if map_fn is not None:\n            with use_credentials(conf):\n                pdf[\"value\"] = pdf[\"value\"].map(map_fn)\n\n        pdf = pdf.rename(columns={\"value\": column.name})\n        if \"idx\" in pdf.columns:\n            pdf = pdf.set_index(\"idx\", drop=True)\n            pdf.index.name = column.index.name\n\n        # Convert to Dask for downstream compatibility.\n        # Note: dataframe.convert-string is disabled globally in RayBackend.initialize()\n        # to prevent object-dtype columns from being coerced to PyArrow strings.\n        df = self.df_engine.from_pandas(pdf)\n        return df[column.name]\n\n    @property\n    def num_nodes(self) -> int:\n        if not ray.is_initialized():\n            return 1\n        return len(ray.nodes())\n\n    @property\n    def num_training_workers(self) -> int:\n        return self._distributed_kwargs.get(\"num_workers\", 1)\n\n    def max_concurrent_trials(self, hyperopt_config) -> int | None:\n        # Limit concurrency based on available resources to avoid deadlocks between\n        # Ray Tune trials and the Ray Datasets used internally for distributed training.\n        resources = self.get_available_resources()\n        num_cpus_per_trial = self._distributed_kwargs.get(\"resources_per_worker\", {}).get(\"CPU\", 1)\n        num_workers = self._distributed_kwargs.get(\"num_workers\", 1)\n        cpus_per_trial = num_cpus_per_trial * num_workers\n        if cpus_per_trial > 0 and resources.cpus > 0:\n            return max(1, int(resources.cpus // cpus_per_trial))\n        return None\n\n    def tune_batch_size(self, evaluator_cls, dataset_len: int) -> int:\n        evaluator = evaluator_cls()\n        return evaluator.select_best_batch_size(dataset_len)\n\n    def batch_transform(self, df, batch_size: int, transform_fn, name: str | None = None):\n        name = name or \"Batch Transform\"\n        import dask.dataframe as dd\n\n        from ludwig.utils.dataframe_utils import from_batches, to_batches\n\n        # Compute Dask DataFrame to pandas before batching, as Dask-expr\n        # doesn't support row slicing via integer indexing (df[i:j]).\n        npartitions = df.npartitions if hasattr(df, \"npartitions\") else 1\n        df = self.df_engine.compute(df)\n        batches = to_batches(df, batch_size)\n        transform = transform_fn()\n        out_batches = [transform(batch.reset_index(drop=True)) for batch in batches]\n        out_df = from_batches(out_batches).reset_index(drop=True)\n        # Convert back to Dask so downstream code (split, etc.) still works\n        return dd.from_pandas(out_df, npartitions=max(1, npartitions))\n\n    def get_available_resources(self) -> Resources:\n        resources = ray.cluster_resources()\n        return Resources(cpus=resources.get(\"CPU\", 0), gpus=resources.get(\"GPU\", 0))\n\n\ndef initialize_ray():\n    if not ray.is_initialized():\n        try:\n            ray.init(\"auto\", ignore_reinit_error=True)\n        except ConnectionError:\n            init_ray_local()\n\n\ndef init_ray_local():\n    logger.info(\"Initializing new Ray cluster...\")\n    ray.init(ignore_reinit_error=True)\n"
  },
  {
    "path": "ludwig/backend/utils/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/backend/utils/storage.py",
    "content": "import contextlib\nfrom typing import Any, Optional, Union\n\nfrom ludwig.utils import data_utils\n\nCredInputs = Optional[Union[str, dict[str, Any]]]\n\n\nDEFAULTS = \"defaults\"\nARTIFACTS = \"artifacts\"\nDATASETS = \"datasets\"\nCACHE = \"cache\"\n\n\nclass Storage:\n    def __init__(self, creds: dict[str, Any] | None):\n        self._creds = creds\n\n    @contextlib.contextmanager\n    def use_credentials(self):\n        with data_utils.use_credentials(self._creds):\n            yield\n\n    @property\n    def credentials(self) -> dict[str, Any] | None:\n        return self._creds\n\n\nclass StorageManager:\n    def __init__(\n        self,\n        defaults: CredInputs = None,\n        artifacts: CredInputs = None,\n        datasets: CredInputs = None,\n        cache: CredInputs = None,\n    ):\n        defaults = load_creds(defaults)\n        cred_inputs = {\n            DEFAULTS: defaults,\n            ARTIFACTS: load_creds(artifacts),\n            DATASETS: load_creds(datasets),\n            CACHE: load_creds(cache),\n        }\n\n        self.storages = {k: Storage(v if v is not None else defaults) for k, v in cred_inputs.items()}\n\n    @property\n    def defaults(self) -> Storage:\n        return self.storages[DEFAULTS]\n\n    @property\n    def artifacts(self) -> Storage:\n        \"\"\"TODO(travis): Currently used for hyperopt, but should be used for all outputs.\"\"\"\n        return self.storages[ARTIFACTS]\n\n    @property\n    def datasets(self) -> Storage:\n        \"\"\"TODO(travis): Should be used to read in datasets.\"\"\"\n        return self.storages[DATASETS]\n\n    @property\n    def cache(self) -> Storage:\n        return self.storages[CACHE]\n\n\ndef load_creds(cred: CredInputs) -> dict[str, Any]:\n    if isinstance(cred, str):\n        cred = data_utils.load_json(cred)\n    return cred\n"
  },
  {
    "path": "ludwig/benchmarking/README.md",
    "content": "# Ludwig Benchmarking\n\n### Some use cases\n\n- Regression testing for ML experiments across releases and PRs.\n- Model performance testing for experimenting with new features and hyperparameters.\n- Resource usage tracking for the full ML pipeline.\n\n## Ludwig benchmarking CLI and API\n\nTo run benchmarks, run the following command from the command line\n\n```\nludwig benchmark --benchmarking_config path/to/benchmarking/config.yaml\n```\n\nTo use the API\n\n```\nfrom ludwig.benchmarking.benchmark import benchmark\n\nbenchmarking_config_path = \"path/to/benchmarking/config.yaml\"\nbenchmark(benchmarking_config_path)\n```\n\nIn what follows, we describe what the benchmarking config looks for\nmultiple use cases.\n\n## The benchmarking config\n\nThe benchmarking config is where you can specify\n\n1. The datasets you want to run the benchmarks on and their configs.\n1. Whether these experiments are hyperopt or regular train and eval experiments.\n1. The name of the experiment.\n1. A python script to edit the specified Ludwig configs programmatically/on the fly.\n1. The export path of these experiment's artifacts. (remotely or locally)\n1. Whether to use `LudwigProfiler` to track resource\n   usage for preprocessing, training, and evaluation of the experiment.\n\nYou can find an example of a benchmarking config in the `examples/` directory.\n\n## Basic Usage\n\nSay you implemented a new feature and would like to test it on several datasets.\nIn this case, this is what the benchmarking config could look like\n\n```\nexperiment_name: SMOTE_test\nhyperopt: false\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nexperiments:\n  - dataset_name: ames_housing\n    config_path: /home/ray/configs/ames_housing_SMOTE.yaml\n    experiment_name: SMOTE_test_with_hyperopt\n    hyperopt: true\n  - dataset_name: protein\n  - ...\n    ...\n  - dataset_name: mercedes_benz_greener\n    config_path: /home/ray/configs/mercedes_benz_greener_SMOTE.yaml\n```\n\nFor each experiment:\n\n- `dataset_name`: name of the dataset in `ludwig.datasets` to run the benchmark on.\n- `config_path` (optional): path to Ludwig config. If not specified, this will load\n  the config corresponding to the dataset only containing `input_features` and\n  `output_features`.\n\nThis will run `LudwigModel.experiment` on the datasets with their specified configs.\nIf these configs contain a hyperopt section and you'd like to run hyperopt, change\nto `hyperopt: true`.\nYou can specify the same dataset multiple times with different configs.\n\n**Exporting artifacts**\nBy specifying `export_artifacts: true`, this will export the experiment artifacts\nto the `export_base_path`. Once the model is trained and the artifacts are pushed\nto the specified path, you will get a similar message to the following:\n\n```\nUploaded metrics report and experiment config to\n\t s3://benchmarking.us-west-2.ludwig.com/bench/ames_housing/SMOTE_test\n```\n\nThis is the directory structure of the exported artifacts for one of the experiments.\n\n```\ns3://benchmarking.us-west-2.ludwig.com/bench/\n└── ames_housing\n    └── SMOTE_test\n        ├── config.yaml\n        └── experiment_run\n            ├── description.json\n            ├── model\n            │   ├── logs\n            │   │   ├── test\n            │   │   │   └── events.out.tfevents.1663320893.macbook-pro.lan.8043.2\n            │   │   ├── training\n            │   │   │   └── events.out.tfevents.1663320893.macbook-pro.lan.8043.0\n            │   │   └── validation\n            │   │       └── events.out.tfevents.1663320893.macbook-pro.lan.8043.1\n            │   ├── model_hyperparameters.json\n            │   ├── training_progress.json\n            │   └── training_set_metadata.json\n            ├── test_statistics.json\n            └── training_statistics.json\n```\n\nNote that model checkpoints are not exported. Any other experiments on\nthe `ames_housing` dataset will also live under\n`s3://benchmarking.us-west-2.ludwig.com/bench/ames_housing/`\n\n**Overriding parameters**\nThe benchmarking config's global parameters `experiment_name` and `hyperopt` can be overridden\nif specified within an experiment.\n\n## Programmatically editing Ludwig configs\n\nTo apply some changes to multiple Ludwig configs, you can specify a path to a python script\nthat does this without the need to do manual modifications across many configs. Example:\n\n```\nexperiment_name: logistic_regression_hyperopt\nhyperopt: true\nprocess_config_file_path: /home/ray/process_config.py\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nexperiments:\n  - dataset_name: ames_housing\n    config_path: /home/ray/configs/ames_housing_SMOTE.yaml\n  ...\n```\n\nIn `/home/ray/process_config.py`, define the following function and add custom code to modify\nludwig configs\n\n```\ndef process_config(ludwig_config: dict, experiment_dict: dict) -> dict:\n    \"\"\"Modify a Ludwig config.\n\n    :param ludwig_config: a Ludwig config.\n    :param experiment_dict: a benchmarking config experiment dictionary.\n\n    returns: a modified Ludwig config.\n    \"\"\"\n\n    # code to modify the Ludwig config.\n\n    return ludwig_config\n```\n\nView the `examples/` folder for an example `process_config.py`.\n\n## Benchmarking the resource usage with `LudwigProfiler`\n\nTo benchmark the resource usage of the preprocessing, training, and evaluation\nsteps of `LudwigModel.experiment`, you can specify in the benchmarking config\nglobal parameters\n\n```\nprofiler:\n  enable: true\n  use_torch_profiler: false\n  logging_interval: 0.1\n```\n\n- `enable: true` will run benchmarking with `LudwigProfiler`.\n- `use_torch_profiler: false` will skip using the torch profiler.\n- `logging_interval: 0.1` will instruct `LudwigProfiler` to collect\n  resource usage information every 0.1 seconds.\n\nNote that profiling is only enabled in the case where `hyperopt: false`.\n`LudwigProfiler` is passed in to `LudwigModel` callbacks. The specific\ncallbacks that will be called are:\n\n- `on_preprocess_(start/end)`\n- `on_train_(start/end)`\n- `on_evaluation_(start/end)`\n\nThis is an example directory output when using the profiler:\n\n```\nfull_bench_with_profiler_with_torch\n├── config.yaml\n├── experiment_run\n├── system_resource_usage\n│   ├── evaluation\n│   │   └── run_0.json\n│   ├── preprocessing\n│   │   └── run_0.json\n│   └── training\n│       └── run_0.json\n└── torch_ops_resource_usage\n    ├── evaluation\n    │   └── run_0.json\n    ├── preprocessing\n    │   └── run_0.json\n    └── training\n        └── run_0.json\n```\n\nThe only difference is the `system_resource_usage` and `torch_ops_resource_usage`.\nThe difference between these two outputs can be found in the `LudwigProfiler` README.\n\n## Parameters and defaults\n\nEach of these parameters can also be specified in the experiments section to override the global value.\nIf not specified, the value of the global parameter will be propagated to the experiments.\n\n- `experiment_name` (required): name of the benchmarking run.\n- `export` (required): dictionary specifying whether to export the experiment artifacts and the export path.\n- `hyperopt` (optional): whether this is a hyperopt run or `LudwigModel.experiment`.\n- `process_config_file_path` (optional): path to python script that will modify configs.\n- `profiler` (optional): dictionary specifying whether to use the profiler and its parameters.\n\n## Comparing experiments\n\nYou can summarize the exported artifacts of two experiments on multiple datasets.\nFor example, if you ran two experiments on the datasets `ames_housing` called\n`small_batch_size` and `big_batch_size` where you varied the batch size,\nyou can create a diff summary of the model performance and resource usage of the two\nexperiments. This is how:\n\n```\nfrom ludwig.benchmarking.summarize import summarize_metrics\n\ndataset_list, metric_diffs, resource_usage_diffs = summarize_metrics(\n    bench_config_path = \"path/to/benchmarking_config.yaml\",\n    base_experiment = \"small_batch_size\",\n    experimental_experiment = \"big_batch_size\",\n    download_base_path = \"s3://benchmarking.us-west-2.ludwig.com/bench/\")\n```\n\nThis will print\n\n```\nModel performance metrics for *small_batch_size* vs. *big_batch_size* on dataset *ames_housing*\nOutput Feature Name  Metric Name                       small_batch_size big_batch_size Diff          Diff Percentage\nSalePrice            mean_absolute_error               180551.609    180425.109    -126.5        -0.07\nSalePrice            mean_squared_error                38668763136.0 38618021888.0 -50741248.0   -0.131\nSalePrice            r2                                -5.399        -5.391        0.008         -0.156\nSalePrice            root_mean_squared_error           196643.75     196514.688    -129.062      -0.066\nSalePrice            root_mean_squared_percentage_error 1.001         1.001         -0.001        -0.07\nExported a CSV report to summarize_output/performance_metrics/ames_housing/small_batch_size-big_batch_size.csv\n\nResource usage for *small_batch_size* vs. *big_batch_size* on *training* of dataset *ames_housing*\nMetric Name                          small_batch_size     big_batch_size       Diff                 Diff Percentage\naverage_cpu_memory_usage             106.96 Mb            109.43 Mb            2.48 Mb              2.315\naverage_cpu_utilization              1.2966666666666666   1.345                0.04833333333333334  3.728\naverage_global_cpu_memory_available  3.46 Gb              3.46 Gb              -1.10 Mb             -0.031\naverage_global_cpu_utilization       37.43333333333334    40.49                3.056666666666665    8.166\ndisk_footprint                       372736               413696               40960                10.989\nmax_cpu_memory_usage                 107.50 Mb            111.93 Mb            4.43 Mb              4.117\nmax_cpu_utilization                  1.44                 1.67                 0.22999999999999998  15.972\nmax_global_cpu_utilization           54.1                 60.9                 6.799999999999997    12.569\nmin_global_cpu_memory_available      3.46 Gb              3.46 Gb              -712.00 Kb           -0.02\nnum_cpu                              10                   10                   0                    0.0\nnum_oom_events                       0                    0                    0                    inf\nnum_runs                             1                    1                    0                    0.0\ntorch_cpu_average_memory_used        81.44 Kb             381.15 Kb            299.70 Kb            367.992\ntorch_cpu_max_memory_used            334.26 Kb            2.65 Mb              2.32 Mb              711.877\ntorch_cpu_time                       57.400ms             130.199ms            72.799ms             126.828\ntorch_cuda_time                      0.000us              0.000us              0.000us              inf\ntotal_cpu_memory_size                32.00 Gb             32.00 Gb             0 b                  0.0\ntotal_execution_time                 334.502ms            1.114s               779.024ms            232.891\nExported a CSV report to summarize_output/resource_usage_metrics/ames_housing/training-small_batch_size-big_batch_size.csv\n\nResource usage for *small_batch_size* vs. *big_batch_size* on *evaluation* of dataset *ames_housing*\n...\nResource usage for *small_batch_size* vs. *big_batch_size* on *preprocessing* of dataset *ames_housing*\n...\n```\n"
  },
  {
    "path": "ludwig/benchmarking/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/benchmarking/artifacts.py",
    "content": "import os\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.types import ModelConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.data_utils import load_json, load_yaml\n\n\n@dataclass\nclass BenchmarkingResult:\n    # The Ludwig benchmarking config.\n    benchmarking_config: dict[str, Any]\n\n    # The config for one experiment.\n    experiment_config: dict[str, Any]\n\n    # The Ludwig config used to run the experiment.\n    ludwig_config: ModelConfigDict\n\n    # The python script that is used to process the config before being used.\n    process_config_file: str\n\n    # Loaded `description.json` file.\n    description: dict[str, Any]\n\n    # Loaded `test_statistics.json` file.\n    test_statistics: dict[str, Any]\n\n    # Loaded `training_statistics.json` file.\n    training_statistics: dict[str, Any]\n\n    # Loaded `model_hyperparameters.json` file.\n    model_hyperparameters: dict[str, Any]\n\n    # Loaded `training_progress.json` file.\n    training_progress: dict[str, Any]\n\n    # Loaded `training_set_metadata.json` file.\n    training_set_metadata: TrainingSetMetadataDict\n\n\ndef build_benchmarking_result(benchmarking_config: dict, experiment_idx: int):\n    experiment_config = benchmarking_config[\"experiments\"][experiment_idx]\n    process_config_file = \"\"\n    if experiment_config[\"process_config_file_path\"]:\n        with open(experiment_config[\"process_config_file_path\"]) as f:\n            process_config_file = \"\".join(f.readlines())\n    experiment_run_path = os.path.join(experiment_config[\"experiment_name\"], \"experiment_run\")\n\n    return BenchmarkingResult(\n        benchmarking_config=benchmarking_config,\n        experiment_config=experiment_config,\n        ludwig_config=load_yaml(experiment_config[\"config_path\"]),\n        process_config_file=process_config_file,\n        description=load_json(os.path.join(experiment_run_path, \"description.json\")),\n        test_statistics=load_json(os.path.join(experiment_run_path, \"test_statistics.json\")),\n        training_statistics=load_json(os.path.join(experiment_run_path, \"training_statistics.json\")),\n        model_hyperparameters=load_json(\n            os.path.join(experiment_run_path, MODEL_FILE_NAME, \"model_hyperparameters.json\")\n        ),\n        training_progress=load_json(os.path.join(experiment_run_path, MODEL_FILE_NAME, \"training_progress.json\")),\n        training_set_metadata=load_json(\n            os.path.join(experiment_run_path, MODEL_FILE_NAME, \"training_set_metadata.json\")\n        ),\n    )\n"
  },
  {
    "path": "ludwig/benchmarking/benchmark.py",
    "content": "import argparse\nimport importlib\nimport logging\nimport os\nimport shutil\nfrom typing import Any\n\nimport ludwig.datasets\nfrom ludwig.api import LudwigModel\nfrom ludwig.benchmarking.artifacts import BenchmarkingResult, build_benchmarking_result\nfrom ludwig.benchmarking.profiler_callbacks import LudwigProfilerCallback\nfrom ludwig.benchmarking.utils import (\n    create_default_config,\n    delete_hyperopt_outputs,\n    delete_model_checkpoints,\n    export_artifacts,\n    load_from_module,\n    populate_benchmarking_config_with_defaults,\n    propagate_global_parameters,\n    save_yaml,\n    validate_benchmarking_config,\n)\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.hyperopt.run import hyperopt\nfrom ludwig.utils.data_utils import load_yaml\n\nlogger = logging.getLogger()\n\n\ndef setup_experiment(experiment: dict[str, str]) -> dict[Any, Any]:\n    \"\"\"Set up the backend and load the Ludwig config.\n\n    Args:\n        experiment: dictionary containing the dataset name, config path, and experiment name.\n        Returns a Ludwig config.\n    \"\"\"\n    shutil.rmtree(os.path.join(experiment[\"experiment_name\"]), ignore_errors=True)\n    if \"config_path\" not in experiment:\n        experiment[\"config_path\"] = create_default_config(experiment)\n    model_config = load_yaml(experiment[\"config_path\"])\n\n    if experiment[\"process_config_file_path\"]:\n        process_config_spec = importlib.util.spec_from_file_location(\n            \"process_config_file_path.py\", experiment[\"process_config_file_path\"]\n        )\n        process_module = importlib.util.module_from_spec(process_config_spec)\n        process_config_spec.loader.exec_module(process_module)\n        model_config = process_module.process_config(model_config, experiment)\n        experiment[\"config_path\"] = experiment[\"config_path\"].replace(\n            \".yaml\", \"-\" + experiment[\"experiment_name\"] + \"-modified.yaml\"\n        )\n        save_yaml(experiment[\"config_path\"], model_config)\n\n    return model_config\n\n\ndef benchmark_one(experiment: dict[str, str | dict[str, str]]) -> None:\n    \"\"\"Run a Ludwig exepriment and track metrics given a dataset name.\n\n    Args:\n        experiment: dictionary containing the dataset name, config path, and experiment name.\n    \"\"\"\n    logger.info(f\"\\nRunning experiment *{experiment['experiment_name']}* on dataset *{experiment['dataset_name']}*\")\n\n    # configuring backend and paths\n    model_config = setup_experiment(experiment)\n\n    # loading dataset\n    # dataset_module = importlib.import_module(f\"ludwig.datasets.{experiment['dataset_name']}\")\n    dataset_module = ludwig.datasets.get_dataset(experiment[\"dataset_name\"])\n    dataset = load_from_module(dataset_module, model_config[\"output_features\"][0])\n\n    if experiment[\"hyperopt\"]:\n        # run hyperopt\n        hyperopt(\n            config=model_config,\n            dataset=dataset,\n            output_directory=experiment[\"experiment_name\"],\n            skip_save_model=True,\n            skip_save_training_statistics=True,\n            skip_save_progress=True,\n            skip_save_log=True,\n            skip_save_processed_input=True,\n            skip_save_unprocessed_output=True,\n            skip_save_predictions=True,\n            skip_save_training_description=True,\n            hyperopt_log_verbosity=0,\n        )\n        delete_hyperopt_outputs(experiment[\"experiment_name\"])\n    else:\n        backend = None\n        ludwig_profiler_callbacks = None\n        if experiment[\"profiler\"][\"enable\"]:\n            ludwig_profiler_callbacks = [LudwigProfilerCallback(experiment)]\n            # Currently, only local backend is supported with LudwigProfiler.\n            backend = \"local\"\n            logger.info(\"Currently, only local backend is supported with LudwigProfiler.\")\n        # run model and capture metrics\n        model = LudwigModel(\n            config=model_config, callbacks=ludwig_profiler_callbacks, logging_level=logging.ERROR, backend=backend\n        )\n        model.experiment(\n            dataset=dataset,\n            output_directory=experiment[\"experiment_name\"],\n            skip_save_processed_input=True,\n            skip_save_unprocessed_output=True,\n            skip_save_predictions=True,\n            skip_collect_predictions=True,\n        )\n        delete_model_checkpoints(experiment[\"experiment_name\"])\n\n\ndef benchmark(benchmarking_config: dict[str, Any] | str) -> dict[str, tuple[BenchmarkingResult, Exception]]:\n    \"\"\"Launch benchmarking suite from a benchmarking config.\n\n    Args:\n        benchmarking_config: config or config path for the benchmarking tool. Specifies datasets and their\n            corresponding Ludwig configs, as well as export options.\n    \"\"\"\n    if isinstance(benchmarking_config, str):\n        benchmarking_config = load_yaml(benchmarking_config)\n    validate_benchmarking_config(benchmarking_config)\n    benchmarking_config = populate_benchmarking_config_with_defaults(benchmarking_config)\n    benchmarking_config = propagate_global_parameters(benchmarking_config)\n\n    experiment_artifacts = {}\n    for experiment_idx, experiment in enumerate(benchmarking_config[\"experiments\"]):\n        dataset_name = experiment[\"dataset_name\"]\n        try:\n            benchmark_one(experiment)\n            experiment_artifacts[dataset_name] = (build_benchmarking_result(benchmarking_config, experiment_idx), None)\n        except Exception as e:\n            logger.exception(\n                f\"Experiment *{experiment['experiment_name']}* on dataset *{experiment['dataset_name']}* failed\"\n            )\n            experiment_artifacts[dataset_name] = (None, e)\n        finally:\n            if benchmarking_config[\"export\"][\"export_artifacts\"]:\n                export_base_path = benchmarking_config[\"export\"][\"export_base_path\"]\n                export_artifacts(experiment, experiment[\"experiment_name\"], export_base_path)\n    return experiment_artifacts\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script runs a ludwig experiment on datasets specified in the benchmark config and exports \"\n        \"the experiment artifact for each of the datasets following the export parameters specified in\"\n        \"the benchmarking config.\",\n        prog=\"ludwig benchmark\",\n        usage=\"%(prog)s [options]\",\n    )\n    parser.add_argument(\"--benchmarking_config\", type=str, help=\"The benchmarking config.\")\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n    benchmark(args.benchmarking_config)\n"
  },
  {
    "path": "ludwig/benchmarking/examples/benchmarking_config.yaml",
    "content": "experiment_name: example_benchmarking_run\nhyperopt: false\nprocess_config_file_path: /home/ray/process_config.py\nprofiler:\n  enable: true\n  use_torch_profiler: false\n  logging_interval: 0.1\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nexperiments:\n  - dataset_name: ames_housing\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/ames_housing.yaml\n  - dataset_name: protein\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/mercedes_benz_greener.yaml\n  - dataset_name: santander_customer_satisfaction\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/santander_customer_satisfaction.yaml\n  - dataset_name: connect4\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/connect4.yaml\n  - dataset_name: otto_group_product\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/otto_group_product.yaml\n  - dataset_name: bnp_claims_management\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/bnp_claims_management.yaml\n  - dataset_name: santander_customer_transaction\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/santander_customer_transaction.yaml\n  - dataset_name: allstate_claims_severity\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/allstate_claims_severity.yaml\n  - dataset_name: naval\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/naval.yaml\n  - dataset_name: sarcos\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/sarcos.yaml\n  - dataset_name: walmart_recruiting\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/walmart_recruiting.yaml\n  - dataset_name: numerai28pt6\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/numerai28pt6.yaml\n  - dataset_name: adult_census_income\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/adult_census_income.yaml\n  - dataset_name: amazon_employee_access_challenge\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/amazon_employee_access_challenge.yaml\n  - dataset_name: forest_cover\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/forest_cover.yaml\n  - dataset_name: mushroom_edibility\n    config_path: /home/ray/anaconda3/lib/python3.12/site-packages/ludwig/benchmarking/configs/mushroom_edibility.yaml\n"
  },
  {
    "path": "ludwig/benchmarking/examples/process_config.py",
    "content": "\"\"\"This function will take in a Ludwig config, strip away all its parameters except input and output featuresand\nadd some other parameters to run logistic regression hyperopt.\"\"\"\n\n\ndef process_config(ludwig_config: dict, experiment_dict: dict) -> dict:\n    \"\"\"Modify a Ludwig config by programmatically adding elements to the config dictionary.\n\n    The purpose is to apply changes for all datasets that are the same or are based on the\n     attributes of `experiment_dict` (e.g. dataset_name) removing the need to manually apply\n     small changes to configs on many datasets.\n\n    :param ludwig_config: a Ludwig config.\n    :param experiment_dict: a benchmarking config experiment dictionary.\n\n    Returns: a modified Ludwig config.\n    \"\"\"\n\n    # only keep input_features and output_features\n    main_config_keys = list(ludwig_config.keys())\n    for key in main_config_keys:\n        if key not in [\"input_features\", \"output_features\"]:\n            del ludwig_config[key]\n\n    temp = {\n        \"preprocessing\": {\"split\": {\"type\": \"fixed\"}},\n        \"trainer\": {\"epochs\": 1024, \"early_stop\": 7, \"eval_batch_size\": 16384, \"evaluate_training_set\": False},\n        \"hyperopt\": {\n            \"goal\": \"maximize\",\n            \"output_feature\": None,\n            \"metric\": None,\n            \"split\": \"validation\",\n            \"parameters\": {\n                \"defaults.number.preprocessing.normalization\": {\"space\": \"choice\", \"categories\": [\"zscore\", None]},\n                \"defaults.number.preprocessing.missing_value_strategy\": {\n                    \"space\": \"choice\",\n                    \"categories\": [\"fill_with_const\", \"fill_with_mean\"],\n                },\n                \"combiner.type\": {\"space\": \"choice\", \"categories\": [\"tabnet\", \"concat\"]},\n                \"trainer.learning_rate_scheduler.decay\": {\"space\": \"choice\", \"categories\": [True, False]},\n                \"trainer.learning_rate\": {\"space\": \"loguniform\", \"lower\": 0.0001, \"upper\": 0.1},\n                \"trainer.learning_rate_scheduler.decay_rate\": {\"space\": \"uniform\", \"lower\": 0.4, \"upper\": 0.96},\n                \"trainer.batch_size\": {\"space\": \"randint\", \"lower\": 32, \"upper\": 2048},\n            },\n            \"search_alg\": {\"type\": \"variant_generator\"},\n            \"executor\": {\"type\": \"ray\", \"num_samples\": 1000},\n            \"scheduler\": {\"type\": \"bohb\", \"reduction_factor\": 2},\n        },\n    }\n\n    # add config parameters from temp\n    for key, value in temp.items():\n        ludwig_config[key] = value\n\n    dataset_name_to_metric = {\n        \"ames_housing\": \"r2\",\n        \"mercedes_benz_greener\": \"r2\",\n        \"mushroom_edibility\": \"accuracy\",\n        \"amazon_employee_access_challenge\": \"roc_auc\",\n        \"naval\": \"r2\",\n        \"sarcos\": \"r2\",\n        \"protein\": \"r2\",\n        \"adult_census_income\": \"accuracy\",\n        \"otto_group_product\": \"accuracy\",\n        \"santander_customer_satisfaction\": \"accuracy\",\n        \"amazon_employee_access\": \"roc_auc\",\n        \"numerai28pt6\": \"accuracy\",\n        \"bnp_claims_management\": \"accuracy\",\n        \"allstate_claims_severity\": \"r2\",\n        \"santander_customer_transaction\": \"accuracy\",\n        \"connect4\": \"accuracy\",\n        \"forest_cover\": \"accuracy\",\n        \"ieee_fraud\": \"accuracy\",\n        \"porto_seguro_safe_driver\": \"accuracy\",\n        \"walmart_recruiting\": \"accuracy\",\n        \"poker_hand\": \"accuracy\",\n        \"higgs\": \"accuracy\",\n    }\n\n    # add hyperopt output feature and metric.\n    dataset_name = experiment_dict[\"dataset_name\"]\n    ludwig_config[\"hyperopt\"][\"metric\"] = dataset_name_to_metric[dataset_name]\n    ludwig_config[\"hyperopt\"][\"output_feature\"] = ludwig_config[\"output_features\"][0][\"name\"]\n\n    # use sparse encoder for categorical features to mimic logistic regression.\n    for i, feature in enumerate(ludwig_config[\"input_features\"]):\n        if feature[\"type\"] == \"category\":\n            ludwig_config[\"input_features\"][i][\"encoder\"] = \"sparse\"\n    for i, feature in enumerate(ludwig_config[\"output_features\"]):\n        if feature[\"type\"] == \"category\":\n            ludwig_config[\"output_features\"][i][\"encoder\"] = \"sparse\"\n\n    # make sure to return the ludwig_config\n    return ludwig_config\n"
  },
  {
    "path": "ludwig/benchmarking/profiler.py",
    "content": "import contextlib\nimport glob\nimport logging\nimport os\nimport shutil\nimport threading\nimport time\nfrom queue import Empty as EmptyQueueException\nfrom queue import Queue\nfrom subprocess import PIPE, Popen\nfrom typing import Any\nfrom xml.etree.ElementTree import fromstring\n\nimport psutil\nimport torch\nfrom cpuinfo import get_cpu_info\nfrom gpustat.core import GPUStatCollection\n\nfrom ludwig.benchmarking.profiler_dataclasses import profiler_dataclass_to_flat_dict, TorchProfilerMetrics\nfrom ludwig.benchmarking.reporting import get_metrics_from_system_usage_profiler, get_metrics_from_torch_profiler\nfrom ludwig.constants import LUDWIG_TAG\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.data_utils import save_json\n\nSTOP_MESSAGE = \"stop\"\nlogger = logging.getLogger()\n\n\ndef get_gpu_info():\n    \"\"\"Gathers general hardware information about an nvidia GPU.\n\n    This function was copied from `experiment_impact_tracker` to get around a Pandas 2.0 breaking change impacting the\n    package. https://github.com/Breakend/experiment-impact-\n    tracker/blob/master/experiment_impact_tracker/gpu/nvidia.py#L48-L73\n    \"\"\"\n    p = Popen([\"nvidia-smi\", \"-q\", \"-x\"], stdout=PIPE)\n    outs, errors = p.communicate()\n    xml = fromstring(outs)\n    data = []\n    driver_version = xml.findall(\"driver_version\")[0].text\n    cuda_version = xml.findall(\"cuda_version\")[0].text\n\n    for gpu_id, gpu in enumerate(xml.getiterator(\"gpu\")):\n        gpu_data = {}\n        name = [x for x in gpu.getiterator(\"product_name\")][0].text\n        memory_usage = gpu.findall(\"fb_memory_usage\")[0]\n        total_memory = memory_usage.findall(\"total\")[0].text\n\n        gpu_data[\"name\"] = name\n        gpu_data[\"total_memory\"] = total_memory\n        gpu_data[\"driver_version\"] = driver_version\n        gpu_data[\"cuda_version\"] = cuda_version\n        data.append(gpu_data)\n    return data\n\n\ndef monitor(queue: Queue, info: dict[str, Any], logging_interval: int, cuda_is_available: bool) -> None:\n    \"\"\"Monitors hardware resource use.\n\n    Collects system specific metrics (CPU/CUDA, CPU/CUDA memory) at a `logging_interval` interval and pushes\n    results back to the parent process.\n\n    Args:\n        queue: queue from which we can push and retrieve messages sent to the function targeted by the thread.\n        info: dictionary containing system resource usage information about the running process.\n        logging_interval: time interval at which we will poll the system for usage metrics.\n        cuda_is_available: stores torch.cuda.is_available().\n    \"\"\"\n    info[\"global_cpu_memory_available\"] = [psutil.virtual_memory().available]\n    info[\"global_cpu_utilization\"] = [psutil.cpu_percent()]\n    # get the pid of the parent process.\n    tracked_process = psutil.Process(os.getpid())\n\n    # will return a meaningless 0 value on the first call because `interval` arg is set to None.\n    tracked_process.cpu_percent(interval=logging_interval)\n    with tracked_process.oneshot():\n        info[\"cpu_utilization\"] = [tracked_process.cpu_percent() / info[\"num_cpu\"]]\n        info[\"cpu_memory_usage\"] = [tracked_process.memory_full_info().uss]\n        try:\n            info[\"num_accessible_cpus\"] = len(tracked_process.cpu_affinity())\n        except Exception:\n            pass\n\n    while True:\n        try:\n            message = queue.get(block=False)\n            if isinstance(message, str):\n                if message == STOP_MESSAGE:\n                    # synchronize CUDA to get accurate timing for jobs running on GPU.\n                    if cuda_is_available:\n                        torch.cuda.synchronize()\n                    queue.put(info)\n                    return\n            else:\n                queue.put(message)\n        except EmptyQueueException:\n            pass\n        if cuda_is_available:\n            gpu_infos = GPUStatCollection.new_query()\n            for i, gpu_info in enumerate(gpu_infos):\n                gpu_key = f\"cuda_{i}\"\n                info[f\"{gpu_key}_memory_used\"].append(gpu_info.memory_used)\n        with tracked_process.oneshot():\n            info[\"cpu_utilization\"].append(tracked_process.cpu_percent() / info[\"num_cpu\"])\n            info[\"cpu_memory_usage\"].append(tracked_process.memory_full_info().uss)\n        info[\"global_cpu_memory_available\"].append(psutil.virtual_memory().available)\n        info[\"global_cpu_utilization\"].append(psutil.cpu_percent())\n        time.sleep(logging_interval)\n\n\nclass LudwigProfiler(contextlib.ContextDecorator):\n    \"\"\"Track system resource (hardware and software) usage.\n\n    Warning: If `use_torch_profiler=True` while profiling on CUDA, it's not possible to benchmark DataLoaders\n    with `num_workers > 0` due to CUDA multiprocessing limitations. See warning under `profile` class\n    definition: https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler.py\n\n    Attributes:\n        tag: a string tag describing the code block/function that we're tracking.\n            (e.g trainer.train, preprocessing, etc.)\n        output_dir: path where metrics are saved.\n        logging_interval: time interval in seconds at which system is polled for resource usage.\n    \"\"\"\n\n    def __init__(self, tag: str, use_torch_profiler: bool, output_dir: str, logging_interval: float = 0.1) -> None:\n        self.tag = tag\n        self._tag = LUDWIG_TAG + self.tag\n        self.use_torch_profiler = use_torch_profiler\n        self.output_dir = output_dir\n        self.logging_interval = logging_interval\n        self.cuda_is_available = torch.cuda.is_available()\n        self.launched = False\n        if self.use_torch_profiler:\n            self.profiler_activities = [torch.profiler.ProfilerActivity.CPU]\n            if self.cuda_is_available:\n                self.profiler_activities.append(torch.profiler.ProfilerActivity.CUDA)\n        os.makedirs(os.path.join(self.output_dir), exist_ok=True)\n\n    def _init_tracker_info(self):\n        \"\"\"Initialize new self.info, self.torch_profiler, and self.torch_record_function instances.\n\n        Important to call this in __enter__ if the user decides not to create a new class instance and therefore\n        __init__ wouldn't be called.\n        \"\"\"\n        self.info = {\"code_block_tag\": self.tag}\n        if self.use_torch_profiler:\n            self.torch_profiler = torch.profiler.profile(activities=self.profiler_activities, profile_memory=True)\n            self.torch_record_function = torch.profiler.record_function(self._tag)\n\n    def _populate_static_information(self) -> None:\n        \"\"\"Populate the report with static software and hardware information.\"\"\"\n        self.info[\"ludwig_version\"] = LUDWIG_VERSION\n        self.info[\"start_disk_usage\"] = shutil.disk_usage(os.path.expanduser(\"~\")).used\n\n        # CPU information\n        cpu_info = get_cpu_info()\n        self.info[\"cpu_architecture\"] = cpu_info[\"arch\"]\n        self.info[\"num_cpu\"] = psutil.cpu_count()\n        self.info[\"cpu_name\"] = cpu_info.get(\"brand_raw\", \"unknown\")\n        self.info[\"total_cpu_memory_size\"] = psutil.virtual_memory().total\n\n        # GPU information\n        if self.cuda_is_available:\n            gpu_infos = get_gpu_info()\n            gpu_usage = GPUStatCollection.new_query()\n            for i, gpu_info in enumerate(gpu_infos):\n                gpu_key = f\"cuda_{i}\"\n                self.info[f\"{gpu_key}_memory_used\"] = [gpu_usage[i].memory_used]\n                self.info[f\"{gpu_key}_name\"] = gpu_info[\"name\"]\n                self.info[f\"{gpu_key}_total_memory\"] = gpu_info[\"total_memory\"]\n                self.info[f\"{gpu_key}_driver_version\"] = gpu_info[\"driver_version\"]\n                self.info[f\"{gpu_key}_cuda_version\"] = gpu_info[\"cuda_version\"]\n\n        # recording in microseconds to be in line with torch profiler time recording.\n        self.info[\"start_time\"] = time.perf_counter_ns() / 1000\n\n    def __enter__(self):\n        \"\"\"Populate static information and monitors resource usage.\"\"\"\n        if self.launched:\n            raise RuntimeError(\"LudwigProfiler already launched. You can't use the same instance.\")\n\n        self._init_tracker_info()\n        self._populate_static_information()\n\n        if self.use_torch_profiler:\n            # contextlib.ExitStack gracefully handles situations where __enter__ or __exit__ calls throw exceptions.\n            with contextlib.ExitStack() as ctx_exit_stack:\n                try:\n                    # Launch torch.profiler to track PyTorch operators.\n                    ctx_exit_stack.enter_context(self.torch_profiler)\n                except RuntimeError:\n                    # PyTorch profiler is already enabled on this thread.\n                    # Using the running PyTorch profiler to track events.\n                    self.torch_profiler = None\n\n                ctx_exit_stack.enter_context(self.torch_record_function)\n                self._ctx_exit_stack = ctx_exit_stack.pop_all()\n        try:\n            # Starting thread to monitor system resource usage.\n            self.queue = Queue()\n            self.t = threading.Thread(\n                target=monitor,\n                args=(\n                    self.queue,\n                    self.info,\n                    self.logging_interval,\n                    self.cuda_is_available,\n                ),\n            )\n            self.t.start()\n            self.launched = True\n        except Exception:\n            self.launched = False\n            logger.exception(\"Encountered exception when launching tracker thread.\")\n\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb) -> None:\n        \"\"\"Stop profiling, postprocess and export resource usage metrics.\"\"\"\n        try:\n            self.queue.put(STOP_MESSAGE)\n            self.t.join()\n            result = self.queue.get()\n            # If monitor thread crashed, result may be a string instead of dict\n            if isinstance(result, dict):\n                self.info = result\n            # recording in microseconds to be in line with torch profiler time recording.\n            self.info[\"end_time\"] = time.perf_counter_ns() / 1000\n            self.info[\"end_disk_usage\"] = shutil.disk_usage(os.path.expanduser(\"~\")).used\n            self.launched = False\n        except Exception:\n            logger.exception(\"Encountered exception when joining tracker thread.\")\n        finally:\n            if self.use_torch_profiler:\n                self._ctx_exit_stack.close()\n                self._export_torch_metrics()\n            self._export_system_usage_metrics()\n\n    def _export_system_usage_metrics(self):\n        \"\"\"Export system resource usage metrics (no torch operators).\"\"\"\n        system_usage_metrics = get_metrics_from_system_usage_profiler(self.info)\n        output_subdir = os.path.join(self.output_dir, \"system_resource_usage\", system_usage_metrics.code_block_tag)\n        os.makedirs(output_subdir, exist_ok=True)\n        num_prev_runs = len(glob.glob(os.path.join(output_subdir, \"run_*.json\")))\n        file_name = os.path.join(output_subdir, f\"run_{num_prev_runs}.json\")\n        save_json(file_name, profiler_dataclass_to_flat_dict(system_usage_metrics))\n\n    def _reformat_torch_usage_metrics_tags(\n        self, torch_usage_metrics: dict[str, Any]\n    ) -> dict[str, list[TorchProfilerMetrics]]:\n        reformatted_dict = {}\n        for key, value in torch_usage_metrics.items():\n            assert key.startswith(LUDWIG_TAG)\n            reformatted_key = key[len(LUDWIG_TAG) :]\n            reformatted_dict[reformatted_key] = value\n        return reformatted_dict\n\n    def _export_torch_metrics(self):\n        \"\"\"Export resource usage metrics of torch operators.\"\"\"\n        if self.torch_profiler:\n            torch_usage_metrics = get_metrics_from_torch_profiler(self.torch_profiler)\n            torch_usage_metrics = self._reformat_torch_usage_metrics_tags(torch_usage_metrics)\n            for tag, runs in torch_usage_metrics.items():\n                temp_dir = os.path.join(self.output_dir, \"torch_ops_resource_usage\", tag)\n                os.makedirs(temp_dir, exist_ok=True)\n                for run in runs:\n                    num_prev_runs = len(glob.glob(os.path.join(temp_dir, \"run_*.json\")))\n                    save_json(os.path.join(temp_dir, f\"run_{num_prev_runs}.json\"), profiler_dataclass_to_flat_dict(run))\n"
  },
  {
    "path": "ludwig/benchmarking/profiler_callbacks.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.benchmarking.profiler import LudwigProfiler\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import EVALUATION, PREPROCESSING, TRAINING\n\n\n# TODO: Change annotation to PublicAPI once Ludwig 0.7 is released\n@DeveloperAPI\nclass LudwigProfilerCallback(Callback):\n    \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n    def __init__(self, experiment: dict[str, Any]):\n        self.experiment_name = experiment[\"experiment_name\"]\n        self.use_torch_profiler = experiment[\"profiler\"][\"use_torch_profiler\"]\n        self.logging_interval = experiment[\"profiler\"][\"logging_interval\"]\n        self.preprocess_profiler = None\n        self.train_profiler = None\n        self.evaluation_profiler = None\n\n    def on_preprocess_start(self, *args, **kwargs):\n        self.preprocess_profiler = LudwigProfiler(\n            tag=PREPROCESSING,\n            output_dir=self.experiment_name,\n            use_torch_profiler=self.use_torch_profiler,\n            logging_interval=self.logging_interval,\n        )\n        self.preprocess_profiler.__enter__()\n\n    def on_preprocess_end(self, *args, **kwargs):\n        self.preprocess_profiler.__exit__(None, None, None)\n        del self.preprocess_profiler\n\n    def on_train_start(self, *args, **kwargs):\n        self.train_profiler = LudwigProfiler(\n            tag=TRAINING,\n            output_dir=self.experiment_name,\n            use_torch_profiler=self.use_torch_profiler,\n            logging_interval=self.logging_interval,\n        )\n        self.train_profiler.__enter__()\n\n    def on_train_end(self, *args, **kwargs):\n        self.train_profiler.__exit__(None, None, None)\n        del self.train_profiler\n\n    def on_evaluation_start(self):\n        self.evaluation_profiler = LudwigProfiler(\n            tag=EVALUATION,\n            output_dir=self.experiment_name,\n            use_torch_profiler=self.use_torch_profiler,\n            logging_interval=self.logging_interval,\n        )\n        self.evaluation_profiler.__enter__()\n\n    def on_evaluation_end(self):\n        self.evaluation_profiler.__exit__(None, None, None)\n        del self.evaluation_profiler\n"
  },
  {
    "path": "ludwig/benchmarking/profiler_dataclasses.py",
    "content": "import dataclasses\nfrom dataclasses import dataclass\n\nfrom ludwig.utils.data_utils import flatten_dict\n\n\n@dataclass\nclass DeviceUsageMetrics:\n    # Max CUDA memory utilization of the code block.\n    max_memory_used: float\n\n    # Average CUDA memory utilization of the code block.\n    average_memory_used: float\n\n\n@dataclass\nclass SystemResourceMetrics:\n    # Name of the code block/function to be profiled.\n    code_block_tag: str\n\n    # Name of the CPU that the code ran on.\n    cpu_name: str\n\n    # CPU architecture that the code ran on.\n    cpu_architecture: str\n\n    # Number of CPUs on the machine.\n    num_cpu: int\n\n    # Total CPU memory size.\n    total_cpu_memory_size: float\n\n    # Ludwig version in the environment.\n    ludwig_version: str\n\n    # Total execution time of the code block.\n    total_execution_time: float\n\n    # The change in disk memory before and after the code block ran.\n    disk_footprint: float\n\n    # Max CPU utilization of the code block.\n    max_cpu_utilization: float\n\n    # Max CPU memory (RAM) utilization of the code block.\n    max_cpu_memory_usage: float\n\n    # Min system-wide CPU memory available (how much physical memory is left).\n    min_global_cpu_memory_available: float\n\n    # Max system-wide CPU utilization.\n    max_global_cpu_utilization: float\n\n    # Average CPU utilization of the code block.\n    average_cpu_utilization: float\n\n    # Average CPU memory (RAM) utilization of the code block.\n    average_cpu_memory_usage: float\n\n    # Average system-wide CPU memory available (how much physical memory is left).\n    average_global_cpu_memory_available: float\n\n    # Average system-wide CPU utilization.\n    average_global_cpu_utilization: float\n\n    # Per device usage. Dictionary containing max and average memory used per device.\n    device_usage: dict[str, DeviceUsageMetrics]\n\n\n@dataclass\nclass TorchProfilerMetrics:\n    # Time taken by torch ops to execute on the CPU.\n    torch_cpu_time: float\n\n    # Time taken by torch ops to execute on CUDA devices.\n    torch_cuda_time: float\n\n    # Number of out of memory events.\n    num_oom_events: int\n\n    # Per device usage by torch ops. Dictionary containing max and average memory used per device.\n    device_usage: dict[str, DeviceUsageMetrics]\n\n\ndef profiler_dataclass_to_flat_dict(data: SystemResourceMetrics | TorchProfilerMetrics) -> dict:\n    \"\"\"Returns a flat dictionary representation, with the device_usage key removed.\"\"\"\n    nested_dict = dataclasses.asdict(data)\n    nested_dict[\"\"] = nested_dict.pop(\"device_usage\")\n    return flatten_dict(nested_dict, sep=\"\")\n"
  },
  {
    "path": "ludwig/benchmarking/reporting.py",
    "content": "from collections import Counter, defaultdict\nfrom statistics import mean\nfrom typing import Any\n\nimport torch\nfrom torch._C._autograd import _KinetoEvent\nfrom torch.autograd import DeviceType, profiler_util\n\nfrom ludwig.benchmarking.profiler_dataclasses import DeviceUsageMetrics, SystemResourceMetrics, TorchProfilerMetrics\nfrom ludwig.constants import LUDWIG_TAG\n\n\ndef initialize_stats_dict(main_function_events: list[profiler_util.FunctionEvent]) -> dict[str, list]:\n    \"\"\"Initialize dictionary which stores resource usage information per tagged code block.\n\n    :param main_function_events: list of main function events.\n    \"\"\"\n    info = {}\n    for event_name in [evt.name for evt in main_function_events]:\n        info[event_name] = []\n    return info\n\n\ndef get_memory_details(kineto_event: _KinetoEvent) -> tuple[str, int]:\n    \"\"\"Get device name and number of bytes (de)allocated during an event.\n\n    :param kineto_event: a Kineto event instance.\n    \"\"\"\n    if kineto_event.device_type() in [DeviceType.CPU, DeviceType.MKLDNN, DeviceType.IDEEP]:\n        return \"cpu\", kineto_event.nbytes()\n    elif kineto_event.device_type() in [DeviceType.CUDA, DeviceType.HIP]:\n        return f\"cuda_{kineto_event.device_index()}\", kineto_event.nbytes()\n    else:\n        raise ValueError(f\"Device {kineto_event.device_type()} is not valid.\")\n\n\ndef get_device_memory_usage(\n    kineto_event: _KinetoEvent, memory_events: list[list[_KinetoEvent | bool]]\n) -> dict[str, DeviceUsageMetrics]:\n    \"\"\"Get CPU and CUDA memory usage for an event.\n\n    :param kineto_event: a Kineto event instance.\n    :param memory_events: list of memory events.\n    \"\"\"\n    mem_records_acc = profiler_util.MemRecordsAcc(memory_events)\n    start_us = kineto_event.start_ns() / 1000\n    end_us = start_us + kineto_event.duration_ns() / 1000\n    records_in_interval = mem_records_acc.in_interval(start_us, end_us)\n    memory_so_far = defaultdict(int)\n    count_so_far = defaultdict(int)\n    average_so_far = defaultdict(float)\n    max_so_far = defaultdict(int)\n\n    for mem_record in records_in_interval:\n        device, nbytes = get_memory_details(mem_record[0])\n        memory_so_far[device] += nbytes\n        max_so_far[device] = max(max_so_far[device], memory_so_far[device])\n        average_so_far[device] = (memory_so_far[device] + (average_so_far[device] * count_so_far[device])) / (\n            count_so_far[device] + 1\n        )\n        count_so_far[device] += 1\n    memory_info_per_device = {}\n    for device in count_so_far:\n        memory_info_per_device[f\"torch_{device}_\"] = DeviceUsageMetrics(\n            max_memory_used=max_so_far[device], average_memory_used=average_so_far[device]\n        )\n    return memory_info_per_device\n\n\ndef get_torch_op_time(events: list[profiler_util.FunctionEvent], attr: str) -> int | float:\n    \"\"\"Get time torch operators spent executing for a list of events.\n\n    :param events: list of events.\n    :param attr: a FunctionEvent attribute. Expecting one of \"cpu_time_total\", \"device_time_total\".\n    \"\"\"\n    if attr not in [\"cpu_time_total\", \"device_time_total\"]:\n        return -1\n\n    total = 0\n    for e in events:\n        # Possible trace_names are torch ops, or tagged code blocks by LudwigProfiler (which are\n        # prepended with LUDWIG_TAG).\n        if LUDWIG_TAG not in e.trace_name:\n            total += getattr(e, attr)\n        else:\n            total += get_torch_op_time(e.cpu_children, attr)\n    return total\n\n\ndef get_device_run_durations(function_event: profiler_util.FunctionEvent) -> tuple[float, float]:\n    \"\"\"Get CPU and device run durations for an event.\n\n    :param function_event: a function event instance.\n    \"\"\"\n    torch_cpu_time = get_torch_op_time(function_event.cpu_children, \"cpu_time_total\")\n    torch_device_time = get_torch_op_time(function_event.cpu_children, \"device_time_total\")\n    return torch_cpu_time, torch_device_time\n\n\ndef get_num_oom_events(kineto_event: _KinetoEvent, out_of_memory_events: list[list[_KinetoEvent | bool]]) -> int:\n    oom_records_acc = profiler_util.MemRecordsAcc(out_of_memory_events)\n    start_us = kineto_event.start_ns() / 1000\n    end_us = start_us + kineto_event.duration_ns() / 1000\n    records_in_interval = oom_records_acc.in_interval(start_us, end_us)\n    return len(list(records_in_interval))\n\n\ndef get_resource_usage_report(\n    main_kineto_events: list[_KinetoEvent],\n    main_function_events: list[profiler_util.FunctionEvent],\n    memory_events: list[list[_KinetoEvent | bool]],\n    out_of_memory_events: list[list[_KinetoEvent | bool]],\n    info: dict[str, Any],\n) -> dict[str, list[TorchProfilerMetrics]]:\n    \"\"\"Get relevant information from Kineto events and function events exported by the profiler.\n\n    :param main_kineto_events: list of main Kineto events.\n    :param main_function_events: list of main function events.\n    :param memory_events: list of memory events.\n    :param out_of_memory_events: list of out of memory events.\n    :param info: dictionary used to record resource usage metrics.\n    \"\"\"\n    main_kineto_events = sorted(\n        (evt for evt in main_kineto_events if LUDWIG_TAG in evt.name()), key=lambda x: x.correlation_id()\n    )\n    main_function_events = sorted((evt for evt in main_function_events if LUDWIG_TAG in evt.name), key=lambda x: x.id)\n\n    for kineto_event, function_event in zip(main_kineto_events, main_function_events):\n        # Two different instances of `function_event` can have the same name if a the same\n        # tagged code block/function was executed more than once.\n        memory_info_per_device = get_device_memory_usage(kineto_event, memory_events)\n        torch_cpu_time, torch_cuda_time = get_device_run_durations(function_event)\n        num_oom_events = get_num_oom_events(kineto_event, out_of_memory_events)\n        torch_profiler_metrics = TorchProfilerMetrics(\n            torch_cpu_time=torch_cpu_time,\n            torch_cuda_time=torch_cuda_time,\n            num_oom_events=num_oom_events,\n            device_usage=memory_info_per_device,\n        )\n        info[function_event.name].append(torch_profiler_metrics)\n    return info\n\n\ndef get_all_events(kineto_events: list[_KinetoEvent], function_events: profiler_util.EventList) -> tuple[\n    list[_KinetoEvent],\n    list[profiler_util.FunctionEvent],\n    list[list[_KinetoEvent | bool]],\n    list[list[_KinetoEvent | bool]],\n]:\n    \"\"\"Return main Kineto and function events, memory and OOM events for functions/code blocks tagged in\n    LudwigProfiler.\n\n    :param kineto_events: list of Kineto Events.\n    :param function_events: list of function events.\n    \"\"\"\n    # LUDWIG_TAG is prepended to LudwigProfiler tags. This edited tag is passed in to `torch.profiler.record_function`\n    # so we can easily retrieve events for code blocks wrapped with LudwigProfiler.\n    main_function_events = [evt for evt in function_events if LUDWIG_TAG in evt.name]\n    main_kineto_events = [event for event in kineto_events if LUDWIG_TAG in event.name()]\n    memory_events = [[event, False] for event in kineto_events if profiler_util.MEMORY_EVENT_NAME in event.name()]\n    # profiler_util.OUT_OF_MEMORY_EVENT_NAME seems to only be in newer versions of torch.\n    out_of_memory_events = [[event, False] for event in kineto_events if \"[OutOfMemory]\" in event.name()]\n    return main_kineto_events, main_function_events, memory_events, out_of_memory_events\n\n\ndef get_metrics_from_torch_profiler(profile: torch.profiler.profiler.profile) -> dict[str, list[TorchProfilerMetrics]]:\n    \"\"\"Export time and resource usage metrics (CPU and CUDA) from a PyTorch profiler.\n\n    The profiler keeps track of *torch operations* being executed in C++. It keeps track\n    of what device they're executed on, their execution time, and memory usage.\n    We only track the aforementioned metrics, but the torch profiler can keep track of\n    the stack trace, FLOPs, and torch modules. Tracking each additional item adds overhead.\n\n    The torch profiler surfaces these metrics that are tracked under the hood by `libkineto`.\n    More on the Kineto project: https://github.com/pytorch/kineto\n\n    :param profile: profiler object that contains all the events that\n        were registered during the execution of the wrapped code block.\n    \"\"\"\n    # events in both of these lists are in chronological order.\n    kineto_events = profile.profiler.kineto_results.events()\n    function_events = profile.profiler.function_events\n    main_kineto_events, main_function_events, memory_events, out_of_memory_events = get_all_events(\n        kineto_events, function_events\n    )\n\n    assert Counter([event.name for event in main_function_events]) == Counter(\n        [event.name() for event in main_kineto_events]\n    )\n    info = initialize_stats_dict(main_function_events)\n    info = get_resource_usage_report(\n        main_kineto_events, main_function_events, memory_events, out_of_memory_events, info\n    )\n    return info\n\n\ndef get_metrics_from_system_usage_profiler(system_usage_info: dict) -> SystemResourceMetrics:\n    \"\"\"Package system resource usage metrics (no torch operators) in a dataclass.\n\n    :param system_usage_info: dictionary containing resource usage information.\n    \"\"\"\n    device_usage_dict: dict[str, DeviceUsageMetrics] = {}\n    for key in system_usage_info:\n        if \"cuda_\" in key and \"_memory_used\" in key:\n            cuda_device_name = \"_\".join(key.split(\"_\")[:2]) + \"_\"\n            max_memory_used = max(system_usage_info[key], default=0)\n            average_memory_used = mean(system_usage_info.get(key, [0]))\n            device_usage_dict[cuda_device_name] = DeviceUsageMetrics(\n                max_memory_used=max_memory_used, average_memory_used=average_memory_used\n            )\n    return SystemResourceMetrics(\n        code_block_tag=system_usage_info[\"code_block_tag\"],\n        cpu_name=system_usage_info.get(\"cpu_name\", \"unknown\"),\n        cpu_architecture=system_usage_info[\"cpu_architecture\"],\n        num_cpu=system_usage_info[\"num_cpu\"],\n        total_cpu_memory_size=system_usage_info[\"total_cpu_memory_size\"],\n        ludwig_version=system_usage_info[\"ludwig_version\"],\n        total_execution_time=system_usage_info[\"end_time\"] - system_usage_info[\"start_time\"],\n        disk_footprint=system_usage_info[\"end_disk_usage\"] - system_usage_info[\"start_disk_usage\"],\n        max_cpu_utilization=max(system_usage_info[\"cpu_utilization\"], default=0),\n        max_cpu_memory_usage=max(system_usage_info[\"cpu_memory_usage\"], default=0),\n        min_global_cpu_memory_available=min(system_usage_info[\"global_cpu_memory_available\"], default=0),\n        max_global_cpu_utilization=max(system_usage_info[\"global_cpu_utilization\"], default=0),\n        average_cpu_utilization=mean(system_usage_info.get(\"cpu_utilization\", [0])),\n        average_cpu_memory_usage=mean(system_usage_info.get(\"cpu_memory_usage\", [0])),\n        average_global_cpu_memory_available=mean(system_usage_info.get(\"global_cpu_memory_available\", [0])),\n        average_global_cpu_utilization=mean(system_usage_info.get(\"global_cpu_utilization\", [0])),\n        device_usage=device_usage_dict,\n    )\n"
  },
  {
    "path": "ludwig/benchmarking/summarize.py",
    "content": "import argparse\nimport logging\nimport os\nimport shutil\n\nfrom ludwig.benchmarking.summary_dataclasses import (\n    build_metrics_diff,\n    build_resource_usage_diff,\n    export_metrics_diff_to_csv,\n    export_resource_usage_diff_to_csv,\n    MetricsDiff,\n    ResourceUsageDiff,\n)\nfrom ludwig.benchmarking.utils import download_artifacts\n\nlogger = logging.getLogger()\n\n\ndef summarize_metrics(\n    bench_config_path: str, base_experiment: str, experimental_experiment: str, download_base_path: str\n) -> tuple[list[str], list[MetricsDiff], list[list[ResourceUsageDiff]]]:\n    \"\"\"Build metric and resource usage diffs from experiment artifacts.\n\n    bench_config_path: bench config file path. Can be the same one that was used to run\n        these experiments.\n    base_experiment: name of the experiment we're comparing against.\n    experimental_experiment: name of the experiment we're comparing.\n    download_base_path: base path under which live the stored artifacts of\n        the benchmarking experiments.\n    \"\"\"\n    local_dir, dataset_list = download_artifacts(\n        bench_config_path, base_experiment, experimental_experiment, download_base_path\n    )\n    metric_diffs, resource_usage_diffs = [], []\n    for dataset_name in dataset_list:\n        try:\n            metric_diff = build_metrics_diff(dataset_name, base_experiment, experimental_experiment, local_dir)\n            metric_diffs.append(metric_diff)\n\n            base_path = os.path.join(local_dir, dataset_name, base_experiment)\n            experimental_path = os.path.join(local_dir, dataset_name, experimental_experiment)\n            resource_usage_diff = build_resource_usage_diff(\n                base_path, experimental_path, base_experiment, experimental_experiment\n            )\n            resource_usage_diffs.append(resource_usage_diff)\n        except Exception:\n            logger.exception(f\"Exception encountered while creating diff summary for {dataset_name}.\")\n    shutil.rmtree(local_dir, ignore_errors=True)\n    export_and_print(dataset_list, metric_diffs, resource_usage_diffs)\n    return dataset_list, metric_diffs, resource_usage_diffs\n\n\ndef export_and_print(\n    dataset_list: list[str], metric_diffs: list[MetricsDiff], resource_usage_diffs: list[list[ResourceUsageDiff]]\n) -> None:\n    \"\"\"Export to CSV and print a diff of performance and resource usage metrics of two experiments.\n\n    :param dataset_list: list of datasets for which to print the diffs.\n    :param metric_diffs: Diffs for the performance metrics by dataset.\n    :param resource_usage_diffs: Diffs for the resource usage metrics per dataset per LudwigProfiler tag.\n    \"\"\"\n    for dataset_name, experiment_metric_diff in zip(dataset_list, metric_diffs):\n        output_path = os.path.join(\"summarize_output\", \"performance_metrics\", dataset_name)\n        os.makedirs(output_path, exist_ok=True)\n\n        logger.info(\n            \"Model performance metrics for *{}* vs. *{}* on dataset *{}*\".format(\n                experiment_metric_diff.base_experiment_name,\n                experiment_metric_diff.experimental_experiment_name,\n                experiment_metric_diff.dataset_name,\n            )\n        )\n        logger.info(experiment_metric_diff.to_string())\n        filename = (\n            \"-\".join([experiment_metric_diff.base_experiment_name, experiment_metric_diff.experimental_experiment_name])\n            + \".csv\"\n        )\n        export_metrics_diff_to_csv(experiment_metric_diff, os.path.join(output_path, filename))\n\n    for dataset_name, experiment_resource_diff in zip(dataset_list, resource_usage_diffs):\n        output_path = os.path.join(\"summarize_output\", \"resource_usage_metrics\", dataset_name)\n        os.makedirs(output_path, exist_ok=True)\n        for tag_diff in experiment_resource_diff:\n            logger.info(\n                \"Resource usage for *{}* vs. *{}* on *{}* of dataset *{}*\".format(\n                    tag_diff.base_experiment_name,\n                    tag_diff.experimental_experiment_name,\n                    tag_diff.code_block_tag,\n                    dataset_name,\n                )\n            )\n            logger.info(tag_diff.to_string())\n            filename = (\n                \"-\".join(\n                    [tag_diff.code_block_tag, tag_diff.base_experiment_name, tag_diff.experimental_experiment_name]\n                )\n                + \".csv\"\n            )\n            export_resource_usage_diff_to_csv(tag_diff, os.path.join(output_path, filename))\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(\n        description=\"Summarize the model performance metrics and resource usage metrics of two experiments.\",\n        prog=\"python summarize.py\",\n        usage=\"%(prog)s [options]\",\n    )\n    parser.add_argument(\"--benchmarking_config\", type=str, help=\"The benchmarking config.\")\n    parser.add_argument(\"--base_experiment\", type=str, help=\"The name of the first experiment.\")\n    parser.add_argument(\"--experimental_experiment\", type=str, help=\"The name of the second experiment.\")\n    parser.add_argument(\"--download_base_path\", type=str, help=\"The base path to download experiment artifacts from.\")\n    args = parser.parse_args()\n    summarize_metrics(\n        args.benchmarking_config, args.base_experiment, args.experimental_experiment, args.download_base_path\n    )\n"
  },
  {
    "path": "ludwig/benchmarking/summary_dataclasses.py",
    "content": "import csv\nimport logging\nimport os\nfrom dataclasses import dataclass\nfrom statistics import mean\n\nimport ludwig.modules.metric_modules  # noqa: F401\nfrom ludwig.benchmarking.utils import format_memory, format_time\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\nfrom ludwig.modules.metric_registry import get_metric_classes, metric_feature_type_registry  # noqa: F401\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.data_utils import load_json\n\nlogger = logging.getLogger()\n\n\n@dataclass\nclass MetricDiff:\n    \"\"\"Diffs for a metric.\"\"\"\n\n    # Name of the metric.\n    name: str\n\n    # Value of the metric in base experiment (the one we benchmark against).\n    base_value: float\n\n    # Value of the metric in the experimental experiment.\n    experimental_value: float\n\n    # experimental_value - base_value.\n    diff: float\n\n    # Percentage of change the metric with respect to base_value.\n    diff_percentage: float | str\n\n    def __post_init__(self):\n        \"\"\"Add human-readable string representations to the field.\"\"\"\n\n        if \"memory\" in self.name:\n            self.base_value_str = format_memory(self.base_value)\n            self.experimental_value_str = format_memory(self.experimental_value)\n            self.diff_str = format_memory(self.diff)\n        elif \"time\" in self.name:\n            self.base_value_str = format_time(self.base_value)\n            self.experimental_value_str = format_time(self.experimental_value)\n            self.diff_str = format_time(self.diff)\n        else:\n            self.base_value_str = str(self.base_value)\n            self.experimental_value_str = str(self.experimental_value)\n            self.diff_str = str(self.diff)\n\n\ndef build_diff(name: str, base_value: float, experimental_value: float) -> MetricDiff:\n    \"\"\"Build a diff between any type of metric.\n\n    :param name: name assigned to the metric to be diff-ed.\n    :param base_value: base value of the metric.\n    :param experimental_value: experimental value of the metric.\n    \"\"\"\n    diff = experimental_value - base_value\n    diff_percentage = 100 * diff / base_value if base_value != 0 else \"inf\"\n\n    return MetricDiff(\n        name=name,\n        base_value=base_value,\n        experimental_value=experimental_value,\n        diff=diff,\n        diff_percentage=diff_percentage,\n    )\n\n\n##############################\n# Resource Usage Dataclasses #\n##############################\n\n\n@dataclass\nclass MetricsSummary:\n    \"\"\"Summary of metrics from one experiment.\"\"\"\n\n    # Path containing the artifacts for the experiment.\n    experiment_local_directory: str\n\n    # Full Ludwig config.\n    config: ModelConfigDict\n\n    # LudwigModel output feature type.\n    output_feature_type: str\n\n    # LudwigModel output feature name.\n    output_feature_name: str\n\n    # Dictionary that maps from metric name to their values.\n    metric_to_values: dict[str, float | int]\n\n    # Names of metrics for the output feature.\n    metric_names: set[str]\n\n\n@dataclass\nclass MetricsDiff:\n    \"\"\"Store diffs for two experiments.\"\"\"\n\n    # Dataset the two experiments are being compared on.\n    dataset_name: str\n\n    # Name of the base experiment (the one we benchmark against).\n    base_experiment_name: str\n\n    # Name of the experimental experiment.\n    experimental_experiment_name: str\n\n    # Path under which all artifacts live on the local machine.\n    local_directory: str\n\n    # `MetricsSummary` of the base_experiment.\n    base_summary: MetricsSummary\n\n    # `MetricsSummary` of the experimental_experiment.\n    experimental_summary: MetricsSummary\n\n    # `List[MetricDiff]` containing diffs for metric of the two experiments.\n    metrics: list[MetricDiff]\n\n    def to_string(self):\n        ret = []\n        spacing_str = \"{:<20} {:<33} {:<13} {:<13} {:<13} {:<5}\"\n        ret.append(\n            spacing_str.format(\n                \"Output Feature Name\",\n                \"Metric Name\",\n                self.base_experiment_name,\n                self.experimental_experiment_name,\n                \"Diff\",\n                \"Diff Percentage\",\n            )\n        )\n        for metric in sorted(self.metrics, key=lambda m: m.name):\n            output_feature_name = self.base_summary.output_feature_name\n            metric_name = metric.name\n            experiment1_val = round(metric.base_value, 3)\n            experiment2_val = round(metric.experimental_value, 3)\n            diff = round(metric.diff, 3)\n            diff_percentage = metric.diff_percentage\n            if isinstance(diff_percentage, float):\n                diff_percentage = round(metric.diff_percentage, 3)\n            ret.append(\n                spacing_str.format(\n                    output_feature_name,\n                    metric_name,\n                    experiment1_val,\n                    experiment2_val,\n                    diff,\n                    diff_percentage,\n                )\n            )\n        return \"\\n\".join(ret)\n\n\ndef export_metrics_diff_to_csv(metrics_diff: MetricsDiff, path: str):\n    \"\"\"Export metrics report to .csv.\n\n    :param metrics_diff: MetricsDiff object containing the diff for two experiments on a dataset.\n    :param path: file name of the exported csv.\n    \"\"\"\n    with open(path, \"w\", newline=\"\") as f:\n        writer = csv.DictWriter(\n            f,\n            fieldnames=[\n                \"Dataset Name\",\n                \"Output Feature Name\",\n                \"Metric Name\",\n                metrics_diff.base_experiment_name,\n                metrics_diff.experimental_experiment_name,\n                \"Diff\",\n                \"Diff Percentage\",\n            ],\n        )\n        writer.writeheader()\n\n        for metric in sorted(metrics_diff.metrics, key=lambda m: m.name):\n            output_feature_name = metrics_diff.base_summary.output_feature_name\n            metric_name = metric.name\n            experiment1_val = round(metric.base_value, 3)\n            experiment2_val = round(metric.experimental_value, 3)\n            diff = round(metric.diff, 3)\n            diff_percentage = metric.diff_percentage\n            if isinstance(diff_percentage, float):\n                diff_percentage = round(metric.diff_percentage, 3)\n            writer.writerow(\n                {\n                    \"Dataset Name\": metrics_diff.dataset_name,\n                    \"Output Feature Name\": output_feature_name,\n                    \"Metric Name\": metric_name,\n                    metrics_diff.base_experiment_name: experiment1_val,\n                    metrics_diff.experimental_experiment_name: experiment2_val,\n                    \"Diff\": diff,\n                    \"Diff Percentage\": diff_percentage,\n                }\n            )\n        logger.info(f\"Exported a CSV report to {path}\\n\")\n\n\ndef build_metrics_summary(experiment_local_directory: str) -> MetricsSummary:\n    \"\"\"Build a metrics summary for an experiment.\n\n    :param experiment_local_directory: directory where the experiment artifacts live.\n        e.g. local_experiment_repo/ames_housing/some_experiment/\n    \"\"\"\n    config = load_json(\n        os.path.join(experiment_local_directory, \"experiment_run\", MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)\n    )\n    report = load_json(os.path.join(experiment_local_directory, \"experiment_run\", \"test_statistics.json\"))\n    output_feature_type: str = config[\"output_features\"][0][\"type\"]\n    output_feature_name: str = config[\"output_features\"][0][\"name\"]\n    metric_dict = report[output_feature_name]\n    full_metric_names = get_metric_classes(output_feature_type)\n    metric_to_values: dict[str, float | int] = {\n        metric_name: metric_dict[metric_name] for metric_name in full_metric_names if metric_name in metric_dict\n    }\n    metric_names: set[str] = set(metric_to_values)\n\n    return MetricsSummary(\n        experiment_local_directory=experiment_local_directory,\n        config=config,\n        output_feature_name=output_feature_name,\n        output_feature_type=output_feature_type,\n        metric_to_values=metric_to_values,\n        metric_names=metric_names,\n    )\n\n\ndef build_metrics_diff(\n    dataset_name: str, base_experiment_name: str, experimental_experiment_name: str, local_directory: str\n) -> MetricsDiff:\n    \"\"\"Build a MetricsDiff object between two experiments on a dataset.\n\n    :param dataset_name: the name of the Ludwig dataset.\n    :param base_experiment_name: the name of the base experiment.\n    :param experimental_experiment_name: the name of the experimental experiment.\n    :param local_directory: the local directory where the experiment artifacts are downloaded.\n    \"\"\"\n    base_summary: MetricsSummary = build_metrics_summary(\n        os.path.join(local_directory, dataset_name, base_experiment_name)\n    )\n    experimental_summary: MetricsSummary = build_metrics_summary(\n        os.path.join(local_directory, dataset_name, experimental_experiment_name)\n    )\n\n    metrics_in_common = set(base_summary.metric_names).intersection(set(experimental_summary.metric_names))\n\n    metrics: list[MetricDiff] = [\n        build_diff(name, base_summary.metric_to_values[name], experimental_summary.metric_to_values[name])\n        for name in metrics_in_common\n    ]\n\n    return MetricsDiff(\n        dataset_name=dataset_name,\n        base_experiment_name=base_experiment_name,\n        experimental_experiment_name=experimental_experiment_name,\n        local_directory=local_directory,\n        base_summary=base_summary,\n        experimental_summary=experimental_summary,\n        metrics=metrics,\n    )\n\n\n##############################\n# Resource Usage Dataclasses #\n##############################\n\n\n@dataclass\nclass ResourceUsageSummary:\n    \"\"\"Summary of resource usage metrics from one experiment.\"\"\"\n\n    # The tag with which the code block/function is labeled.\n    code_block_tag: str\n\n    # Dictionary that maps from metric name to their values.\n    metric_to_values: dict[str, float | int]\n\n    # Names of metrics for the output feature.\n    metric_names: set[str]\n\n\n@dataclass\nclass ResourceUsageDiff:\n    \"\"\"Store resource usage diffs for two experiments.\"\"\"\n\n    # The tag with which the code block/function is labeled.\n    code_block_tag: str\n\n    # Name of the base experiment (the one we benchmark against).\n    base_experiment_name: str\n\n    # Name of the experimental experiment.\n    experimental_experiment_name: str\n\n    # `List[Diff]` containing diffs for metric of the two experiments.\n    metrics: list[MetricDiff]\n\n    def to_string(self):\n        ret = []\n        spacing_str = \"{:<36} {:<20} {:<20} {:<20} {:<5}\"\n        ret.append(\n            spacing_str.format(\n                \"Metric Name\",\n                self.base_experiment_name,\n                self.experimental_experiment_name,\n                \"Diff\",\n                \"Diff Percentage\",\n            )\n        )\n        for metric in sorted(self.metrics, key=lambda m: m.name):\n            diff_percentage = metric.diff_percentage\n            if isinstance(metric.diff_percentage, float):\n                diff_percentage = round(metric.diff_percentage, 3)\n            ret.append(\n                spacing_str.format(\n                    metric.name,\n                    metric.base_value_str,\n                    metric.experimental_value_str,\n                    metric.diff_str,\n                    diff_percentage,\n                )\n            )\n        return \"\\n\".join(ret)\n\n\ndef export_resource_usage_diff_to_csv(resource_usage_diff: ResourceUsageDiff, path: str):\n    \"\"\"Export resource usage metrics report to .csv.\n\n    :param resource_usage_diff: ResourceUsageDiff object containing the diff for two experiments on a dataset.\n    :param path: file name of the exported csv.\n    \"\"\"\n    with open(path, \"w\", newline=\"\") as f:\n        writer = csv.DictWriter(\n            f,\n            fieldnames=[\n                \"Code Block Tag\",\n                \"Metric Name\",\n                resource_usage_diff.base_experiment_name,\n                resource_usage_diff.experimental_experiment_name,\n                \"Diff\",\n                \"Diff Percentage\",\n            ],\n        )\n        writer.writeheader()\n\n        for metric in sorted(resource_usage_diff.metrics, key=lambda m: m.name):\n            diff_percentage = metric.diff_percentage\n            if isinstance(metric.diff_percentage, float):\n                diff_percentage = round(metric.diff_percentage, 3)\n            writer.writerow(\n                {\n                    \"Code Block Tag\": resource_usage_diff.code_block_tag,\n                    \"Metric Name\": metric.name,\n                    resource_usage_diff.base_experiment_name: metric.base_value_str,\n                    resource_usage_diff.experimental_experiment_name: metric.experimental_value_str,\n                    \"Diff\": metric.diff_str,\n                    \"Diff Percentage\": diff_percentage,\n                }\n            )\n        logger.info(f\"Exported a CSV report to {path}\\n\")\n\n\ndef average_runs(path_to_runs_dir: str) -> dict[str, int | float]:\n    \"\"\"Return average metrics from code blocks/function that ran more than once.\n\n    Metrics for code blocks/functions that were executed exactly once will be returned as is.\n\n    :param path_to_runs_dir: path to where metrics specific to a tag are stored.\n        e.g. resource_usage_out_dir/torch_ops_resource_usage/LudwigModel.evaluate/\n        This directory will contain JSON files with the following pattern run_*.json\n    \"\"\"\n    runs = [load_json(os.path.join(path_to_runs_dir, run)) for run in os.listdir(path_to_runs_dir)]\n    # asserting that keys to each of the dictionaries are consistent throughout the runs.\n    assert len(runs) == 1 or all(runs[i].keys() == runs[i + 1].keys() for i in range(len(runs) - 1))\n    runs_average = {\"num_runs\": len(runs)}\n    for key in runs[0]:\n        if isinstance(runs[0][key], (int, float)):\n            runs_average[key] = mean([run[key] for run in runs])\n    return runs_average\n\n\ndef summarize_resource_usage(path: str, tags: list[str] | None = None) -> list[ResourceUsageSummary]:\n    \"\"\"Create resource usage summaries for each code block/function that was decorated with ResourceUsageTracker.\n\n    Each entry of the list corresponds to the metrics collected from a code block/function run.\n    Important: code blocks that ran more than once are averaged.\n\n    :param path: corresponds to the `output_dir` argument in a ResourceUsageTracker run.\n    :param tags: (optional) list of tags to create summary for. If None, metrics from all tags will be summarized.\n    \"\"\"\n    summary = dict()\n    # metric types: system_resource_usage, torch_ops_resource_usage.\n    all_metric_types = {\"system_resource_usage\", \"torch_ops_resource_usage\"}\n    for metric_type in all_metric_types.intersection(os.listdir(path)):\n        metric_type_path = os.path.join(path, metric_type)\n        # code block tags correspond to the `tag` argument in ResourceUsageTracker.\n        for code_block_tag in os.listdir(metric_type_path):\n            if tags and code_block_tag not in tags:\n                continue\n            if code_block_tag not in summary:\n                summary[code_block_tag] = {}\n            run_path = os.path.join(metric_type_path, code_block_tag)\n            # Metrics from code blocks/functions that ran more than once are averaged.\n            summary[code_block_tag][metric_type] = average_runs(run_path)\n\n    summary_list = []\n    for code_block_tag, metric_type_dicts in summary.items():\n        merged_summary: dict[str, float | int] = {}\n        for metrics in metric_type_dicts.values():\n            assert \"num_runs\" in metrics\n            assert \"num_runs\" not in merged_summary or metrics[\"num_runs\"] == merged_summary[\"num_runs\"]\n            merged_summary.update(metrics)\n        summary_list.append(\n            ResourceUsageSummary(\n                code_block_tag=code_block_tag, metric_to_values=merged_summary, metric_names=set(merged_summary)\n            )\n        )\n    return summary_list\n\n\ndef build_resource_usage_diff(\n    base_path: str,\n    experimental_path: str,\n    base_experiment_name: str | None = None,\n    experimental_experiment_name: str | None = None,\n) -> list[ResourceUsageDiff]:\n    \"\"\"Build and return a ResourceUsageDiff object to diff resource usage metrics between two experiments.\n\n    :param base_path: corresponds to the `output_dir` argument in the base ResourceUsageTracker run.\n    :param experimental_path: corresponds to the `output_dir` argument in the experimental ResourceUsageTracker run.\n    \"\"\"\n    base_summary_list = summarize_resource_usage(base_path)\n    experimental_summary_list = summarize_resource_usage(experimental_path)\n\n    summaries_list = []\n    for base_summary in base_summary_list:\n        for experimental_summary in experimental_summary_list:\n            if base_summary.code_block_tag == experimental_summary.code_block_tag:\n                summaries_list.append((base_summary, experimental_summary))\n\n    diffs = []\n    for base_summary, experimental_summary in summaries_list:\n        metrics_in_common = set(base_summary.metric_names).intersection(set(experimental_summary.metric_names))\n        metrics: list[MetricDiff] = [\n            build_diff(name, base_summary.metric_to_values[name], experimental_summary.metric_to_values[name])\n            for name in metrics_in_common\n        ]\n        diff = ResourceUsageDiff(\n            code_block_tag=base_summary.code_block_tag,\n            base_experiment_name=base_experiment_name if base_experiment_name else \"experiment_1\",\n            experimental_experiment_name=(\n                experimental_experiment_name if experimental_experiment_name else \"experiment_2\"\n            ),\n            metrics=metrics,\n        )\n        diffs.append(diff)\n    return diffs\n"
  },
  {
    "path": "ludwig/benchmarking/utils.py",
    "content": "import asyncio\nimport functools\nimport logging\nimport os\nimport shutil\nimport uuid\nfrom concurrent.futures import ThreadPoolExecutor\nfrom types import ModuleType\nfrom typing import Any\n\nimport fsspec\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, CATEGORY\nfrom ludwig.datasets import model_configs_for_dataset\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\nfrom ludwig.globals import CONFIG_YAML, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.utils.data_utils import load_yaml\nfrom ludwig.utils.dataset_utils import get_repeatable_train_val_test_split\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import get_fs_and_path\n\nHYPEROPT_OUTDIR_RETAINED_FILES = [\n    \"hyperopt_statistics.json\",\n    \"params.json\",\n    \"stderr\",\n    \"stdout\",\n    \"result.json\",\n    \"error.txt\",\n]\nlogger = logging.getLogger()\n\n\ndef load_from_module(\n    dataset_module: DatasetLoader | ModuleType, output_feature: dict[str, str], subsample_frac: float = 1\n) -> pd.DataFrame:\n    \"\"\"Load the ludwig dataset, optionally subsamples it, and returns a repeatable split. A stratified split is\n    used for classification datasets.\n\n    Args:\n        dataset_module: ludwig datasets module (e.g. ludwig.datasets.sst2, ludwig.datasets.ames_housing, etc.)\n        subsample_frac: percentage of the total dataset to load.\n    \"\"\"\n    dataset = dataset_module.load(split=False)\n    if subsample_frac < 1:\n        dataset = dataset.sample(frac=subsample_frac, replace=False, random_state=default_random_seed)\n\n    if output_feature[\"type\"] in [CATEGORY, BINARY]:\n        return get_repeatable_train_val_test_split(\n            dataset,\n            stratify_colname=output_feature[\"name\"],\n            random_seed=default_random_seed,\n        )\n    else:\n        return get_repeatable_train_val_test_split(dataset, random_seed=default_random_seed)\n\n\ndef export_artifacts(experiment: dict[str, str], experiment_output_directory: str, export_base_path: str):\n    \"\"\"Save the experiment artifacts to the `bench_export_directory`.\n\n    Args:\n        experiment: experiment dict that contains \"dataset_name\" (e.g. ames_housing),\n            \"experiment_name\" (specified by user), and \"config_path\" (path to experiment config.\n            Relative to ludwig/benchmarks/configs).\n        experiment_output_directory: path where the model, data, and logs of the experiment are saved.\n        export_base_path: remote or local path (directory) where artifacts are\n            exported. (e.g. s3://benchmarking.us-west-2.ludwig.com/bench/ or your/local/bench/)\n    \"\"\"\n    protocol, _ = fsspec.core.split_protocol(export_base_path)\n    fs, _ = get_fs_and_path(export_base_path)\n    try:\n        export_full_path = os.path.join(export_base_path, experiment[\"dataset_name\"], experiment[\"experiment_name\"])\n\n        # override previous experiment with the same name\n        if fs.exists(export_full_path):\n            fs.rm(export_full_path, recursive=True)\n        fs.put(experiment_output_directory, export_full_path, recursive=True)\n        fs.put(\n            os.path.join(experiment[\"config_path\"]),\n            os.path.join(export_full_path, CONFIG_YAML),\n        )\n        logger.info(f\"Uploaded experiment artifact to\\n\\t{export_full_path}\")\n    except Exception:\n        logger.exception(\n            f\"Failed to upload experiment artifacts for experiment *{experiment['experiment_name']}* on \"\n            f\"dataset {experiment['dataset_name']}\"\n        )\n\n\ndef download_artifacts(\n    bench_config_path: str,\n    base_experiment: str,\n    experimental_experiment: str,\n    download_base_path: str,\n    local_dir: str = \"benchmarking_summaries\",\n) -> tuple[str, list[str]]:\n    \"\"\"Download benchmarking artifacts for two experiments.\n\n    Args:\n        bench_config_path: bench config file path. Can be the same one that was used to run\n            these experiments.\n        base_experiment: name of the experiment we're comparing against.\n        experimental_experiment: name of the experiment we're comparing.\n        download_base_path: base path under which live the stored artifacts of\n            the benchmarking experiments.\n    \"\"\"\n    bench_config = load_yaml(bench_config_path)\n    protocol, _ = fsspec.core.split_protocol(download_base_path)\n    fs, _ = get_fs_and_path(download_base_path)\n    os.makedirs(local_dir, exist_ok=True)\n\n    coroutines = []\n    for experiment in bench_config[\"experiments\"]:\n        dataset_name = experiment[\"dataset_name\"]\n        for experiment_name in [base_experiment, experimental_experiment]:\n            coroutines.append(download_one(fs, download_base_path, dataset_name, experiment_name, local_dir))\n    downloaded_names = asyncio.run(asyncio.gather(*coroutines, return_exceptions=True))\n\n    dataset_names = [experiment_tuple[0] for experiment_tuple in set(downloaded_names) if experiment_tuple[0]]\n    assert (\n        len({experiment_tuple[1] for experiment_tuple in downloaded_names}) == 1 and downloaded_names[0][1] == local_dir\n    ), \"Experiments not downloaded to the same path\"\n\n    return local_dir, dataset_names\n\n\n@DeveloperAPI\nasync def download_one(\n    fs, download_base_path: str, dataset_name: str, experiment_name: str, local_dir: str\n) -> tuple[str, str]:\n    \"\"\"Download `config.yaml` and `report.json` for an experiment.\n\n    Args:\n        fs: filesystem to use to download.\n        download_base_path: base path under which live the stored artifacts of\n            the benchmarking experiments.\n        dataset_name: name of the dataset we ran the experiments on.\n        experiment_name: name of the experiment (e.g. `v0.5.3_with_bert`)\n        local_dir: local directory under which the artifacts will be downloaded.\n    \"\"\"\n    loop = asyncio.get_running_loop()\n    local_experiment_dir = os.path.join(local_dir, dataset_name, experiment_name)\n    remote_experiment_directory = os.path.join(download_base_path, dataset_name, experiment_name)\n    os.makedirs(local_experiment_dir, exist_ok=True)\n    try:\n        with ThreadPoolExecutor() as pool:\n            func = functools.partial(\n                fs.get,\n                remote_experiment_directory,\n                local_experiment_dir,\n                recursive=True,\n            )\n            await loop.run_in_executor(pool, func)\n    except Exception:\n        logger.exception(f\"Couldn't download experiment *{experiment_name}* of dataset *{dataset_name}*.\")\n        return \"\", local_dir\n    return dataset_name, local_dir\n\n\ndef validate_benchmarking_config(benchmarking_config: dict[str, Any]) -> None:\n    \"\"\"Validates the parameters of the benchmarking config.\n\n    Args:\n        benchmarking_config: benchmarking config dictionary.\n\n    Raises:\n        ValueError if any of the expected parameters is not there.\n    \"\"\"\n    if \"experiment_name\" not in benchmarking_config and not all(\n        \"experiment_name\" in experiment for experiment in benchmarking_config[\"experiments\"]\n    ):\n        raise ValueError(\"You must either specify a global experiment name or an experiment name for each experiment.\")\n    if \"export\" not in benchmarking_config:\n        raise ValueError(\"\"\"You must specify export parameters. Example:\n            export:\n              export_artifacts: true\n              export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\n        \"\"\")\n    if \"experiments\" not in benchmarking_config:\n        raise ValueError(\"You must specify a list of experiments.\")\n    for experiment in benchmarking_config[\"experiments\"]:\n        if \"dataset_name\" not in experiment:\n            raise ValueError(\"A Ludwig dataset must be specified.\")\n\n\ndef populate_benchmarking_config_with_defaults(benchmarking_config: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Populates the parameters of the benchmarking config with defaults.\n\n    Args:\n        benchmarking_config: benchmarking config dictionary.\n    \"\"\"\n    if \"hyperopt\" not in benchmarking_config:\n        benchmarking_config[\"hyperopt\"] = False\n    if \"process_config_file_path\" not in benchmarking_config:\n        benchmarking_config[\"process_config_file_path\"] = None\n    if \"profiler\" not in benchmarking_config:\n        benchmarking_config[\"profiler\"] = {\"enable\": False, \"use_torch_profiler\": False, \"logging_interval\": None}\n    return benchmarking_config\n\n\ndef propagate_global_parameters(benchmarking_config: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Propagate the global parameters of the benchmarking config to local experiments.\n\n    Args:\n        benchmarking_config: benchmarking config dictionary.\n    \"\"\"\n    for experiment in benchmarking_config[\"experiments\"]:\n        if \"experiment_name\" not in experiment:\n            experiment[\"experiment_name\"] = benchmarking_config[\"experiment_name\"]\n        if \"export\" not in experiment:\n            experiment[\"export\"] = benchmarking_config[\"export\"]\n        if \"hyperopt\" not in experiment:\n            experiment[\"hyperopt\"] = benchmarking_config[\"hyperopt\"]\n        if \"process_config_file_path\" not in experiment:\n            experiment[\"process_config_file_path\"] = benchmarking_config[\"process_config_file_path\"]\n        if \"profiler\" not in experiment:\n            experiment[\"profiler\"] = benchmarking_config[\"profiler\"]\n    return benchmarking_config\n\n\ndef create_default_config(experiment: dict[str, Any]) -> str:\n    \"\"\"Create a Ludwig config that only contains input and output features.\n\n    Args:\n        experiment: experiment dictionary.\n\n    Returns:\n        path where the default config is saved.\n    \"\"\"\n    model_config = model_configs_for_dataset(experiment[\"dataset_name\"])[\"default\"]\n\n    # only keep input_features and output_features\n    main_config_keys = list(model_config.keys())\n    for key in main_config_keys:\n        if key not in [\"input_features\", \"output_features\"]:\n            del model_config[key]\n    config_path = f\"{experiment['dataset_name']}-{uuid.uuid4().hex}.yaml\"\n    save_yaml(config_path, model_config)\n    return config_path\n\n\ndef delete_model_checkpoints(output_directory: str):\n    \"\"\"Deletes outputs of the experiment run that we don't want to save with the artifacts.\n\n    Args:\n        output_directory: output directory of the hyperopt run.\n    \"\"\"\n    shutil.rmtree(os.path.join(output_directory, MODEL_FILE_NAME, \"training_checkpoints\"), ignore_errors=True)\n    if os.path.isfile(os.path.join(output_directory, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)):\n        os.remove(os.path.join(output_directory, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME))\n\n\ndef delete_hyperopt_outputs(output_directory: str):\n    \"\"\"Deletes outputs of the hyperopt run that we don't want to save with the artifacts.\n\n    Args:\n        output_directory: output directory of the hyperopt run.\n    \"\"\"\n    for path, currentDirectory, files in os.walk(output_directory):\n        for file in files:\n            filename = os.path.join(path, file)\n            if file not in HYPEROPT_OUTDIR_RETAINED_FILES:\n                os.remove(filename)\n\n\ndef save_yaml(filename, dictionary):\n    with open(filename, \"w\") as f:\n        yaml.dump(dictionary, f, default_flow_style=False)\n\n\ndef format_time(time_us):\n    \"\"\"Defines how to format time in FunctionEvent.\n\n    from https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler_util.py\n    \"\"\"\n    US_IN_SECOND = 1000.0 * 1000.0\n    US_IN_MS = 1000.0\n    if time_us >= US_IN_SECOND:\n        return f\"{time_us / US_IN_SECOND:.3f}s\"\n    if time_us >= US_IN_MS:\n        return f\"{time_us / US_IN_MS:.3f}ms\"\n    return f\"{time_us:.3f}us\"\n\n\ndef format_memory(nbytes):\n    \"\"\"Returns a formatted memory size string.\n\n    from https://github.com/pytorch/pytorch/blob/master/torch/autograd/profiler_util.py\n    \"\"\"\n    KB = 1024\n    MB = 1024 * KB\n    GB = 1024 * MB\n    if abs(nbytes) >= GB:\n        return f\"{nbytes * 1.0 / GB:.2f} Gb\"\n    elif abs(nbytes) >= MB:\n        return f\"{nbytes * 1.0 / MB:.2f} Mb\"\n    elif abs(nbytes) >= KB:\n        return f\"{nbytes * 1.0 / KB:.2f} Kb\"\n    else:\n        return str(nbytes) + \" b\"\n"
  },
  {
    "path": "ludwig/callbacks.py",
    "content": "# !/usr/bin/env python\n# Copyright (c) 2021 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom abc import ABC\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.types import HyperoptConfigDict, ModelConfigDict, TrainingSetMetadataDict\n\n\n@PublicAPI\nclass Callback(ABC):\n    def on_cmdline(self, cmd: str, *args: list[str]):\n        \"\"\"Called when Ludwig is run on the command line with the callback enabled.\n\n        :param cmd: The Ludwig subcommand being run, ex. \"train\", \"evaluate\", \"predict\", ...\n        :param args: The full list of command-line arguments (sys.argv).\n        \"\"\"\n\n    def on_preprocess_start(self, config: ModelConfigDict, **kwargs):\n        \"\"\"Called before preprocessing starts.\n\n        :param config: The config dictionary.\n        \"\"\"\n\n    def on_preprocess_end(\n        self,\n        training_set,\n        validation_set,\n        test_set,\n        training_set_metadata: TrainingSetMetadataDict,\n        **kwargs,\n    ):\n        \"\"\"Called after preprocessing ends.\n\n        :param training_set: The training set.\n        :type training_set: ludwig.dataset.base.Dataset\n        :param validation_set: The validation set.\n        :type validation_set: ludwig.dataset.base.Dataset\n        :param test_set: The test set.\n        :type test_set: ludwig.dataset.base.Dataset\n        :param training_set_metadata: Values inferred from the training set, including preprocessing settings,\n            vocabularies, feature statistics, etc. Same as training_set_metadata.json.\n        \"\"\"\n\n    def on_hyperopt_init(self, experiment_name: str, **kwargs):\n        \"\"\"Called to initialize state before hyperparameter optimization begins.\n\n        :param experiment_name: The name of the current experiment.\n        \"\"\"\n\n    def on_hyperopt_preprocessing_start(self, experiment_name: str, **kwargs):\n        \"\"\"Called before data preprocessing for hyperparameter optimization begins.\n\n        :param experiment_name: The name of the current experiment.\n        \"\"\"\n\n    def on_hyperopt_preprocessing_end(self, experiment_name: str, **kwargs):\n        \"\"\"Called after data preprocessing for hyperparameter optimization is completed.\n\n        :param experiment_name: The name of the current experiment.\n        \"\"\"\n\n    def on_hyperopt_start(self, experiment_name: str, **kwargs):\n        \"\"\"Called before any hyperparameter optimization trials are started.\n\n        :param experiment_name: The name of the current experiment.\n        \"\"\"\n\n    def on_hyperopt_end(self, experiment_name: str, **kwargs):\n        \"\"\"Called after all hyperparameter optimization trials are completed.\n\n        :param experiment_name: The name of the current experiment.\n        \"\"\"\n\n    def on_hyperopt_finish(self, experiment_name: str, **kwargs):\n        \"\"\"Deprecated.\n\n        Use on_hyperopt_end instead.\n        \"\"\"\n        # TODO(travis): remove in favor of on_hyperopt_end for naming consistency\n\n    def on_hyperopt_trial_start(self, parameters: HyperoptConfigDict, **kwargs):\n        \"\"\"Called before the start of each hyperparameter optimization trial.\n\n        :param parameters: The complete dictionary of parameters for this hyperparameter optimization experiment.\n        \"\"\"\n\n    def on_hyperopt_trial_end(self, parameters: HyperoptConfigDict, **kwargs):\n        \"\"\"Called after the end of each hyperparameter optimization trial.\n\n        :param parameters: The complete dictionary of parameters for this hyperparameter optimization experiment.\n        \"\"\"\n\n    def should_stop_hyperopt(self):\n        \"\"\"Returns true if the entire hyperopt run (all trials) should be stopped.\n\n        See: https://docs.ray.io/en/latest/tune/api_docs/stoppers.html#ray.tune.Stopper\n        \"\"\"\n        return False\n\n    def on_resume_training(self, is_coordinator: bool, **kwargs):\n        pass\n\n    def on_train_init(\n        self,\n        base_config: ModelConfigDict,\n        experiment_directory: str,\n        experiment_name: str,\n        model_name: str,\n        output_directory: str,\n        resume_directory: str | None,\n        **kwargs,\n    ):\n        \"\"\"Called after preprocessing, but before the creation of the model and trainer objects.\n\n        :param base_config: The user-specified config, before the insertion of defaults or inferred values.\n        :param experiment_directory: The experiment directory, same as output_directory if no experiment specified.\n        :param experiment_name: The experiment name.\n        :param model_name: The model name.\n        :param output_directory: file path to where training results are stored.\n        :param resume_directory: model directory to resume training from, or None.\n        \"\"\"\n\n    def on_train_start(\n        self,\n        model,\n        config: ModelConfigDict,\n        config_fp: str | None,\n        **kwargs,\n    ):\n        \"\"\"Called after creation of trainer, before the start of training.\n\n        :param model: The ludwig model.\n        :type model: ludwig.utils.torch_utils.LudwigModule\n        :param config: The config dictionary.\n        :param config_fp: The file path to the config, or none if config was passed to stdin.\n        \"\"\"\n\n    def on_train_end(self, output_directory: str, **kwargs):\n        \"\"\"Called at the end of training, before the model is saved.\n\n        :param output_directory: file path to where training results are stored.\n        \"\"\"\n\n    def on_trainer_train_setup(self, trainer, save_path: str, is_coordinator: bool, **kwargs):\n        \"\"\"Called in every trainer (distributed or local) before training starts.\n\n        :param trainer: The trainer instance.\n        :type trainer: trainer: ludwig.models.Trainer\n        :param save_path: The path to the directory model is saved in.\n        :param is_coordinator: Is this trainer the coordinator.\n        \"\"\"\n\n    def on_trainer_train_teardown(self, trainer, progress_tracker, save_path: str, is_coordinator: bool, **kwargs):\n        \"\"\"Called in every trainer (distributed or local) after training completes.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        :param is_coordinator: Is this trainer the coordinator.\n        \"\"\"\n\n    def on_batch_start(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator only before each batch.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_batch_end(self, trainer, progress_tracker, save_path: str, sync_step: bool = True, **kwargs):\n        \"\"\"Called on coordinator only after each batch.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        :param sync_step: Whether the model params were updated and synced in this step.\n        \"\"\"\n\n    def on_eval_start(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator at the start of evaluation.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_eval_end(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator at the end of evaluation.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_epoch_start(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator only before the start of each epoch.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_epoch_end(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator only after the end of each epoch.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_validation_start(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator before validation starts.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_validation_end(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator after validation is complete.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_test_start(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator before testing starts.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def on_test_end(self, trainer, progress_tracker, save_path: str, **kwargs):\n        \"\"\"Called on coordinator after testing ends.\n\n        :param trainer: The trainer instance.\n        :type trainer: ludwig.models.trainer.Trainer\n        :param progress_tracker: An object which tracks training progress.\n        :type progress_tracker: ludwig.utils.trainer_utils.ProgressTracker\n        :param save_path: The path to the directory model is saved in.\n        \"\"\"\n\n    def should_early_stop(self, trainer, progress_tracker, is_coordinator, **kwargs):\n        # Triggers early stopping if any callback on any worker returns True\n        return False\n\n    def on_checkpoint(self, trainer, progress_tracker, **kwargs):\n        \"\"\"Called after each checkpoint is passed, regardless of whether the model was evaluated or saved at that\n        checkpoint.\"\"\"\n\n    def on_save_best_checkpoint(self, trainer, progress_tracker, save_path, **kwargs):\n        \"\"\"Called on every worker immediately after a new best model is checkpointed.\"\"\"\n\n    def on_build_metadata_start(self, df, mode: str, **kwargs):\n        \"\"\"Called before building metadata for dataset.\n\n        :param df: The dataset.\n        :type df: pd.DataFrame\n        :param mode: \"prediction\", \"training\", or None.\n        \"\"\"\n\n    def on_build_metadata_end(self, df, mode, **kwargs):\n        \"\"\"Called after building dataset metadata.\n\n        :param df: The dataset.\n        :type df: pd.DataFrame\n        :param mode: \"prediction\", \"training\", or None.\n        \"\"\"\n\n    def on_build_data_start(self, df, mode, **kwargs):\n        \"\"\"Called before build_data, which does preprocessing, handling missing values, adding metadata to\n        training_set_metadata.\n\n        :param df: The dataset.\n        :type df: pd.DataFrame\n        :param mode: \"prediction\", \"training\", or None.\n        \"\"\"\n\n    def on_build_data_end(self, df, mode, **kwargs):\n        \"\"\"Called after build_data completes.\n\n        :param df: The dataset.\n        :type df: pd.DataFrame\n        :param mode: \"prediction\", \"training\", or None.\n        \"\"\"\n\n    def on_evaluation_start(self, **kwargs):\n        \"\"\"Called before preprocessing for evaluation.\"\"\"\n\n    def on_evaluation_end(self, **kwargs):\n        \"\"\"Called after evaluation is complete.\"\"\"\n\n    def on_visualize_figure(self, fig, **kwargs):\n        \"\"\"Called after a visualization is generated.\n\n        :param fig: The figure.\n        :type fig: matplotlib.figure.Figure\n        \"\"\"\n\n    def on_ludwig_end(self, **kwargs):\n        \"\"\"Convenience method for any cleanup.\n\n        Not yet implemented.\n        \"\"\"\n\n    def prepare_ray_tune(\n        self,\n        train_fn: Callable,\n        tune_config: dict[str, Any],\n        tune_callbacks: list[Callable],\n        **kwargs,\n    ):\n        \"\"\"Configures Ray Tune callback and config.\n\n        :param train_fn: The function which runs the experiment trial.\n        :param tune_config: The ray tune configuration dictionary.\n        :param tune_callbacks: List of callbacks (not used yet).\n        :returns: Tuple[Callable, Dict] The train_fn and tune_config, which will be passed to ray tune.\n        \"\"\"\n        return train_fn, tune_config\n"
  },
  {
    "path": "ludwig/check.py",
    "content": "import argparse\nimport logging\nimport tempfile\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import INPUT_FEATURES, OUTPUT_FEATURES, TRAINER\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nNUM_EXAMPLES = 100\n\n\n@DeveloperAPI\ndef check_install(logging_level: int = logging.INFO, **kwargs):\n    config = {\n        INPUT_FEATURES: [\n            {\"name\": \"in1\", \"type\": \"text\"},\n            {\"name\": \"in2\", \"type\": \"category\"},\n            {\"name\": \"in3\", \"type\": \"number\"},\n        ],\n        OUTPUT_FEATURES: [{\"name\": \"out1\", \"type\": \"binary\"}],\n        TRAINER: {\"epochs\": 2, \"batch_size\": 8},\n    }\n\n    try:\n        df = build_synthetic_dataset_df(NUM_EXAMPLES, config)\n        model = LudwigModel(config, logging_level=logging_level)\n        with tempfile.TemporaryDirectory() as tmpdir:\n            model.train(dataset=df, output_directory=tmpdir)\n    except Exception:\n        print(\"=== CHECK INSTALL COMPLETE... FAILURE ===\")\n        raise\n\n    print(\"=== CHECK INSTALL COMPLETE... SUCCESS ===\")\n\n\n@DeveloperAPI\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This command checks Ludwig installation on a synthetic dataset.\",\n        prog=\"ludwig check_install\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"warning\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    args = parser.parse_args(sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.check\")\n\n    print_ludwig(\"Check Install\", LUDWIG_VERSION)\n    check_install(**vars(args))\n"
  },
  {
    "path": "ludwig/cli.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport sys\n\nimport ludwig.contrib\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logo\n\n\nclass CLI:\n    \"\"\"CLI describes a command line interface for interacting with Ludwig.\n\n    Functions are described below.\n    \"\"\"\n\n    def __init__(self):\n        parser = argparse.ArgumentParser(\n            description=\"ludwig cli runner\",\n            usage=f\"\"\"\\n{get_logo(\"ludwig cli\", LUDWIG_VERSION)}\nludwig <command> [<args>]\n\nAvailable sub-commands:\n   train                 Trains a model\n   predict               Predicts using a pretrained model\n   evaluate              Evaluate a pretrained model's performance\n   forecast              Forecast the next n data points in a timeseries using a pretrained model\n   experiment            Runs a full experiment training a model and evaluating it\n   hyperopt              Perform hyperparameter optimization\n   benchmark             Run and track experiments on a number of datasets and configs, and export experiment artifacts.\n   serve                 Serves a pretrained model\n   visualize             Visualizes experimental results\n   collect_summary       Prints names of weights and layers activations to use with other collect commands\n   collect_weights       Collects tensors containing a pretrained model weights\n   collect_activations   Collects tensors for each datapoint using a pretrained model\n   datasets              Downloads and lists Ludwig-ready datasets\n   export_torchscript    Exports Ludwig models to Torchscript\n   export_triton         Exports Ludwig models to Triton\n   export_mlflow         Exports Ludwig models to MLflow\n   export_schema         Exports the Ludwig config JSON schema\n   preprocess            Preprocess data and saves it into HDF5 and JSON format\n   synthesize_dataset    Creates synthetic data for testing purposes\n   init_config           Initialize a user config from a dataset and targets\n   render_config         Renders the fully populated config with all defaults set\n   check_install         Runs a quick training run on synthetic data to verify installation status\n   upload                Push trained model artifacts to a registry (e.g., Predibase, HuggingFace Hub)\n\"\"\",\n        )\n        parser.add_argument(\"command\", help=\"Subcommand to run\")\n        # parse_args defaults to [1:] for args, but you need to\n        # exclude the rest of the args too, or validation will fail\n        args = parser.parse_args(sys.argv[1:2])\n        if not hasattr(self, args.command):\n            print(\"Unrecognized command\")\n            parser.print_help()\n            exit(1)\n        # use dispatch pattern to invoke method with same name\n        getattr(self, args.command)()\n\n    def train(self):\n        from ludwig import train\n\n        train.cli(sys.argv[2:])\n\n    def predict(self):\n        from ludwig import predict\n\n        predict.cli(sys.argv[2:])\n\n    def evaluate(self):\n        from ludwig import evaluate\n\n        evaluate.cli(sys.argv[2:])\n\n    def forecast(self):\n        from ludwig import forecast\n\n        forecast.cli(sys.argv[2:])\n\n    def experiment(self):\n        from ludwig import experiment\n\n        experiment.cli(sys.argv[2:])\n\n    def hyperopt(self):\n        from ludwig import hyperopt_cli\n\n        hyperopt_cli.cli(sys.argv[2:])\n\n    def benchmark(self):\n        from ludwig.benchmarking import benchmark\n\n        benchmark.cli(sys.argv[2:])\n\n    def serve(self):\n        from ludwig import serve\n\n        serve.cli(sys.argv[2:])\n\n    def visualize(self):\n        from ludwig import visualize\n\n        visualize.cli(sys.argv[2:])\n\n    def collect_summary(self):\n        from ludwig import collect\n\n        collect.cli_collect_summary(sys.argv[2:])\n\n    def collect_weights(self):\n        from ludwig import collect\n\n        collect.cli_collect_weights(sys.argv[2:])\n\n    def collect_activations(self):\n        from ludwig import collect\n\n        collect.cli_collect_activations(sys.argv[2:])\n\n    def export_torchscript(self):\n        from ludwig import export\n\n        export.cli_export_torchscript(sys.argv[2:])\n\n    def export_triton(self):\n        from ludwig import export\n\n        export.cli_export_triton(sys.argv[2:])\n\n    def export_mlflow(self):\n        from ludwig import export\n\n        export.cli_export_mlflow(sys.argv[2:])\n\n    def export_schema(self):\n        from ludwig.schema.export_schema import main as export_schema_main\n\n        export_schema_main(sys.argv[2:])\n\n    def preprocess(self):\n        from ludwig import preprocess\n\n        preprocess.cli(sys.argv[2:])\n\n    def synthesize_dataset(self):\n        from ludwig.data import dataset_synthesizer\n\n        dataset_synthesizer.cli(sys.argv[2:])\n\n    def init_config(self):\n        from ludwig import automl\n\n        automl.cli_init_config(sys.argv[2:])\n\n    def render_config(self):\n        from ludwig.utils import defaults\n\n        defaults.cli_render_config(sys.argv[2:])\n\n    def check_install(self):\n        from ludwig import check\n\n        check.cli(sys.argv[2:])\n\n    def datasets(self):\n        from ludwig import datasets\n\n        datasets.cli(sys.argv[2:])\n\n    def upload(self):\n        from ludwig import upload\n\n        upload.cli(sys.argv[2:])\n\n\ndef main():\n    ludwig.contrib.preload(sys.argv)\n    CLI()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "ludwig/collect.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport importlib\nimport logging\nimport os\nimport sys\n\nimport numpy as np\nimport torch\nimport torchinfo\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import FULL, TEST, TRAINING, VALIDATION\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_boxed, print_ludwig\nfrom ludwig.utils.strings_utils import make_safe_filename\n\nlogger = logging.getLogger(__name__)\n\n\ndef collect_activations(\n    model_path: str,\n    layers: list[str],\n    dataset: str,\n    data_format: str = None,\n    split: str = FULL,\n    batch_size: int = 128,\n    output_directory: str = \"results\",\n    gpus: list[str] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    **kwargs,\n) -> list[str]:\n    \"\"\"Uses the pretrained model to collect the tensors corresponding to a datapoint in the dataset. Saves the\n    tensors to the experiment directory.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param layers: (List[str]) list of strings for layer names in the model\n        to collect activations.\n    :param dataset: (str) source\n        containing the data to make predictions.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param split: (str, default: `full`) split on which\n        to perform predictions. Valid values are `'training'`, `'validation'`,\n        `'test'` and `'full'`.\n    :param batch_size: (int, default `128`) size of batches for processing.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n\n    # Return\n\n    :return: (List[str]) list of filepath to `*.npy` files containing\n        the activations.\n    \"\"\"\n    logger.info(f\"Dataset path: {dataset}\")\n    logger.info(f\"Model path: {model_path}\")\n    logger.info(f\"Output path: {output_directory}\")\n    logger.info(\"\\n\")\n\n    model = LudwigModel.load(\n        model_path,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n        backend=backend,\n    )\n\n    # collect activations\n    print_boxed(\"COLLECT ACTIVATIONS\")\n    collected_tensors = model.collect_activations(\n        layers, dataset, data_format=data_format, split=split, batch_size=batch_size\n    )\n\n    # saving\n    os.makedirs(output_directory, exist_ok=True)\n    saved_filenames = save_tensors(collected_tensors, output_directory)\n\n    logger.info(f\"Saved to: {output_directory}\")\n    return saved_filenames\n\n\ndef collect_weights(model_path: str, tensors: list[str], output_directory: str = \"results\", **kwargs) -> list[str]:\n    \"\"\"Loads a pretrained model and collects weights.\n\n    # Inputs\n    :param model_path: (str) filepath to pre-trained model.\n    :param tensors: (list, default: `None`) List of tensor names to collect\n        weights\n    :param output_directory: (str, default: `'results'`) the directory where\n        collected weights will be stored.\n\n    # Return\n\n    :return: (List[str]) list of filepath to `*.npy` files containing\n        the weights.\n    \"\"\"\n    logger.info(f\"Model path: {model_path}\")\n    logger.info(f\"Output path: {output_directory}\")\n    logger.info(\"\\n\")\n\n    model = LudwigModel.load(model_path)\n\n    # collect weights\n    print_boxed(\"COLLECT WEIGHTS\")\n    collected_tensors = model.collect_weights(tensors)\n\n    # saving\n    os.makedirs(output_directory, exist_ok=True)\n    saved_filenames = save_tensors(collected_tensors, output_directory)\n\n    logger.info(f\"Saved to: {output_directory}\")\n    return saved_filenames\n\n\ndef save_tensors(collected_tensors, output_directory):\n    filenames = []\n    for tensor_name, tensor_value in collected_tensors:\n        np_filename = os.path.join(output_directory, make_safe_filename(tensor_name) + \".npy\")\n        if isinstance(tensor_value, torch.Tensor):\n            # Skip non-tensor collected artifacts, e.g. used_tokens.\n            np.save(np_filename, tensor_value.detach().cpu().numpy())\n            filenames.append(np_filename)\n    return filenames\n\n\ndef print_model_summary(model_path: str, **kwargs) -> None:\n    \"\"\"Loads a pretrained model and prints names of weights and layers activations.\n\n    # Inputs\n    :param model_path: (str) filepath to pre-trained model.\n\n    # Return\n    :return: (`None`)\n    \"\"\"\n    model = LudwigModel.load(model_path)\n    # Move model to CPU for torchinfo summary to avoid device mismatch issues.\n    model.model.cpu()\n    logger.info(torchinfo.summary(model.model, input_data=[model.model.get_model_inputs()], depth=20))\n\n    logger.info(\"\\nModules:\\n\")\n    for name, _ in model.model.named_children():\n        logger.info(name)\n\n    logger.info(\"\\nParameters:\\n\")\n    for name, _ in model.model.named_parameters():\n        logger.info(name)\n\n\ndef pretrained_summary(pretrained_model: str, **kwargs) -> None:\n    \"\"\"Loads a pretrained model from Huggingface or Torchvision models and prints names of layers.\n\n    # Inputs\n    :param pretrained_model: (str) name of model to load (case sensitive).\n\n    # Return\n    :return: (`None`)\n    \"\"\"\n    from transformers import AutoConfig, AutoModel\n\n    model = None\n    # get access token if available\n    token = os.getenv(\"HUGGING_FACE_HUB_TOKEN\")\n    if token is None:\n        logger.info(\"No token provided. Continuing loading without token access.\")\n    elif not token:\n        raise ValueError(\"Invalid token provided. Exiting.\")\n    else:\n        logger.info(\"Valid token provided. Proceeding with token access.\")\n\n    # Try to load from transformers/HF\n    # TODO -> Fix OOM on large models e.g. llama 3 8B\n    try:\n        config = AutoConfig.from_pretrained(pretrained_model, token=token, low_cpu_mem_usage=True)\n        model = AutoModel.from_config(config=config)\n        logger.info(f\"Loaded {pretrained_model} from Hugging Face Transformers.\")\n    except Exception as e:\n        logger.error(f\"Failed to load {pretrained_model} from Hugging Face Transformers: {e}\")\n\n    # Try and load from torchvision-models\n    if model is None:\n        try:\n            module = importlib.import_module(\"torchvision.models\")\n            model = getattr(module, pretrained_model)(weights=None)\n        except AttributeError:\n            logger.error(f\"{pretrained_model} is not a valid torchvision model.\")\n\n    if model:\n        for name, _ in model.named_parameters():\n            logger.info(name)\n    else:\n        logger.error(f\"Unable to load the model {pretrained_model} from any known source.\")\n\n\ndef cli_collect_activations(sys_argv):\n    \"\"\"Command Line Interface to communicate with the collection of tensors and there are several options that can\n    specified when calling this function:\n\n    --data_csv: Filepath for the input csv\n    --data_hdf5: Filepath for the input hdf5 file, if there is a csv file, this\n                 is not read\n    --d: Refers to the dataset type of the file being read, by default is\n         *generic*\n    --s: Refers to the split of the data, can be one of: train, test,\n         validation, full\n    --m: Input model that is necessary to collect to the tensors, this is a\n         required *option*\n    --t: Tensors to collect\n    --od: Output directory of the model, defaults to results\n    --bs: Batch size\n    --g: Number of gpus that are to be used\n    --gf: Fraction of each GPUs memory to use.\n    --v: Verbose: Defines the logging level that the user will be exposed to\n    \"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model and uses it collect \"\n        \"tensors for each datapoint in the dataset.\",\n        prog=\"ludwig collect_activations\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\"--dataset\", help=\"input data file path\", required=True)\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n    parser.add_argument(\n        \"-s\",\n        \"--split\",\n        default=FULL,\n        choices=[TRAINING, VALIDATION, TEST, FULL],\n        help=\"the split to obtain the model activations from\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\"-lyr\", \"--layers\", help=\"tensors to collect\", nargs=\"+\", required=True)\n\n    # -------------------------\n    # Output results parameters\n    # -------------------------\n    parser.add_argument(\n        \"-od\", \"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\"\n    )\n\n    # ------------------\n    # Generic parameters\n    # ------------------\n    parser.add_argument(\"-bs\", \"--batch_size\", type=int, default=128, help=\"size of batches\")\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\"-g\", \"--gpus\", type=int, default=0, help=\"list of gpu to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-dpt\",\n        \"--disable_parallel_threads\",\n        action=\"store_false\",\n        dest=\"allow_parallel_threads\",\n        help=\"disable PyTorch from using multithreading for reproducibility\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"collect_activations\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.collect\")\n\n    print_ludwig(\"Collect Activations\", LUDWIG_VERSION)\n\n    collect_activations(**vars(args))\n\n\ndef cli_collect_weights(sys_argv):\n    \"\"\"Command Line Interface to collecting the weights for the model.\n\n    --m: Input model that is necessary to collect to the tensors, this is a\n         required *option*\n    --t: Tensors to collect\n    --od: Output directory of the model, defaults to results\n    --v: Verbose: Defines the logging level that the user will be exposed to\n    \"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \" \"and uses it collect weights.\",\n        prog=\"ludwig collect_weights\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\"-t\", \"--tensors\", help=\"tensors to collect\", nargs=\"+\", required=True)\n\n    # -------------------------\n    # Output results parameters\n    # -------------------------\n    parser.add_argument(\n        \"-od\", \"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\"\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"collect_weights\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.collect\")\n\n    print_ludwig(\"Collect Weights\", LUDWIG_VERSION)\n\n    collect_weights(**vars(args))\n\n\ndef cli_collect_summary(sys_argv):\n    \"\"\"Command Line Interface to collecting a summary of the model layers and weights.\n\n    --m: Input model that is necessary to collect to the tensors\n    --pm: Model name in order to fetch from Huggingface or Torchvision\n    --v: Verbose: Defines the logging level that the user will be exposed to\n    \"\"\"\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \"\n        \"and prints names of weights and layers activations \"\n        \"to use with other collect commands\",\n        prog=\"ludwig collect_summary\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=False)\n    parser.add_argument(\n        \"-pm\", \"--pretrained_model\", help=\"pretrained model to summarize (torchvision and huggingface)\", required=False\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"collect_summary\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.collect\")\n\n    print_ludwig(\"Collect Summary\", LUDWIG_VERSION)\n\n    if args.model_path:\n        print_model_summary(**vars(args))\n    elif args.pretrained_model and not args.model_path:\n        pretrained_summary(**vars(args))\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) > 1:\n        if sys.argv[1] == \"activations\":\n            cli_collect_activations(sys.argv[2:])\n        elif sys.argv[1] == \"weights\":\n            cli_collect_weights(sys.argv[2:])\n        elif sys.argv[1] == \"names\":\n            cli_collect_summary(sys.argv[2:])\n        else:\n            print(\"Unrecognized command\")\n    else:\n        print(\"Unrecognized command\")\n"
  },
  {
    "path": "ludwig/combiners/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/combiners/combiners.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom abc import ABC\nfrom dataclasses import dataclass\nfrom functools import lru_cache\n\nimport torch\nfrom torch.nn import Linear, ModuleList\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, ENCODER_OUTPUT, NUMBER\nfrom ludwig.encoders.registry import get_sequence_encoder_registry\nfrom ludwig.features.base_feature import InputFeature\nfrom ludwig.modules.attention_modules import TransformerStack\nfrom ludwig.modules.embedding_modules import Embed\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.modules.tabnet_modules import TabNet\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.comparator import ComparatorCombinerConfig\nfrom ludwig.schema.combiners.concat import ConcatCombinerConfig\nfrom ludwig.schema.combiners.project_aggregate import ProjectAggregateCombinerConfig\nfrom ludwig.schema.combiners.sequence import SequenceCombinerConfig\nfrom ludwig.schema.combiners.sequence_concat import SequenceConcatCombinerConfig\nfrom ludwig.schema.combiners.tab_transformer import TabTransformerCombinerConfig\nfrom ludwig.schema.combiners.tabnet import TabNetCombinerConfig\nfrom ludwig.schema.combiners.transformer import TransformerCombinerConfig\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.registry import Registry\nfrom ludwig.utils.torch_utils import LudwigModule, sequence_length_3D\nfrom ludwig.utils.torch_utils import sequence_mask as torch_sequence_mask\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass Handle:\n    \"\"\"This class provides an opaque handle to the input features, preventing them from being registered as state.\n\n    This is important because we already reference the `input_features` as an attribute of ECD, so we don't need it to\n    appear twice in the state_dict. Furthermore, DeepSpeed will get terribly confused if have the input features set as\n    an attribute of the combiner, and lead to shape mismatch errors when we go to load a saved checkpoint.\n    \"\"\"\n\n    input_features: dict[str, \"InputFeature\"]\n\n\n@DeveloperAPI\nclass Combiner(LudwigModule, ABC):\n    \"\"\"Base class for combiners, which implements common properties.\n\n    Subclasses will usually override:     __init__()        to set properties and allocate resources.  Should call\n    super().__init__(input_features).     forward()         performs the forward pass given a dictionary of encoder\n    outputs.     get_schema_cls()  must returns the class of the corresponding schema for the combiner type.\n    \"\"\"\n\n    def __init__(self, input_features: dict[str, \"InputFeature\"]):\n        super().__init__()\n        self.handle = Handle(input_features)\n\n    @property\n    def concatenated_shape(self) -> torch.Size:\n        # compute the size of the last dimension for the incoming encoder outputs\n        # this is required to setup the fully connected layer\n        shapes = [\n            torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape]))\n            for k in self.handle.input_features\n        ]\n        return torch.Size([torch.sum(torch.Tensor(shapes)).type(torch.int32)])\n\n    @property\n    def input_shape(self) -> dict:\n        # input to combiner is a dictionary of the input features encoder\n        # outputs, this property returns dictionary of output shapes for each\n        # input feature's encoder output shapes.\n        return {k: self.handle.input_features.get(k).output_shape for k in self.handle.input_features}\n\n    @property\n    @lru_cache(maxsize=1)\n    def output_shape(self) -> torch.Size:\n        pseudo_input = {}\n        for k in self.handle.input_features:\n            pseudo_input[k] = {\n                ENCODER_OUTPUT: torch.rand(\n                    2, *self.handle.input_features.get(k).output_shape, dtype=self.input_dtype, device=self.device\n                )\n            }\n        output_tensor = self.forward(pseudo_input)\n        return output_tensor[\"combiner_output\"].size()[1:]\n\n\ncombiner_impl_registry = Registry[type[Combiner]]()\n\n\ndef register_combiner(config_cls: type[BaseCombinerConfig]):\n    def wrap(cls: type[Combiner]):\n        combiner_impl_registry[config_cls] = cls\n        return cls\n\n    return wrap\n\n\ndef create_combiner(config: BaseCombinerConfig, **kwargs) -> Combiner:\n    return combiner_impl_registry[type(config)](config=config, **kwargs)\n\n\n@register_combiner(ConcatCombinerConfig)\nclass ConcatCombiner(Combiner):\n    def __init__(self, input_features: dict[str, \"InputFeature\"] = None, config: ConcatCombinerConfig = None, **kwargs):\n        super().__init__(input_features)\n        self.name = \"ConcatCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        self.flatten_inputs = config.flatten_inputs\n        self.fc_stack = None\n\n        # todo future: this may be redundant, check\n        fc_layers = config.fc_layers\n        if fc_layers is None:\n            fc_layers = []\n            for i in range(config.num_fc_layers):\n                fc_layers.append({\"output_size\": config.output_size})\n        self.fc_layers = fc_layers\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=self.concatenated_shape[-1],\n            layers=config.fc_layers,\n            num_layers=config.num_fc_layers,\n            default_output_size=config.output_size,\n            default_use_bias=config.use_bias,\n            default_weights_initializer=config.weights_initializer,\n            default_bias_initializer=config.bias_initializer,\n            default_norm=config.norm,\n            default_norm_params=config.norm_params,\n            default_activation=config.activation,\n            default_dropout=config.dropout,\n            residual=config.residual,\n        )\n\n        if input_features and len(input_features) == 1 and self.fc_layers is None:\n            self.supports_masking = True\n\n    def forward(self, inputs: dict) -> dict:  # encoder outputs\n        encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs]\n\n        # ================ Flatten ================\n        if self.flatten_inputs:\n            batch_size = encoder_outputs[0].shape[0]\n            encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs]\n\n        # ================ Concat ================\n        if len(encoder_outputs) > 1:\n            hidden = torch.cat(encoder_outputs, 1)\n        else:\n            hidden = list(encoder_outputs)[0]\n\n        # ================ Fully Connected ================\n        hidden = self.fc_stack(hidden)\n\n        return_data = {\"combiner_output\": hidden}\n\n        if len(inputs) == 1:\n            # Workaround for including additional tensors from output of input encoders for\n            # potential use in decoders, e.g. LSTM state for seq2seq.\n            # TODO(Justin): Think about how to make this communication work for multi-sequence\n            # features. Other combiners.\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n\n\n@register_combiner(SequenceConcatCombinerConfig)\nclass SequenceConcatCombiner(Combiner):\n    def __init__(\n        self, input_features: dict[str, \"InputFeature\"], config: SequenceConcatCombinerConfig = None, **kwargs\n    ):\n        super().__init__(input_features)\n        self.name = \"SequenceConcatCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        self.reduce_output = config.reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=config.reduce_output,\n            max_sequence_length=self.concatenated_shape[0],\n            encoding_size=self.concatenated_shape[1],\n        )\n        if self.reduce_output is None:\n            self.supports_masking = True\n        self.main_sequence_feature = config.main_sequence_feature\n\n    @property\n    def concatenated_shape(self) -> torch.Size:\n        # computes the effective shape of the input tensor after combining\n        # all the encoder outputs\n        # determine max sequence length by finding the first sequence tensor\n        # assume all the sequences are of the same size, if not true\n        # this will be caught during processing\n        seq_size = None\n        for k in self.handle.input_features:\n            # dim-2 output_shape implies a sequence [seq_size, hidden]\n            if len(self.handle.input_features.get(k).output_shape) == 2:\n                seq_size = self.handle.input_features.get(k).output_shape[0]\n                break\n\n        # collect the size of the last dimension for all input feature\n        # encoder outputs\n        shapes = [\n            self.handle.input_features.get(k).output_shape[-1] for k in self.handle.input_features\n        ]  # output shape not input shape\n        return torch.Size([seq_size, sum(shapes)])\n\n    def forward(self, inputs: dict) -> dict:  # encoder outputs\n        if self.main_sequence_feature is None or self.main_sequence_feature not in inputs:\n            for if_name, if_outputs in inputs.items():\n                # todo: when https://github.com/ludwig-ai/ludwig/issues/810 is closed\n                #       convert following test from using shape to use explicit\n                #       if_outputs[TYPE] values for sequence features\n                if len(if_outputs[ENCODER_OUTPUT].shape) == 3:\n                    self.main_sequence_feature = if_name\n                    break\n\n        if self.main_sequence_feature is None:\n            raise Exception(\"No sequence feature available for sequence combiner\")\n\n        main_sequence_feature_encoding = inputs[self.main_sequence_feature]\n\n        representation = main_sequence_feature_encoding[ENCODER_OUTPUT]\n        representations = [representation]\n\n        sequence_max_length = representation.shape[1]\n        sequence_length = sequence_length_3D(representation)\n\n        # ================ Concat ================\n        for if_name, if_outputs in inputs.items():\n            if if_name != self.main_sequence_feature:\n                if_representation = if_outputs[ENCODER_OUTPUT]\n                if len(if_representation.shape) == 3:\n                    # The following check makes sense when\n                    # both representations have a specified\n                    # sequence length dimension. If they do not,\n                    # then this check is simply checking if None == None\n                    # and will not catch discrepancies in the different\n                    # feature length dimension. Those errors will show up\n                    # at training time. Possible solutions to this is\n                    # to enforce a length second dimension in\n                    # sequential feature placeholders, but that\n                    # does not work with BucketedBatcher that requires\n                    # the second dimension to be undefined in order to be\n                    # able to trim the data points and speed up computation.\n                    # So for now we are keeping things like this, make sure\n                    # to write in the documentation that training time\n                    # dimensions mismatch may occur if the sequential\n                    # features have different lengths for some data points.\n                    if if_representation.shape[1] != representation.shape[1]:\n                        raise ValueError(\n                            \"The sequence length of the input feature {} \"\n                            \"is {} and is different from the sequence \"\n                            \"length of the main sequence feature {} which \"\n                            \"is {}.\\n Shape of {}: {}, shape of {}: {}.\\n\"\n                            \"Sequence lengths of all sequential features \"\n                            \"must be the same  in order to be concatenated \"\n                            \"by the sequence concat combiner. \"\n                            \"Try to impose the same max sequence length \"\n                            \"as a preprocessing parameter to both features \"\n                            \"or to reduce the output of {}.\".format(\n                                if_name,\n                                if_representation.shape[1],\n                                self.main_sequence_feature,\n                                representation.shape[1],\n                                if_name,\n                                if_representation.shape,\n                                if_name,\n                                representation.shape,\n                                if_name,\n                            )\n                        )\n                    # this assumes all sequence representations have the\n                    # same sequence length, 2nd dimension\n                    representations.append(if_representation)\n\n                elif len(if_representation.shape) == 2:\n                    multipliers = (1, sequence_max_length, 1)\n                    tiled_representation = torch.tile(torch.unsqueeze(if_representation, 1), multipliers)\n                    representations.append(tiled_representation)\n\n                else:\n                    raise ValueError(\n                        \"The representation of {} has rank {} and cannot be\"\n                        \" concatenated by a sequence concat combiner. \"\n                        \"Only rank 2 and rank 3 tensors are supported.\".format(if_name, len(if_representation.shape))\n                    )\n\n        hidden = torch.cat(representations, 2)\n        logger.debug(f\"  concat_hidden: {hidden}\")\n\n        # ================ Mask ================\n        sequence_mask = torch_sequence_mask(sequence_length, sequence_max_length)\n        hidden = torch.multiply(hidden, torch.unsqueeze(sequence_mask, -1).type(torch.float32))\n\n        # ================ Reduce ================\n        hidden = self.reduce_sequence(hidden)\n\n        return_data = {\"combiner_output\": hidden}\n\n        if len(inputs) == 1:\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n\n\n@register_combiner(SequenceCombinerConfig)\nclass SequenceCombiner(Combiner):\n    def __init__(self, input_features: dict[str, \"InputFeature\"], config: SequenceCombinerConfig = None, **kwargs):\n        super().__init__(input_features)\n        self.name = \"SequenceCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        self.combiner = SequenceConcatCombiner(\n            input_features,\n            config=SequenceConcatCombinerConfig(reduce_output=None, main_sequence_feature=config.main_sequence_feature),\n        )\n\n        logger.debug(\n            f\"combiner input shape {self.combiner.concatenated_shape}, \" f\"output shape {self.combiner.output_shape}\"\n        )\n\n        self.encoder_obj = get_from_registry(config.encoder.type, get_sequence_encoder_registry())(\n            should_embed=False,\n            reduce_output=config.reduce_output,\n            embedding_size=self.combiner.output_shape[1],\n            max_sequence_length=self.combiner.output_shape[0],\n            **kwargs,\n        )\n\n        if hasattr(self.encoder_obj, \"supports_masking\") and self.encoder_obj.supports_masking:\n            self.supports_masking = True\n\n    @property\n    def concatenated_shape(self) -> torch.Size:\n        # computes the effective shape of the input tensor after combining\n        # all the encoder outputs\n        # determine max sequence length by finding the first sequence tensor\n        # assume all the sequences are of the same size, if not true\n        # this will be caught during processing\n        seq_size = None\n        for k in self.handle.input_features:\n            # dim-2 output_shape implies a sequence [seq_size, hidden]\n            if len(self.handle.input_features.get(k).output_shape) == 2:\n                seq_size = self.handle.input_features.get(k).output_shape[0]\n                break\n\n        # collect the size of the last dimension for all input feature\n        # encoder outputs\n        shapes = [\n            self.handle.input_features.get(k).output_shape[-1] for k in self.handle.input_features\n        ]  # output shape not input shape\n        return torch.Size([seq_size, sum(shapes)])\n\n    def forward(self, inputs: dict) -> dict:  # encoder outputs\n        # ================ Concat ================\n        hidden = self.combiner(inputs)\n\n        # ================ Sequence encoding ================\n        hidden = self.encoder_obj(hidden[\"combiner_output\"])\n\n        return_data = {\"combiner_output\": hidden[ENCODER_OUTPUT]}\n        for key, value in hidden.items():\n            if key != ENCODER_OUTPUT:\n                return_data[key] = value\n\n        return return_data\n\n\n@register_combiner(TabNetCombinerConfig)\nclass TabNetCombiner(Combiner):\n    def __init__(\n        self, input_features: dict[str, \"InputFeature\"], config: TabNetCombinerConfig = None, **kwargs\n    ) -> None:\n        super().__init__(input_features)\n        self.name = \"TabNetCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        self.tabnet = TabNet(\n            self.concatenated_shape[-1],\n            config.size,\n            config.output_size,\n            num_steps=config.num_steps,\n            num_total_blocks=config.num_total_blocks,\n            num_shared_blocks=config.num_shared_blocks,\n            relaxation_factor=config.relaxation_factor,\n            bn_epsilon=config.bn_epsilon,\n            bn_momentum=config.bn_momentum,\n            bn_virtual_bs=config.bn_virtual_bs,\n            sparsity=config.sparsity,\n            entmax_mode=config.entmax_mode,\n            entmax_alpha=config.entmax_alpha,\n        )\n\n        if config.dropout > 0:\n            self.dropout = torch.nn.Dropout(config.dropout)\n        else:\n            self.dropout = None\n\n    @property\n    def concatenated_shape(self) -> torch.Size:\n        # compute the size of the last dimension for the incoming encoder outputs\n        # this is required to setup\n        shapes = [\n            torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape]))\n            for k in self.handle.input_features\n        ]\n        return torch.Size([torch.sum(torch.Tensor(shapes)).type(torch.int32)])\n\n    def forward(\n        self,\n        inputs: torch.Tensor,  # encoder outputs\n    ) -> dict:\n        encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs]\n\n        # ================ Flatten ================\n        batch_size = encoder_outputs[0].shape[0]\n        encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs]\n\n        # ================ Concat ================\n        if len(encoder_outputs) > 1:\n            hidden = torch.cat(encoder_outputs, 1)\n        else:\n            hidden = list(encoder_outputs)[0]\n\n        # ================ TabNet ================\n        hidden, aggregated_mask, masks = self.tabnet(hidden)\n        if self.dropout:\n            hidden = self.dropout(hidden)\n\n        return_data = {\n            \"combiner_output\": hidden,\n            \"aggregated_attention_masks\": aggregated_mask,\n            \"attention_masks\": masks,\n        }\n\n        if len(inputs) == 1:\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.tabnet.output_shape\n\n\n@register_combiner(TransformerCombinerConfig)\nclass TransformerCombiner(Combiner):\n    def __init__(\n        self, input_features: dict[str, \"InputFeature\"] = None, config: TransformerCombinerConfig = None, **kwargs\n    ):\n        super().__init__(input_features)\n        self.name = \"TransformerCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        self.reduce_output = config.reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=config.reduce_output,\n            max_sequence_length=len(input_features),\n            encoding_size=config.hidden_size,\n        )\n        if self.reduce_output is None:\n            self.supports_masking = True\n\n        # max sequence length for Transformer layer is number of input features\n        self.max_sequence_length = len(input_features)\n\n        logger.debug(\"  Projectors\")\n        self.projectors = ModuleList(\n            # regardless of rank-2 or rank-3 input, torch.prod() calculates size\n            # after flattening the encoder output tensor\n            [\n                Linear(\n                    torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32),\n                    config.hidden_size,\n                )\n                for inp in input_features\n            ]\n        )\n\n        logger.debug(\"  TransformerStack\")\n        self.transformer_stack = TransformerStack(\n            input_size=config.hidden_size,\n            max_sequence_length=self.max_sequence_length,\n            hidden_size=config.hidden_size,\n            num_heads=config.num_heads,\n            output_size=config.transformer_output_size,\n            num_layers=config.num_layers,\n            dropout=config.dropout,\n        )\n\n        if self.reduce_output is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.transformer_stack.output_shape[-1],\n                layers=config.fc_layers,\n                num_layers=config.num_fc_layers,\n                default_output_size=config.output_size,\n                default_use_bias=config.use_bias,\n                default_weights_initializer=config.weights_initializer,\n                default_bias_initializer=config.bias_initializer,\n                default_norm=config.norm,\n                default_norm_params=config.norm_params,\n                default_activation=config.fc_activation,\n                default_dropout=config.fc_dropout,\n                fc_residual=config.fc_residual,\n            )\n\n    def forward(\n        self,\n        inputs,  # encoder outputs\n    ) -> dict:\n        encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs]\n\n        # ================ Flatten ================\n        batch_size = encoder_outputs[0].shape[0]\n        encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs]\n\n        # ================ Project & Concat ================\n        projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)]\n        hidden = torch.stack(projected)  # shape [num_eo, bs, h]\n        hidden = torch.permute(hidden, (1, 0, 2))  # shape [bs, num_eo, h]\n\n        # ================ Transformer Layers ================\n        hidden = self.transformer_stack(hidden)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden)\n\n        return_data = {\"combiner_output\": hidden}\n\n        if len(inputs) == 1:\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n\n\n@register_combiner(TabTransformerCombinerConfig)\nclass TabTransformerCombiner(Combiner):\n    def __init__(\n        self, input_features: dict[str, \"InputFeature\"] = None, config: TabTransformerCombinerConfig = None, **kwargs\n    ):\n        super().__init__(input_features)\n        self.name = \"TabTransformerCombiner\"\n        logger.debug(f\"Initializing {self.name}\")\n\n        self.reduce_output = config.reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=config.reduce_output, max_sequence_length=len(input_features), encoding_size=config.hidden_size\n        )\n        self.supports_masking = True\n\n        self.embed_input_feature_name = config.embed_input_feature_name\n        if self.embed_input_feature_name:\n            vocab = [\n                i_f\n                for i_f in input_features\n                if input_features.get(i_f).type() != NUMBER or input_features.get(i_f).type() != BINARY\n            ]\n            if self.embed_input_feature_name == \"add\":\n                self.embed_i_f_name_layer = Embed(vocab, config.hidden_size, force_embedding_size=True)\n                projector_size = config.hidden_size\n            elif isinstance(self.embed_input_feature_name, int):\n                if self.embed_input_feature_name > config.hidden_size:\n                    raise ValueError(\n                        \"TabTransformer parameter \"\n                        \"`embed_input_feature_name` \"\n                        \"specified integer value ({}) \"\n                        \"needs to be smaller than \"\n                        \"`hidden_size` ({}).\".format(self.embed_input_feature_name, config.hidden_size)\n                    )\n                self.embed_i_f_name_layer = Embed(\n                    vocab,\n                    self.embed_input_feature_name,\n                    force_embedding_size=True,\n                )\n                projector_size = config.hidden_size - self.embed_input_feature_name\n            else:\n                raise ValueError(\n                    \"TabTransformer parameter \"\n                    \"`embed_input_feature_name` \"\n                    \"should be either None, an integer or `add`, \"\n                    \"the current value is \"\n                    \"{}\".format(self.embed_input_feature_name)\n                )\n        else:\n            projector_size = config.hidden_size\n\n        logger.debug(\"  Projectors\")\n        self.unembeddable_features = []\n        self.embeddable_features = []\n        for i_f in input_features:\n            if input_features.get(i_f).type() in {NUMBER, BINARY}:\n                self.unembeddable_features.append(i_f)\n            else:\n                self.embeddable_features.append(i_f)\n\n        self.projectors = ModuleList()\n        for i_f in self.embeddable_features:\n            flatten_size = self.get_flatten_size(input_features.get(i_f).output_shape)\n            self.projectors.append(Linear(flatten_size[0], projector_size))\n\n        # input to layer_norm are the encoder outputs for unembeddable features,\n        # which are number or binary features.  These should be 2-dim\n        # tensors.  Size should be concatenation of these tensors.\n        concatenated_unembeddable_encoders_size = 0\n        for i_f in self.unembeddable_features:\n            concatenated_unembeddable_encoders_size += input_features.get(i_f).output_shape[0]\n\n        # Skip LayerNorm when normalizing a single value — LayerNorm(1) always\n        # outputs zero which kills gradients for all downstream parameters.\n        if concatenated_unembeddable_encoders_size > 1:\n            self.layer_norm = torch.nn.LayerNorm(concatenated_unembeddable_encoders_size)\n        else:\n            self.layer_norm = torch.nn.Identity()\n\n        logger.debug(\"  TransformerStack\")\n        self.transformer_stack = TransformerStack(\n            input_size=config.hidden_size,\n            max_sequence_length=len(self.embeddable_features),\n            hidden_size=config.hidden_size,\n            # todo: can we just use projector_size? # hidden_size,\n            num_heads=config.num_heads,\n            output_size=config.transformer_output_size,\n            num_layers=config.num_layers,\n            dropout=config.dropout,\n        )\n\n        logger.debug(\"  FCStack\")\n\n        # determine input size to fully connected layer based on reducer\n        if config.reduce_output == \"concat\":\n            fc_input_size = len(self.embeddable_features) * config.hidden_size\n        else:\n            fc_input_size = self.reduce_sequence.output_shape[-1] if len(self.embeddable_features) > 0 else 0\n        self.fc_stack = FCStack(\n            fc_input_size + concatenated_unembeddable_encoders_size,\n            layers=config.fc_layers,\n            num_layers=config.num_fc_layers,\n            default_output_size=config.output_size,\n            default_use_bias=config.use_bias,\n            default_weights_initializer=config.weights_initializer,\n            default_bias_initializer=config.bias_initializer,\n            default_norm=config.norm,\n            default_norm_params=config.norm_params,\n            default_activation=config.fc_activation,\n            default_dropout=config.fc_dropout,\n            fc_residual=config.fc_residual,\n        )\n\n        self._empty_hidden = torch.empty([1, 0])\n        self._embeddable_features_indices = torch.arange(0, len(self.embeddable_features))\n\n        # Create empty tensor of shape [1, 0] to use as hidden in case there are no category or numeric/binary features.\n        self.register_buffer(\"empty_hidden\", self._empty_hidden)\n        self.register_buffer(\"embeddable_features_indices\", self._embeddable_features_indices)\n\n    @staticmethod\n    def get_flatten_size(output_shape: torch.Size) -> torch.Size:\n        size = torch.prod(torch.Tensor([*output_shape]))\n        return torch.Size([size.type(torch.int32)])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n    def forward(\n        self,\n        inputs: dict,  # encoder outputs\n    ) -> dict:\n        unembeddable_encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs if k in self.unembeddable_features]\n        embeddable_encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs if k in self.embeddable_features]\n\n        batch_size = (\n            embeddable_encoder_outputs[0].shape[0]\n            if len(embeddable_encoder_outputs) > 0\n            else unembeddable_encoder_outputs[0].shape[0]\n        )\n\n        # ================ Project & Concat embeddables ================\n        if len(embeddable_encoder_outputs) > 0:\n            # ============== Flatten =================\n            embeddable_encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in embeddable_encoder_outputs]\n\n            projected = [self.projectors[i](eo) for i, eo in enumerate(embeddable_encoder_outputs)]\n            hidden = torch.stack(projected)  # num_eo, bs, h\n            hidden = torch.permute(hidden, (1, 0, 2))  # bs, num_eo, h\n\n            if self.embed_input_feature_name:\n                i_f_names_idcs = torch.reshape(\n                    torch.arange(0, len(embeddable_encoder_outputs), device=self.device), [-1, 1]\n                )\n                embedded_i_f_names = self.embed_i_f_name_layer(i_f_names_idcs)\n                embedded_i_f_names = torch.unsqueeze(embedded_i_f_names, dim=0)\n                embedded_i_f_names = torch.tile(embedded_i_f_names, [batch_size, 1, 1])\n                if self.embed_input_feature_name == \"add\":\n                    hidden = hidden + embedded_i_f_names\n                else:\n                    hidden = torch.cat([hidden, embedded_i_f_names], -1)\n\n            # ================ Transformer Layers ================\n            hidden = self.transformer_stack(hidden)\n\n            # ================ Sequence Reduction ================\n            hidden = self.reduce_sequence(hidden)\n        else:\n            # create empty tensor because there are no category features\n            hidden = torch.empty([batch_size, 0], device=self.device)\n\n        # ================ Concat Skipped ================\n        if len(unembeddable_encoder_outputs) > 0:\n            unembeddable_encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in unembeddable_encoder_outputs]\n            # ================ Flatten ================\n            if len(unembeddable_encoder_outputs) > 1:\n                unembeddable_hidden = torch.cat(unembeddable_encoder_outputs, -1)  # tf.keras.layers.concatenate\n            else:\n                unembeddable_hidden = list(unembeddable_encoder_outputs)[0]\n            unembeddable_hidden = self.layer_norm(unembeddable_hidden)\n\n        else:\n            # create empty tensor because there are not numeric/binary features\n            unembeddable_hidden = torch.tile(self.empty_hidden, [batch_size, 0])\n\n        # ================ Concat Skipped and Others ================\n        # When reduce_output is None, hidden is 3D [batch, seq, dim] but\n        # unembeddable_hidden is 2D [batch, dim]. Expand to match.\n        if hidden.dim() == 3 and unembeddable_hidden.dim() == 2:\n            unembeddable_hidden = unembeddable_hidden.unsqueeze(1).expand(-1, hidden.size(1), -1)\n        hidden = torch.cat([hidden, unembeddable_hidden], -1)\n\n        # ================ FC Layers ================\n        hidden = self.fc_stack(hidden)\n\n        return_data = {\"combiner_output\": hidden}\n\n        if len(inputs) == 1:\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n\n\n@register_combiner(ComparatorCombinerConfig)\nclass ComparatorCombiner(Combiner):\n    def __init__(\n        self,\n        input_features: dict[str, \"InputFeature\"],\n        config: ComparatorCombinerConfig = None,\n        **kwargs,\n    ):\n        super().__init__(input_features)\n        self.name = \"ComparatorCombiner\"\n        logger.debug(f\"Entering {self.name}\")\n\n        self.entity_1 = config.entity_1\n        self.entity_2 = config.entity_2\n        self.required_inputs = set(config.entity_1 + config.entity_2)\n        self.output_size = config.output_size\n\n        self.fc_stack = None\n\n        # todo future: this may be redundant, check\n        fc_layers = config.fc_layers\n        if fc_layers is None and config.num_fc_layers is not None:\n            fc_layers = []\n            for _ in range(config.num_fc_layers):\n                fc_layers.append({\"output_size\": config.output_size})\n\n        if fc_layers is not None:\n            logger.debug(\"Setting up FCStack\")\n            self.e1_fc_stack = FCStack(\n                self.get_entity_shape(config.entity_1)[-1],\n                layers=fc_layers,\n                num_layers=config.num_fc_layers,\n                default_output_size=config.output_size,\n                default_use_bias=config.use_bias,\n                default_weights_initializer=config.weights_initializer,\n                default_bias_initializer=config.bias_initializer,\n                default_norm=config.norm,\n                default_norm_params=config.norm_params,\n                default_activation=config.activation,\n                default_dropout=config.dropout,\n            )\n            self.e2_fc_stack = FCStack(\n                self.get_entity_shape(config.entity_2)[-1],\n                layers=fc_layers,\n                num_layers=config.num_fc_layers,\n                default_output_size=config.output_size,\n                default_use_bias=config.use_bias,\n                default_weights_initializer=config.weights_initializer,\n                default_bias_initializer=config.bias_initializer,\n                default_norm=config.norm,\n                default_norm_params=config.norm_params,\n                default_activation=config.activation,\n                default_dropout=config.dropout,\n            )\n\n        self.last_fc_layer_output_size = fc_layers[-1][\"output_size\"]\n\n        # todo: set initializer and regularization\n        self.register_buffer(\n            \"bilinear_weights\",\n            torch.randn([self.last_fc_layer_output_size, self.last_fc_layer_output_size], dtype=torch.float32),\n        )\n\n    def get_entity_shape(self, entity: list) -> torch.Size:\n        sizes = [torch.prod(torch.Tensor([*self.handle.input_features.get(k).output_shape])) for k in entity]\n        return torch.Size([torch.sum(torch.Tensor(sizes)).type(torch.int32)])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([2 * self.last_fc_layer_output_size + 2])\n\n    def forward(\n        self,\n        inputs: dict,  # encoder outputs\n    ) -> dict[str, torch.Tensor]:  # encoder outputs\n        if inputs.keys() != self.required_inputs:\n            raise ValueError(f\"Missing inputs {self.required_inputs - set(inputs.keys())}\")\n\n        ############\n        # Entity 1 #\n        ############\n        e1_enc_outputs = [inputs[k][ENCODER_OUTPUT] for k in self.entity_1]\n\n        # ================ Flatten ================\n        batch_size = e1_enc_outputs[0].shape[0]\n        e1_enc_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in e1_enc_outputs]\n\n        # ================ Concat ================\n        if len(e1_enc_outputs) > 1:\n            e1_hidden = torch.cat(e1_enc_outputs, 1)\n        else:\n            e1_hidden = list(e1_enc_outputs)[0]\n\n        # ================ Fully Connected ================\n        e1_hidden = self.e1_fc_stack(e1_hidden)  # [bs, output_size]\n\n        ############\n        # Entity 2 #\n        ############\n        e2_enc_outputs = [inputs[k][ENCODER_OUTPUT] for k in self.entity_2]\n\n        # ================ Flatten ================\n        batch_size = e2_enc_outputs[0].shape[0]\n        e2_enc_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in e2_enc_outputs]\n\n        # ================ Concat ================\n        if len(e2_enc_outputs) > 1:\n            e2_hidden = torch.cat(e2_enc_outputs, 1)\n        else:\n            e2_hidden = list(e2_enc_outputs)[0]\n\n        # ================ Fully Connected ================\n        e2_hidden = self.e2_fc_stack(e2_hidden)  # [bs, output_size]\n\n        ###########\n        # Compare #\n        ###########\n        if e1_hidden.shape != e2_hidden.shape:\n            raise ValueError(\n                f\"Mismatching shapes among dimensions! \"\n                f\"entity1 shape: {e1_hidden.shape} \"\n                f\"entity2 shape: {e2_hidden.shape}\"\n            )\n\n        element_wise_mul = e1_hidden * e2_hidden  # [bs, output_size]\n        dot_product = torch.sum(element_wise_mul, 1, keepdim=True)  # [bs, 1]\n        abs_diff = torch.abs(e1_hidden - e2_hidden)  # [bs, output_size]\n        bilinear_prod = torch.sum(\n            torch.mm(e1_hidden, self.bilinear_weights) * e2_hidden, dim=1, keepdim=True\n        )  # [bs, 1]\n\n        logger.debug(\n            \"preparing combiner output by concatenating these tensors: \"\n            f\"dot_product: {dot_product.shape}, element_size_mul: {element_wise_mul.shape}\"\n            f\", abs_diff: {abs_diff.shape}, bilinear_prod {bilinear_prod.shape}\"\n        )\n        hidden = torch.cat([dot_product, element_wise_mul, abs_diff, bilinear_prod], 1)  # [bs, 2 * output_size + 2]\n\n        return {\"combiner_output\": hidden}\n\n\n@register_combiner(ProjectAggregateCombinerConfig)\nclass ProjectAggregateCombiner(Combiner):\n    def __init__(\n        self, input_features: dict[str, \"InputFeature\"] = None, config: ProjectAggregateCombinerConfig = None, **kwargs\n    ):\n        super().__init__(input_features)\n        self.name = \"ProjectAggregateCombiner\"\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Projectors\")\n        self.projectors = ModuleList(\n            # regardless of rank-2 or rank-3 input, torch.prod() calculates size\n            # after flattening the encoder output tensor\n            [\n                Linear(\n                    torch.prod(torch.Tensor([*input_features.get(inp).output_shape])).type(torch.int32),\n                    config.projection_size,\n                )\n                for inp in input_features\n            ]\n        )\n\n        self.fc_stack = None\n\n        # todo future: this may be redundant, check\n        fc_layers = config.fc_layers\n        if fc_layers is None and config.num_fc_layers is not None:\n            fc_layers = []\n            for i in range(config.num_fc_layers):\n                fc_layers.append({\"output_size\": config.output_size})\n\n        self.fc_layers = fc_layers\n        if self.fc_layers is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                first_layer_input_size=config.projection_size,\n                layers=config.fc_layers,\n                num_layers=config.num_fc_layers,\n                default_output_size=config.output_size,\n                default_use_bias=config.use_bias,\n                default_weights_initializer=config.weights_initializer,\n                default_bias_initializer=config.bias_initializer,\n                default_norm=config.norm,\n                default_norm_params=config.norm_params,\n                default_activation=config.activation,\n                default_dropout=config.dropout,\n                residual=config.residual,\n            )\n\n        if input_features and len(input_features) == 1 and self.fc_layers is None:\n            self.supports_masking = True\n\n    def forward(self, inputs: dict) -> dict:  # encoder outputs\n        encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs]\n\n        # ================ Flatten ================\n        batch_size = encoder_outputs[0].shape[0]\n        encoder_outputs = [torch.reshape(eo, [batch_size, -1]) for eo in encoder_outputs]\n\n        # ================ Project ================\n        projected = [self.projectors[i](eo) for i, eo in enumerate(encoder_outputs)]\n        hidden = torch.stack(projected)\n        hidden = torch.permute(hidden, (1, 0, 2))  # shape [bs, num_eo, h]\n\n        # ================ Aggregate ================\n        hidden = torch.mean(hidden, dim=1)\n\n        # ================ Fully Connected ================\n        if self.fc_stack is not None:\n            hidden = self.fc_stack(hidden)\n\n        return_data = {\"combiner_output\": hidden}\n\n        if len(inputs) == 1:\n            # Workaround for including additional tensors from output of input encoders for\n            # potential use in decoders, e.g. LSTM state for seq2seq.\n            # TODO(Justin): Think about how to make this communication work for multi-sequence\n            # features. Other combiners.\n            for key, value in [d for d in inputs.values()][0].items():\n                if key != ENCODER_OUTPUT:\n                    return_data[key] = value\n\n        return return_data\n"
  },
  {
    "path": "ludwig/config_sampling/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/config_sampling/explore_schema.py",
    "content": "import copy\nimport random\nfrom collections import deque, namedtuple\nfrom typing import Any, Deque\n\nimport pandas as pd\n\nfrom ludwig.config_sampling.parameter_sampling import handle_property_type, ParameterBaseTypes\nfrom ludwig.constants import SEQUENCE, TEXT, TIMESERIES\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.misc_utils import merge_dict\n\n# number of examples to generate for synthetic dataset\nNUM_SYNTHETIC_EXAMPLES = 10\n\nConfigOption = namedtuple(\"ConfigOption\", [\"config_option\", \"fully_explored\"])\n\n\ndef explore_properties(\n    jsonschema_properties: dict[str, Any],\n    parent_parameter_path: str,\n    dq: Deque[ConfigOption],\n    allow_list: list[str] = [],\n) -> Deque[tuple[dict, bool]]:\n    \"\"\"Recursively explores the `properties` part of any subsection of the schema.\n\n    Args:\n        jsonschema_properties: any properties section of the schema.\n        parent_parameter_path: period-delimited list of parent dictionary keys up to the given jsonschema_properties\n            (e.g. defaults.number.preprocessing)\n        dq: dequeue data structure that stores tuples of (config_options, fully_explored).\n            config_options: Dict[str, List], fully_explored: bool is a dictionary is a dictionary of parameter name to\n            list of values to explore.\n            fully_explored is a boolean value indicating whether all subsections of the properties dictionary have been\n            explored.\n        allow_list: list of top level keys of the properties dictionary to skip.\n\n    Returns:\n        A deque of (dict, bool) tuples.\n        - The first element of the tuple contains a dictionary of config options, which maps from a ludwig\n            config parameter to a list of the values to be explored for that parameter. Here's an example:\n                trainer.batch_size: [\"auto\", 2, 43]\n                trainer.learning_rate: [\"auto\", 0.1, 0.00002, 0.32424]\n                ...\n        - The second element of the tuple is whether we've explored this \"config path\"\n            fully. This is important to track when recursing into nested structures.\n    \"\"\"\n    # processed_dq will contain complete config options with all the parameters in the properties dictionary\n    # dq will contain configs options that are still being completed.\n    processed_dq = deque()\n    while dq and not dq[0].fully_explored:\n        for parameter_name_or_section, jsonschema_property in jsonschema_properties.items():\n            if allow_list and parameter_name_or_section not in allow_list:\n                continue\n\n            parameter_path = (\n                f\"{parent_parameter_path}.{parameter_name_or_section}\"\n                if parent_parameter_path\n                else parameter_name_or_section\n            )\n            config_options, _ = dq.popleft()\n\n            if \"properties\" in jsonschema_property and \"allOf\" in jsonschema_property:\n                for child_item in jsonschema_property[\"allOf\"]:\n                    expanded_config_options_dq = explore_from_all_of(\n                        config_options=copy.deepcopy(config_options), item=child_item, key_so_far=parameter_path\n                    )\n                    # add returned child config options to the deque to be processed.\n                    dq.extend(expanded_config_options_dq)\n\n            elif \"properties\" in jsonschema_property and \"allOf\" not in jsonschema_property:\n                # This is the case where we don't have a list of properties, just a properties\n                # dictionary nested inside another.\n                child_properties = jsonschema_property[\"properties\"]\n                # a new dequeue to be passed to explore parameters from\n                raw_entry = deque([ConfigOption(copy.deepcopy(config_options), False)])\n                child_config_options_dq = explore_properties(child_properties, parameter_path, raw_entry)\n                merged_config_options_dq = merge_dq(config_options, child_config_options_dq)\n                # add returned config options to the deque to be processed.\n                dq.extend(merged_config_options_dq)\n\n            else:\n                # this is the base case.\n                parameter_samples = get_samples(jsonschema_property)\n                if parameter_samples:\n                    config_options[parameter_path] = parameter_samples\n\n                # add config_options back to queue. fully_explored = False because we still didn't finish\n                # exploring all the keys in the properties dictionary.\n                dq.appendleft(ConfigOption(config_options, False))\n\n        # at this point, we finished exploring all keys of the properties dictionary. Add all config options\n        # to the processed queue.\n        while dq:\n            config_options, _ = dq.popleft()\n            processed_dq.append(ConfigOption(config_options, True))\n\n    return processed_dq\n\n\ndef get_samples(jsonschema_property: dict[str, Any]) -> list[ParameterBaseTypes]:\n    \"\"\"Get possible values for a leaf property (no sub-properties).\n\n    Args:\n        jsonschema_property: leaf property in the schema. Has no sub-properties.\n    \"\"\"\n    if \"oneOf\" in jsonschema_property:\n        temp = []\n        for elem in jsonschema_property[\"oneOf\"]:\n            temp += get_potential_values(elem)\n        return temp\n    else:\n        return get_potential_values(jsonschema_property)\n\n\ndef merge_dq(config_options: dict[str, Any], child_config_options_dq: Deque[ConfigOption]) -> Deque[ConfigOption]:\n    \"\"\"Merge config_options with the child_config_options in the dq.\"\"\"\n    dq = deque()\n    while child_config_options_dq:\n        child_config_options, visited = child_config_options_dq.popleft()\n        cfg = merge_dict(child_config_options, config_options)\n        dq.append(ConfigOption(cfg, visited))\n    return dq\n\n\ndef explore_from_all_of(config_options: dict[str, Any], item: dict[str, Any], key_so_far: str) -> Deque[ConfigOption]:\n    \"\"\"Takes a child of `allOf` and calls `explore_properties` on it.\"\"\"\n    for parameter_name_or_section in item[\"if\"][\"properties\"]:\n        config_options[key_so_far + \".\" + parameter_name_or_section] = item[\"if\"][\"properties\"][\n            parameter_name_or_section\n        ][\"const\"]\n    jsonschema_properties = item[\"then\"][\"properties\"]\n    raw_entry = deque([ConfigOption(copy.deepcopy(config_options), False)])\n    return explore_properties(jsonschema_properties, parent_parameter_path=key_so_far, dq=raw_entry)\n\n\ndef get_potential_values(item: dict[str, Any]) -> list[ParameterBaseTypes | list[ParameterBaseTypes]]:\n    \"\"\"Returns a list of values to explore for a config parameter.\n\n    Param:\n        item: config parameter-specific dictionary. Considered as a leaf in the schema. Contains type, default, and\n            parameter metadata, etc.\n    \"\"\"\n    temp = []\n    item_type = item.get(\"type\")\n    if item_type is None:\n        # No explicit type — try to infer from enum/const/default\n        if \"enum\" in item:\n            return [v for v in item[\"enum\"] if v is not None]\n        if \"const\" in item:\n            return [item[\"const\"]]\n        if \"default\" in item:\n            return [item[\"default\"]]\n        return []\n    # Case where we're using OneOf (e.g. to allow batch size 'auto' and integers)\n    if isinstance(item_type, list):\n        for property_type in item_type:\n            temp += handle_property_type(property_type, item)\n    else:\n        temp += handle_property_type(item_type, item)\n\n    # Make sure values are unique. Not using set because some values are unhashable.\n    unique_temp = []\n    for temp_item in temp:\n        if temp_item not in unique_temp:\n            unique_temp.append(temp_item)\n    return unique_temp\n\n\ndef generate_possible_configs(config_options: dict[str, Any]):\n    \"\"\"Generate exhaustive configs from config_options.\n\n    This function does not take a cross product of all the options for all the config parameters. It selects parameter\n    values independently from each other.\n\n    Args:\n        config_options: dictionary mapping from ludwig config parameter to all values to be explored.\n            Here's an example of what it could look like:\n\n                trainer.batch_size: [\"auto\", 2, 43]\n                trainer.learning_rate: [\"auto\", 0.1, 0.00002, 0.32424]\n                ...\n    \"\"\"\n    # The number of configs to generate is the max length of the lists of samples over all parameters.\n    num_configs = 1\n    for parameter_name in config_options:\n        if isinstance(config_options[parameter_name], list):\n            num_configs = max(num_configs, len(config_options[parameter_name]))\n            config_options[parameter_name] = deque(config_options[parameter_name])\n\n    for _ in range(num_configs):\n        config = {}\n        for parameter_name in config_options:\n            # if parameter is regular parameter with explored values.\n            if config_options[parameter_name] and not isinstance(config_options[parameter_name], str):\n                config[parameter_name] = config_options[parameter_name].popleft()\n            # case for parameters where we don't have choices such as `encoder.type: parallel_cnn` that\n            # cause the downstream parameters to change.\n            elif isinstance(config_options[parameter_name], str):\n                config[parameter_name] = config_options[parameter_name]\n        yield create_nested_dict(config)\n\n\ndef create_nested_dict(flat_dict: dict[str, float | str]) -> ModelConfigDict:\n    \"\"\"Generate a nested dict out of a flat dict whose keys are delimited by a delimiter character.\n\n    Args:\n        flat_dict: potential generated baseline config. Here's an example of what it could look like:\n\n            trainer.batch_size: 324\n            trainer.learning_rate: 0.0635\n\n        The expected output would be\n\n            trainer:\n                batch_size: 324\n                learning_rate: 0.0635\n    \"\"\"\n\n    def to_nested_format(parameter_name: str, value: str | int | float, delimiter: str = \".\") -> dict[str, Any]:\n        # https://stackoverflow.com/a/40401961\n        split_parameter_name = parameter_name.split(delimiter)\n        for parameter_name_or_section in reversed(split_parameter_name):\n            value = {parameter_name_or_section: value}\n        return value\n\n    config = {}\n    for parameter_name_or_section in flat_dict:\n        config = merge_dict(\n            config, to_nested_format(parameter_name_or_section, copy.deepcopy(flat_dict[parameter_name_or_section]))\n        )\n    return config\n\n\ndef combine_configs(\n    explored: Deque[tuple[dict, bool]], config: ModelConfigDict\n) -> list[tuple[ModelConfigDict, pd.DataFrame]]:\n    \"\"\"Merge base config with explored sections.\n\n    Args:\n        explored: deque containing all the config options.\n        config: base Ludwig config to merge the explored configs with.\n    \"\"\"\n    dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config)\n    ret = []\n    for config_options, _ in explored:\n        for default_config in generate_possible_configs(config_options=config_options):\n            merged_config = merge_dict(copy.deepcopy(config), default_config)\n            try:\n                ModelConfig.from_dict(merged_config)\n                ret.append((merged_config, dataset))\n            except Exception:\n                pass\n    return ret\n\n\ndef combine_configs_for_comparator_combiner(\n    explored: Deque[tuple], config: ModelConfigDict\n) -> list[tuple[ModelConfigDict, pd.DataFrame]]:\n    \"\"\"Merge base config with explored sections.\n\n    Completes the entity_1 and entity_2 paramters of the comparator combiner.\n\n    Args:\n        explored: deque containing all the config options.\n        config: base Ludwig config to merge the explored configs with.\n    \"\"\"\n    dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config)\n    ret = []\n    for item in explored:\n        for default_config in generate_possible_configs(config_options=item[0]):\n            merged_config = merge_dict(copy.deepcopy(config), default_config)\n\n            # create two random lists for entity1 and entity2\n            entity_names = [feature[\"name\"] for feature in config[\"input_features\"]]\n            random.shuffle(entity_names)\n            entity_1_size = random.randint(1, len(entity_names) - 1)\n            merged_config[\"combiner\"][\"entity_1\"] = entity_names[:entity_1_size]\n            merged_config[\"combiner\"][\"entity_2\"] = entity_names[entity_1_size:]\n            try:\n                ModelConfig.from_dict(merged_config)\n                ret.append((merged_config, dataset))\n            except Exception:\n                pass\n    return ret\n\n\ndef combine_configs_for_sequence_combiner(\n    explored: Deque[tuple], config: ModelConfigDict\n) -> list[tuple[ModelConfigDict, pd.DataFrame]]:\n    \"\"\"Merge base config with explored sections.\n\n    Uses the right reduce_output strategy for the sequence and sequence_concat combiners.\n\n    Args:\n        explored: deque containing all the config options.\n        config: base Ludwig config to merge the explored configs with.\n    \"\"\"\n    dataset = build_synthetic_dataset_df(NUM_SYNTHETIC_EXAMPLES, config)\n    ret = []\n    for item in explored:\n        for default_config in generate_possible_configs(config_options=item[0]):\n            merged_config = merge_dict(copy.deepcopy(config), default_config)\n            for i in range(len(merged_config[\"input_features\"])):\n                if merged_config[\"input_features\"][i][\"type\"] in {SEQUENCE, TEXT, TIMESERIES}:\n                    merged_config[\"input_features\"][0][\"encoder\"] = {\"type\": \"embed\", \"reduce_output\": None}\n            try:\n                ModelConfig.from_dict(merged_config)\n                ret.append((merged_config, dataset))\n            except Exception:\n                pass\n    return ret\n"
  },
  {
    "path": "ludwig/config_sampling/parameter_sampling.py",
    "content": "import random\nfrom typing import Any, Union\n\nfrom ludwig.schema.metadata.parameter_metadata import ExpectedImpact\n\n# base types for ludwig config parameters.\nParameterBaseTypes = Union[str, float, int, bool, None]\n\n\ndef handle_property_type(\n    property_type: str, item: dict[str, Any], expected_impact: ExpectedImpact = ExpectedImpact.HIGH\n) -> list[ParameterBaseTypes | list[ParameterBaseTypes]]:\n    \"\"\"Return possible parameter values for a parameter type.\n\n    Args:\n        property_type: type of the parameter (e.g. array, number, etc.)\n        item: dictionary containing details on the parameter such as default, min and max values.\n        expected_impact: threshold expected impact that we'd like to include.\n    \"\"\"\n    parameter_metadata = item.get(\"parameter_metadata\", None)\n    if not parameter_metadata:\n        return []\n\n    # don't explore internal only parameters.\n    if parameter_metadata.get(\"internal_only\", True):\n        return []\n\n    # don't explore parameters that have expected impact less than HIGH.\n    if parameter_metadata.get(\"expected_impact\", ExpectedImpact.LOW) < expected_impact:\n        return []\n\n    if property_type == \"number\":\n        return explore_number(item)\n    elif property_type == \"integer\":\n        return explore_integer(item)\n    elif property_type == \"string\":\n        return explore_string(item)\n    elif property_type == \"boolean\":\n        return explore_boolean()\n    elif property_type == \"null\":\n        return explore_null()\n    elif property_type == \"array\":\n        return explore_array(item)\n    else:\n        return []\n\n\ndef explore_array(item: dict[str, Any]) -> list[list[ParameterBaseTypes]]:\n    \"\"\"Return possible parameter values for the `array` parameter type.\n\n    Args:\n        item: dictionary containing details on the parameter such as default, min and max values.\n    \"\"\"\n\n    candidates = []\n    if \"default\" in item and item[\"default\"]:\n        candidates.append(item[\"default\"])\n\n    item_choices = []\n    maxlen = 0\n\n    # In the case where the length of the array isn't defined.\n    if not isinstance(item[\"items\"], list):\n        return []\n\n    for item_of in item[\"items\"]:\n        choices = handle_property_type(item_of[\"type\"], item_of)\n        maxlen = max(maxlen, len(choices))\n        item_choices.append(choices)\n\n    # pad to same length\n    for i in range(len(item_choices)):\n        item_choices[i] = maxlen * item_choices[i]\n        item_choices[i] = item_choices[i][:maxlen]\n\n    merged = list(zip(*item_choices)) + candidates\n    return [list(tup) for tup in merged]\n\n\ndef explore_number(item: dict[str, Any]) -> list[ParameterBaseTypes]:\n    \"\"\"Return possible parameter values for the `number` parameter type.\n\n    Args:\n        item: dictionary containing details on the parameter such as default, min and max values.\n    TODO(Wael): Improve logic.\n    \"\"\"\n    minimum, maximum = 0, 1\n    if \"default\" not in item or item[\"default\"] is None:\n        candidates = []\n    else:\n        candidates = [1, 2, item[\"default\"], 2 * (item[\"default\"] + 1), item[\"default\"] // 2, -1 * item[\"default\"]]\n\n    if \"minimum\" in item:\n        minimum = item[\"minimum\"]\n        candidates = [num for num in candidates if num > minimum]\n    if \"maximum\" in item:\n        maximum = item[\"maximum\"]\n        candidates = [num for num in candidates if num < maximum]\n    return candidates + [random.random() * 0.99 * maximum]\n\n\ndef explore_integer(item: dict[str, Any]) -> list[ParameterBaseTypes]:\n    \"\"\"Return possible parameter values for the `integer` parameter type.\n\n    Args:\n        item: dictionary containing details on the parameter such as default, min and max values.\n    TODO(Wael): Improve logic.\n    \"\"\"\n    minimum, maximum = 0, 10\n\n    if \"default\" not in item or item[\"default\"] is None:\n        candidates = []\n    else:\n        candidates = [item[\"default\"], 2 * (item[\"default\"] + 1), item[\"default\"] // 2, -1 * item[\"default\"]]\n\n    if \"minimum\" in item:\n        minimum = item[\"minimum\"]\n        candidates = [num for num in candidates if num >= item[\"minimum\"]]\n    if \"maximum\" in item:\n        maximum = item[\"maximum\"]\n        candidates = [num for num in candidates if num <= item[\"maximum\"]]\n\n    return candidates + [random.randint(minimum, maximum)]\n\n\ndef explore_string(item: dict[str, Any]) -> list[ParameterBaseTypes]:\n    \"\"\"Return possible parameter values for the `string` parameter type.\n\n    Args:\n        item: dictionary containing details on the parameter such as default, min and max values.\n    \"\"\"\n\n    if \"enum\" in item:\n        return item[\"enum\"]\n    return [item[\"default\"]]\n\n\ndef explore_boolean() -> list[bool]:\n    \"\"\"Return possible parameter values for the `boolean` parameter type (i.e. [True, False])\"\"\"\n    return [True, False]\n\n\ndef explore_null() -> list[None]:\n    \"\"\"Return possible parameter values for the `null` parameter type (i.e. [None])\"\"\"\n    return [None]\n"
  },
  {
    "path": "ludwig/config_validation/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/config_validation/checks.py",
    "content": "\"\"\"Checks that are not easily covered by marshmallow JSON schema validation like parameter interdependencies.\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable\nfrom re import findall\nfrom typing import TYPE_CHECKING\n\nfrom transformers import AutoConfig\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BINARY,\n    IMAGE,\n    IN_MEMORY,\n    MIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD,\n    MODEL_ECD,\n    MODEL_LLM,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    VECTOR,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.utils.metric_utils import get_feature_to_metric_names_map_from_feature_collection\nfrom ludwig.utils.misc_utils import merge_dict\n\nif TYPE_CHECKING:\n    from ludwig.schema.model_config import ModelConfig\n\n# Set of all sequence feature types.\nSEQUENCE_OUTPUT_FEATURE_TYPES = {SEQUENCE, TEXT, SET, VECTOR}\n\n\nclass ConfigCheckRegistry:\n    \"\"\"A registry of configuration checks.\"\"\"\n\n    def __init__(self):\n        self._registry = []\n\n    def register(self, check_fn):\n        self._registry.append(check_fn)\n\n    def check_config(self, config: \"ModelConfig\") -> None:  # noqa: F821\n        for check_fn in self._registry:\n            check_fn(config)\n\n\n_CONFIG_CHECK_REGISTRY = ConfigCheckRegistry()\n\n\ndef get_config_check_registry():\n    \"\"\"Returns the config check registry.\"\"\"\n    return _CONFIG_CHECK_REGISTRY\n\n\n@DeveloperAPI\ndef register_config_check(fn) -> Callable:\n    \"\"\"Registers a config check function.\"\"\"\n    _CONFIG_CHECK_REGISTRY.register(fn)\n\n\nclass ConfigCheck(ABC):\n    \"\"\"Checks instances of comprehensive (all parameters and defaults filled in) schema-validated config.\"\"\"\n\n    @staticmethod\n    @abstractmethod\n    def check(config: \"ModelConfig\") -> None:  # noqa: F821\n        \"\"\"Checks config for validity.\"\"\"\n        raise NotImplementedError\n\n\n@register_config_check\ndef check_feature_names_unique(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that all feature names are unique.\"\"\"\n    input_features = config.input_features\n    input_feature_names = {input_feature.name for input_feature in input_features}\n\n    output_features = config.output_features\n    output_feature_names = {output_feature.name for output_feature in output_features}\n\n    if len(input_feature_names) + len(output_feature_names) != len(input_features) + len(output_features):\n        raise ConfigValidationError(\"Feature names must be unique.\")\n\n\n@register_config_check\ndef check_tied_features_valid(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that all tied features are valid.\"\"\"\n    input_features = config.input_features\n    input_feature_names = {input_feature.name for input_feature in input_features}\n\n    for input_feature in input_features:\n        if input_feature.tied and input_feature.tied not in input_feature_names:\n            raise ConfigValidationError(\n                f\"Feature {input_feature.name} is tied to feature {input_feature.tied}, but the \"\n                f\"'{input_feature.tied}' feature does not exist.\"\n            )\n\n\n@register_config_check\ndef check_training_runway(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that checkpoints_per_epoch and steps_per_checkpoint aren't simultaneously defined.\"\"\"\n    if config.model_type == MODEL_ECD:\n        if config.trainer.checkpoints_per_epoch != 0 and config.trainer.steps_per_checkpoint != 0:\n            raise ConfigValidationError(\n                \"It is invalid to specify both trainer.checkpoints_per_epoch AND \"\n                \"trainer.steps_per_checkpoint. Please specify one or the other, or specify neither to \"\n                \"checkpoint/eval the model every epoch.\"\n            )\n\n\n@register_config_check\ndef check_ray_backend_in_memory_preprocessing(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that in memory preprocessing is used with Ray backend.\"\"\"\n    if config.backend is None:\n        return\n    if not hasattr(config.trainer, \"preprocessing\") or not hasattr(config.trainer.preprocessing, IN_MEMORY):\n        return\n\n    if config.backend.type == \"ray\" and not config.trainer.preprocessing.in_memory:\n        raise ConfigValidationError(\n            \"RayBackend does not support lazy loading of data files at train time. \"\n            \"Set preprocessing config `in_memory: True`\"\n        )\n\n    for input_feature in config.input_features:\n        if input_feature.type == AUDIO or input_feature.type == IMAGE:\n            if not input_feature.preprocessing.in_memory and config.backend.type != \"ray\":\n                raise ConfigValidationError(\n                    \"RayBackend does not support lazy loading of data files at train time. \"\n                    f\"Set preprocessing config `in_memory: True` for input feature {input_feature.name}\"\n                )\n\n\ndef check_sequence_concat_combiner_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that sequence concat combiner has at least one input feature that's sequential.\"\"\"\n    if config.model_type != MODEL_ECD:\n        return\n    if config.combiner != \"sequence_concat\":\n        return\n    has_sequence_input = False\n    for input_feature in config.input_features:\n        if input_feature.type in SEQUENCE_OUTPUT_FEATURE_TYPES:\n            has_sequence_input = True\n            break\n    if not has_sequence_input:\n        raise ConfigValidationError(\n            \"Sequence concat combiner should only be used for at least one sequential input feature.\"\n        )\n\n\n@register_config_check\ndef check_comparator_combiner_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that all of the feature names for entity_1 and entity_2 are valid features.\"\"\"\n    if config.model_type != MODEL_ECD:\n        return\n    if config.combiner.type != \"comparator\":\n        return\n\n    input_feature_names = [input_feature.name for input_feature in config.input_features]\n    for feature_name in config.combiner.entity_1:\n        if feature_name not in input_feature_names:\n            raise ConfigValidationError(\n                f\"Feature {feature_name} in entity_1 for the comparator combiner is not a valid \" \"input feature name.\"\n            )\n    for feature_name in config.combiner.entity_2:\n        if feature_name not in input_feature_names:\n            raise ConfigValidationError(\n                f\"Feature {feature_name} in entity_2 for the comparator combiner is not a valid \" \"input feature name.\"\n            )\n\n    if sorted(config.combiner.entity_1 + config.combiner.entity_2) != sorted(input_feature_names):\n        raise ConfigValidationError(\"Not all input features are present as entities in the comparator combiner.\")\n\n\n@register_config_check\ndef check_class_balance_preprocessing(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Class balancing is only available for datasets with a single output feature.\"\"\"\n    if config.preprocessing.oversample_minority or config.preprocessing.undersample_majority:\n        if len(config.output_features) != 1:\n            raise ConfigValidationError(\"Class balancing is only available for datasets with a single output feature.\")\n        if config.output_features[0].type != BINARY:\n            raise ConfigValidationError(\"Class balancing is only supported for binary output features.\")\n\n\n@register_config_check\ndef check_sampling_exclusivity(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Oversample minority and undersample majority are mutually exclusive.\"\"\"\n    if config.preprocessing.oversample_minority and config.preprocessing.undersample_majority:\n        raise ConfigValidationError(\n            \"Oversample minority and undersample majority are mutually exclusive. Specify only one method.\"\n        )\n\n\n@register_config_check\ndef check_validation_metric_exists(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that the specified validation metric exists.\"\"\"\n    validation_metric_name = config.trainer.validation_metric\n\n    # Get all valid metrics.\n    feature_to_metric_names_map = get_feature_to_metric_names_map_from_feature_collection(config.output_features)\n    all_valid_metrics = set()\n    for metric_names in feature_to_metric_names_map.values():\n        all_valid_metrics.update(metric_names)\n\n    if validation_metric_name not in all_valid_metrics:\n        raise ConfigValidationError(\n            f\"User-specified trainer.validation_metric '{validation_metric_name}' is not valid. \"\n            f\"Available metrics are: {all_valid_metrics}\"\n        )\n\n\n@register_config_check\ndef check_splitter(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks the validity of the splitter configuration.\"\"\"\n    from ludwig.data.split import get_splitter\n\n    splitter = get_splitter(**config.preprocessing.split.to_dict())\n    splitter.validate(config)\n\n\n@register_config_check\ndef check_hf_tokenizer_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that the HuggingFace tokenizer has a pretrained_model_name_or_path specified.\"\"\"\n\n    for input_feature in config.input_features:\n        if input_feature.type == TEXT:\n            if input_feature.preprocessing.tokenizer == \"hf_tokenizer\":\n                if input_feature.preprocessing.pretrained_model_name_or_path is None:\n                    raise ConfigValidationError(\n                        \"Pretrained model name or path must be specified for HuggingFace tokenizer.\"\n                    )\n\n\n@register_config_check\ndef check_hf_encoder_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that a HuggingFace encoder has a pretrained_model_name_or_path specified.\"\"\"\n\n    for input_feature in config.input_features:\n        if input_feature.type == TEXT:\n            if hasattr(input_feature.encoder, \"use_pretrained\"):\n                if input_feature.preprocessing.pretrained_model_name_or_path is None:\n                    raise ConfigValidationError(\n                        \"Pretrained model name or path must be specified for HuggingFace encoder.\"\n                    )\n\n\n@register_config_check\ndef check_stacked_transformer_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that the transformer encoder type correctly configures `num_heads` and `hidden_size`\"\"\"\n\n    def is_divisible(hidden_size: int, num_heads: int) -> bool:\n        \"\"\"Checks that hidden_size is divisible by num_heads.\"\"\"\n        return hidden_size % num_heads == 0\n\n    sequence_types = [SEQUENCE, TEXT, TIMESERIES]\n\n    for input_feature in config.input_features:\n        if_type = input_feature.type\n        encoder = input_feature.encoder\n        if (\n            if_type in sequence_types\n            and encoder.type == \"transformer\"\n            and not is_divisible(encoder.hidden_size, encoder.num_heads)\n        ):\n            raise ConfigValidationError(\n                f\"Input feature {input_feature.name} transformer encoder requires encoder.hidden_size to be divisible \"\n                f\"by encoder.num_heads. Found hidden_size {encoder.hidden_size} and num_heads {encoder.num_heads}.\"\n            )\n\n\n@register_config_check\ndef check_hyperopt_search_algorithm_dependencies_installed(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Check that the hyperopt search algorithm dependencies are installed.\"\"\"\n    if config.hyperopt is None:\n        return\n\n    try:\n        config.hyperopt.search_alg.dependencies_installed()\n    except ImportError as e:\n        raise ConfigValidationError(e.msg)\n\n\n@register_config_check\ndef check_hyperopt_scheduler_dependencies_installed(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Check that the hyperopt scheduler dependencies are installed.\"\"\"\n    if config.hyperopt is None:\n        return\n\n    try:\n        config.hyperopt.executor.scheduler.dependencies_installed()\n    except ImportError as e:\n        raise ConfigValidationError(e.msg)\n\n\n@register_config_check\ndef check_tagger_decoder_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that the tagger decoder has at least one sequence, text or timeseries input feature where the\n    encoder's reduce_output will produce a 3D shaped output from the combiner.\"\"\"\n    # Check if there is a text or sequence output feature using a tagger decoder\n    output_feature_with_tagger_decoder = False\n    for output_feature in config.output_features:\n        if output_feature.type in {TEXT, SEQUENCE} and output_feature.decoder.type == \"tagger\":\n            output_feature_with_tagger_decoder = True\n\n    if not output_feature_with_tagger_decoder:\n        return\n\n    # Check that there is at least one sequence, text or timeseries input feature that doesn't reduce the\n    # output of the encoder.\n    has_sequence_feature = False\n    for input_feature in config.input_features:\n        if input_feature.type in {SEQUENCE, TEXT, TIMESERIES}:\n            has_sequence_feature = True\n            if input_feature.encoder.reduce_output is None:\n                return\n\n    if not has_sequence_feature:\n        raise ConfigValidationError(\"Tagger decoder requires at least one text, sequence or timeseries input feature.\")\n    else:\n        raise ConfigValidationError(\n            \"Tagger decoder requires at least one of the text, sequence or timeseries input feature encoders to have \"\n            \"`reduce_output` set to `None`.\"\n        )\n\n\n@register_config_check\ndef check_hyperopt_parameter_dicts(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks for hyperopt parameter dicts against their config objects.\"\"\"\n    if config.hyperopt is None:\n        return\n\n    from ludwig.schema.hyperopt.utils import get_parameter_cls, parameter_config_registry  # noqa: F401\n\n    for parameter, space in config.hyperopt.parameters.items():\n        # skip nested hyperopt parameters\n        if parameter != \".\":\n            parameter_attribute_path = parameter.split(\".\")\n            passed = False\n\n            for root in [config, config.input_features, config.output_features]:\n                current = root\n                for p in parameter_attribute_path:\n                    try:\n                        current = current.__getattribute__(p)\n                        if p == parameter_attribute_path[-1]:\n                            passed = True\n                    except AttributeError:\n                        break\n                if passed:\n                    break\n\n            if not passed:\n                raise ConfigValidationError(\n                    f\"The supplied hyperopt parameter {parameter} is not a valid config field. Check the Ludwig \"\n                    \"docs for the list of valid parameters.\"\n                )\n\n            try:\n                space_cls = get_parameter_cls(space[\"space\"])\n                space_cls.from_dict(space)\n            except KeyError:\n                space_types = \", \".join(parameter_config_registry.keys())\n                raise ConfigValidationError(\n                    f\"Invalid hyperopt parameter space requested for `hyperopt.parameters.{parameter}`. Valid spaces \"\n                    f\"are {space_types}.\"\n                )\n\n\n@register_config_check\ndef check_concat_combiner_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that if the concat combiner receives a mixture of sequence and non-sequence features, that all\n    sequence features are configured with reduce_output to be 2D tensors.\"\"\"\n    if config.model_type != MODEL_ECD:\n        return\n    if config.combiner.type != \"concat\":\n        return\n\n    has_unreduced_sequence_feature = False\n    has_non_sequence_feature = False\n    for input_feature in config.input_features:\n        if (\n            input_feature.type in {SEQUENCE, TEXT, TIMESERIES}\n            and hasattr(input_feature.encoder, \"reduce_output\")\n            and input_feature.encoder.reduce_output is None\n        ):\n            has_unreduced_sequence_feature = True\n        else:\n            has_non_sequence_feature = True\n\n    if has_unreduced_sequence_feature and has_non_sequence_feature:\n        raise ConfigValidationError(\n            \"The concat combiner cannot receive a mix of unreduced sequence features (3D) and non-sequence features \"\n            \"(2D). Options: 1) Set reduce_output in sequence feature encoders to a value other than None to ensure 2D \"\n            \"encoder outputs, 2) Choose a different combiner like `sequence_concat` which can handle a mix of 2D and \"\n            \"3D encoder output shapes, or 3) Remove features to ensure that output shapes from all encoders are the \"\n            \"same dimension (all 2D or all 3D).\"\n        )\n\n\n@register_config_check\ndef check_hyperopt_nested_parameter_dicts(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that all nested parameters in a hyperopt config exist.\"\"\"\n    if config.hyperopt is None or \".\" not in config.hyperopt.parameters:\n        return\n\n    from ludwig.schema.hyperopt.utils import get_parameter_cls  # noqa: F401\n    from ludwig.schema.model_types.base import ModelConfig\n\n    space = config.hyperopt.parameters[\".\"]\n\n    # Build the config that would be produced by each parameter dict to validate subsections that may be in\n    config_dict = config.to_dict()\n    del config_dict[\"hyperopt\"]\n    for category in space[\"categories\"]:\n        for i, k in enumerate(category.keys()):\n            try:\n                config.__getattribute__(k)\n            except AttributeError:\n                raise ConfigValidationError(f\"Invalid config block {k} in nested hyperopt parameter dict {i}: {space}.\")\n\n        category_dict = merge_dict(config_dict, category)\n        try:\n            ModelConfig.from_dict(category_dict)\n        except ConfigValidationError as e:\n            raise ConfigValidationError(f\"Invalid config in hyperopt nested parameter config: {category}. {e.message}\")\n\n    try:\n        space_cls = get_parameter_cls(\"choice\")\n        space_cls.from_dict(space)\n    except KeyError:\n        raise ConfigValidationError(\n            f\"Nested hyperparameter search spaces must be of type 'choice'. Requested space type: {space['space']}\"\n        )\n\n\n@register_config_check\ndef check_llm_exactly_one_input_text_feature(config: \"ModelConfig\"):  # noqa: F821\n    if config.model_type != MODEL_LLM:\n        return\n\n    if len(config.input_features) == 1 and config.input_features[0].type == TEXT:\n        return\n    else:\n        raise ConfigValidationError(\"LLM requires exactly one text input feature.\")\n\n\n@register_config_check\ndef check_llm_finetuning_output_feature_config(config: \"ModelConfig\"):  # noqa: F821\n    \"\"\"Checks that the output feature config for LLM finetuning is valid.\"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    if config.trainer.type != \"finetune\":\n        return\n\n    if config.output_features[0].type != TEXT:\n        raise ConfigValidationError(\n            \"LLM finetuning requires the output feature to be a text feature. If you are trying to use a different \"\n            \"output feature type such as category or binary, please change the output feature type to text.\"\n        )\n\n\n@register_config_check\ndef check_llm_finetuning_trainer_config(config: \"ModelConfig\"):  # noqa: F821\n    \"\"\"Ensures that trainer type is finetune if adapter is not None.\"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    if (\n        config.trainer.type == \"none\"\n        and config.adapter is not None\n        and config.adapter.pretrained_adapter_weights is not None\n    ):\n        # If performing zero-shot, we must specify pretrained adapter weights\n        return\n\n    if config.adapter is not None and config.trainer.type != \"finetune\":\n        raise ConfigValidationError(\"LLM finetuning requires trainer type to be finetune.\")\n\n\n@register_config_check\ndef check_llm_finetuning_backend_config(config: \"ModelConfig\"):  # noqa: F821\n    \"\"\"Checks that the LLM finetuning using Ray is configured correctly.\n\n    DDP strategy is not supported for LLM finetuning because it leads to OOMs since the model is large and DDP strategy\n    requires a copy of the model on each GPU.\n    \"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    # LLM finetuning is only supported by the finetune trainer type\n    if (\n        config.trainer.type != \"finetune\"\n        and config.adapter is not None\n        and config.adapter.pretrained_adapter_weights is not None\n    ):\n        return\n\n    # Using local backend, so skip the checks below\n    if not hasattr(config.backend, \"type\"):\n        return\n\n    backend = config.backend\n    if not hasattr(backend.trainer, \"strategy\") or backend.trainer.strategy != \"deepspeed\":\n        raise ConfigValidationError(\"LLM finetuning with Ray requires the DeepSpeed strategy.\")\n\n    # Deepspeed requires GPU\n    if not backend.trainer.use_gpu or backend.trainer.resources_per_worker.GPU < 1:\n        raise ConfigValidationError(\"LLM finetuning with DeepSpeed requires GPU.\")\n\n\n@register_config_check\ndef check_llm_finetuning_adalora_config(config: \"ModelConfig\"):\n    \"\"\"Checks that the adalora adapter is configured correctly.\n\n    We check against PEFT's predefined target module list for ADALORA to see if this target_modules is present there. If\n    not, AdaloraModel will run into issues downstream.\n    \"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"adalora\":\n        return\n\n    from peft.utils import TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING\n\n    model_config = _get_llm_model_config(config.base_model)\n    if model_config.model_type not in TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING:\n        raise ConfigValidationError(\n            f\"Adalora adapter is not supported for {model_config.model_type} model. \"\n            f\"Supported model types are: {list(TRANSFORMERS_MODELS_TO_ADALORA_TARGET_MODULES_MAPPING.keys())}. \"\n            \"If you know the target modules for your model, please specify them in the config through the \"\n            \"`target_modules` key.\"\n        )\n\n\n@register_config_check\ndef check_llm_finetuning_adaption_prompt_parameters(config: \"ModelConfig\"):\n    \"\"\"Checks that the adaption_prompt adapter is configured correctly.\n\n    Adaption prompt is only supported for Llama models.\n    \"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"adaption_prompt\":\n        return\n\n    from peft.tuners.adaption_prompt.config import TRANSFORMERS_MODEL_CONFIG\n\n    # Adaption Config is currently only supported for Llama model types\n    model_config = _get_llm_model_config(config.base_model)\n    if model_config.model_type not in TRANSFORMERS_MODEL_CONFIG:\n        raise ConfigValidationError(\n            f\"Adaption prompt adapter is not supported for {model_config.model_type} model. \"\n            f\"Supported model types are: {list(TRANSFORMERS_MODEL_CONFIG.keys())}.\"\n        )\n\n\ndef _get_llm_model_config(model_name: str) -> AutoConfig:\n    \"\"\"Returns the LLM model config.\"\"\"\n    return AutoConfig.from_pretrained(model_name)\n\n\n# TODO(geoffrey, arnav): uncomment this when we have reconciled the config with the backend kwarg in api.py\n# @register_config_check\ndef check_llm_quantization_backend_incompatibility(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that LLM model type with quantization uses the local backend.\"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    if config.quantization is None:\n        return\n\n    backend_type = None\n    if config.backend:\n        backend_type = config.backend.get(\"type\", None)\n\n    # If backend was explicitly set to Ray, then we need to raise an error\n    if backend_type == \"ray\":\n        raise ConfigValidationError(f\"LLM with quantization requires the 'local' backend, found: '{backend_type}'\")\n\n    # If the backend is not explicitly set, then we need to check if a Ray process is running\n    # If a Ray process is running, then we need to raise an error because the backend will be set to Ray\n    if config.backend is None:\n        try:\n            # May not be installed, so we need to catch the ImportError\n            import ray\n\n            if ray.is_initialized():\n                raise ConfigValidationError(\n                    \"LLM with quantization requires the 'local' backend, but backend will be set \"\n                    \"to Ray since Ray is already running locally.\"\n                )\n        except ImportError:\n            pass\n\n\n@register_config_check\ndef check_llm_text_encoder_is_not_used_with_ecd(config: \"ModelConfig\") -> None:\n    \"\"\"Checks that a pretrained text encoder is not used for ECD models with a text output feature.\"\"\"\n    if config.model_type != MODEL_ECD:\n        return\n\n    if config.input_features[0].type != TEXT:\n        return\n\n    if config.output_features[0].type != TEXT:\n        return\n\n    if (\n        hasattr(config.input_features[0].encoder, \"pretrained_model_name_or_path\")\n        and config.input_features[0].encoder.pretrained_model_name_or_path\n    ):\n        raise ConfigValidationError(\"Please use the `model_type: llm` for text-to-text models.\")\n\n\n@register_config_check\ndef check_qlora_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that all the necessary settings are in place for QLoRA.\"\"\"\n    if config.model_type != MODEL_LLM or config.trainer.type == \"none\":\n        return\n\n    if config.quantization and (not config.adapter or config.adapter.type != \"lora\"):\n        raise ConfigValidationError(\"Fine-tuning and LLM with quantization requires using the 'lora' adapter\")\n\n\n@register_config_check\ndef check_qlora_merge_and_unload_compatibility(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that model.merge_and_unload() is supported by underlying model.save_pretrained() when merging QLoRA\n    layers.\"\"\"\n    if config.model_type != MODEL_LLM or config.trainer.type == \"none\":\n        return\n\n    if not (\n        config.adapter\n        and config.adapter.type in [\"lora\", \"adalora\"]\n        and config.adapter.postprocessor\n        and config.adapter.postprocessor.merge_adapter_into_base_model\n        and config.quantization\n    ):\n        return\n\n    if config.quantization.bits < MIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD:\n        raise ConfigValidationError(\n            f\"\"\"This operation will entail merging LoRA layers on a {config.quantization.bits}-bit \\\nquantized model.  Calling \"save_pretrained()\" on that model is currently unsupported.  If you want to merge the LoRA \\\nadapter weights into the base model, you need to use 8-bit quantization or do non-quantized based training by removing \\\nthe quantization section from your Ludwig configuration.\"\"\"\n        )\n\n\n@register_config_check\ndef check_prompt_requirements(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Checks that prompt's template and task properties are valid, according to the description on the schema.\"\"\"\n    if config.model_type != MODEL_LLM:\n        return\n\n    # TODO: `prompt` by default should be set to null, not a default dict:\n    # # If no prompt is provided, no validation necessary:\n    # if not config.prompt:\n    #     return\n    from ludwig.schema.llms.prompt import PromptConfig, RetrievalConfig\n\n    if config.prompt == PromptConfig():\n        return\n\n    template = config.prompt.template\n    task = config.prompt.task\n    retrieval = config.prompt.retrieval\n\n    # If template is NOT provided, then task is required for zero/few shot learning:\n    if not template and not task:\n        raise ConfigValidationError(\"A prompt task is required if no template is provided!\")\n\n    template_refs = set(findall(r\"\\{(.*?)\\}\", template)) if isinstance(template, str) else set()\n\n    # If a template IS provided (i.e. we are not doing a built-in zero/few-shot learning), then...\n    if template:\n        # If task is also provided, the template must contain it:\n        if task and \"__task__\" not in template_refs:\n            raise ConfigValidationError(\n                \"When providing a task, you must make sure that the task keyword `{__task__} is \"\n                \"present somewhere in the template string!\"\n            )\n\n        # If retrieval is also provided, the template must reference it:\n        # TODO: retrieval by default should be set to null, not a default dict:\n        if retrieval and retrieval != RetrievalConfig() and \"__context__\" not in template_refs:\n            raise ConfigValidationError(\n                \"When providing a retrieval config, you must make sure that the task keyword `{__context__}` is \"\n                \"present somewhere in the template string!\"\n            )\n\n        # Otherwise, the template should at least contain the sample keyword or some input column:\n        # TODO: len(template_refs) is a hacky attempt to check that there are references to *something* in the\n        # string. The proper validation is to check the references against the features in the user's dataset - but we\n        # do not have access to the dataset in this code path right now.\n        if not task:\n            if len(template_refs) == 0 and \"__sample__\" not in template_refs:\n                raise ConfigValidationError(\n                    \"A template must contain at least one reference to a column or the sample keyword {__sample__} for \"\n                    \"a JSON-serialized representation of non-output feature columns.\"\n                )\n\n        # Raise an error if template has a placeholder for the output feature name (column).\n        output_feature_col = config.output_features[0].column\n        if output_feature_col in template_refs:\n            raise ConfigValidationError(\n                \"Prompt template should not have a reference to the output feature. The output feature is \"\n                \"automatically added to the end of the prompt template merged with the input at training time.\"\n            )\n\n\n@register_config_check\ndef check_sample_ratio_and_size_compatible(config: \"ModelConfig\") -> None:\n    sample_ratio = config.preprocessing.sample_ratio\n    sample_size = config.preprocessing.sample_size\n    if sample_size is not None and sample_ratio < 1.0:\n        raise ConfigValidationError(\"sample_size cannot be used when sample_ratio < 1.0\")\n"
  },
  {
    "path": "ludwig/config_validation/preprocessing.py",
    "content": "def check_global_max_sequence_length_fits_prompt_template(metadata, global_preprocessing_parameters):\n    \"\"\"Checks that the prompt template fits within the global max sequence length.\"\"\"\n\n    if (\n        \"global_max_sequence_length\" in global_preprocessing_parameters\n        and global_preprocessing_parameters[\"global_max_sequence_length\"] is not None\n    ):\n        for feature_name, feature_metadata in metadata.items():\n            if (\n                \"prompt_template_num_tokens\" in feature_metadata\n                and feature_metadata[\"prompt_template_num_tokens\"]\n                > global_preprocessing_parameters[\"global_max_sequence_length\"]\n            ):\n                raise ValueError(\n                    f'The prompt contains ({feature_metadata[\"prompt_template_num_tokens\"]}) tokens, which is more '\n                    f\"than the the global_max_sequence_length \"\n                    f'({global_preprocessing_parameters[\"global_max_sequence_length\"]}), which will remove all unique '\n                    \"information. Shorten the prompt, or increase the global max sequence length to > \"\n                    f'({feature_metadata[\"prompt_template_num_tokens\"]}) to include the full prompt.'\n                )\n"
  },
  {
    "path": "ludwig/config_validation/validation.py",
    "content": "from functools import lru_cache\nfrom threading import Lock\n\nimport jsonschema.exceptions\nfrom jsonschema import Draft7Validator, validate\nfrom jsonschema.validators import extend\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BASE_MODEL, MODEL_ECD, MODEL_LLM, MODEL_TYPE\nfrom ludwig.error import ConfigValidationError\n\n# TODO(travis): figure out why we need these imports to avoid circular import error\nfrom ludwig.schema.combiners.utils import get_combiner_jsonschema  # noqa\nfrom ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema  # noqa\nfrom ludwig.schema.hyperopt import get_hyperopt_jsonschema  # noqa\nfrom ludwig.schema.trainer import get_model_type_jsonschema, get_trainer_jsonschema  # noqa\nfrom ludwig.schema.utils import unload_jsonschema_from_marshmallow_class\n\nVALIDATION_LOCK = Lock()\n\n\n@DeveloperAPI\n@lru_cache(maxsize=3)\ndef get_schema(model_type: str = MODEL_ECD):\n    # Force populate combiner registry:\n    import ludwig.combiners.combiners  # noqa: F401\n    from ludwig.schema.model_types.base import model_type_schema_registry\n\n    cls = model_type_schema_registry[model_type]\n    props = unload_jsonschema_from_marshmallow_class(cls)[\"properties\"]\n\n    # TODO: Replace with more robust required logic later.\n    required = [\"input_features\", \"output_features\"]\n    if model_type == MODEL_LLM:\n        required += [BASE_MODEL]\n\n    return {\n        \"type\": \"object\",\n        \"properties\": props,\n        \"title\": \"model_options\",\n        \"description\": \"Settings for Ludwig configuration\",\n        \"required\": required,\n        \"additionalProperties\": True,\n    }\n\n\n@lru_cache(maxsize=1)\ndef get_validator():\n    # Manually add support for tuples (pending upstream changes: https://github.com/Julian/jsonschema/issues/148):\n    def custom_is_array(checker, instance):\n        return isinstance(instance, list) or isinstance(instance, tuple)\n\n    # This creates a new class, so cache to prevent a memory leak:\n    # https://github.com/python-jsonschema/jsonschema/issues/868\n    type_checker = Draft7Validator.TYPE_CHECKER.redefine(\"array\", custom_is_array)\n    return extend(Draft7Validator, type_checker=type_checker)\n\n\n@DeveloperAPI\ndef check_schema(updated_config):\n    \"\"\"Emulates the pure JSONSchema validation that could be used in an environment without marshmallow.\n\n    The incoming config may not be comprehensive, but is assumed to be up to date with the latest ludwig schema.\n    \"\"\"\n    model_type = updated_config.get(MODEL_TYPE, MODEL_ECD)\n    error = None\n    with VALIDATION_LOCK:\n        try:\n            validate(instance=updated_config, schema=get_schema(model_type=model_type), cls=get_validator())\n        except jsonschema.exceptions.ValidationError as e:\n            # Capture error but don't raise here, otherwise we get the full output from `e`, which contains a dump\n            # of the entire schema\n            error = e\n\n    if error is not None:\n        raise ConfigValidationError(f\"Failed to validate JSON schema for config. Error: {error.message}\") from error\n"
  },
  {
    "path": "ludwig/constants.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nINPUT_FEATURES = \"input_features\"\nOUTPUT_FEATURES = \"output_features\"\n\nINPUT = \"input\"\nOUTPUT = \"output\"\nBINARY = \"binary\"\nCATEGORY = \"category\"\nCATEGORY_DISTRIBUTION = \"category_distribution\"\nINT = \"int\"\nFLOAT = \"float\"\nSPACE = \"space\"\nNUMBER = \"number\"\nSET = \"set\"\nBAG = \"bag\"\nTEXT = \"text\"\nSEQUENCE = \"sequence\"\nTIMESERIES = \"timeseries\"\nIMAGE = \"image\"\nAUDIO = \"audio\"\nDATE = \"date\"\nH3 = \"h3\"\nVECTOR = \"vector\"\nHEIGHT = \"height\"\nWIDTH = \"width\"\nINFER_IMAGE_DIMENSIONS = \"infer_image_dimensions\"\nINFER_IMAGE_MAX_HEIGHT = \"infer_image_max_height\"\nINFER_IMAGE_MAX_WIDTH = \"infer_image_max_width\"\nINFER_IMAGE_SAMPLE_SIZE = \"infer_image_sample_size\"\nINFER_IMAGE_NUM_CLASSES = \"infer_image_num_classes\"\nIMAGE_MAX_CLASSES = 128\nNUM_CLASSES = \"num_classes\"\nNUM_CHANNELS = \"num_channels\"\nREQUIRES_EQUAL_DIMENSIONS = \"requires_equal_dimensions\"\nUSE_PRETRAINED = \"use_pretrained\"\nTRAINABLE = \"trainable\"\nCLASS_WEIGHTS = \"class_weights\"\nUSED_TOKENS = \"used_tokens\"\nLOSS = \"loss\"\nROC_AUC = \"roc_auc\"\nEVAL_LOSS = \"eval_loss\"\nTRAIN_MEAN_LOSS = \"train_mean_loss\"\nSEQUENCE_SOFTMAX_CROSS_ENTROPY = \"sequence_softmax_cross_entropy\"\nNEXT_TOKEN_SOFTMAX_CROSS_ENTROPY = \"next_token_softmax_cross_entropy\"\nSOFTMAX_CROSS_ENTROPY = \"softmax_cross_entropy\"\nSIGMOID_CROSS_ENTROPY = \"sigmoid_cross_entropy\"\nBINARY_WEIGHTED_CROSS_ENTROPY = \"binary_weighted_cross_entropy\"\nTHRESHOLD = \"threshold\"\nVALIDATION_METRIC = \"validation_metric\"\nACCURACY = \"accuracy\"\nACCURACY_MICRO = \"accuracy_micro\"\nHITS_AT_K = \"hits_at_k\"\nMEAN_HITS_AT_K = \"mean_hits_at_k\"\nERROR = \"error\"\nABSOLUTE_ERROR = \"absolute_error\"\nSQUARED_ERROR = \"squared_error\"\nMEAN_SQUARED_ERROR = \"mean_squared_error\"\nROOT_MEAN_SQUARED_ERROR = \"root_mean_squared_error\"\nROOT_MEAN_SQUARED_PERCENTAGE_ERROR = \"root_mean_squared_percentage_error\"\nMEAN_ABSOLUTE_ERROR = \"mean_absolute_error\"\nMEAN_ABSOLUTE_PERCENTAGE_ERROR = \"mean_absolute_percentage_error\"\nHUBER = \"huber\"\nCORN = \"corn\"\nR2 = \"r2\"\nEDIT_DISTANCE = \"edit_distance\"\nPERPLEXITY = \"perplexity\"\nNEXT_TOKEN_PERPLEXITY = \"next_token_perplexity\"\nJACCARD = \"jaccard\"\nPRECISION = \"precision\"\nRECALL = \"recall\"\nSPECIFICITY = \"specificity\"\nPREDICTIONS = \"predictions\"\nRESPONSE = \"RESPONSE\"\nTOP_K = \"top_k\"\nTOP_K_PREDICTIONS = \"top_k_predictions\"\nPROBABILITY = \"probability\"\nPROBABILITIES = \"probabilities\"\nSPLIT_PROBABILITIES = \"split_probabilities\"\nTOKEN_ACCURACY = \"token_accuracy\"\nLAST_ACCURACY = \"last_accuracy\"\nSEQUENCE_ACCURACY = \"sequence_accuracy\"\nLAST_PROBABILITIES = \"last_probabilities\"\nLAST_PREDICTIONS = \"last_predictions\"\nLENGTHS = \"lengths\"\nTIED = \"tied\"\nCOMBINED = \"combined\"\n\nPREPROCESSING = \"preprocessing\"\nFILL_WITH_CONST = \"fill_with_const\"\nFILL_WITH_MODE = \"fill_with_mode\"\nFILL_WITH_MEAN = \"fill_with_mean\"\nFILL_WITH_FALSE = \"fill_with_false\"\nFILL_WITH_TRUE = \"fill_with_true\"\nBFILL = \"bfill\"\nFFILL = \"ffill\"\nDROP_ROW = \"drop_row\"\nMISSING_VALUE_STRATEGY = \"missing_value_strategy\"\nMISSING_VALUE_STRATEGY_OPTIONS = [\n    FILL_WITH_CONST,\n    FILL_WITH_MODE,\n    BFILL,\n    FFILL,\n    DROP_ROW,\n]\n\nCROP_OR_PAD = \"crop_or_pad\"\nINTERPOLATE = \"interpolate\"\nRESIZE_METHODS = [CROP_OR_PAD, INTERPOLATE]\n\n# Special symbols for text.\nSTOP_SYMBOL = \"<EOS>\"\nSTART_SYMBOL = \"<SOS>\"\nPADDING_SYMBOL = \"<PAD>\"\nUNKNOWN_SYMBOL = \"<UNK>\"\n\nTRAINER = \"trainer\"\nOPTIMIZER = \"optimizer\"\nMETRIC = \"metric\"\nPREDICTION = \"prediction\"\nLOGITS = \"logits\"\nHIDDEN = \"hidden\"\nLAST_HIDDEN = \"last_hidden\"\nENCODER_OUTPUT = \"encoder_output\"\nENCODER_OUTPUT_STATE = \"encoder_output_state\"\nPROJECTION_INPUT = \"projection_input\"\nLEARNING_RATE_SCHEDULER = \"learning_rate_scheduler\"\n\nSEMANTIC = \"semantic\"\n\nRANDOM = \"random\"\nSUM = \"sum\"\nAPPEND = \"append\"\nSEQ_SUM = \"seq_sum\"\nAVG_EXP = \"avg_exp\"\n\nTRAIN = \"train\"\nTRAINING = \"training\"\nVALIDATION = \"validation\"\nTEST = \"test\"\nEVALUATION = \"evaluation\"\nSPLIT = \"split\"\nFORCE_SPLIT = \"force_split\"\nSTRATIFY = \"stratify\"\nFULL = \"full\"\nTRAIN_SPLIT = 0\nVALIDATION_SPLIT = 1\nTEST_SPLIT = 2\nMIN_DATASET_SPLIT_ROWS = 3  # The minimum number of rows in a split. Splits smaller than this size are treated as empty.\n\nMETA = \"meta\"\n\nHYPEROPT = \"hyperopt\"\nSTRATEGY = \"strategy\"\nEXECUTOR = \"executor\"\nMINIMIZE = \"minimize\"\nMAXIMIZE = \"maximize\"\nSAMPLER = \"sampler\"\nNUM_SAMPLES = \"num_samples\"\nSEARCH_ALG = \"search_alg\"\nSCHEDULER = \"scheduler\"\nPARAMETERS = \"parameters\"\nMAX_CONCURRENT_TRIALS = \"max_concurrent_trials\"\nCPU_RESOURCES_PER_TRIAL = \"cpu_resources_per_trial\"\nGPU_RESOURCES_PER_TRIAL = \"gpu_resources_per_trial\"\nGOAL = \"goal\"\nGRID_SEARCH = \"grid_search\"\n\nNAME = \"name\"\nCOLUMN = \"column\"\nTYPE = \"type\"\nACTIVE = \"active\"\n\nRAY = \"ray\"\nIN_MEMORY = \"in_memory\"\n\nPROC_COLUMN = \"proc_column\"\n\nCHECKSUM = \"checksum\"\n\nHDF5 = \"hdf5\"\nPARQUET = \"parquet\"\n\nSRC = \"dataset_src\"\n\nEARLY_STOP = \"early_stop\"\nEPOCHS = \"epochs\"\nBATCH_SIZE = \"batch_size\"\nEVAL_BATCH_SIZE = \"eval_batch_size\"\nEFFECTIVE_BATCH_SIZE = \"effective_batch_size\"\nMAX_BATCH_SIZE = \"max_batch_size\"\nDEFAULT_BATCH_SIZE = \"auto\"\nFALLBACK_BATCH_SIZE = 128\n# The smallest batch size that is supported on Ludwig.\nMINIMUM_BATCH_SIZE = 1\n# 2^40. Used for `max_batch_size` config param. Not a hard constraint for `batch_size` config param.\nMAX_POSSIBLE_BATCH_SIZE = 1099511627776\n# min batch size. Used as a floor for batch size tuning.\nMIN_POSSIBLE_BATCH_SIZE = 1\n# max batch size for dataset is 20% of dataset size\nMAX_BATCH_SIZE_DATASET_FRACTION = 0.2\nMAX_CPU_BATCH_SIZE = 128\nLEARNING_RATE = \"learning_rate\"\nINPUT_SIZE = \"input_size\"\nUSE_BIAS = \"use_bias\"\nBIAS = \"bias\"\nDEFAULT_USE_BIAS = \"default_use_bias\"\nDEFAULT_BIAS = \"default_bias\"\nCONV_USE_BIAS = \"conv_use_bias\"\nCONV_BIAS = \"conv_bias\"\nAUTO = \"auto\"\nCONFIG = \"config\"\n\nCLIP = \"clip\"\nDEPENDENCIES = \"dependencies\"\nREDUCE_INPUT = \"reduce_input\"\nREDUCE_DEPENDENCIES = \"reduce_dependencies\"\n\nBACKEND = \"backend\"\nCOMBINER = \"combiner\"\n\nENCODER = \"encoder\"\nDECODER = \"decoder\"\n\nTRAINABLE = \"trainable\"\n\nDEFAULTS = \"defaults\"\nDEFAULT = \"default\"\nDEFAULT_VALIDATION_METRIC = \"default_validation_metric\"\n\nBALANCE_PERCENTAGE_TOLERANCE = 0.03\nIMBALANCE_DETECTION_RATIO = 0.05\n\nTABULAR = \"tabular\"\nAUTOML_DEFAULT_TABULAR_MODEL = \"tabnet\"\nAUTOML_DEFAULT_TEXT_ENCODER = \"bert\"\nAUTOML_SMALLER_TEXT_ENCODER = \"distilbert\"\nAUTOML_TEXT_ENCODER_MAX_TOKEN_LEN = 512\nAUTOML_SMALLER_TEXT_LENGTH = 128\nAUTOML_LARGE_TEXT_DATASET = 100000\nAUTOML_MAX_ROWS_PER_CHECKPOINT = 350000\nAUTOML_DEFAULT_IMAGE_ENCODER = \"stacked_cnn\"\n\nHYPEROPT_WARNING = (\n    \"You are running the ludwig train command but there’s a hyperopt section present in your config. \"\n    \"It will be ignored. If you want to run hyperopt you should use the following command: ludwig \"\n    \"hyperopt\\n\\n\"\n)\n\nCONTINUE_PROMPT = \"Do you want to continue? \"\n\nDEFAULT_AUDIO_TENSOR_LENGTH = 70000\nAUDIO_FEATURE_KEYS = [\n    \"type\",\n    \"window_length_in_s\",\n    \"window_shift_in_s\",\n    \"num_fft_points\",\n    \"window_type\",\n    \"num_filter_bands\",\n]\n\nBASE_MODEL = \"base_model\"\nMODEL_TYPE = \"model_type\"\nMODEL_ECD = \"ecd\"\nMODEL_LLM = \"llm\"\nDASK_MODULE_NAME = \"dask.dataframe\"\nLUDWIG_VERSION = \"ludwig_version\"\n\nPREPROCESSOR = \"preprocessor\"\nPREDICTOR = \"predictor\"\nPOSTPROCESSOR = \"postprocessor\"\nTARGET_MODULES = \"target_modules\"\n\nGENERATION = \"generation\"\nPROMPT = \"prompt\"\nADAPTER = \"adapter\"\nQUANTIZATION = \"quantization\"\nMIN_QUANTIZATION_BITS_FOR_MERGE_AND_UNLOAD = 8\nPRETRAINED_ADAPTER_WEIGHTS = \"pretrained_adapter_weights\"\nMERGE_ADAPTER_INTO_BASE_MODEL = \"merge_adapter_into_base_model\"\nPROGRESSBAR = \"progressbar\"\n\n# CrossEntropyLoss for LLMs\nIGNORE_INDEX_TOKEN_ID = -100\n\nS3 = \"s3\"\nCACHE = \"cache\"\n\n# If `use_torch_profiler=True` in LudwigProfiler, LUDWIG_TAG is prepended to the specified experiment tag\n# (LudwigProfiler(tag=\"...\", ..)). This edited tag is passed in to `torch.profiler.record_function` so we can\n# retrieve torch ops for the tagged code blocks/functions.\nLUDWIG_TAG = \"[ludwig]\"\n\n# Retry constants\nTRIES = 5\nDELAY = 1\nBACKOFF = 2\nJITTER = (0, 1)\n\n# image support constants\nIMAGENET1K = \"imagenet1k\"\n\nAUGMENTATION = \"augmentation\"\n\nLUDWIG_SCHEMA_VALIDATION_POLICY = \"LUDWIG_SCHEMA_VALIDATION_POLICY\"\n"
  },
  {
    "path": "ludwig/contrib.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\"\"\"Module for handling contributed support.\"\"\"\n\nimport argparse\n\nfrom ludwig.contribs import contrib_registry, ContribLoader\n\n\ndef create_load_action(contrib_loader: ContribLoader) -> argparse.Action:\n    class LoadContribAction(argparse.Action):\n        def __call__(self, parser, namespace, values, option_string):\n            items = getattr(namespace, self.dest) or []\n            items.append(contrib_loader.load())\n            setattr(namespace, self.dest, items)\n\n    return LoadContribAction\n\n\ndef add_contrib_callback_args(parser: argparse.ArgumentParser):\n    for contrib_name, contrib_loader in contrib_registry.items():\n        parser.add_argument(\n            f\"--{contrib_name}\",\n            dest=\"callbacks\",\n            nargs=0,\n            action=create_load_action(contrib_loader),\n        )\n\n\ndef preload(argv):\n    for arg in argv:\n        if arg.startswith(\"--\"):\n            arg = arg[2:]\n\n        if arg in contrib_registry:\n            contrib_registry[arg].preload()\n"
  },
  {
    "path": "ludwig/contribs/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\"\"\"All contrib classes must implement the `ludwig.callbacks.Callback` interface.\n\nIf you don't want to handle the call, either provide an empty method with `pass`, or just don't implement the method.\n\"\"\"\n\nfrom abc import ABC, abstractmethod\n\nfrom ludwig.callbacks import Callback\n\n\nclass ContribLoader(ABC):\n    @abstractmethod\n    def load(self) -> Callback:\n        \"\"\"Returns an instantiation of the callback instance, whose callback hooks will be invoked at runtime.\"\"\"\n\n    def preload(self):\n        \"\"\"Will always be called when Ludwig CLI is invoked, preload gives the callback an opportunity to import or\n        create any shared resources.\n\n        Importing required 3rd-party libraries should be done here i.e. import wandb. preload is guaranteed to be called\n        before any other callback method, and will only be called once per process.\n        \"\"\"\n\n\n# Contributors, load your class here:\n\n\nclass AimLoader(ContribLoader):\n    def load(self) -> Callback:\n        from ludwig.contribs.aim import AimCallback\n\n        return AimCallback()\n\n    def preload(self):\n        import aim  # noqa\n\n\nclass CometLoader(ContribLoader):\n    def load(self) -> Callback:\n        from ludwig.contribs.comet import CometCallback\n\n        return CometCallback()\n\n    def preload(self):\n        import comet_ml  # noqa\n\n\nclass WandbLoader(ContribLoader):\n    def load(self) -> Callback:\n        from ludwig.contribs.wandb import WandbCallback\n\n        return WandbCallback()\n\n    def preload(self):\n        import wandb  # noqa\n\n\nclass MlflowLoader(ContribLoader):\n    def load(self) -> Callback:\n        from ludwig.contribs.mlflow import MlflowCallback\n\n        return MlflowCallback()\n\n\ncontrib_registry = {\n    # Contributors, add your class here:\n    \"comet\": CometLoader(),\n    \"wandb\": WandbLoader(),\n    \"mlflow\": MlflowLoader(),\n    \"aim\": AimLoader(),\n}\n"
  },
  {
    "path": "ludwig/contribs/aim.py",
    "content": "import json\nimport logging\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.callbacks import Callback\nfrom ludwig.utils.data_utils import NumpyEncoder\nfrom ludwig.utils.package_utils import LazyLoader\n\naim = LazyLoader(\"aim\", globals(), \"aim\")\n\nlogger = logging.getLogger(__name__)\n\n\n@PublicAPI\nclass AimCallback(Callback):\n    \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n    def __init__(self, repo=None):\n        self.repo = repo\n\n    def on_train_init(\n        self,\n        base_config,\n        experiment_directory,\n        experiment_name,\n        model_name,\n        output_directory,\n        resume_directory,\n    ):\n        logger.info(\"aim.on_train_init() called...\")\n\n        try:\n            query = f'run.name == \"{model_name}\"'\n            if self.repo is None:\n                aim_repo = aim.Repo.default_repo()\n            else:\n                aim_repo = aim.Repo.from_path(self.repo)\n            runs_generator = aim_repo.query_runs(query)\n            run = next(runs_generator.iter_runs())\n            run_hash = run.run.hash\n            self.aim_run = aim.Run(run_hash=run_hash, repo=self.repo, experiment=experiment_name)\n        except Exception:\n            self.aim_run = aim.Run(repo=self.repo, experiment=experiment_name)\n            self.aim_run.name = model_name\n\n        self.aim_run[\"base_config\"] = self.normalize_config(base_config)\n\n        params = dict(name=model_name, dir=experiment_directory)\n        self.aim_run[\"params\"] = params\n\n    def aim_track(self, progress_tracker):\n        logger.info(f\"aim.aim_track() called for epoch {progress_tracker.epoch}, step: {progress_tracker.steps}\")\n\n        if self.aim_run:\n            for key, value in progress_tracker.log_metrics().items():\n                if \"metrics\" in key and \"best\" not in key:\n                    metrics_dict_name, feature_name, metric_name = key.split(\".\")\n\n                    self.aim_run.track(\n                        value,\n                        name=metric_name,\n                        context={metrics_dict_name: feature_name},\n                        epoch=progress_tracker.epoch,\n                        step=progress_tracker.steps,\n                    )\n\n    def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator: bool):\n        pass\n\n    def on_train_start(self, model, config, *args, **kwargs):\n        logger.info(\"aim.on_train_start() called...\")\n\n        config = config.copy()\n        del config[\"input_features\"]\n        del config[\"output_features\"]\n\n        self.aim_run[\"train_config\"] = self.normalize_config(config)\n\n    def on_train_end(self, output_directory, *args, **kwargs):\n        pass\n\n    def on_eval_end(self, trainer, progress_tracker, save_path):\n        optimizer_config = {}\n        for index, group in enumerate(trainer.optimizer.param_groups):\n            for key in group:\n                if \"param\" not in key:\n                    optimizer_config[f\"param_group_{index}_{key}\"] = group[key]\n\n        self.aim_run[\"optimizer_config\"] = self.normalize_config(optimizer_config)\n\n        self.aim_track(progress_tracker)\n\n    def on_ludwig_end(self):\n        self.aim_run.close()\n        self.aim_run = None\n\n    def on_visualize_figure(self, fig):\n        logger.info(\"aim.on_visualize_figure() called...\")\n        if self.aim_run:\n            self.aim_run.track(aim.Figure(fig), name=\"Figure\", context={\"type\": \"Training Figure\"})\n\n    @staticmethod\n    def normalize_config(config):\n        \"\"\"Convert to json string and back again to remove numpy types.\"\"\"\n        return json.loads(json.dumps(config, cls=NumpyEncoder))\n"
  },
  {
    "path": "ludwig/contribs/comet.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\nfrom datetime import datetime\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.callbacks import Callback\nfrom ludwig.utils.package_utils import LazyLoader\n\ncomet_ml = LazyLoader(\"comet_ml\", globals(), \"comet_ml\")\n\nlogger = logging.getLogger(__name__)\n\n\n@PublicAPI\nclass CometCallback(Callback):\n    \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n    def __init__(self):\n        self.cometml_experiment = None\n\n    def on_train_init(\n        self,\n        base_config,\n        experiment_directory,\n        experiment_name,\n        model_name,\n        output_directory,\n        resume_directory,\n    ):\n        if self.cometml_experiment:\n            # Comet ML already initialized\n            return\n\n        try:\n            self.cometml_experiment = comet_ml.Experiment(log_code=False, project_name=experiment_name)\n        except Exception:\n            self.cometml_experiment = None\n            logger.exception(\"comet_ml.Experiment() had errors. Perhaps you need to define COMET_API_KEY\")\n            raise\n\n        self.cometml_experiment.set_name(model_name)\n        self.cometml_experiment.set_filename(\"Ludwig API\")\n        config = comet_ml.get_config()\n        self._save_config(config, directory=experiment_directory)\n\n    def on_train_start(self, model, config, config_fp, *args, **kwargs):\n        if self.cometml_experiment:\n            # todo v0.4: currently not clear way to set model graph\n            # see: https://github.com/comet-ml/issue-tracking/issues/296\n            # if model:\n            #     self.cometml_experiment.set_model_graph(\n            #         str(model._graph.as_graph_def()))\n\n            if config:\n                if config_fp:\n                    base_name = os.path.basename(config_fp)\n                else:\n                    base_name = \"config.yaml\"\n                if \".\" in base_name:\n                    base_name = base_name.rsplit(\".\", 1)[0] + \".json\"\n                else:\n                    base_name = base_name + \".json\"\n                self.cometml_experiment.log_asset_data(config, base_name)\n\n    def on_train_end(self, output_directory, *args, **kwargs):\n        if self.cometml_experiment:\n            self.cometml_experiment.log_asset_folder(output_directory)\n\n    def on_eval_end(self, trainer, progress_tracker, save_path):\n        \"\"\"Called from ludwig/models/model.py.\"\"\"\n        if self.cometml_experiment:\n            for key, value in progress_tracker.log_metrics().items():\n                self.cometml_experiment.log_metric(key, value)\n\n    def on_epoch_end(self, trainer, progress_tracker, save_path):\n        \"\"\"Called from ludwig/models/model.py.\"\"\"\n        if self.cometml_experiment:\n            for key, value in progress_tracker.log_metrics().items():\n                self.cometml_experiment.log_metric(key, value)\n\n    def on_visualize_figure(self, fig):\n        if self.cometml_experiment:\n            self.cometml_experiment.log_figure(fig)\n\n    def on_cmdline(self, cmd, *args):\n        self.cometml_experiment = None\n        if cmd in {\"train\", \"experiment\"}:\n            # create a new experiment\n            try:\n                self.cometml_experiment = comet_ml.Experiment(log_code=False)\n            except Exception:\n                logger.exception(\"comet_ml.Experiment() had errors. Perhaps you need to define COMET_API_KEY\")\n                return\n        elif cmd in {\"visualize\", \"predict\", \"evaluate\"}:\n            # restore from an existing experiment\n            try:\n                self.cometml_experiment = comet_ml.ExistingExperiment()\n            except Exception:\n                logger.exception(\"Ignored --comet. No '.comet.config' file\")\n                return\n        else:\n            # unhandled command\n            return\n\n        cli = self._make_command_line(cmd, args)\n        self.cometml_experiment.set_code(cli)\n        self.cometml_experiment.set_filename(\"Ludwig CLI\")\n        self._log_html(cli)\n        config = comet_ml.get_config()\n        self._save_config(config)\n\n    def _save_config(self, config, directory=\".\"):\n        # save the .comet.config here:\n        config[\"comet.experiment_key\"] = self.cometml_experiment.id\n        config.save(directory=directory)\n\n    def _log_html(self, text):\n        # log the text to the html tab:\n        now = datetime.now()\n        timestamp = now.strftime(\"%m/%d/%Y %H:%M:%S\")\n        self.cometml_experiment.log_html(f\"<p><b>{timestamp}</b>: {text}</p>\")\n\n    def _make_command_line(self, cmd, args):\n        # put the commet flag back in:\n        arg_str = \" \".join(list(args[:2]) + [\"--comet\"] + list(args[2:]))\n        return f\"ludwig {cmd} {arg_str}\"\n"
  },
  {
    "path": "ludwig/contribs/mlflow/__init__.py",
    "content": "import logging\nimport os\nimport queue\nimport threading\n\nfrom ludwig.api_annotations import DeveloperAPI, PublicAPI\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import TRAINER\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, TRAIN_SET_METADATA_FILE_NAME\nfrom ludwig.types import TrainingSetMetadataDict\nfrom ludwig.utils.data_utils import chunk_dict, flatten_dict, save_json, to_json_dict\nfrom ludwig.utils.package_utils import LazyLoader\n\nmlflow = LazyLoader(\"mlflow\", globals(), \"mlflow\")\n\nlogger = logging.getLogger(__name__)\n\n\ndef _get_runs(experiment_id: str):\n    return mlflow.tracking.client.MlflowClient().search_runs([experiment_id])\n\n\n@DeveloperAPI\ndef get_or_create_experiment_id(experiment_name, artifact_uri: str = None):\n    \"\"\"Gets experiment id from mlflow.\"\"\"\n    experiment = mlflow.get_experiment_by_name(experiment_name)\n    if experiment is not None:\n        return experiment.experiment_id\n    return mlflow.create_experiment(name=experiment_name, artifact_location=artifact_uri)\n\n\n# Included for backwards compatibility, Deprecated.\n# TODO(daniel): delete this.\n_get_or_create_experiment_id = get_or_create_experiment_id\n\n\n@PublicAPI\nclass MlflowCallback(Callback):\n    def __init__(self, tracking_uri=None, log_artifacts: bool = True):\n        self.logged_steps = set()\n\n        if tracking_uri:\n            mlflow.set_tracking_uri(tracking_uri)\n        self.tracking_uri = mlflow.get_tracking_uri()\n\n        active_run = mlflow.active_run()\n        if active_run is not None:\n            # Use experiment already set in the current environment\n            self.run = active_run\n            self.experiment_id = self.run.info.experiment_id\n            self.experiment_name = mlflow.get_experiment(self.experiment_id).name\n            self.external_run = True\n        else:\n            # Will create an experiment at training time\n            self.run = None\n            self.experiment_id = None\n            self.experiment_name = None\n            self.external_run = False\n\n        self.run_ended = False\n        self.training_set_metadata = None\n        self.config = None\n        self.save_in_background = True\n        self.save_fn = None\n        self.save_thread = None\n        self.log_artifacts = log_artifacts\n\n    def get_experiment_id(self, experiment_name):\n        return get_or_create_experiment_id(experiment_name)\n\n    def on_preprocess_end(\n        self,\n        training_set: \"Dataset\",  # noqa\n        validation_set: \"Dataset\",  # noqa\n        test_set: \"Dataset\",  # noqa\n        training_set_metadata: TrainingSetMetadataDict,\n    ):\n        self.training_set_metadata = training_set_metadata\n\n    def on_hyperopt_init(self, experiment_name):\n        self.experiment_id = self.get_experiment_id(experiment_name)\n        self.experiment_name = experiment_name\n\n    def on_hyperopt_trial_start(self, parameters):\n        # Filter out mlflow params like tracking URI, experiment ID, etc.\n        params = {k: v for k, v in parameters.items() if k != \"mlflow\"}\n        self._log_params({\"hparam\": params})\n\n        # TODO(travis): figure out a good way to support this. The problem with\n        # saving artifacts in the background with hyperopt is early stopping. If\n        # the scheduler decides to terminate a process, then currently there's no\n        # mechanism to detect this a \"flush\" the queue of pending writes before\n        # stopping. Should work with Ray Tune team to come up with a solution.\n        self.save_in_background = False\n\n    def on_train_init(self, base_config, experiment_name, output_directory, resume_directory, **kwargs):\n        # Experiment may already have been set during hyperopt init, in\n        # which case we don't want to create a new experiment / run, as\n        # this should be handled by the executor.\n        if self.experiment_id is None:\n            mlflow.end_run()\n            self.experiment_id = self.get_experiment_id(experiment_name)\n            self.experiment_name = experiment_name\n\n        active_run = mlflow.active_run()\n        if active_run is not None:\n            # Currently active run started by Ray Tune MLflow mixin or external run\n            self.run = active_run\n        else:\n            run_id = None\n            if resume_directory is not None:\n                previous_runs = _get_runs(self.experiment_id)\n                if len(previous_runs) > 0:\n                    run_id = previous_runs[0].info.run_id\n            if run_id is not None:\n                self.run = mlflow.start_run(run_id=run_id)\n            else:\n                run_name = os.path.basename(output_directory)\n                self.run = mlflow.start_run(experiment_id=self.experiment_id, run_name=run_name)\n\n        self.log_config(base_config)\n\n    def log_config(self, config):\n        if self.log_artifacts:\n            mlflow.log_dict(to_json_dict(config), \"config.yaml\")\n\n    def on_train_start(self, config, **kwargs):\n        self.config = config\n        self._log_params({TRAINER: config[TRAINER]})\n\n    def on_train_end(self, output_directory):\n        if self.log_artifacts:\n            _log_artifacts(output_directory)\n        if self.run is not None and not self.external_run:\n            # Only end runs managed internally to this callback\n            mlflow.end_run()\n            self.run_ended = True\n\n    def on_trainer_train_setup(self, trainer, save_path, is_coordinator):\n        if not is_coordinator:\n            return\n\n        # When running on a remote worker, the model metadata files will only have been\n        # saved to the driver process, so re-save it here before uploading.\n        training_set_metadata_path = os.path.join(save_path, TRAIN_SET_METADATA_FILE_NAME)\n        if not os.path.exists(training_set_metadata_path):\n            save_json(training_set_metadata_path, self.training_set_metadata)\n\n        model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n        if not os.path.exists(model_hyperparameters_path):\n            save_json(model_hyperparameters_path, self.config)\n\n        if self.save_in_background:\n            save_queue = queue.Queue()\n            self.save_fn = lambda args: save_queue.put(args)\n            self.save_thread = threading.Thread(target=_log_mlflow_loop, args=(save_queue, self.log_artifacts))\n            self.save_thread.start()\n        else:\n            self.save_fn = lambda args: _log_mlflow(*args, self.log_artifacts)\n\n    def on_eval_end(self, trainer, progress_tracker, save_path):\n        if progress_tracker.steps not in self.logged_steps:\n            self.logged_steps.add(progress_tracker.steps)\n            # Adds a tuple to the logging queue.\n            # True is passed to indicate that the background saving loop should continue.\n            self.save_fn((progress_tracker.log_metrics(), progress_tracker.steps, save_path, True))\n\n    def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator):\n        if is_coordinator:\n            if progress_tracker.steps not in self.logged_steps:\n                self.logged_steps.add(progress_tracker.steps)\n                # Adds a tuple to the logging queue.\n                # False is passed to indicate that the background saving loop should break.\n                self.save_fn((progress_tracker.log_metrics(), progress_tracker.steps, save_path, False))\n            # False ensures that the background saving loop breaks.\n            # TODO(Justin): This should probably live in on_ludwig_end, once that's implemented.\n            self.save_fn((None, None, None, False))\n\n            # Close the save_thread.\n            if self.save_thread is not None:\n                self.save_thread.join()\n                # if self.save_thread.is_alive():\n                #     logger.warning(\"MLFlow save thread timed out and did not close properly.\")\n\n    def on_visualize_figure(self, fig):\n        # TODO: need to also include a filename for this figure\n        # mlflow.log_figure(fig)\n        pass\n\n    def prepare_ray_tune(self, train_fn, tune_config, tune_callbacks):\n        from functools import wraps\n\n        from ray.air.integrations.mlflow import setup_mlflow\n\n        mlflow_config = {\n            \"experiment_id\": self.experiment_id,\n            \"experiment_name\": self.experiment_name,\n            \"tracking_uri\": mlflow.get_tracking_uri(),\n        }\n\n        @wraps(train_fn)\n        def wrapper(config, **kwargs):\n            setup_mlflow(config, **mlflow_config)\n            return train_fn(config, **kwargs)\n\n        return wrapper, {\n            **tune_config,\n        }\n\n    def _log_params(self, params):\n        flat_params = flatten_dict(params)\n        for chunk in chunk_dict(flat_params, chunk_size=100):\n            mlflow.log_params(chunk)\n\n    def __setstate__(self, d):\n        self.__dict__ = d\n        if self.tracking_uri:\n            mlflow.set_tracking_uri(self.tracking_uri)\n        if self.run and not self.run_ended:\n            # Run has already been set, but may not be active due to training workers running in a separate\n            # process, so resume the run\n            mlflow.end_run()\n            self.run = mlflow.start_run(run_id=self.run.info.run_id, experiment_id=self.run.info.experiment_id)\n\n\ndef _log_mlflow_loop(q: queue.Queue, log_artifacts: bool = True):\n    \"\"\"The save_fn for the background thread that logs to MLFlow when save_in_background is True.\"\"\"\n    should_continue = True\n    while should_continue:\n        elem = q.get()\n        log_metrics, steps, save_path, should_continue = elem\n        if log_metrics is None:\n            # Break out of the loop if we're not going to log anything.\n            break\n\n        if \"llm_eval_examples\" in log_metrics and log_metrics[\"llm_eval_examples\"] is not None:\n            # mlflow.log_dict(log_metrics[\"llm_eval_examples\"], artifact_file=\"llm_eval_examples.json\")\n            # Delete the table from the metrics dict so we don't try to log it with the other metrics\n            del log_metrics[\"llm_eval_examples\"]\n        mlflow.log_metrics(log_metrics, step=steps)\n\n        if not q.empty():\n            # in other words, don't bother saving the model artifacts\n            # if we're about to do it again\n            continue\n\n        if log_artifacts:\n            _log_model(save_path)\n\n\ndef _log_mlflow(log_metrics, steps, save_path, should_continue, log_artifacts: bool = True):\n    \"\"\"The save_fn for the MlflowCallback.\n\n    This is used when save_in_background is False.\n    \"\"\"\n    if log_metrics is not None:\n        if \"llm_eval_examples\" in log_metrics and log_metrics[\"llm_eval_examples\"] is not None:\n            # mlflow.log_dict(log_metrics[\"llm_eval_examples\"], artifact_file=\"llm_eval_examples.json\")\n            # Delete the table from the metrics dict so we don't try to log it with the other metrics\n            del log_metrics[\"llm_eval_examples\"]\n        mlflow.log_metrics(log_metrics, step=steps)\n        if log_artifacts:\n            _log_model(save_path)\n\n\ndef _log_artifacts(output_directory):\n    try:\n        contents = os.listdir(output_directory)\n    except FileNotFoundError:\n        logger.warning(f\"_log_artifacts: output_directory does not exist: {output_directory}\")\n        return\n    for fname in contents:\n        lpath = os.path.join(output_directory, fname)\n        if fname == MODEL_FILE_NAME:\n            _log_model(lpath)\n        else:\n            mlflow.log_artifact(lpath)\n\n\ndef _log_model(lpath):\n    # Lazy import to avoid requiring this package\n    from ludwig.contribs.mlflow.model import log_saved_model\n\n    log_saved_model(lpath)\n"
  },
  {
    "path": "ludwig/contribs/mlflow/model.py",
    "content": "import logging\nimport os\nimport shutil\nimport tempfile\n\nimport mlflow\nimport yaml\nfrom mlflow import pyfunc\nfrom mlflow.exceptions import MlflowException\nfrom mlflow.models import Model\nfrom mlflow.models.model import MLMODEL_FILE_NAME\nfrom mlflow.models.signature import ModelSignature\nfrom mlflow.models.utils import _save_example, ModelInputExample\nfrom mlflow.tracking._model_registry import DEFAULT_AWAIT_MAX_SLEEP_SECONDS\nfrom mlflow.tracking.artifact_utils import _download_artifact_from_uri\nfrom mlflow.utils.environment import _mlflow_conda_env\nfrom mlflow.utils.model_utils import _get_flavor_configuration\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\nfrom ludwig.utils.data_utils import load_json\n\nFLAVOR_NAME = \"ludwig\"\n\n_logger = logging.getLogger(__name__)\n\n\ndef get_default_conda_env():\n    \"\"\"\n    :return: The default Conda environment for MLflow Models produced by calls to\n             :func:`save_model()` and :func:`log_model()`.\n    \"\"\"\n    import ludwig\n\n    # Ludwig is not yet available via the default conda channels, so we install it via pip\n    return _mlflow_conda_env(\n        additional_conda_deps=None,\n        additional_pip_deps=[f\"ludwig=={ludwig.__version__}\"],\n        additional_conda_channels=None,\n    )\n\n\ndef save_model(\n    ludwig_model,\n    path,\n    conda_env=None,\n    mlflow_model=None,\n    signature: ModelSignature = None,\n    input_example: ModelInputExample = None,\n    **kwargs,\n):\n    \"\"\"Save a Ludwig model to a path on the local file system.\n\n    :param ludwig_model: Ludwig model (an instance of `ludwig.api.LudwigModel`_) to be saved.\n    :param path: Local path where the model is to be saved.\n    :param conda_env: Either a dictionary representation of a Conda environment or the path to a\n                      Conda environment yaml file. If provided, this describes the environment\n                      this model should be run in. At minimum, it should specify the dependencies\n                      contained in :func:`get_default_conda_env()`. If ``None``, the default\n                      :func:`get_default_conda_env()` environment is added to the model.\n                      The following is an *example* dictionary representation of a Conda\n                      environment::\n\n                        {\n                            'name': 'mlflow-env',\n                            'channels': ['defaults'],\n                            'dependencies': [\n                                'python=3.7.0',\n                                'pip': [\n                                    'ludwig==0.4.0'\n                                ]\n                            ]\n                        }\n\n    :param mlflow_model: :py:mod:`mlflow.models.Model` this flavor is being added to.\n\n    :param signature: (Experimental) :py:class:`ModelSignature <mlflow.models.ModelSignature>`\n                      describes model input and output :py:class:`Schema <mlflow.types.Schema>`.\n                      The model signature can be :py:func:`inferred <mlflow.models.infer_signature>`\n                      from datasets with valid model input (e.g. the training dataset with target\n                      column omitted) and valid model output (e.g. model predictions generated on\n                      the training dataset), for example:\n\n                      .. code-block:: python\n\n                        from mlflow.models.signature import infer_signature\n\n                        train = df.drop_column(\"target_label\")\n                        predictions = ...  # compute model predictions\n                        signature = infer_signature(train, predictions)\n    :param input_example: (Experimental) Input example provides one or several instances of valid\n                          model input. The example can be used as a hint of what data to feed the\n                          model. The given example will be converted to a Pandas DataFrame and then\n                          serialized to json using the Pandas split-oriented format. Bytes are\n                          base64-encoded.\n    \"\"\"\n    import ludwig\n\n    path = os.path.abspath(path)\n    if os.path.exists(path):\n        raise MlflowException(f\"Path '{path}' already exists\")\n    model_data_subpath = MODEL_FILE_NAME\n    model_data_path = os.path.join(path, model_data_subpath)\n    os.makedirs(path)\n    if mlflow_model is None:\n        mlflow_model = Model()\n    if signature is not None:\n        mlflow_model.signature = signature\n    if input_example is not None:\n        _save_example(mlflow_model, input_example, path)\n\n    # Save the Ludwig model\n    ludwig_model.save(model_data_path)\n\n    conda_env_subpath = \"conda.yaml\"\n    if conda_env is None:\n        conda_env = get_default_conda_env()\n    elif not isinstance(conda_env, dict):\n        with open(conda_env) as f:\n            conda_env = yaml.safe_load(f)\n    with open(os.path.join(path, conda_env_subpath), \"w\") as f:\n        yaml.safe_dump(conda_env, stream=f, default_flow_style=False)\n\n    pyfunc.add_to_model(\n        mlflow_model,\n        loader_module=\"ludwig.contribs.mlflow.model\",\n        data=model_data_subpath,\n        env=conda_env_subpath,\n    )\n\n    schema_keys = {\"name\", \"column\", \"type\"}\n    config = ludwig_model.config\n\n    mlflow_model.add_flavor(\n        FLAVOR_NAME,\n        ludwig_version=ludwig.__version__,\n        ludwig_schema={\n            \"input_features\": [\n                {k: v for k, v in feature.items() if k in schema_keys} for feature in config[\"input_features\"]\n            ],\n            \"output_features\": [\n                {k: v for k, v in feature.items() if k in schema_keys} for feature in config[\"output_features\"]\n            ],\n        },\n        data=model_data_subpath,\n    )\n    mlflow_model.save(os.path.join(path, MLMODEL_FILE_NAME))\n\n\ndef log_model(\n    ludwig_model,\n    artifact_path,\n    conda_env=None,\n    registered_model_name=None,\n    signature: ModelSignature = None,\n    input_example: ModelInputExample = None,\n    await_registration_for=DEFAULT_AWAIT_MAX_SLEEP_SECONDS,\n):\n    \"\"\"Log a Ludwig model as an MLflow artifact for the current run.\n\n    Saves the model locally in MLflow format, then logs it as a run artifact using mlflow.log_artifacts(). This ensures\n    the model appears as a run artifact (compatible with MLflow 3.x where Model.log() uses the model registry instead).\n    \"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        local_path = os.path.join(tmpdir, \"model\")\n        save_model(\n            ludwig_model,\n            path=local_path,\n            conda_env=conda_env,\n            signature=signature,\n            input_example=input_example,\n        )\n        mlflow.log_artifacts(local_path, artifact_path)\n\n    if registered_model_name is not None:\n        run_id = mlflow.active_run().info.run_id\n        mlflow.register_model(\n            f\"runs:/{run_id}/{artifact_path}\",\n            registered_model_name,\n            await_registration_for=await_registration_for,\n        )\n\n\ndef _load_model(path):\n    from ludwig.api import LudwigModel\n\n    return LudwigModel.load(path, backend=\"local\")\n\n\ndef _load_pyfunc(path):\n    \"\"\"Load PyFunc implementation. Called by ``pyfunc.load_pyfunc``.\n\n    :param path: Local filesystem path to the MLflow Model with the ``ludwig`` flavor.\n    \"\"\"\n    return _LudwigModelWrapper(_load_model(path))\n\n\ndef load_model(model_uri):\n    \"\"\"Load a Ludwig model from a local file or a run.\n\n    :param model_uri: The location, in URI format, of the MLflow model. For example:\n\n                      - ``/Users/me/path/to/local/model``\n                      - ``relative/path/to/local/model``\n                      - ``s3://my_bucket/path/to/model``\n                      - ``runs:/<mlflow_run_id>/run-relative/path/to/model``\n\n                      For more information about supported URI schemes, see\n                      `Referencing Artifacts <https://www.mlflow.org/docs/latest/tracking.html#\n                      artifact-locations>`_.\n\n    :return: A Ludwig model (an instance of `ludwig.api.LudwigModel`_).\n    \"\"\"\n    local_model_path = _download_artifact_from_uri(artifact_uri=model_uri)\n    flavor_conf = _get_flavor_configuration(model_path=local_model_path, flavor_name=FLAVOR_NAME)\n    model_data_path = os.path.join(local_model_path, flavor_conf.get(\"data\", \"model\"))\n    return _load_model(path=model_data_path)\n\n\nclass _LudwigModelWrapper:\n    def __init__(self, ludwig_model):\n        self.ludwig_model = ludwig_model\n\n    def predict(self, dataframe):\n        pred_df, _ = self.ludwig_model.predict(dataframe)\n        return pred_df\n\n\ndef export_model(model_path, output_path, registered_model_name=None):\n    if registered_model_name:\n        if not model_path.startswith(\"runs:/\") or output_path is not None:\n            # No run specified, so in order to register the model in mlflow, we need\n            # to create a new run and upload the model as an artifact first\n            output_path = output_path or MODEL_FILE_NAME\n            log_model(\n                _CopyModel(model_path),\n                artifact_path=output_path,\n                registered_model_name=registered_model_name,\n            )\n        else:\n            # Registering a model from an artifact of an existing run\n            mlflow.register_model(\n                model_path,\n                registered_model_name,\n            )\n    else:\n        # No model name means we only want to save the model locally\n        save_model(\n            _CopyModel(model_path),\n            path=output_path,\n        )\n\n\n@DeveloperAPI\ndef log_saved_model(lpath):\n    \"\"\"Log a saved Ludwig model directory as a proper MLflow model artifact.\"\"\"\n    if os.path.isdir(lpath):\n        log_model(\n            _CopyModel(lpath),\n            artifact_path=\"model\",\n        )\n    elif os.path.isfile(lpath):\n        mlflow.log_artifact(lpath, \"model\")\n\n\nclass _CopyModel:\n    \"\"\"Get model data without requiring us to read the model weights into memory.\"\"\"\n\n    def __init__(self, lpath):\n        self.lpath = lpath\n\n    def save(self, path):\n        shutil.copytree(self.lpath, path)\n\n    @property\n    def config(self):\n        return load_json(os.path.join(self.lpath, MODEL_HYPERPARAMETERS_FILE_NAME))\n"
  },
  {
    "path": "ludwig/contribs/wandb.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.callbacks import Callback\nfrom ludwig.utils.package_utils import LazyLoader\n\nwandb = LazyLoader(\"wandb\", globals(), \"wandb\")\n\nlogger = logging.getLogger(__name__)\n\n\n@PublicAPI\nclass WandbCallback(Callback):\n    \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n    def on_train_init(\n        self,\n        base_config,\n        experiment_directory,\n        experiment_name,\n        model_name,\n        output_directory,\n        resume_directory,\n    ):\n        logger.info(\"wandb.on_train_init() called...\")\n        wandb.init(\n            project=os.getenv(\"WANDB_PROJECT\", experiment_name),\n            name=model_name,\n            sync_tensorboard=True,\n            dir=output_directory,\n        )\n        wandb.save(os.path.join(experiment_directory, \"*\"))\n\n    def on_train_start(self, model, config, *args, **kwargs):\n        logger.info(\"wandb.on_train_start() called...\")\n        config = config.copy()\n        del config[\"input_features\"]\n        del config[\"output_features\"]\n        wandb.config.update(config)\n\n    def on_eval_end(self, trainer, progress_tracker, save_path):\n        \"\"\"Called from ludwig/models/model.py.\"\"\"\n        for key, value in progress_tracker.log_metrics().items():\n            wandb.log({key: value})\n\n    def on_epoch_end(self, trainer, progress_tracker, save_path):\n        \"\"\"Called from ludwig/models/model.py.\"\"\"\n        for key, value in progress_tracker.log_metrics().items():\n            wandb.log({key: value})\n\n    def on_visualize_figure(self, fig):\n        logger.info(\"wandb.on_visualize_figure() called...\")\n        if wandb.run:\n            wandb.log({\"figure\": fig})\n\n    def on_train_end(self, output_directory):\n        wandb.finish()\n"
  },
  {
    "path": "ludwig/data/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/data/batcher/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/data/batcher/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom abc import ABC, abstractmethod\n\nimport numpy as np\n\n\nclass Batcher(ABC):\n    @abstractmethod\n    def next_batch(self) -> dict[str, np.ndarray]:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def last_batch(self) -> bool:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def set_epoch(self, epoch: int, batch_size: int):\n        raise NotImplementedError()\n"
  },
  {
    "path": "ludwig/data/batcher/bucketed.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport numpy as np\n\nfrom ludwig.data.batcher.base import Batcher\n\n\nclass BucketedBatcher(Batcher):\n    def __init__(\n        self,\n        dataset,\n        bucketing_field,\n        batch_size=128,\n        buckets=10,\n        should_shuffle=True,\n        ignore_last=False,\n        should_trim=False,\n        trim_side=\"right\",\n    ):\n        self.should_shuffle = should_shuffle\n        self.bucketing_field = bucketing_field\n        self.should_trim = should_trim\n        self.trim_side = trim_side\n\n        # store our dataset as well\n        self.dataset = dataset\n\n        field = dataset.get_dataset()[bucketing_field]\n        field_lengths = np.apply_along_axis(lambda x: np.sign(x).sum(), 1, field)\n        sorted_idcs = np.argsort(field_lengths)\n        self.buckets_idcs = []\n        datapoints_per_bucket = len(field) // buckets\n        for b in range(buckets):\n            start = datapoints_per_bucket * b\n            end = datapoints_per_bucket * (b + 1) if b < buckets - 1 else len(sorted_idcs)\n            self.buckets_idcs.append(sorted_idcs[start:end])\n\n        if should_shuffle:\n            self.shuffle(self.buckets_idcs)\n\n        self.ignore_last = ignore_last\n        self.batch_size = batch_size\n        self.total_size = min(map(len, dataset.get_dataset().values()))\n        self.bucket_sizes = np.array([x for x in map(len, self.buckets_idcs)])\n        self.steps_per_epoch = self._compute_steps_per_epoch()\n        self.indices = np.array([0] * buckets)\n        self.step = 0\n        self.epoch = 0\n\n    def shuffle(self, buckets_idcs):\n        for i in range(len(buckets_idcs)):\n            np.random.shuffle(buckets_idcs[i])\n\n    def next_batch(self):\n        if self.last_batch():\n            if self.should_shuffle:\n                self.shuffle(self.buckets_idcs)\n            self.set_epoch(self.epoch + 1)\n\n        if self.ignore_last:\n            idcs_below_size = self.indices + self.batch_size < self.bucket_sizes\n        else:\n            idcs_below_size = self.indices < self.bucket_sizes\n        i = np.random.choice(np.arange(0, len(self.buckets_idcs))[idcs_below_size])\n\n        selected_bucket = self.buckets_idcs[i]\n        selected_idcs = selected_bucket[self.indices[i] : self.indices[i] + self.batch_size]\n\n        sub_batch = {}\n        for key in self.dataset.get_dataset():\n            if key == self.bucketing_field and self.should_trim:\n                selected_samples = self.dataset.get(key, selected_idcs)\n                max_length = np.sign(selected_samples).sum(axis=1).max()\n                if self.trim_side == \"right\":\n                    sub_batch[key] = selected_samples[:, :max_length]\n                elif self.trim_side == \"left\":\n                    sub_batch[key] = selected_samples[:, -max_length:]\n                else:\n                    raise ValueError(\"Invalid trim side:\", self.trim_side)\n\n            else:\n                sub_batch[key] = self.dataset.get(key, selected_idcs)\n\n        self.indices[i] += self.batch_size\n        self.step += 1\n        return sub_batch\n\n    def last_batch(self):\n        return not np.any(self.indices < self.bucket_sizes) or (\n            self.ignore_last and not np.any(self.indices + self.batch_size < self.bucket_sizes)\n        )\n\n    def set_epoch(self, epoch, batch_size):\n        self.indices = np.array([0] * len(self.buckets_idcs))\n        self.step = 0\n        self.epoch = epoch\n        self.batch_size = batch_size\n        self.steps_per_epoch = self._compute_steps_per_epoch()\n\n    def _compute_steps_per_epoch(self) -> int:\n        return int(np.sum(np.ceil(self.bucket_sizes / self.batch_size)).item())\n\n\n# dynamic_length_encoders = {\n#     'rnn',\n#     'embed'\n# }\n#\n# todo future: reintroduce the bucketed batcher\n# def initialize_batcher(dataset, batch_size=128, bucketing_field=None,\n#                        input_features=None, preprocessing=None,\n#                        should_shuffle=True, ignore_last=False):\n#     if bucketing_field is not None:\n#         bucketing_feature = [\n#             feature for feature in input_features if\n#             feature[NAME] == bucketing_field\n#         ]\n#         if not bucketing_feature:\n#             raise ValueError(\n#                 'Bucketing field {} not present in input features'.format(\n#                     bucketing_field\n#                 )\n#             )\n#         else:\n#             bucketing_feature = bucketing_feature[0]\n#         should_trim = bucketing_feature[\n#                           'encoder'] in dynamic_length_encoders\n#         if 'preprocessing' in bucketing_feature:\n#             trim_side = bucketing_feature['preprocessing']['padding']\n#         else:\n#             trim_side = preprocessing[bucketing_feature[TYPE]]['padding']\n#\n#         batcher = BucketedBatcher(\n#             dataset,\n#             bucketing_field=bucketing_field,\n#             batch_size=batch_size,\n#             buckets=10,\n#             ignore_last=ignore_last,\n#             should_shuffle=should_shuffle,\n#             should_trim=should_trim,\n#             trim_side=trim_side\n#         )\n#     else:\n#         batcher = Batcher(\n#             dataset,\n#             batch_size,\n#             should_shuffle=should_shuffle,\n#             ignore_last=ignore_last\n#         )\n#     return batcher\n"
  },
  {
    "path": "ludwig/data/batcher/iterable.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom ludwig.data.batcher.base import Batcher\n\n\nclass IterableBatcher(Batcher):\n    def __init__(self, dataset, data, steps_per_epoch, ignore_last=False):\n        self.dataset = dataset\n        self.data = data\n        self.data_it = iter(data)\n\n        self.ignore_last = ignore_last\n        self.steps_per_epoch = steps_per_epoch\n        self.step = 0\n\n    def next_batch(self):\n        if self.last_batch():\n            raise StopIteration()\n\n        sub_batch = {}\n        batch = next(self.data_it)\n        for features_name in self.dataset.features:\n            sub_batch[features_name] = self.dataset.get(features_name, batch)\n\n        self.step += 1\n        return sub_batch\n\n    def last_batch(self):\n        return self.step >= self.steps_per_epoch or (self.ignore_last and self.step + 1 >= self.steps_per_epoch)\n\n    def set_epoch(self, epoch, batch_size):\n        # TODO ray: implement dynamic batch size\n        self.step = 0\n"
  },
  {
    "path": "ludwig/data/batcher/random_access.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport math\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.data.batcher.base import Batcher\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\nclass RandomAccessBatcher(Batcher):\n    def __init__(self, dataset, sampler, batch_size=128, ignore_last=False, augmentation_pipeline=None):\n        # store our dataset as well\n        self.dataset = dataset\n        self.sampler = sampler\n        self.sample_it = iter(self.sampler)\n\n        self.ignore_last = ignore_last\n        self.batch_size = batch_size\n        self.total_size = len(sampler)\n        self.augmentation_pipeline = augmentation_pipeline\n        self.steps_per_epoch = self._compute_steps_per_epoch()\n        self.index = 0\n        self.step = 0\n\n    def next_batch(self):\n        if self.last_batch():\n            raise StopIteration()\n\n        indices = []\n        for _ in range(self.batch_size):\n            try:\n                indices.append(next(self.sample_it))\n                self.index += 1\n            except StopIteration:\n                break\n\n        sub_batch = {feature_name: self.dataset.get(feature_name, indices) for feature_name in self.dataset.features}\n\n        if self.augmentation_pipeline:\n            for feature_name, augmentations in self.augmentation_pipeline.items():\n                logger.debug(f\"RandomAccessBatcher applying augmentation pipeline to batch for feature {feature_name}\")\n                sub_batch[feature_name] = augmentations(torch.tensor(sub_batch[feature_name]))\n\n        self.step += 1\n        return sub_batch\n\n    def last_batch(self):\n        \"\"\"Returns whether we've exhausted all batches for this epoch.\n\n        If False, then there is at least 1 more batch available with next_batch().\n        \"\"\"\n        # If our current index in the dataset exceeds the size of the dataset,\n        # we've finished the epoch and can indicate that this is the last batch\n        if self.index >= self.total_size:\n            return True\n        # This avoids the case where batch size > total size and no steps have been done.\n        # For e.g., batch size = 128 but the dataset only has 100 rows.\n        elif self.ignore_last and self.step:\n            # index += batch_size after each epoch. So, if our current index in total dataset is 1 less than the total\n            # dataset size, then the last batch will only have 1 row.\n            # If this happens, we drop the last batch, unless batch_size is 1.\n            if self.batch_size > 1 and self.index - self.total_size == -1:\n                logger.info(\"Last batch in epoch only has 1 sample and will be dropped.\")\n                return True\n        return False\n\n    def set_epoch(self, epoch, batch_size):\n        self.batch_size = batch_size\n        self.steps_per_epoch = self._compute_steps_per_epoch()\n        self.index = 0\n        self.step = 0\n        self.sampler.set_epoch(epoch)\n        self.sample_it = iter(self.sampler)\n\n    def _compute_steps_per_epoch(self):\n        return int(math.ceil(self.total_size / self.batch_size))\n"
  },
  {
    "path": "ludwig/data/batcher/test_batcher.py",
    "content": "import logging\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.dataset.pandas import PandasDataset\n\n\ndef test_pandas_size():\n    df = pd.DataFrame(\n        {\"name\": [\"joe\", \"janice\", \"sara\"], \"mask\": [\"green\", \"black\", \"pink\"], \"weapon\": [\"stick\", \"gun\", \"gun\"]}\n    )\n    config = yaml.safe_load(\"\"\"\n    model_type: llm\n    base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM\n    input_features:\n    - name: name\n      type: text\n      preprocessing:\n        max_sequence_length: 256\n      column: name\n    output_features:\n    - name: weapon\n      type: text\n      preprocessing:\n        max_sequence_length: 256\n      column: weapon\n    preprocessing:\n      split:\n        type: random\n        probabilities:\n          - 1\n          - 0\n          - 0\n    \"\"\")\n    model = LudwigModel(config=config, logging_level=logging.INFO)\n    data = model.preprocess(df, skip_save_processed_input=False)\n    training_set = data[0]\n    assert training_set.size == len(df)\n\n    # Check if string loading works as well\n    # data[0].data_hdf5_fp is the string filepath to the cached data from preprocessing\n    data_from_str = PandasDataset(data[0].data_hdf5_fp, data[0].features, None)\n    assert data_from_str.size == len(df)\n\n\ndef test_pandas_batcher_use_all_samples():\n    df = pd.DataFrame(\n        {\"name\": [\"joe\", \"janice\", \"sara\"], \"mask\": [\"green\", \"black\", \"pink\"], \"weapon\": [\"stick\", \"gun\", \"gun\"]}\n    )\n    config = yaml.safe_load(\"\"\"\n    model_type: llm\n    base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM\n    input_features:\n    - name: name\n      type: text\n      preprocessing:\n        max_sequence_length: 256\n      column: name\n    output_features:\n    - name: weapon\n      type: text\n      preprocessing:\n        max_sequence_length: 256\n      column: weapon\n    preprocessing:\n      split:\n        type: random\n        probabilities:\n          - 1\n          - 0\n          - 0\n    \"\"\")\n    model = LudwigModel(config=config, logging_level=logging.INFO)\n    data = model.preprocess(df, skip_save_processed_input=False)\n    training_set = data[0]\n    features = training_set.dataset.keys()\n\n    batches = []\n    with training_set.initialize_batcher(batch_size=1) as batcher:\n        while not batcher.last_batch():\n            batch = batcher.next_batch()\n            batches.append(batch)\n    assert (len(batches)) == training_set.size\n\n    # Check to see if all items are used exactly once\n    for feature in features:\n        for i in range(len(training_set.dataset[feature])):\n            # Each of the arrays in the line below should contain the vector representation of a feature of sample i\n            assert (batches[i][feature].squeeze() == training_set.dataset[feature][i].squeeze()).all()\n\n    # Check if string loading works as well\n    batches = []\n    # data[0].data_hdf5_fp is the string filepath to the cached data from preprocessing\n    data_from_str = PandasDataset(data[0].data_hdf5_fp, data[0].features, None)\n    features = data_from_str.dataset.keys()\n\n    with data_from_str.initialize_batcher(batch_size=1) as batcher:\n        while not batcher.last_batch():\n            batch = batcher.next_batch()\n            batches.append(batch)\n    assert (len(batches)) == data_from_str.size\n\n    # Check to see if all items are used exactly once\n    for feature in features:\n        for i in range(len(data_from_str.dataset[feature])):\n            # Each of the arrays in the line below should contain the vector representation of a feature of sample i\n            assert (batches[i][feature].squeeze() == data_from_str.dataset[feature][i].squeeze()).all()\n"
  },
  {
    "path": "ludwig/data/cache/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/data/cache/manager.py",
    "content": "import logging\nimport os\n\nfrom ludwig.constants import CHECKSUM, META, TEST, TRAINING, VALIDATION\nfrom ludwig.data.cache.types import alphanum, CacheableDataset\nfrom ludwig.data.cache.util import calculate_checksum\nfrom ludwig.data.dataset.base import DatasetManager\nfrom ludwig.utils import data_utils\nfrom ludwig.utils.fs_utils import delete, path_exists\n\nlogger = logging.getLogger(__name__)\n\n\nclass DatasetCache:\n    def __init__(self, config, checksum, cache_map, dataset_manager):\n        self.config = config\n        self.checksum = checksum\n        self.cache_map = cache_map\n        self.dataset_manager = dataset_manager\n\n    def get(self):\n        training_set_metadata_fp = self.cache_map[META]\n        if not path_exists(training_set_metadata_fp):\n            return None\n\n        try:\n            cached_training_set_metadata = data_utils.load_json(training_set_metadata_fp)\n        except Exception:\n            logger.exception(f\"Failed to load cached training set metadata at {training_set_metadata_fp}\")\n            return None\n\n        cached_training_set = self.cache_map[TRAINING] if path_exists(self.cache_map[TRAINING]) else None\n        if not cached_training_set:\n            logger.warning(f\"Failed to load cached training set at {self.cache_map[TRAINING]}\")\n\n        cached_validation_set = self.cache_map[VALIDATION] if path_exists(self.cache_map[VALIDATION]) else None\n        if not cached_validation_set:\n            logger.warning(f\"Failed to load cached validation set at {self.cache_map[VALIDATION]}\")\n\n        cached_test_set = self.cache_map[TEST] if path_exists(self.cache_map[TEST]) else None\n        if not cached_test_set:\n            logger.warning(f\"Failed to load cached test set at {self.cache_map[TEST]}\")\n\n        valid = self.checksum == cached_training_set_metadata.get(CHECKSUM) and cached_training_set is not None\n\n        return valid, cached_training_set_metadata, cached_training_set, cached_test_set, cached_validation_set\n\n    def put(self, training_set, test_set, validation_set, training_set_metadata):\n        logger.info(f\"Writing preprocessed training set cache to {self.cache_map[TRAINING]}\")\n        training_set = self.dataset_manager.save(\n            self.cache_map[TRAINING],\n            training_set,\n            self.config,\n            training_set_metadata,\n            TRAINING,\n        )\n\n        if validation_set is not None:\n            logger.info(f\"Writing preprocessed validation set cache to {self.cache_map[VALIDATION]}\")\n            validation_set = self.dataset_manager.save(\n                self.cache_map[VALIDATION],\n                validation_set,\n                self.config,\n                training_set_metadata,\n                VALIDATION,\n            )\n\n        if test_set is not None:\n            logger.info(f\"Writing preprocessed test set cache to {self.cache_map[TEST]}\")\n            test_set = self.dataset_manager.save(\n                self.cache_map[TEST],\n                test_set,\n                self.config,\n                training_set_metadata,\n                TEST,\n            )\n\n        logger.info(f\"Writing train set metadata to {self.cache_map[META]}\")\n        data_utils.save_json(self.cache_map[META], training_set_metadata)\n\n        return training_set, test_set, validation_set, training_set_metadata\n\n    def delete(self):\n        for fname in self.cache_map.values():\n            if path_exists(fname):\n                # Parquet entries in the cache_ma can be pointers to directories.\n                delete(fname, recursive=True)\n\n    def get_cached_obj_path(self, cached_obj_name: str) -> str:\n        return self.cache_map.get(cached_obj_name)\n\n\nclass CacheManager:\n    def __init__(\n        self,\n        dataset_manager: DatasetManager,\n        cache_dir: str | None = None,\n    ):\n        self._dataset_manager = dataset_manager\n        self._cache_dir = cache_dir\n\n    def get_dataset_cache(\n        self,\n        config: dict,\n        dataset: CacheableDataset | None = None,\n        training_set: CacheableDataset | None = None,\n        test_set: CacheableDataset | None = None,\n        validation_set: CacheableDataset | None = None,\n    ) -> DatasetCache:\n        if dataset is not None:\n            key = self.get_cache_key(dataset, config)\n            cache_map = {\n                META: self.get_cache_path(dataset, key, META, \"json\"),\n                TRAINING: self.get_cache_path(dataset, key, TRAINING),\n                TEST: self.get_cache_path(dataset, key, TEST),\n                VALIDATION: self.get_cache_path(dataset, key, VALIDATION),\n            }\n            return DatasetCache(config, key, cache_map, self._dataset_manager)\n        else:\n            key = self.get_cache_key(training_set, config)\n            cache_map = {\n                META: self.get_cache_path(training_set, key, META, \"json\"),\n                TRAINING: self.get_cache_path(training_set, key, TRAINING),\n                TEST: self.get_cache_path(test_set, key, TEST),\n                VALIDATION: self.get_cache_path(validation_set, key, VALIDATION),\n            }\n            return DatasetCache(config, key, cache_map, self._dataset_manager)\n\n    def get_cache_key(self, dataset: CacheableDataset, config: dict) -> str:\n        return calculate_checksum(dataset, config)\n\n    def get_cache_path(self, dataset: CacheableDataset | None, key: str, tag: str, ext: str | None = None) -> str:\n        if self._cache_dir is None and dataset is not None:\n            # Use the input dataset filename (minus the extension) as the cache path\n            stem = dataset.get_cache_path()\n        else:\n            # To avoid collisions across different directories, we use the unique checksum\n            # as the cache path\n            stem = alphanum(key)\n\n        ext = ext or self.data_format\n        cache_fname = f\"{stem}.{tag}.{ext}\"\n        return os.path.join(self.get_cache_directory(dataset), cache_fname)\n\n    def get_cache_directory(self, dataset: CacheableDataset | None) -> str:\n        if self._cache_dir is None:\n            if dataset is None:\n                return os.getcwd()\n            return dataset.get_cache_directory()\n        return self._cache_dir\n\n    def can_cache(self, skip_save_processed_input: bool) -> bool:\n        return self._dataset_manager.can_cache(skip_save_processed_input)\n\n    @property\n    def data_format(self) -> str:\n        return self._dataset_manager.data_format\n"
  },
  {
    "path": "ludwig/data/cache/types.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport os\nimport re\nimport uuid\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Union\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.fs_utils import checksum\nfrom ludwig.utils.types import DataFrame\n\n\ndef alphanum(v):\n    \"\"\"Filters a string to only its alphanumeric characters.\"\"\"\n    return re.sub(r\"\\W+\", \"\", v)\n\n\n@DeveloperAPI\nclass CacheableDataset(ABC):\n    name: str\n    checksum: str\n\n    @abstractmethod\n    def get_cache_path(self) -> str:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_cache_directory(self) -> str:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def unwrap(self) -> str | DataFrame:\n        raise NotImplementedError()\n\n\n@DeveloperAPI\n@dataclass\nclass CacheableDataframe(CacheableDataset):\n    df: DataFrame\n    name: str\n    checksum: str\n\n    def get_cache_path(self) -> str:\n        return alphanum(self.name)\n\n    def get_cache_directory(self) -> str:\n        return os.getcwd()\n\n    def unwrap(self) -> str | DataFrame:\n        return self.df\n\n\n@DeveloperAPI\n@dataclass\nclass CacheablePath(CacheableDataset):\n    path: str\n\n    @property\n    def name(self) -> str:\n        return Path(self.path).stem\n\n    @property\n    def checksum(self) -> str:\n        return checksum(self.path)\n\n    def get_cache_path(self) -> str:\n        return self.name\n\n    def get_cache_directory(self) -> str:\n        return os.path.dirname(self.path)\n\n    def unwrap(self) -> str | DataFrame:\n        return self.path\n\n\nCacheInput = Union[str, DataFrame, CacheableDataset]\n\n\ndef wrap(dataset: CacheInput | None) -> CacheableDataset:\n    if dataset is None:\n        return None\n\n    if isinstance(dataset, CacheableDataset):\n        return dataset\n    if isinstance(dataset, str):\n        return CacheablePath(path=dataset)\n\n    # TODO(travis): could try hashing the in-memory dataset, but this is tricky for Dask\n    checksum = str(uuid.uuid1())\n    name = checksum\n    return CacheableDataframe(df=dataset, name=name, checksum=checksum)\n"
  },
  {
    "path": "ludwig/data/cache/util.py",
    "content": "import ludwig\nfrom ludwig.constants import DEFAULTS, INPUT_FEATURES, OUTPUT_FEATURES, PREPROCESSING, PROC_COLUMN, TYPE\nfrom ludwig.data.cache.types import CacheableDataset\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.data_utils import hash_dict\n\n\ndef calculate_checksum(original_dataset: CacheableDataset, config: ModelConfigDict):\n    \"\"\"Calculates a checksum for a dataset and model config.\n\n    The checksum is used to determine if the dataset and model config have changed since the last time the model was\n    trained. If either has changed, a different checksum will be produced which will lead to a cache miss and force\n    preprocessing to be performed again.\n    \"\"\"\n    features = config.get(INPUT_FEATURES, []) + config.get(OUTPUT_FEATURES, []) + config.get(\"features\", [])\n    info = {\n        \"ludwig_version\": ludwig.globals.LUDWIG_VERSION,\n        \"dataset_checksum\": original_dataset.checksum,\n        \"global_preprocessing\": config.get(PREPROCESSING, {}),\n        \"global_defaults\": config.get(DEFAULTS, {}),\n        # PROC_COLUMN contains both the feature name and the feature hash that is computed\n        # based on each feature's preprocessing parameters and the feature's type.\n        # creating a sorted list out of the dict because hash_dict requires all values\n        # of the dict to be ordered object to ensure the creation fo the same hash\n        \"feature_proc_columns\": sorted({feature[PROC_COLUMN] for feature in features}),\n        \"feature_types\": [feature[TYPE] for feature in features],\n        \"feature_preprocessing\": [feature.get(PREPROCESSING, {}) for feature in features],\n    }\n\n    # LLM-specific params\n    if \"prompt\" in config:\n        info[\"prompt\"] = config[\"prompt\"]\n\n    return hash_dict(info, max_length=None).decode(\"ascii\")\n"
  },
  {
    "path": "ludwig/data/concatenate_datasets.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\n\nimport numpy as np\n\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.constants import SPLIT\nfrom ludwig.utils.data_utils import read_csv\n\nlogger = logging.getLogger(__name__)\n\n\ndef concatenate_csv(train_csv, vali_csv, test_csv, output_csv):\n    concatenated_df = concatenate_files(train_csv, vali_csv, test_csv, read_csv, LOCAL_BACKEND)\n\n    logger.info(\"Saving concatenated dataset as csv..\")\n    concatenated_df.to_csv(output_csv, encoding=\"utf-8\", index=False)\n    logger.info(\"done\")\n\n\ndef concatenate_files(train_fname, vali_fname, test_fname, read_fn, backend):\n    df_lib = backend.df_engine.df_lib\n\n    logger.info(\"Loading training file...\")\n    train_df = read_fn(train_fname, df_lib)\n    logger.info(\"done\")\n\n    logger.info(\"Loading validation file..\")\n    vali_df = read_fn(vali_fname, df_lib) if vali_fname is not None else None\n    logger.info(\"done\")\n\n    logger.info(\"Loading test file..\")\n    test_df = read_fn(test_fname, df_lib) if test_fname is not None else None\n    logger.info(\"done\")\n\n    logger.info(\"Concatenating files..\")\n    concatenated_df = concatenate_df(train_df, vali_df, test_df, backend)\n    logger.info(\"done\")\n\n    return concatenated_df\n\n\ndef concatenate_df(train_df, vali_df, test_df, backend):\n    train_size = len(train_df)\n    vali_size = len(vali_df) if vali_df is not None else 0\n\n    concatenated_df = backend.df_engine.df_lib.concat(\n        [df for df in [train_df, vali_df, test_df] if df is not None], ignore_index=True\n    )\n\n    def get_split(idx):\n        if idx < train_size:\n            return 0\n        if idx < train_size + vali_size:\n            return 1\n        return 2\n\n    concatenated_df[SPLIT] = concatenated_df.index.to_series().map(get_split).astype(np.int8)\n    return concatenated_df\n\n\ndef concatenate_splits(train_df, vali_df, test_df, backend):\n    def to_frame(df, split):\n        if df is None:\n            return None\n\n        df = df.index.to_frame(name=SPLIT)\n        df[SPLIT] = split\n        return df\n\n    dfs = [train_df, vali_df, test_df]\n    dfs = [to_frame(df, split) for split, df in enumerate(dfs)]\n    return backend.df_engine.df_lib.concat([df for df in dfs if df is not None])\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Concatenate train validation and test set\")\n\n    parser.add_argument(\"-train\", \"--train_csv\", help=\"CSV containing the training set\")\n    parser.add_argument(\"-vali\", \"--vali_csv\", help=\"CSV containing the validation set\")\n    parser.add_argument(\"-test\", \"--test_csv\", help=\"CSV containing the test set\")\n\n    parser.add_argument(\"-o\", \"--output_csv\", help=\"output csv\")\n    args = parser.parse_args()\n\n    concatenate_csv(args.train_csv, args.vali_csv, args.test_csv, args.output_csv)\n"
  },
  {
    "path": "ludwig/data/dataframe/__init__.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "ludwig/data/dataframe/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom abc import ABC, abstractmethod\n\nfrom ludwig.utils.types import DataFrame\n\n\nclass DataFrameEngine(ABC):\n    @abstractmethod\n    def df_like(self, df, proc_cols):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def parallelize(self, data):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def persist(self, data):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def compute(self, data):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def from_pandas(self, df):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def map_objects(self, series, map_fn, meta=None):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def map_partitions(self, series, map_fn, meta=None):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def map_batches(self, df, map_fn, enable_tensor_extension_casting=True):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def apply_objects(self, series, map_fn, meta=None):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def reduce_objects(self, series, reduce_fn):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def split(self, df, probabilities):\n        \"\"\"Splits the input DataFrame into sections with the given proportions.\"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def to_parquet(self, df, path, index=False):\n        \"\"\"Write the input DataFrame to the path in the Parquet format.\n\n        Optionally includes the DataFrame index in the Parquet file.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def write_predictions(self, df: DataFrame, path: str):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def read_predictions(self, path: str) -> DataFrame:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def to_ray_dataset(self, df):\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def array_lib(self):\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def df_lib(self):\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def partitioned(self):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def set_parallelism(self, parallelism):\n        raise NotImplementedError()\n"
  },
  {
    "path": "ludwig/data/dataframe/dask.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport collections\nimport logging\nfrom contextlib import contextmanager\n\nimport dask\nimport dask.array as da\nimport dask.dataframe as dd\nimport ray\nfrom dask.diagnostics import ProgressBar\nfrom packaging import version\nfrom pyarrow.fs import FSSpecHandler, PyFileSystem\nfrom ray.data import Dataset, read_parquet\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.utils.data_utils import get_pa_schema, get_parquet_filename, split_by_slices\nfrom ludwig.utils.dataframe_utils import set_index_name\nfrom ludwig.utils.fs_utils import get_fs_and_path\n\nTMP_COLUMN = \"__TMP_COLUMN__\"\n\n# This is to be compatible with pyarrow.lib.schema\nPandasBlockSchema = collections.namedtuple(\"PandasBlockSchema\", [\"names\", \"types\"])\n\nlogger = logging.getLogger(__name__)\n\n\n_ray_230 = version.parse(ray.__version__) >= version.parse(\"2.3.0\")\n\n\n@DeveloperAPI\ndef set_scheduler(scheduler):\n    dask.config.set(scheduler=scheduler)\n\n\n@DeveloperAPI\ndef reset_index_across_all_partitions(df):\n    \"\"\"Compute a monotonically increasing index across all partitions.\n\n    This differs from dd.reset_index, which computes an independent index for each partition.\n    Source: https://stackoverflow.com/questions/61395351/how-to-reset-index-on-concatenated-dataframe-in-dask\n    \"\"\"\n    # Create temporary column of ones\n    df = df.assign(**{TMP_COLUMN: 1})\n\n    # Set the index to the cumulative sum of TMP_COLUMN, which we know to be sorted; this improves efficiency.\n    df = df.set_index(df[TMP_COLUMN].cumsum() - 1, sorted=True)\n\n    # Drop temporary column and ensure the index is not named TMP_COLUMN\n    df = df.drop(columns=TMP_COLUMN)\n    df = df.map_partitions(lambda pd_df: set_index_name(pd_df, None))\n    return df\n\n\n@DeveloperAPI\nclass DaskEngine(DataFrameEngine):\n    def __init__(self, parallelism=None, persist=True, _use_ray=True, **kwargs):\n        from ray.util.dask import ray_dask_get\n\n        self._parallelism = parallelism\n        self._persist = persist\n        if _use_ray:\n            set_scheduler(ray_dask_get)\n\n    def set_parallelism(self, parallelism):\n        self._parallelism = parallelism\n\n    def df_like(self, df: dd.DataFrame, proc_cols: dict[str, dd.Series]):\n        \"\"\"Outer joins the given DataFrame with the given processed columns.\n\n        NOTE: If any of the processed columns have been repartitioned, the original index is replaced with a\n        monotonically increasing index, which is used to define the new divisions and align the various partitions.\n        \"\"\"\n        # Our goal is to preserve the index of the input dataframe but to drop\n        # all its columns. Because to_frame() creates a column from the index,\n        # we need to drop it immediately following creation.\n        dataset = df.index.to_frame(name=TMP_COLUMN).drop(columns=TMP_COLUMN)\n\n        repartitioned_cols = {}\n        for k, v in proc_cols.items():\n            if v.npartitions == dataset.npartitions:\n                # Outer join cols with equal partitions.\n                # Dask aligns by index automatically, so no need to force divisions.\n                dataset[k] = v\n            else:\n                # If partitions have changed (e.g. due to conversion from Ray dataset), we handle separately\n                repartitioned_cols[k] = v\n\n        # Assumes that there is a globally unique index (see preprocessing.build_dataset)\n        if repartitioned_cols:\n            if not dataset.known_divisions:\n                # Sometimes divisions are unknown despite having a usable index– set_index to know divisions\n                dataset = dataset.assign(**{TMP_COLUMN: dataset.index})\n                dataset = dataset.set_index(TMP_COLUMN, drop=True)\n                dataset = dataset.map_partitions(lambda pd_df: set_index_name(pd_df, dataset.index.name))\n\n            # Find the divisions of the column with the largest number of partitions\n            proc_col_with_max_npartitions = max(repartitioned_cols.values(), key=lambda x: x.npartitions)\n            new_divisions = proc_col_with_max_npartitions.divisions\n\n            # Repartition all columns to have the same divisions\n            dataset = dataset.repartition(divisions=new_divisions)\n            repartitioned_cols = {k: v.repartition(divisions=new_divisions) for k, v in repartitioned_cols.items()}\n\n            # Outer join the remaining columns\n            for k, v in repartitioned_cols.items():\n                dataset[k] = v\n\n        return dataset\n\n    def parallelize(self, data):\n        if self.parallelism:\n            return data.repartition(npartitions=self.parallelism)\n        return data\n\n    def persist(self, data):\n        # No graph optimizations to prevent dropping custom annotations\n        # https://github.com/dask/dask/issues/7036\n        return data.persist(optimize_graph=False) if self._persist else data\n\n    def concat(self, dfs):\n        return self.df_lib.concat(dfs)\n\n    def compute(self, data):\n        return data.compute()\n\n    def from_pandas(self, df):\n        parallelism = self._parallelism or 1\n        return dd.from_pandas(df, npartitions=parallelism)\n\n    def map_objects(self, series, map_fn, meta=None):\n        meta = meta if meta is not None else (series.name, \"object\")\n        return series.map(map_fn, meta=meta)\n\n    def map_partitions(self, series, map_fn, meta=None):\n        meta = meta if meta is not None else (series.name, \"object\")\n        return series.map_partitions(map_fn, meta=meta)\n\n    def map_batches(self, series, map_fn, enable_tensor_extension_casting=True):\n        \"\"\"Map a function over batches of a Dask Series.\n\n        Args:\n            series: Dask Series\n            map_fn: Function to apply to each batch\n            enable_tensor_extension_casting: Whether to enable tensor extension casting at the end of the Ray Datasets\n                map_batches call. This is useful in cases where the output is not supported by the ray Tensor dtype\n                extension, such as when the output consists of ragged tensors.\n        \"\"\"\n        import ray.data\n\n        with tensor_extension_casting(enable_tensor_extension_casting):\n            ds = ray.data.from_dask(series)\n            ds = ds.map_batches(map_fn, batch_format=\"pandas\")\n            return ds.to_dask()\n\n    def apply_objects(self, df, apply_fn, meta=None):\n        meta = meta if meta is not None else (\"result\", \"object\")\n        return df.apply(apply_fn, axis=1, meta=meta)\n\n    def reduce_objects(self, series, reduce_fn):\n        result = series.reduction(reduce_fn, aggregate=reduce_fn, meta=(series.name, \"object\")).compute()\n        # The result type depends on the Dask version and what reduce_fn returns.\n        # Access the scalar value safely regardless of return type.\n        if hasattr(result, \"iloc\"):\n            return result.iloc[0]\n        return result\n\n    def split(self, df, probabilities):\n        # Split the DataFrame proprotionately along partitions. This is an inexact solution designed\n        # to speed up the split process, as splitting within partitions would be significantly\n        # more expensive.\n        # TODO(travis): revisit in the future to make this more precise\n\n        # First ensure that every split receives at least one partition.\n        # If not, we need to increase the number of partitions to satisfy this constraint.\n        min_prob = min(probabilities)\n        min_partitions = int(1 / min_prob)\n        if df.npartitions < min_partitions:\n            df = df.repartition(npartitions=min_partitions)\n\n        n = df.npartitions\n        slices = df.partitions\n        return split_by_slices(slices, n, probabilities)\n\n    def remove_empty_partitions(self, df):\n        # Reference: https://stackoverflow.com/questions/47812785/remove-empty-partitions-in-dask\n        ll = list(df.map_partitions(len).compute())\n        if all([ll_i > 0 for ll_i in ll]):\n            return df\n\n        df_delayed = df.to_delayed()\n        df_delayed_new = list()\n        empty_partition = None\n        for ix, n in enumerate(ll):\n            if n == 0:\n                empty_partition = df.get_partition(ix)\n            else:\n                df_delayed_new.append(df_delayed[ix])\n        if not df_delayed_new:\n            # All partitions are empty, return a single empty partition\n            return empty_partition\n        df = dd.from_delayed(df_delayed_new, meta=empty_partition)\n        return df\n\n    def to_parquet(self, df, path, index=False):\n        schema = get_pa_schema(df)\n        with ProgressBar():\n            df.to_parquet(\n                path,\n                engine=\"pyarrow\",\n                write_index=index,\n                schema=schema,\n                name_function=get_parquet_filename,\n            )\n\n    def write_predictions(self, df: dd.DataFrame, path: str):\n        ds = self.to_ray_dataset(df)\n        # We disable tensor extension casting here because we are writing out to Parquet and there is no need\n        # to cast to the ray Tensor dtype extension before doing so (they will be written out as object dtype as if\n        # we were writing to parquet using dask).\n        with tensor_extension_casting(False):\n            fs, path = get_fs_and_path(path)\n            ds.write_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs)))\n\n    def read_predictions(self, path: str) -> dd.DataFrame:\n        fs, path = get_fs_and_path(path)\n        ds = read_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs)))\n        return self.from_ray_dataset(ds)\n\n    def to_ray_dataset(self, df) -> Dataset:\n        from ray.data import from_dask\n\n        return from_dask(df)\n\n    def from_ray_dataset(self, dataset) -> dd.DataFrame:\n        # NOTE: When the dataset is an empty MapBatches(BatchInferModel), Ray's native to_dask() raises an IndexError.\n        try:\n            return dataset.to_dask()\n        except IndexError as e:\n            logging.warning(\n                f\"Encountered an empty Dataset, {dataset.show()} with error {e}. Manually returning an empty dask \"\n                \"DataFrame.\"\n            )\n            return dd.DataFrame.from_dict({}, npartitions=1)\n\n    def reset_index(self, df):\n        return reset_index_across_all_partitions(df)\n\n    @property\n    def array_lib(self):\n        return da\n\n    @property\n    def df_lib(self):\n        return dd\n\n    @property\n    def parallelism(self):\n        return self._parallelism\n\n    @property\n    def partitioned(self):\n        return True\n\n\n@contextmanager\ndef tensor_extension_casting(enforced: bool):\n    \"\"\"This context manager is used to enforce or disable tensor extension casting.\n\n    Ray Datasets will automatically cast tensor columns to the ray Tensor dtype extension at the end of\n    map_batches calls and before writing to Parquet. This context manager can be used to disable this behavior\n    and keep the tensor columns as object dtype. This is useful for writing to Parquet using dask.\n\n    Args:\n        enforced (bool): Whether to enforce tensor extension casting.\n    \"\"\"\n    from ray.data.context import DatasetContext\n\n    ctx = DatasetContext.get_current()\n    prev_enable_tensor_extension_casting = ctx.enable_tensor_extension_casting\n    try:\n        ctx.enable_tensor_extension_casting = enforced\n        yield\n    finally:\n        ctx.enable_tensor_extension_casting = prev_enable_tensor_extension_casting\n"
  },
  {
    "path": "ludwig/data/dataframe/modin.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport os\n\nimport modin.pandas as pd\nimport numpy as np\n\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.globals import PREDICTIONS_SHAPES_FILE_NAME\nfrom ludwig.utils.data_utils import get_pa_schema, load_json, save_json, split_by_slices\nfrom ludwig.utils.dataframe_utils import flatten_df, unflatten_df\n\n\nclass ModinEngine(DataFrameEngine):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def df_like(self, df, proc_cols):\n        # df argument unused for pandas, which can instantiate df directly\n        return pd.DataFrame(proc_cols)\n\n    def parallelize(self, data):\n        return data\n\n    def persist(self, data):\n        return data\n\n    def compute(self, data):\n        return data\n\n    def from_pandas(self, df):\n        return pd.DataFrame(df)\n\n    def map_objects(self, series, map_fn, meta=None):\n        return series.map(map_fn)\n\n    def map_batches(self, df, map_fn, enable_tensor_extension_casting=True):\n        return map_fn(df)\n\n    def map_partitions(self, series, map_fn, meta=None):\n        return map_fn(series)\n\n    def apply_objects(self, df, apply_fn, meta=None):\n        return df.apply(apply_fn, axis=1)\n\n    def reduce_objects(self, series, reduce_fn):\n        return reduce_fn(series)\n\n    def split(self, df, probabilities):\n        return split_by_slices(df.iloc, len(df), probabilities)\n\n    def remove_empty_partitions(self, df):\n        return df\n\n    def to_parquet(self, df, path, index=False):\n        schema = get_pa_schema(df)\n        df.to_parquet(\n            path,\n            engine=\"pyarrow\",\n            index=index,\n            schema=schema,\n        )\n\n    def write_predictions(self, df: pd.DataFrame, path: str):\n        df, column_shapes = flatten_df(df, self)\n        self.to_parquet(df, path)\n        save_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME), column_shapes)\n\n    def read_predictions(self, path: str) -> pd.DataFrame:\n        pred_df = pd.read_parquet(path)\n        column_shapes = load_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME))\n        return unflatten_df(pred_df, column_shapes, self)\n\n    def to_ray_dataset(self, df):\n        from ray.data import from_modin\n\n        return from_modin(df)\n\n    def from_ray_dataset(self, dataset) -> pd.DataFrame:\n        return dataset.to_modin()\n\n    def reset_index(self, df):\n        return df.reset_index(drop=True)\n\n    @property\n    def array_lib(self):\n        return np\n\n    @property\n    def df_lib(self):\n        return pd\n\n    @property\n    def partitioned(self):\n        return False\n\n    def set_parallelism(self, parallelism):\n        pass\n"
  },
  {
    "path": "ludwig/data/dataframe/pandas.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport numpy as np\nimport pandas as pd\n\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.globals import PREDICTIONS_SHAPES_FILE_NAME\nfrom ludwig.utils.data_utils import load_json, save_json, split_by_slices\nfrom ludwig.utils.dataframe_utils import flatten_df, unflatten_df\n\n\nclass PandasEngine(DataFrameEngine):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def df_like(self, df, proc_cols):\n        # df argument unused for pandas, which can instantiate df directly\n        return pd.DataFrame(proc_cols)\n\n    def parallelize(self, data):\n        return data\n\n    def persist(self, data):\n        return data\n\n    def compute(self, data):\n        return data\n\n    @staticmethod\n    def concat(dfs) -> pd.DataFrame:\n        return pd.concat(dfs)\n\n    def from_pandas(self, df):\n        return df\n\n    def map_objects(self, series, map_fn, meta=None):\n        return series.map(map_fn)\n\n    def map_batches(self, df, map_fn, enable_tensor_extension_casting=True):\n        return map_fn(df)\n\n    def map_partitions(self, series, map_fn, meta=None):\n        return map_fn(series)\n\n    def apply_objects(self, df, apply_fn, meta=None):\n        return df.apply(apply_fn, axis=1)\n\n    def reduce_objects(self, series, reduce_fn):\n        return reduce_fn(series)\n\n    def split(self, df, probabilities):\n        return split_by_slices(df.iloc, len(df), probabilities)\n\n    @staticmethod\n    def remove_empty_partitions(df: pd.DataFrame) -> pd.DataFrame:\n        return df\n\n    def to_parquet(self, df, path, index=False):\n        df.to_parquet(path, engine=\"pyarrow\", index=index)\n\n    def write_predictions(self, df: pd.DataFrame, path: str):\n        df, column_shapes = flatten_df(df, self)\n        self.to_parquet(df, path)\n        save_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME), column_shapes)\n\n    def read_predictions(self, path: str) -> pd.DataFrame:\n        pred_df = pd.read_parquet(path)\n        column_shapes = load_json(os.path.join(os.path.dirname(path), PREDICTIONS_SHAPES_FILE_NAME))\n        return unflatten_df(pred_df, column_shapes, self)\n\n    def to_ray_dataset(self, df):\n        from ray.data import from_pandas\n\n        return from_pandas(df)\n\n    @staticmethod\n    def from_ray_dataset(dataset) -> pd.DataFrame:\n        return dataset.to_pandas()\n\n    @staticmethod\n    def reset_index(df) -> pd.DataFrame:\n        return df.reset_index(drop=True)\n\n    @property\n    def array_lib(self):\n        return np\n\n    @property\n    def df_lib(self):\n        return pd\n\n    @property\n    def partitioned(self):\n        return False\n\n    def set_parallelism(self, parallelism):\n        pass\n\n\nPANDAS = PandasEngine()\n"
  },
  {
    "path": "ludwig/data/dataset/__init__.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\n\ndef get_pandas_dataset_manager(**kwargs):\n    from ludwig.data.dataset.pandas import PandasDatasetManager\n\n    return PandasDatasetManager(**kwargs)\n\n\ndef get_ray_dataset_manager(**kwargs):\n    from ludwig.data.dataset.ray import RayDatasetManager\n\n    return RayDatasetManager(**kwargs)\n\n\ndataset_registry = {\n    \"hdf5\": get_pandas_dataset_manager,\n    \"ray\": get_ray_dataset_manager,\n    None: get_pandas_dataset_manager,\n}\n\n\ndef create_dataset_manager(backend, cache_format, **kwargs):\n    return dataset_registry[cache_format](backend=backend, **kwargs)\n"
  },
  {
    "path": "ludwig/data/dataset/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom __future__ import annotations\n\nimport contextlib\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Iterable\n\nfrom ludwig.data.batcher.base import Batcher\nfrom ludwig.distributed import DistributedStrategy\nfrom ludwig.features.base_feature import BaseFeature\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.types import DataFrame\n\n\nclass Dataset(ABC):\n    @abstractmethod\n    def __len__(self) -> int:\n        raise NotImplementedError()\n\n    @contextlib.contextmanager\n    @abstractmethod\n    def initialize_batcher(\n        self,\n        batch_size: int = 128,\n        should_shuffle: bool = True,\n        random_seed: int = default_random_seed,\n        ignore_last: bool = False,\n        distributed: DistributedStrategy = None,\n    ) -> Batcher:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def to_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def to_scalar_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame:\n        raise NotImplementedError()\n\n    @property\n    def in_memory_size_bytes(self) -> int:\n        raise NotImplementedError()\n\n\nclass DatasetManager(ABC):\n    @abstractmethod\n    def create(self, dataset, config, training_set_metadata) -> Dataset:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def save(self, cache_path, dataset, config, training_set_metadata, tag) -> Dataset:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def can_cache(self, skip_save_processed_input) -> bool:\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def data_format(self) -> str:\n        raise NotImplementedError()\n"
  },
  {
    "path": "ludwig/data/dataset/pandas.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom __future__ import annotations\n\nimport contextlib\nfrom collections.abc import Iterable\nfrom typing import TYPE_CHECKING\n\nimport numpy as np\nfrom pandas import DataFrame\n\nfrom ludwig.constants import PREPROCESSING, TRAINING\nfrom ludwig.data.batcher.base import Batcher\nfrom ludwig.data.batcher.random_access import RandomAccessBatcher\nfrom ludwig.data.dataset.base import Dataset, DatasetManager\nfrom ludwig.data.sampler import DistributedSampler\nfrom ludwig.distributed import DistributedStrategy\nfrom ludwig.features.base_feature import BaseFeature\nfrom ludwig.utils.data_utils import DATA_TRAIN_HDF5_FP, load_hdf5, save_hdf5\nfrom ludwig.utils.dataframe_utils import from_numpy_dataset, to_numpy_dataset, to_scalar_df\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import download_h5\nfrom ludwig.utils.misc_utils import get_proc_features\n\nif TYPE_CHECKING:\n    from ludwig.backend.base import Backend\n\n\nclass PandasDataset(Dataset):\n    def __init__(self, dataset, features, data_hdf5_fp):\n        self.features = features\n        self.data_hdf5_fp = data_hdf5_fp\n\n        if isinstance(dataset, str):\n            dataset = load_hdf5(dataset)\n        self.dataset = to_numpy_dataset(dataset)\n        self.size = len(list(self.dataset.values())[0])\n\n    def to_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame:\n        \"\"\"Convert the dataset to a Pandas DataFrame.\"\"\"\n        if features:\n            return from_numpy_dataset({feature.feature_name: self.dataset[feature.proc_column] for feature in features})\n        return from_numpy_dataset(self.dataset)\n\n    def to_scalar_df(self, features: Iterable[BaseFeature] | None = None) -> DataFrame:\n        return to_scalar_df(self.to_df(features))\n\n    def get(self, proc_column, idx=None):\n        if idx is None:\n            idx = range(self.size)\n        if (\n            self.data_hdf5_fp is None\n            or PREPROCESSING not in self.features[proc_column]\n            or \"in_memory\" not in self.features[proc_column][\"preprocessing\"]\n        ):\n            return self.dataset[proc_column][idx]\n        if self.features[proc_column][PREPROCESSING][\"in_memory\"]:\n            return self.dataset[proc_column][idx]\n\n        sub_batch = self.dataset[proc_column][idx]\n\n        indices = np.empty((3, len(sub_batch)), dtype=np.int64)\n        indices[0, :] = sub_batch\n        indices[1, :] = np.arange(len(sub_batch))\n        indices = indices[:, np.argsort(indices[0])]\n\n        with download_h5(self.data_hdf5_fp) as h5_file:\n            im_data = h5_file[proc_column + \"_data\"][indices[0, :], :, :]\n        indices[2, :] = np.arange(len(sub_batch))\n        indices = indices[:, np.argsort(indices[1])]\n        return im_data[indices[2, :]]\n\n    def get_dataset(self) -> dict[str, np.ndarray]:\n        return self.dataset\n\n    def __len__(self):\n        return self.size\n\n    @property\n    def processed_data_fp(self) -> str | None:\n        return self.data_hdf5_fp\n\n    @property\n    def in_memory_size_bytes(self) -> int:\n        df = self.to_df()\n        return df.memory_usage(deep=True).sum() if df is not None else 0\n\n    @contextlib.contextmanager\n    def initialize_batcher(\n        self,\n        batch_size: int = 128,\n        should_shuffle: bool = True,\n        random_seed: int = default_random_seed,\n        ignore_last: bool = False,\n        distributed: DistributedStrategy = None,\n        augmentation_pipeline=None,\n    ) -> Batcher:\n        sampler = DistributedSampler(\n            len(self), shuffle=should_shuffle, random_seed=random_seed, distributed=distributed\n        )\n        batcher = RandomAccessBatcher(\n            self,\n            sampler,\n            batch_size=batch_size,\n            ignore_last=ignore_last,\n            augmentation_pipeline=augmentation_pipeline,\n        )\n        yield batcher\n\n\nclass PandasDatasetManager(DatasetManager):\n    def __init__(self, backend: Backend):\n        self.backend: Backend = backend\n\n    def create(self, dataset, config, training_set_metadata) -> Dataset:\n        return PandasDataset(dataset, get_proc_features(config), training_set_metadata.get(DATA_TRAIN_HDF5_FP))\n\n    def save(self, cache_path, dataset, config, training_set_metadata, tag) -> Dataset:\n        save_hdf5(cache_path, dataset)\n        if tag == TRAINING:\n            training_set_metadata[DATA_TRAIN_HDF5_FP] = cache_path\n        return dataset\n\n    def can_cache(self, skip_save_processed_input) -> bool:\n        return self.backend.is_coordinator() and not skip_save_processed_input\n\n    @property\n    def data_format(self) -> str:\n        return \"hdf5\"\n"
  },
  {
    "path": "ludwig/data/dataset/ray.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport contextlib\nimport math\nimport queue\nimport threading\nfrom functools import lru_cache\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nfrom pyarrow.fs import FSSpecHandler, PyFileSystem\nfrom ray.data import Dataset as RayNativeDataset\nfrom ray.data import read_parquet\nfrom ray.data.extensions import TensorArray\n\nfrom ludwig.backend.base import Backend\nfrom ludwig.constants import BINARY, CATEGORY, NAME, NUMBER, TYPE\nfrom ludwig.data.batcher.base import Batcher\nfrom ludwig.data.dataset.base import Dataset, DatasetManager\nfrom ludwig.utils.data_utils import DATA_TRAIN_HDF5_FP, DATA_TRAIN_PARQUET_FP\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import get_fs_and_path\nfrom ludwig.utils.misc_utils import get_proc_features\nfrom ludwig.utils.types import DataFrame, Series\n\n_SCALAR_TYPES = {BINARY, CATEGORY, NUMBER}\n\n\ndef cast_as_tensor_dtype(series: Series) -> Series:\n    return TensorArray(series)\n\n\ndef read_remote_parquet(path: str):\n    fs, path = get_fs_and_path(path)\n    return read_parquet(path, filesystem=PyFileSystem(FSSpecHandler(fs)))\n\n\nclass RayDataset(Dataset):\n    \"\"\"Wrapper around ray.data.Dataset.\"\"\"\n\n    def __init__(\n        self,\n        df: str | DataFrame,\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n        backend: Backend,\n    ):\n        self.df_engine = backend.df_engine\n        self.ds = self.df_engine.to_ray_dataset(df) if not isinstance(df, str) else read_remote_parquet(df)\n        self.features = features\n        self.training_set_metadata = training_set_metadata\n        self.data_hdf5_fp = training_set_metadata.get(DATA_TRAIN_HDF5_FP)\n        self.data_parquet_fp = training_set_metadata.get(DATA_TRAIN_PARQUET_FP)\n\n    def to_ray_dataset(\n        self,\n        shuffle: bool = True,\n        shuffle_seed: int = default_random_seed,\n    ) -> RayNativeDataset:\n        \"\"\"Returns a ray.data.Dataset, optionally shuffled.\n\n        In modern Ray (2.5+), datasets use lazy execution by default, so there's no need for explicit windowing or\n        pipelining.\n        \"\"\"\n        ds = self.ds\n        if shuffle:\n            ds = ds.random_shuffle(seed=shuffle_seed)\n        return ds\n\n    @contextlib.contextmanager\n    def initialize_batcher(self, batch_size=128, should_shuffle=True, random_seed=0, ignore_last=False, **kwargs):\n        ds = self.ds\n        if should_shuffle:\n            ds = ds.random_shuffle(seed=random_seed)\n        yield RayDatasetBatcher(\n            ds,\n            self.features,\n            self.training_set_metadata,\n            batch_size,\n            self.size,\n        )\n\n    def __len__(self):\n        return self.ds.count()\n\n    @property\n    def size(self):\n        return len(self)\n\n    @property\n    def in_memory_size_bytes(self):\n        return self.ds.size_bytes() if self.ds is not None else 0\n\n    def to_df(self, features=None):\n        return self.df_engine.from_ray_dataset(self.ds)\n\n    def to_scalar_df(self, features=None):\n        from ludwig.utils.dataframe_utils import to_scalar_df\n\n        return to_scalar_df(self.to_df(features))\n\n\nclass RayDatasetManager(DatasetManager):\n    def __init__(self, backend):\n        self.backend = backend\n\n    def create(self, dataset: str | DataFrame, config: dict[str, Any], training_set_metadata: dict[str, Any]):\n        return RayDataset(dataset, get_proc_features(config), training_set_metadata, self.backend)\n\n    def save(\n        self,\n        cache_path: str,\n        dataset: DataFrame,\n        config: dict[str, Any],\n        training_set_metadata: dict[str, Any],\n        tag: str,\n    ):\n        self.backend.df_engine.to_parquet(dataset, cache_path)\n        return cache_path\n\n    def can_cache(self, skip_save_processed_input):\n        return not skip_save_processed_input\n\n    @property\n    def data_format(self):\n        return \"parquet\"\n\n\nclass RayDatasetShard(Dataset):\n    \"\"\"Wraps a Ray DataIterator (from ray.train.get_dataset_shard) for distributed training.\"\"\"\n\n    def __init__(\n        self,\n        dataset_shard,\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n    ):\n        self.dataset_shard = dataset_shard\n        self.features = features\n        self.training_set_metadata = training_set_metadata\n\n    @contextlib.contextmanager\n    def initialize_batcher(self, batch_size=128, should_shuffle=True, random_seed=0, ignore_last=False, **kwargs):\n        yield RayDatasetShardBatcher(\n            self.dataset_shard,\n            self.features,\n            self.training_set_metadata,\n            batch_size,\n            self.size,\n        )\n\n    @lru_cache(1)\n    def __len__(self):\n        # TODO(travis): find way to avoid calling this, as it's expensive\n        # DataIterator doesn't have a direct count method; use iter to count\n        count = 0\n        for batch in self.dataset_shard.iter_batches(batch_size=4096, batch_format=\"pandas\"):\n            count += len(batch)\n        return count\n\n    @property\n    def size(self):\n        return len(self)\n\n    def to_df(self, features=None):\n        raise NotImplementedError(\"RayDatasetShard does not support to_df; use full RayDataset instead.\")\n\n    def to_scalar_df(self, features=None):\n        raise NotImplementedError(\"RayDatasetShard does not support to_scalar_df; use full RayDataset instead.\")\n\n\nclass _BaseBatcher(Batcher):\n    \"\"\"Shared batching logic for preparing batches from pandas DataFrames.\"\"\"\n\n    def __init__(\n        self,\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n        batch_size: int,\n        samples_per_epoch: int,\n    ):\n        self.batch_size = batch_size\n        self.samples_per_epoch = samples_per_epoch\n        self.training_set_metadata = training_set_metadata\n\n        self.features = features\n        self.columns = list(features.keys())\n        self.reshape_map = {\n            proc_column: training_set_metadata[feature[NAME]].get(\"reshape\")\n            for proc_column, feature in features.items()\n        }\n\n        self.dataset_batch_iter = None\n        self._epoch = 0\n        self._next_batch = None\n        self._last_batch = False\n        self._step = 0\n\n    def next_batch(self):\n        if self.last_batch():\n            raise StopIteration()\n\n        batch = self._next_batch\n        self._fetch_next_batch()\n        self._step += 1\n        return batch\n\n    def last_batch(self):\n        return self._last_batch\n\n    def set_epoch(self, epoch, batch_size):\n        self.batch_size = batch_size\n        if epoch != self._epoch:\n            self._fetch_next_epoch()\n            self._epoch = epoch\n\n    @property\n    def step(self):\n        return self._step\n\n    @property\n    def steps_per_epoch(self):\n        return math.ceil(self.samples_per_epoch / self.batch_size)\n\n    def _fetch_next_batch(self):\n        if self.dataset_batch_iter is None:\n            self._last_batch = True\n            return\n\n        self._last_batch = False\n        try:\n            self._next_batch = next(self.dataset_batch_iter)\n        except StopIteration:\n            self._last_batch = True\n\n    def _fetch_next_epoch(self):\n        raise NotImplementedError\n\n    def _to_tensors_fn(self):\n        columns = self.columns\n        features = self.features\n\n        def to_tensors(df: pd.DataFrame) -> pd.DataFrame:\n            for c in columns:\n                # do not convert scalar columns: https://github.com/ray-project/ray/issues/20825\n                if features[c][TYPE] not in _SCALAR_TYPES:\n                    df[c] = cast_as_tensor_dtype(df[c])\n                elif features[c][TYPE] == BINARY:\n                    df[c] = df[c].astype(np.bool_)\n            return df\n\n        return to_tensors\n\n    def _prepare_batch(self, batch: pd.DataFrame) -> dict[str, np.ndarray]:\n        res = {}\n        for c in self.columns:\n            if self.features[c][TYPE] not in _SCALAR_TYPES:\n                res[c] = np.stack(batch[c].values)\n            else:\n                res[c] = batch[c].to_numpy()\n\n        for c in self.columns:\n            reshape = self.reshape_map.get(c)\n            if reshape is not None:\n                res[c] = res[c].reshape((-1, *reshape))\n        return res\n\n\nclass RayDatasetBatcher(_BaseBatcher):\n    \"\"\"Batcher for a full ray.data.Dataset (used by non-distributed/local Ray training).\"\"\"\n\n    def __init__(\n        self,\n        dataset: RayNativeDataset,\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n        batch_size: int,\n        samples_per_epoch: int,\n    ):\n        self.dataset = dataset\n        super().__init__(features, training_set_metadata, batch_size, samples_per_epoch)\n        self._fetch_next_epoch()\n\n    def _fetch_next_epoch(self):\n        \"\"\"Create an async reader over the dataset for one epoch.\"\"\"\n        self.dataset_batch_iter = self._create_async_reader(self.dataset)\n        self._step = 0\n        self._fetch_next_batch()\n\n    def _create_async_reader(self, dataset: RayNativeDataset):\n        q = queue.Queue(maxsize=100)\n        batch_size = self.batch_size\n        to_tensors = self._to_tensors_fn()\n\n        def producer():\n            for batch in dataset.map_batches(to_tensors, batch_format=\"pandas\").iter_batches(\n                prefetch_batches=1, batch_size=batch_size, batch_format=\"pandas\"\n            ):\n                res = self._prepare_batch(batch)\n                q.put(res)\n            q.put(None)\n\n        def async_read():\n            t = threading.Thread(target=producer)\n            t.start()\n            while True:\n                batch = q.get(block=True)\n                if batch is None:\n                    break\n                yield batch\n            t.join()\n\n        return async_read()\n\n\nclass RayDatasetShardBatcher(_BaseBatcher):\n    \"\"\"Batcher for a Ray DataIterator shard (used in distributed training workers).\"\"\"\n\n    def __init__(\n        self,\n        data_iterator,\n        features: dict[str, dict],\n        training_set_metadata: dict[str, Any],\n        batch_size: int,\n        samples_per_epoch: int,\n    ):\n        self.data_iterator = data_iterator\n        super().__init__(features, training_set_metadata, batch_size, samples_per_epoch)\n        self._fetch_next_epoch()\n\n    def _fetch_next_epoch(self):\n        \"\"\"Create an async reader from the DataIterator for one epoch.\"\"\"\n        self.dataset_batch_iter = self._create_async_reader()\n        self._step = 0\n        self._fetch_next_batch()\n\n    def _create_async_reader(self):\n        q = queue.Queue(maxsize=100)\n        batch_size = self.batch_size\n        to_tensors = self._to_tensors_fn()\n\n        def producer():\n            for batch in self.data_iterator.iter_batches(\n                batch_size=batch_size,\n                batch_format=\"pandas\",\n                prefetch_batches=1,\n            ):\n                batch = to_tensors(batch)\n                res = self._prepare_batch(batch)\n                q.put(res)\n            q.put(None)\n\n        def async_read():\n            t = threading.Thread(target=producer)\n            t.start()\n            while True:\n                batch = q.get(block=True)\n                if batch is None:\n                    break\n                yield batch\n            t.join()\n\n        return async_read()\n"
  },
  {
    "path": "ludwig/data/dataset_synthesizer.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport os\nimport random\nimport string\nimport sys\nimport uuid\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torchaudio\nimport yaml\nfrom packaging import version\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    CATEGORY_DISTRIBUTION,\n    DATE,\n    DECODER,\n    ENCODER,\n    H3,\n    IMAGE,\n    INPUT_FEATURES,\n    NAME,\n    NUMBER,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    TYPE,\n    VECTOR,\n)\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.data_utils import save_csv\nfrom ludwig.utils.h3_util import components_to_h3\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.print_utils import print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n_TORCH_AUDIO_210 = version.parse(torchaudio.__version__) >= version.parse(\"2.1.0\")\n\nletters = string.ascii_letters\n\nDATETIME_FORMATS = {\n    \"%m-%d-%Y\": \"{m:02d}-{d:02d}-{Y:04d}\",\n    \"%m-%d-%Y %H:%M:%S\": \"{m:02d}-{d:02d}-{Y:04d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%m/%d/%Y\": \"{m:02d}/{d:02d}/{Y:04d}\",\n    \"%m/%d/%Y %H:%M:%S\": \"{m:02d}/{d:02d}/{Y:04d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%m-%d-%y\": \"{m:02d}-{d:02d}-{y:02d}\",\n    \"%m-%d-%y %H:%M:%S\": \"{m:02d}-{d:02d}-{y:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%m/%d/%y\": \"{m:02d}/{d:02d}/{y:02d}\",\n    \"%m/%d/%y %H:%M:%S\": \"{m:02d}/{d:02d}/{y:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%d-%m-%Y\": \"{d:02d}-{m:02d}-{Y:04d}\",\n    \"%d-%m-%Y %H:%M:%S\": \"{d:02d}-{m:02d}-{Y:04d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%d/%m/%Y\": \"{d:02d}/{m:02d}/{Y:04d}\",\n    \"%d/%m/%Y %H:%M:%S\": \"{d:02d}/{m:02d}/{Y:04d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%d-%m-%y\": \"{d:02d}-{m:02d}-{y:02d}\",\n    \"%d-%m-%y %H:%M:%S\": \"{d:02d}-{m:02d}-{y:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%d/%m/%y\": \"{d:02d}/{m:02d}/{y:02d}\",\n    \"%d/%m/%y %H:%M:%S\": \"{d:02d}/{m:02d}/{y:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%y-%m-%d\": \"{y:02d}-{m:02d}-{d:02d}\",\n    \"%y-%m-%d %H:%M:%S\": \"{y:02d}-{m:02d}-{d:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%y/%m/%d\": \"{y:02d}/{m:02d}/{d:02d}\",\n    \"%y/%m/%d %H:%M:%S\": \"{y:02d}/{m:02d}/{d:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%Y-%m-%d\": \"{Y:04d}-{m:02d}-{d:02d}\",\n    \"%Y-%m-%d %H:%M:%S\": \"{Y:04d}-{m:02d}-{d:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%Y/%m/%d\": \"{Y:04d}/{m:02d}/{d:02d}\",\n    \"%Y/%m/%d %H:%M:%S\": \"{Y:04d}/{m:02d}/{d:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%y-%d-%m\": \"{y:02d}-{d:02d}-{m:02d}\",\n    \"%y-%d-%m %H:%M:%S\": \"{y:02d}-{d:02d}-{m:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%y/%d/%m\": \"{y:02d}/{d:02d}/{m:02d}\",\n    \"%y/%d/%m %H:%M:%S\": \"{y:02d}/{d:02d}/{m:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%Y-%d-%m\": \"{Y:04d}-{d:02d}-{m:02d}\",\n    \"%Y-%d-%m %H:%M:%S\": \"{Y:04d}-{d:02d}-{m:02d} {H:02d}:{M:02d}:{S:02d}\",\n    \"%Y/%d/%m\": \"{Y:04d}/{d:02d}/{m:02d}\",\n    \"%Y/%d/%m %H:%M:%S\": \"{Y:04d}/{d:02d}/{m:02d} {H:02d}:{M:02d}:{S:02d}\",\n}\n\n\ndef _get_feature_encoder_or_decoder(feature):\n    \"\"\"Returns the nested decoder or encoder dictionary for a feature.\n\n    If neither encoder nor decoder is present, creates an empty encoder dict and returns it.\n    \"\"\"\n    if DECODER in feature:\n        return feature[DECODER]\n    elif ENCODER in feature:\n        return feature[ENCODER]\n    else:\n        feature[ENCODER] = {}\n        return feature[ENCODER]\n\n\ndef generate_string(length):\n    sequence = []\n    for _ in range(length):\n        sequence.append(random.choice(letters))\n    return \"\".join(sequence)\n\n\ndef build_vocab(size):\n    vocab = []\n    for _ in range(size):\n        vocab.append(generate_string(random.randint(2, 10)))\n    return vocab\n\n\ndef return_none(feature):\n    return None\n\n\ndef assign_vocab(feature):\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    encoder_or_decoder[\"idx2str\"] = build_vocab(encoder_or_decoder.get(\"vocab_size\", 10))\n    encoder_or_decoder[\"vocab_size\"] = len(encoder_or_decoder[\"idx2str\"])\n\n\ndef build_feature_parameters(features):\n    feature_parameters = {}\n    for feature in features:\n        feature_builder_function = get_from_registry(feature[TYPE], parameters_builders_registry)\n        feature_parameters[feature[NAME]] = feature_builder_function(feature)\n    return feature_parameters\n\n\nparameters_builders_registry = {\n    \"category\": assign_vocab,\n    \"text\": assign_vocab,\n    \"number\": return_none,\n    \"binary\": return_none,\n    \"set\": assign_vocab,\n    \"bag\": assign_vocab,\n    \"sequence\": assign_vocab,\n    \"timeseries\": return_none,\n    \"image\": return_none,\n    \"audio\": return_none,\n    \"date\": return_none,\n    \"h3\": return_none,\n    VECTOR: return_none,\n    CATEGORY_DISTRIBUTION: return_none,\n}\n\n\n@DeveloperAPI\ndef build_synthetic_dataset_df(dataset_size: int, config: ModelConfigDict) -> pd.DataFrame:\n    for feature in config[OUTPUT_FEATURES]:\n        if DECODER not in feature:\n            feature[DECODER] = {}\n    features = config[INPUT_FEATURES] + config[OUTPUT_FEATURES]\n    df = build_synthetic_dataset(dataset_size, features)\n    data = [next(df) for _ in range(dataset_size + 1)]\n    return pd.DataFrame(data[1:], columns=data[0])\n\n\n@DeveloperAPI\ndef build_synthetic_dataset(dataset_size: int, features: list[dict], outdir: str = \".\"):\n    \"\"\"Synthesizes a dataset for testing purposes.\n\n    :param dataset_size: (int) size of the dataset\n    :param features: (List[dict]) list of features to generate in YAML format.\n        Provide a list containing one dictionary for each feature,\n        each dictionary must include a name, a type\n        and can include some generation parameters depending on the type\n    :param outdir: (str) Path to an output directory. Used for saving synthetic image and audio files.\n\n    Example content for features:\n\n    [\n        {name: text_1, type: text, vocab_size: 20, max_len: 20},\n        {name: text_2, type: text, vocab_size: 20, max_len: 20},\n        {name: category_1, type: category, vocab_size: 10},\n        {name: category_2, type: category, vocab_size: 15},\n        {name: number_1, type: number},\n        {name: number_2, type: number},\n        {name: binary_1, type: binary},\n        {name: binary_2, type: binary},\n        {name: set_1, type: set, vocab_size: 20, max_len: 20},\n        {name: set_2, type: set, vocab_size: 20, max_len: 20},\n        {name: bag_1, type: bag, vocab_size: 20, max_len: 10},\n        {name: bag_2, type: bag, vocab_size: 20, max_len: 10},\n        {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20},\n        {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20},\n        {name: timeseries_1, type: timeseries, max_len: 20},\n        {name: timeseries_2, type: timeseries, max_len: 20},\n        {name: date_1, type: date},\n        {name: date_2, type: date},\n        {name: h3_1, type: h3},\n        {name: h3_2, type: h3},\n        {name: vector_1, type: vector},\n        {name: vector_2, type: vector},\n    ]\n    \"\"\"\n    build_feature_parameters(features)\n    header = []\n    for feature in features:\n        header.append(feature[NAME])\n\n    yield header\n    for _ in range(dataset_size):\n        yield generate_datapoint(features=features, outdir=outdir)\n\n\ndef generate_datapoint(features: list[dict], outdir: str) -> str | int | bool:\n    \"\"\"Returns a synthetic example containing features specified by the features spec.\n\n    `outdir` is only used for generating synthetic image and synthetic audio features. Otherwise, it is unused.\n    \"\"\"\n    datapoint = []\n    for feature in features:\n        if \"cycle\" in feature and feature[\"cycle\"] is True and feature[TYPE] in cyclers_registry:\n            cycler_function = cyclers_registry[feature[TYPE]]\n            feature_value = cycler_function(feature)\n        else:\n            generator_function = get_from_registry(feature[TYPE], generators_registry)\n            feature_value = generator_function(feature=feature, outdir=outdir)\n        datapoint.append(feature_value)\n    return datapoint\n\n\ndef generate_category(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random category.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    return random.choice(encoder_or_decoder[\"idx2str\"])\n\n\ndef generate_number(feature, outdir: str | None = None) -> int:\n    \"\"\"Returns a random number.\n\n    `outdir` is unused.\n    \"\"\"\n    return random.uniform(feature[\"min\"] if \"min\" in feature else 0, feature[\"max\"] if \"max\" in feature else 1)\n\n\ndef generate_binary(feature, outdir: str | None = None) -> bool:\n    \"\"\"Returns a random boolean.\n\n    `outdir` is unused.\n    \"\"\"\n    choices = feature.get(\"bool2str\", [False, True])\n    p = feature[\"prob\"] if \"prob\" in feature else 0.5\n    return np.random.choice(choices, p=[1 - p, p])\n\n\ndef generate_sequence(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random sequence.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    length = encoder_or_decoder.get(\"max_len\", 10)\n    if \"min_len\" in encoder_or_decoder:\n        length = random.randint(encoder_or_decoder[\"min_len\"], length)\n    sequence = [random.choice(encoder_or_decoder[\"idx2str\"]) for _ in range(length)]\n    encoder_or_decoder[\"vocab_size\"] = (\n        encoder_or_decoder[\"vocab_size\"] + 4\n    )  # For special symbols: START, STOP, PAD, UNK.\n    return \" \".join(sequence)\n\n\ndef generate_set(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random set.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    elems = []\n    for _ in range(random.randint(0, encoder_or_decoder.get(\"max_len\", 3))):\n        elems.append(random.choice(encoder_or_decoder[\"idx2str\"]))\n    return \" \".join(list(set(elems)))\n\n\ndef generate_bag(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random bag.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    elems = []\n    for _ in range(random.randint(0, encoder_or_decoder.get(\"max_len\", 3))):\n        elems.append(random.choice(encoder_or_decoder[\"idx2str\"]))\n    return \" \".join(elems)\n\n\ndef generate_text(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns random text.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder_or_decoder = _get_feature_encoder_or_decoder(feature)\n    length = encoder_or_decoder.get(\"max_len\", 10)\n    text = []\n    for _ in range(random.randint(length - int(length * 0.2), length)):\n        text.append(random.choice(encoder_or_decoder[\"idx2str\"]))\n    return \" \".join(text)\n\n\ndef generate_timeseries(feature, max_len=10, outdir: str | None = None) -> str:\n    \"\"\"Returns a random timeseries.\n\n    `outdir` is unused.\n    \"\"\"\n    encoder = _get_feature_encoder_or_decoder(feature)\n    series = []\n    max_len = encoder.get(\"max_len\", max_len)\n    series_len = random.randint(max_len - 2, max_len)  # simulates variable length\n    for _ in range(series_len):\n        series.append(str(random.uniform(encoder.get(\"min\", 0), encoder.get(\"max\", 1))))\n    return \" \".join(series)\n\n\ndef generate_audio(feature, outdir: str) -> str:\n    \"\"\"Generates random audio and saves it to the outdir.\n\n    Returns the path to the directory of saved files.\n    \"\"\"\n    destination_folder = feature.get(\"destination_folder\", outdir)\n    if PREPROCESSING in feature:\n        audio_length = feature[PREPROCESSING].get(\"audio_file_length_limit_in_s\", 2)\n    else:\n        audio_length = feature.get(\"audio_file_length_limit_in_s\", 1)\n    sampling_rate = 16000\n    num_samples = int(audio_length * sampling_rate)\n    audio = np.sin(np.arange(num_samples) / 100 * 2 * np.pi) * 2 * (np.random.random(num_samples) - 0.5)\n    audio_tensor = torch.tensor(np.array([audio])).type(torch.float32)\n    audio_filename = uuid.uuid4().hex[:10].upper() + \".wav\"\n\n    if not os.path.exists(destination_folder):\n        os.makedirs(destination_folder)\n    audio_dest_path = os.path.join(destination_folder, audio_filename)\n\n    try:\n        if _TORCH_AUDIO_210:\n            torchaudio.save(audio_dest_path, audio_tensor, sample_rate=sampling_rate, backend=\"sox\")\n        torchaudio.save(audio_dest_path, audio_tensor, sampling_rate)\n\n    except OSError as e:\n        raise OSError(f\"Unable to save audio to disk: {e}\")\n\n    return audio_dest_path\n\n\ndef generate_image(feature, outdir: str, save_as_numpy: bool = False) -> str:\n    \"\"\"Generates random images and saves it to the outdir.\n\n    Returns the path to the directory of saved files.\n    \"\"\"\n    save_as_numpy = feature.get(\"save_as_numpy\", save_as_numpy)\n\n    try:\n        from torchvision.io import write_png\n    except ImportError:\n        logger.error(\n            \" torchvision is not installed. \"\n            \"In order to install all image feature dependencies run \"\n            \"pip install ludwig[image]\"\n        )\n        sys.exit(-1)\n\n    # Read num_channels, width, height\n    destination_folder = feature.get(\"destination_folder\", outdir)\n    if PREPROCESSING in feature:\n        height = feature[PREPROCESSING].get(\"height\", 28)\n        width = feature[PREPROCESSING].get(\"width\", 28)\n        num_channels = feature[PREPROCESSING].get(\"num_channels\", 1)\n    else:\n        encoder = _get_feature_encoder_or_decoder(feature)\n        height = encoder.get(\"height\", 28)\n        width = encoder.get(\"width\", 28)\n        num_channels = encoder.get(\"num_channels\", 1)\n\n    if width <= 0 or height <= 0 or num_channels < 1:\n        raise ValueError(\"Invalid arguments for generating images\")\n\n    # Create a Random Image\n    img = torch.randint(0, 255, (num_channels, width, height), dtype=torch.uint8)\n\n    # Generate a unique random filename\n    image_filename = uuid.uuid4().hex[:10].upper() + \".png\"\n\n    # Save the image to disk either in a specified location/new folder\n    if not os.path.exists(destination_folder):\n        os.makedirs(destination_folder)\n    image_dest_path = os.path.join(destination_folder, image_filename)\n    try:\n        # save_image(torch.from_numpy(img.astype(\"uint8\")), image_dest_path)\n        if save_as_numpy:\n            with open(image_dest_path, \"wb\") as f:\n                np.save(f, img.detach().cpu().numpy())\n        else:\n            write_png(img, image_dest_path)\n    except OSError as e:\n        raise OSError(f\"Unable to save images to disk: {e}\")\n\n    return image_dest_path\n\n\ndef generate_datetime(feature, outdir: str | None = None) -> str:\n    \"\"\"Generates a random date time, picking a format among different types.\n\n    If no format is specified, the first one is used.\n    \"\"\"\n    if \"datetime_format\" in feature:\n        datetime_generation_format = DATETIME_FORMATS[feature[\"datetime_format\"]]\n    elif \"preprocessing\" in feature and \"datetime_format\" in feature[\"preprocessing\"]:\n        datetime_generation_format = DATETIME_FORMATS[feature[\"preprocessing\"][\"datetime_format\"]]\n    else:\n        datetime_generation_format = DATETIME_FORMATS[next(iter(DATETIME_FORMATS))]\n\n    y = random.randint(1, 99)\n    Y = random.randint(1, 9999)\n    m = random.randint(1, 12)\n    d = random.randint(1, 28)\n    H = random.randint(1, 12)\n    M = random.randint(1, 59)\n    S = random.randint(1, 59)\n\n    return datetime_generation_format.format(y=y, Y=Y, m=m, d=d, H=H, M=M, S=S)\n\n\ndef generate_h3(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random h3.\n\n    `outdir` is unused.\n    \"\"\"\n    resolution = random.randint(0, 15)  # valid values [0, 15]\n    h3_components = {\n        \"mode\": 1,  # we can avoid testing other modes\n        \"edge\": 0,  # only used in other modes\n        \"resolution\": resolution,\n        \"base_cell\": random.randint(0, 121),  # valid values [0, 121]\n        # valid values [0, 7]\n        \"cells\": [random.randint(0, 7) for _ in range(resolution)],\n    }\n\n    return components_to_h3(h3_components)\n\n\ndef generate_vector(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random vector.\n\n    `outdir` is unused.\n    \"\"\"\n    # Space delimited string with floating point numbers\n    if PREPROCESSING in feature:\n        vector_size = feature[PREPROCESSING].get(\"vector_size\", 10)\n    else:\n        vector_size = feature.get(\"vector_size\", 10)\n    return \" \".join([str(100 * random.random()) for _ in range(vector_size)])\n\n\ndef generate_category_distribution(feature, outdir: str | None = None) -> str:\n    \"\"\"Returns a random category distribution.\n\n    `outdir` is unused.\n    \"\"\"\n    # Space delimited string with floating point numbers that sum to 1\n    preprocessing = feature.get(PREPROCESSING, {})\n    vector_size = len(preprocessing.get(\"vocab\", [\"a\", \"b\", \"c\"]))\n    v = np.random.rand(vector_size)\n    v = v / v.sum()\n    return \" \".join([str(x) for x in v])\n\n\ngenerators_registry = {\n    BINARY: generate_binary,\n    NUMBER: generate_number,\n    CATEGORY: generate_category,\n    SET: generate_set,\n    BAG: generate_bag,\n    SEQUENCE: generate_sequence,\n    TEXT: generate_text,\n    TIMESERIES: generate_timeseries,\n    IMAGE: generate_image,\n    AUDIO: generate_audio,\n    H3: generate_h3,\n    DATE: generate_datetime,\n    VECTOR: generate_vector,\n    CATEGORY_DISTRIBUTION: generate_category_distribution,\n}\n\ncategory_cycle = 0\n\n\ndef cycle_category(feature):\n    global category_cycle\n    idx2str = feature[DECODER][\"idx2str\"] if DECODER in feature else feature[ENCODER][\"idx2str\"]\n    if category_cycle >= len(idx2str):\n        category_cycle = 0\n    category = idx2str[category_cycle]\n    category_cycle += 1\n    return category\n\n\nbinary_cycle = False\n\n\ndef cycle_binary(feature):\n    global binary_cycle\n    if binary_cycle:\n        binary_cycle = False\n        return True\n    else:\n        binary_cycle = True\n        return False\n\n\ncyclers_registry = {\"category\": cycle_category, \"binary\": cycle_binary}\n\n\ndef cli_synthesize_dataset(dataset_size: int, features: list[dict], output_path: str, **kwargs) -> None:\n    \"\"\"Symthesizes a dataset for testing purposes.\n\n    :param dataset_size: (int) size of the dataset\n    :param features: (List[dict]) list of features to generate in YAML format.\n        Provide a list contaning one dictionary for each feature,\n        each dictionary must include a name, a type\n        and can include some generation parameters depending on the type\n    :param output_path: (str) path where to save the output CSV file\n\n    Example content for features:\n\n    [\n        {name: text_1, type: text, vocab_size: 20, max_len: 20},\n        {name: text_2, type: text, vocab_size: 20, max_len: 20},\n        {name: category_1, type: category, vocab_size: 10},\n        {name: category_2, type: category, vocab_size: 15},\n        {name: number_1, type: number},\n        {name: number_2, type: number},\n        {name: binary_1, type: binary},\n        {name: binary_2, type: binary},\n        {name: set_1, type: set, vocab_size: 20, max_len: 20},\n        {name: set_2, type: set, vocab_size: 20, max_len: 20},\n        {name: bag_1, type: bag, vocab_size: 20, max_len: 10},\n        {name: bag_2, type: bag, vocab_size: 20, max_len: 10},\n        {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20},\n        {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20},\n        {name: timeseries_1, type: timeseries, max_len: 20},\n        {name: timeseries_2, type: timeseries, max_len: 20},\n        {name: date_1, type: date},\n        {name: date_2, type: date},\n        {name: h3_1, type: h3},\n        {name: h3_2, type: h3},\n        {name: vector_1, type: vector},\n        {name: vector_2, type: vector},\n    ]\n    \"\"\"\n    if dataset_size is None or features is None or output_path is None:\n        raise ValueError(\n            \"Missing one or more required parameters: '--dataset_size', \" \"'--features' or '--output_path'\"\n        )\n    dataset = build_synthetic_dataset(dataset_size, features)\n    save_csv(output_path, dataset)\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script generates a synthetic dataset.\",\n        prog=\"ludwig synthesize_dataset\",\n        usage=\"%(prog)s [options]\",\n    )\n    parser.add_argument(\"-od\", \"--output_path\", type=str, help=\"output CSV file path\")\n    parser.add_argument(\"-d\", \"--dataset_size\", help=\"size of the dataset\", type=int, default=100)\n    parser.add_argument(\n        \"-f\",\n        \"--features\",\n        default=\"[\\\n          {name: text_1, type: text, vocab_size: 20, max_len: 20}, \\\n          {name: text_2, type: text, vocab_size: 20, max_len: 20}, \\\n          {name: category_1, type: category, vocab_size: 10}, \\\n          {name: category_2, type: category, vocab_size: 15}, \\\n          {name: number_1, type: number}, \\\n          {name: number_2, type: number}, \\\n          {name: binary_1, type: binary}, \\\n          {name: binary_2, type: binary}, \\\n          {name: set_1, type: set, vocab_size: 20, max_len: 20}, \\\n          {name: set_2, type: set, vocab_size: 20, max_len: 20}, \\\n          {name: bag_1, type: bag, vocab_size: 20, max_len: 10}, \\\n          {name: bag_2, type: bag, vocab_size: 20, max_len: 10}, \\\n          {name: sequence_1, type: sequence, vocab_size: 20, max_len: 20}, \\\n          {name: sequence_2, type: sequence, vocab_size: 20, max_len: 20}, \\\n          {name: timeseries_1, type: timeseries, max_len: 20}, \\\n          {name: timeseries_2, type: timeseries, max_len: 20}, \\\n          {name: date_1, type: date}, \\\n          {name: date_2, type: date}, \\\n          {name: h3_1, type: h3}, \\\n          {name: h3_2, type: h3}, \\\n          {name: vector_1, type: vector}, \\\n          {name: vector_2, type: vector}, \\\n        ]\",\n        type=yaml.safe_load,\n        help=\"list of features to generate in YAML format. \"\n        \"Provide a list containing one dictionary for each feature, \"\n        \"each dictionary must include a name, a type \"\n        \"and can include some generation parameters depending on the type\",\n    )\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"synthesize_dataset\", *sys_argv)\n\n    # No log level parameter this is placeholder if we add at later date\n    # args.logging_level = get_logging_level_registry[args.logging_level]\n    # logging.getLogger('ludwig').setLevel(\n    #     args.logging_level\n    # )\n    # global logger\n    # logger = logging.getLogger('ludwig.data.dataset_synthesizer')\n\n    print_ludwig(\"Synthesize Dataset\", LUDWIG_VERSION)\n\n    cli_synthesize_dataset(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/data/negative_sampling.py",
    "content": "import logging\nimport time\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport scipy\n\nfrom ludwig.utils.types import DataFrame\n\n\ndef _negative_sample_user(interaction_row: np.array, neg_pos_ratio: int, extra_samples: int) -> tuple[list[int], int]:\n    \"\"\"Returns a list of negative item indices for given user-item interactions.\n\n    If there are not enough negative items, takes all of them and adds the difference to the extra_samples\n    otherwise, samples with replacement.\n\n    Params:\n        interaction_row: user-item interaction row\n        neg_pos_ratio: number of negative samples per positive sample\n        extra_samples: number of additional samples to add to the negative sample list\n    Returns:\n        Tuple of list of negative item indices and number of extra samples\n    \"\"\"\n    # Find all items that are not interacted with by the user\n    neg_items = np.where(interaction_row == 0)[1]\n    available_samples = len(neg_items)\n\n    # Randomly sample negative items\n    npos = interaction_row.shape[1] - len(neg_items)\n    samples_required = npos * neg_pos_ratio + extra_samples\n    should_sample = samples_required <= available_samples\n\n    neg_items = np.random.choice(neg_items, samples_required, replace=False) if should_sample else neg_items\n\n    return neg_items.tolist(), max(0, samples_required - available_samples)\n\n\ndef negative_sample(\n    df: DataFrame,\n    user_id_col: str = \"customer_id\",\n    item_id_col: str = \"article_id\",\n    label_col: str = \"label\",\n    neg_pos_ratio: int = 1,\n    neg_val: Any = 0,\n    log_pct: int = 0,\n):\n    \"\"\"Negative sampling for implicit feedback datasets.\n\n    Params:\n        df: DataFrame containing user-item interactions\n        user_id_col: column name for user ids\n        item_id_col: column name for item ids\n        label_col: column name for interaction labels (e.g. 1 for positive interaction)\n        n_neg: number of negative samples per positive sample\n        neg_val: label value for the negative samples\n        percent_print: print progress every percent_print percent. 0 to disable\n    Returns:\n        Input DataFrame with negative samples appended\n\n    Source: https://petamind.com/fast-uniform-negative-sampling-for-rating-matrix/\n    \"\"\"\n    # TODO(joppe): support out of memory negative sampling using Dask\n    if not isinstance(df, pd.DataFrame):\n        df = df.compute()\n\n    # Initialize sparse COOrdinate matrix from users and items in existing interactions\n    user_id_cat = df[user_id_col].astype(\"category\").cat\n    user_id_codes = user_id_cat.codes.values\n\n    item_id_cat = df[item_id_col].astype(\"category\").cat\n    item_id_codes = item_id_cat.codes.values\n\n    interactions_sparse = scipy.sparse.coo_matrix((df[label_col], (user_id_codes, item_id_codes)))\n\n    # Convert to dense user-item matrix so we can iterate\n    interactions_dense = interactions_sparse.todense()\n\n    nrows = interactions_dense.shape[0]\n    niter_log = int(nrows * log_pct / 100)\n    start_time = time.time()\n\n    user_indices, item_indices = [], []\n    extra_samples = 0\n    for user_idx, interaction_row in enumerate(interactions_dense):\n        if log_pct > 0 and user_idx % niter_log == 0:\n            logging.info(\n                f\"Negative sampling progress: {float(user_idx) * 100 / nrows:0.0f}% \"\n                f\"in {time.time() - start_time:0.2f}s\"\n            )\n\n        neg_items_for_user, extra_samples = _negative_sample_user(interaction_row, neg_pos_ratio, extra_samples)\n\n        # Add to negative user-item pairs\n        item_indices += neg_items_for_user\n        user_indices += [user_idx] * len(neg_items_for_user)\n\n    negative_samples = pd.DataFrame(\n        {\n            # Map back to original user and item ids\n            user_id_col: user_id_cat.categories[user_indices],\n            item_id_col: item_id_cat.categories[item_indices],\n            label_col: [neg_val] * len(item_indices),\n        }\n    )\n\n    return pd.concat([df[[user_id_col, item_id_col, label_col]], negative_samples])\n"
  },
  {
    "path": "ludwig/data/postprocessing.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\nfrom typing import Any, Optional\n\nimport numpy as np\nimport pandas as pd\nimport torch\n\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.data.utils import convert_to_dict\nfrom ludwig.utils.data_utils import DATAFRAME_FORMATS, DICT_FORMATS\nfrom ludwig.utils.dataframe_utils import to_numpy_dataset\nfrom ludwig.utils.fs_utils import has_remote_protocol, open_file\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.strings_utils import make_safe_filename\nfrom ludwig.utils.types import DataFrame\n\n\ndef postprocess(\n    predictions,\n    output_features,\n    training_set_metadata,\n    output_directory=\"\",\n    backend=LOCAL_BACKEND,\n    skip_save_unprocessed_output=False,\n) -> DataFrame:\n    if not backend.is_coordinator():\n        # Only save unprocessed output on the coordinator\n        skip_save_unprocessed_output = True\n\n    saved_keys = set()\n    if not skip_save_unprocessed_output:\n        _save_as_numpy(predictions, output_directory, saved_keys, backend)\n\n    def postprocess_batch(df):\n        for of_name, output_feature in output_features.items():\n            df = output_feature.postprocess_predictions(\n                df,\n                training_set_metadata[of_name],\n            )\n        return df\n\n    # We disable tensor extension casting here because this step is the final data processing step and\n    # we do not expect return to Ray Datasets after this point. The dtype of the predictions will be\n    # whatever they would be if we did all postprocessing in Dask.\n    predictions = backend.df_engine.map_batches(predictions, postprocess_batch, enable_tensor_extension_casting=False)\n\n    # Save any new columns but do not save the original columns again\n    if not skip_save_unprocessed_output:\n        _save_as_numpy(predictions, output_directory, saved_keys, backend)\n\n    return predictions\n\n\ndef _save_as_numpy(predictions, output_directory, saved_keys, backend):\n    predictions = predictions[[c for c in predictions.columns if c not in saved_keys]]\n    npy_filename = os.path.join(output_directory, \"{}.npy\")\n    numpy_predictions = to_numpy_dataset(predictions, backend)\n    for k, v in numpy_predictions.items():\n        k = k.replace(\"<\", \"[\").replace(\">\", \"]\")  # Replace <UNK> and <PAD> with [UNK], [PAD]\n        if k not in saved_keys:\n            if has_remote_protocol(output_directory):\n                with open_file(npy_filename.format(make_safe_filename(k)), mode=\"wb\") as f:\n                    np.save(f, v)\n            else:\n                np.save(npy_filename.format(make_safe_filename(k)), v)\n            saved_keys.add(k)\n\n\ndef convert_dict_to_df(predictions: dict[str, dict[str, list[Any] | torch.Tensor | np.ndarray]]) -> pd.DataFrame:\n    \"\"\"Converts a dictionary of predictions into a pandas DataFrame.\n\n    Example format of predictions dictionary:\n\n    {\n        \"binary_C82EB\": {\n            \"predictions\": torch.tensor([True, True, True, False]),\n            \"probabilities\": torch.tensor([[0.4777, 0.5223], [0.4482, 0.5518], [0.4380, 0.5620], [0.5059, 0.4941]]),\n        },\n        \"category_1491D\": {\n            \"predictions\": [\"NkNUG\", \"NkNUG\", \"NkNUG\", \"NkNUG\"],\n            \"probabilities\": torch.tensor(\n                [\n                    [0.1058, 0.4366, 0.1939, 0.2637],\n                    [0.0816, 0.4807, 0.1978, 0.2399],\n                    [0.0907, 0.4957, 0.1829, 0.2308],\n                    [0.0728, 0.5015, 0.1900, 0.2357],\n                ]\n            ),\n        },\n        \"num_7B25F\": {\"predictions\": torch.tensor([2.0436, 2.1158, 2.1222, 2.1964])},\n    }\n    \"\"\"\n    output = {}\n    for of_name, preds_dict in predictions.items():\n        for key, value in preds_dict.items():\n            output_key = f\"{of_name}_{key}\"\n            if not isinstance(value, list):\n                value = value.tolist()\n            output[output_key] = value\n    return pd.DataFrame.from_dict(output)\n\n\ndef convert_predictions(\n    predictions, output_features, return_type=\"dict\", backend: Optional[\"Backend\"] = None  # noqa: F821\n):\n    convert_fn = get_from_registry(return_type, conversion_registry)\n    return convert_fn(predictions, output_features, backend)\n\n\ndef convert_to_df(\n    predictions,\n    output_features,\n    backend: Optional[\"Backend\"] = None,  # noqa: F821\n):\n    return predictions\n\n\nconversion_registry = {\n    **{format: convert_to_dict for format in DICT_FORMATS},\n    **{format: convert_to_df for format in DATAFRAME_FORMATS},\n}\n"
  },
  {
    "path": "ludwig/data/preprocessing.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport contextlib\nimport logging\nimport warnings\nfrom abc import ABC, abstractmethod\n\nimport numpy as np\nimport pandas as pd\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.backend import Backend, LOCAL_BACKEND\nfrom ludwig.config_validation.preprocessing import check_global_max_sequence_length_fits_prompt_template\nfrom ludwig.constants import (\n    BFILL,\n    CHECKSUM,\n    COLUMN,\n    DEFAULTS,\n    DROP_ROW,\n    ENCODER,\n    FFILL,\n    FILL_WITH_CONST,\n    FILL_WITH_FALSE,\n    FILL_WITH_MEAN,\n    FILL_WITH_MODE,\n    FILL_WITH_TRUE,\n    FULL,\n    META,\n    MIN_DATASET_SPLIT_ROWS,\n    MODEL_ECD,\n    NAME,\n    NUMBER,\n    PREPROCESSING,\n    PROC_COLUMN,\n    SPLIT,\n    SRC,\n    TEST,\n    TEXT,\n    TRAINING,\n    TYPE,\n    VALIDATION,\n)\nfrom ludwig.data.cache.manager import DatasetCache\nfrom ludwig.data.cache.types import wrap\nfrom ludwig.data.concatenate_datasets import concatenate_df, concatenate_files, concatenate_splits\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.data.prompt import format_input_with_prompt, index_column\nfrom ludwig.data.split import get_splitter, split_dataset\nfrom ludwig.data.utils import get_input_and_output_features, set_fixed_split\nfrom ludwig.datasets import load_dataset_uris\nfrom ludwig.features.feature_registries import get_base_type_registry\nfrom ludwig.models.embedder import create_embed_batch_size_evaluator, create_embed_transform_fn\nfrom ludwig.schema.encoders.utils import get_encoder_cls\nfrom ludwig.types import FeatureConfigDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils import data_utils, strings_utils\nfrom ludwig.utils.backward_compatibility import upgrade_metadata\nfrom ludwig.utils.data_utils import (\n    CACHEABLE_FORMATS,\n    CSV_FORMATS,\n    DATA_TEST_PARQUET_FP,\n    DATA_TRAIN_HDF5_FP,\n    DATA_TRAIN_PARQUET_FP,\n    DATA_VALIDATION_PARQUET_FP,\n    DATAFRAME_FORMATS,\n    DICT_FORMATS,\n    EXCEL_FORMATS,\n    FEATHER_FORMATS,\n    figure_data_format,\n    FWF_FORMATS,\n    get_split_path,\n    HDF5_FORMATS,\n    HTML_FORMATS,\n    JSON_FORMATS,\n    JSONL_FORMATS,\n    ORC_FORMATS,\n    override_in_memory_flag,\n    PARQUET_FORMATS,\n    PICKLE_FORMATS,\n    read_csv,\n    read_excel,\n    read_feather,\n    read_fwf,\n    read_html,\n    read_json,\n    read_jsonl,\n    read_orc,\n    read_parquet,\n    read_pickle,\n    read_sas,\n    read_spss,\n    read_stata,\n    read_tsv,\n    sanitize_column_names,\n    SAS_FORMATS,\n    SPSS_FORMATS,\n    STATA_FORMATS,\n    TSV_FORMATS,\n)\nfrom ludwig.utils.dataframe_utils import is_dask_series_or_df\nfrom ludwig.utils.defaults import (\n    default_prediction_preprocessing_parameters,\n    default_random_seed,\n    default_training_preprocessing_parameters,\n)\nfrom ludwig.utils.fs_utils import file_lock, path_exists\nfrom ludwig.utils.misc_utils import get_from_registry, merge_dict\nfrom ludwig.utils.types import DataFrame, Series\n\n# Opt-in to future pandas behavior: fillna/ffill/bfill will no longer silently downcast dtypes\npd.set_option(\"future.no_silent_downcasting\", True)\n\nREPARTITIONING_FEATURE_TYPES = {\"image\", \"audio\"}\n\nlogger = logging.getLogger(__name__)\n\n\nclass DataFormatPreprocessor(ABC):\n    @staticmethod\n    @abstractmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        pass\n\n    @staticmethod\n    @abstractmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        pass\n\n    @staticmethod\n    @abstractmethod\n    def prepare_processed_data(\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n    ):\n        pass\n\n\nclass DictPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        num_overrides = override_in_memory_flag(features, True)\n        if num_overrides > 0:\n            logger.warning(\"Using in_memory = False is not supported \" \"with {} data format.\".format(\"dict\"))\n\n        df_engine = backend.df_engine\n        if dataset is not None:\n            dataset = df_engine.from_pandas(pd.DataFrame(dataset))\n        if training_set is not None:\n            training_set = df_engine.from_pandas(pd.DataFrame(training_set))\n        if validation_set is not None:\n            validation_set = df_engine.from_pandas(pd.DataFrame(validation_set))\n        if test_set is not None:\n            test_set = df_engine.from_pandas(pd.DataFrame(test_set))\n\n        return _preprocess_df_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            training_set_metadata=training_set_metadata,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset, training_set_metadata = build_dataset(\n            config,\n            pd.DataFrame(dataset),\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass DataFramePreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        num_overrides = override_in_memory_flag(features, True)\n        if num_overrides > 0:\n            logger.warning(\"Using in_memory = False is not supported \" \"with {} data format.\".format(\"dataframe\"))\n\n        if isinstance(dataset, pd.DataFrame):\n            dataset = backend.df_engine.from_pandas(dataset)\n\n        return _preprocess_df_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            training_set_metadata=training_set_metadata,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        if isinstance(dataset, pd.DataFrame):\n            dataset = backend.df_engine.from_pandas(dataset)\n\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass CSVPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_csv,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_csv(dataset, df_lib=backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass TSVPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_tsv,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_tsv(dataset, df_lib=backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass JSONPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_json,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_json(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass JSONLPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_jsonl,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_jsonl(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass ExcelPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_excel,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_excel(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass ParquetPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_parquet,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_parquet(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n    @staticmethod\n    def prepare_processed_data(\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n    ):\n        test_set = test_set if test_set and path_exists(test_set) else None\n        if test_set and isinstance(test_set, str) and DATA_TEST_PARQUET_FP not in training_set_metadata:\n            training_set_metadata[DATA_TEST_PARQUET_FP] = test_set\n\n        validation_set = validation_set if validation_set and path_exists(validation_set) else None\n        if (\n            validation_set\n            and isinstance(validation_set, str)\n            and DATA_VALIDATION_PARQUET_FP not in training_set_metadata\n        ):\n            training_set_metadata[DATA_VALIDATION_PARQUET_FP] = validation_set\n\n        if training_set and isinstance(training_set, str) and DATA_TRAIN_PARQUET_FP not in training_set_metadata:\n            training_set_metadata[DATA_TRAIN_PARQUET_FP] = training_set\n        return training_set, test_set, validation_set, training_set_metadata\n\n\nclass PicklePreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_pickle,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_pickle(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass FatherPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_feather,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_feather(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass FWFPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_fwf,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_fwf(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass HTMLPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_html,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_html(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass ORCPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_orc,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_orc(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass SASPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_sas,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_sas(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass SPSSPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_spss,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_spss(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass StataPreprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return _preprocess_file_for_training(\n            config,\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            read_fn=read_stata,\n            training_set_metadata=training_set_metadata,\n            skip_save_processed_input=skip_save_processed_input,\n            preprocessing_params=preprocessing_params,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        dataset_df = read_stata(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n        dataset, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"prediction\",\n            metadata=training_set_metadata,\n            backend=backend,\n            callbacks=callbacks,\n        )\n        return dataset, training_set_metadata, None\n\n\nclass HDF5Preprocessor(DataFormatPreprocessor):\n    @staticmethod\n    def preprocess_for_training(\n        config,\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n        callbacks=None,\n    ):\n        return HDF5Preprocessor.prepare_processed_data(\n            features,\n            dataset,\n            training_set,\n            validation_set,\n            test_set,\n            training_set_metadata,\n            skip_save_processed_input,\n            preprocessing_params,\n            backend,\n            random_seed,\n        )\n\n    @staticmethod\n    def preprocess_for_prediction(\n        config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n    ):\n        hdf5_fp = dataset\n        dataset = load_hdf5(dataset, preprocessing_params, backend, split_data=False, shuffle_training=False)\n        return dataset, training_set_metadata, hdf5_fp\n\n    @staticmethod\n    def prepare_processed_data(\n        features,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        skip_save_processed_input=False,\n        preprocessing_params=default_training_preprocessing_parameters,\n        backend=LOCAL_BACKEND,\n        random_seed=default_random_seed,\n    ):\n        if dataset is None and training_set is None:\n            raise ValueError(\"One of `dataset` or `training_set` must be not None\")\n        not_none_set = dataset if dataset is not None else training_set\n\n        if not training_set_metadata:\n            raise ValueError(\"When providing HDF5 data, \" \"training_set_metadata must not be None.\")\n\n        logger.info(\"Using full hdf5 and json\")\n\n        if DATA_TRAIN_HDF5_FP not in training_set_metadata:\n            logger.warning(\n                \"data_train_hdf5_fp not present in training_set_metadata. \"\n                \"Adding it with the current HDF5 file path {}\".format(not_none_set)\n            )\n            training_set_metadata[DATA_TRAIN_HDF5_FP] = not_none_set\n\n        elif training_set_metadata[DATA_TRAIN_HDF5_FP] != not_none_set:\n            logger.warning(\n                \"data_train_hdf5_fp in training_set_metadata is {}, \"\n                \"different from the current HDF5 file path {}. \"\n                \"Replacing it\".format(training_set_metadata[DATA_TRAIN_HDF5_FP], not_none_set)\n            )\n            training_set_metadata[DATA_TRAIN_HDF5_FP] = not_none_set\n\n        if dataset is not None:\n            training_set, test_set, validation_set = load_hdf5(\n                dataset, preprocessing_params, backend, shuffle_training=True\n            )\n\n        elif training_set is not None:\n            kwargs = dict(preprocessing_params=preprocessing_params, backend=backend, split_data=False)\n            training_set = load_hdf5(training_set, shuffle_training=True, **kwargs)\n\n            if validation_set is not None:\n                validation_set = load_hdf5(validation_set, shuffle_training=False, **kwargs)\n\n            if test_set is not None:\n                test_set = load_hdf5(test_set, shuffle_training=False, **kwargs)\n\n        return training_set, test_set, validation_set, training_set_metadata\n\n\ndata_format_preprocessor_registry = {\n    **{fmt: DictPreprocessor for fmt in DICT_FORMATS},\n    **{fmt: DataFramePreprocessor for fmt in DATAFRAME_FORMATS},\n    **{fmt: CSVPreprocessor for fmt in CSV_FORMATS},\n    **{fmt: TSVPreprocessor for fmt in TSV_FORMATS},\n    **{fmt: JSONPreprocessor for fmt in JSON_FORMATS},\n    **{fmt: JSONLPreprocessor for fmt in JSONL_FORMATS},\n    **{fmt: ExcelPreprocessor for fmt in EXCEL_FORMATS},\n    **{fmt: ParquetPreprocessor for fmt in PARQUET_FORMATS},\n    **{fmt: PicklePreprocessor for fmt in PICKLE_FORMATS},\n    **{fmt: FWFPreprocessor for fmt in FWF_FORMATS},\n    **{fmt: FatherPreprocessor for fmt in FEATHER_FORMATS},\n    **{fmt: HTMLPreprocessor for fmt in HTML_FORMATS},\n    **{fmt: ORCPreprocessor for fmt in ORC_FORMATS},\n    **{fmt: SASPreprocessor for fmt in SAS_FORMATS},\n    **{fmt: SPSSPreprocessor for fmt in SPSS_FORMATS},\n    **{fmt: StataPreprocessor for fmt in STATA_FORMATS},\n    **{fmt: HDF5Preprocessor for fmt in HDF5_FORMATS},\n}\n\n\ndef build_dataset(\n    config,\n    dataset_df,\n    features,\n    global_preprocessing_parameters,\n    mode,\n    metadata=None,\n    backend=LOCAL_BACKEND,\n    random_seed=default_random_seed,\n    skip_save_processed_input=False,\n    callbacks=None,\n):\n    \"\"\"Builds a dataset from a dataframe and a list of features.\n\n    Args:\n        config: A dictionary containing the Ludwig model configuration\n        dataset_df: Pandas or Dask dataframe\n        features: List of features\n        global_preprocessing_parameters: Global preprocessing parameters\n        mode: One of ['training', 'prediction']\n        metadata: Training set metadata if available\n        backend: Backend\n        random_seed: Random seed\n        skip_save_processed_input: Whether to skip saving the processed input\n        callbacks: List of callbacks\n\n    Returns:\n        A tuple of (dataset, metadata)\n    \"\"\"\n\n    df_engine = backend.df_engine\n\n    if df_engine.partitioned:\n        if any(f[\"type\"] in REPARTITIONING_FEATURE_TYPES for f in features) and dataset_df.npartitions > 1:\n            # A globally unique index only matters if you know that there will be a repartition downstream for some\n            # particular feature, i.e. for Image and Audio features on a Ray backend.\n            # - There is a join operation in `df_like`, and the only way to do the operation is if the partitions across\n            #   all feature columns are aligned.\n            # - In order to align the partitions, we require a way of matching samples to one another across all\n            #   partitions. Therefore, we must reset_index to create a globally unique index.\n            # - If the number of partitions is 1, it is *highly likely* the index is globally unique. Auto-assigned\n            #   Dask indices in this case are unique, and we pd.concat train, val, and test sets with ignore_index=True\n            # If there will NOT be a repartition downstream, then we can skip this step.\n            # - In this case, the partitions should remain aligned throughout.\n            # - Further, while the indices might not be globally unique, they should be unique within each partition.\n            # - These two properties make it possible to do the join op within each partition without a global index.\n            logger.warning(\n                f\"Dataset has {dataset_df.npartitions} partitions and feature types that cause repartitioning. \"\n                f\"Resetting index to ensure globally unique indices.\"\n            )\n            dataset_df = df_engine.reset_index(dataset_df)\n\n    dataset_df = df_engine.parallelize(dataset_df)\n\n    # Ensure that column names with non-word characters won't cause problems for downstream operations.\n    # NOTE: Must be kept consistent with config sanitization in schema/model_types/base.py.\n    dataset_df = sanitize_column_names(dataset_df)\n\n    if mode == \"training\":\n        sample_ratio = global_preprocessing_parameters[\"sample_ratio\"]\n        sample_size = global_preprocessing_parameters[\"sample_size\"]\n        dataset_df = _get_sampled_dataset_df(dataset_df, df_engine, sample_ratio, sample_size, random_seed)\n\n    # If persisting DataFrames in memory is enabled, we want to do this after\n    # each batch of parallel ops in order to avoid redundant computation\n    dataset_df = df_engine.persist(dataset_df)\n\n    if mode == \"training\":\n        default_preprocessing_parameters = default_training_preprocessing_parameters\n    elif mode == \"prediction\":\n        default_preprocessing_parameters = default_prediction_preprocessing_parameters\n    else:\n        raise ValueError(f\"Invalid mode {mode}\")\n    global_preprocessing_parameters = merge_dict(default_preprocessing_parameters, global_preprocessing_parameters)\n\n    split_col = None\n    if global_preprocessing_parameters[\"split\"][\"type\"] == \"fixed\":\n        if global_preprocessing_parameters[\"split\"][\"column\"] in dataset_df.columns:\n            split_col = dataset_df[global_preprocessing_parameters[\"split\"][\"column\"]]\n        else:\n            logger.warning(\n                f\"Specified split column {global_preprocessing_parameters['split']['column']} for fixed \"\n                f\"split strategy was not found in dataset.\"  # noqa: E713\n            )\n\n    # update input features with prompt configs during preprocessing (as opposed to during the model forward pass)\n    # so that we can compute metadata and build the dataset correctly.\n    logger.debug(\"handle text features with prompt parameters\")\n    synthesized_dataset_cols = handle_features_with_prompt_config(\n        config, dataset_df, features, split_col=split_col, backend=backend\n    )\n\n    # Get all the unique preprocessing features to compute\n    feature_configs = []\n    feature_hashes = set()\n    for feature in features:\n        if feature[PROC_COLUMN] not in feature_hashes:\n            feature_configs.append(feature)\n            feature_hashes.add(feature[PROC_COLUMN])\n\n    dataset_cols = {}\n    for feature_config in feature_configs:\n        col_name = feature_config[COLUMN]\n        dataset_cols[col_name] = (\n            synthesized_dataset_cols[col_name] if col_name in synthesized_dataset_cols else dataset_df[col_name]\n        )\n\n    logger.debug(\"build preprocessing parameters\")\n    feature_name_to_preprocessing_parameters = build_preprocessing_parameters(\n        dataset_cols, feature_configs, global_preprocessing_parameters, backend, metadata=metadata\n    )\n\n    # Happens after preprocessing parameters are built, so we can use precomputed fill values.\n    logger.debug(\"handle missing values\")\n\n    # In some cases, there can be a (temporary) mismatch between the dtype of the column and the type expected by the\n    # preprocessing config (e.g., a categorical feature represented as an int-like column). In particular, Dask\n    # may raise an error even when there are no missing values in the column itself.\n    #\n    # Since we immediately cast all columns in accordance with their expected feature types after filling missing\n    # values, we work around the above issue by temporarily treating all columns as object dtype.\n    for col_key in dataset_cols:\n        dataset_cols[col_key] = dataset_cols[col_key].astype(object)\n\n    for feature_config in feature_configs:\n        preprocessing_parameters = feature_name_to_preprocessing_parameters[feature_config[NAME]]\n        handle_missing_values(dataset_cols, feature_config, preprocessing_parameters, backend)\n\n    # Happens after missing values are handled to avoid NaN casting issues.\n    logger.debug(\"cast columns\")\n    cast_columns(dataset_cols, feature_configs, backend)\n\n    for callback in callbacks or []:\n        callback.on_build_metadata_start(dataset_df, mode)\n\n    logger.debug(\"build metadata\")\n    metadata: TrainingSetMetadataDict = build_metadata(\n        config, metadata, feature_name_to_preprocessing_parameters, dataset_cols, feature_configs, backend\n    )\n\n    check_global_max_sequence_length_fits_prompt_template(metadata, global_preprocessing_parameters)\n\n    for callback in callbacks or []:\n        callback.on_build_metadata_end(dataset_df, mode)\n\n    for callback in callbacks or []:\n        callback.on_build_data_start(dataset_df, mode)\n\n    logger.debug(\"build data\")\n    proc_cols = build_data(dataset_cols, feature_configs, metadata, backend, skip_save_processed_input)\n\n    for callback in callbacks or []:\n        callback.on_build_data_end(dataset_df, mode)\n\n    # Get any additional columns needed for splitting downstream, otherwise they will not be\n    # included in the preprocessed output.\n    split_params = global_preprocessing_parameters.get(SPLIT, {})\n    if \"type\" not in split_params and SPLIT in dataset_df:\n        warnings.warn(\n            'Detected \"split\" column in the data, but using default split type '\n            '\"random\". Did you mean to set split type to \"fixed\"?'\n        )\n\n    splitter = get_splitter(**split_params)\n    for column in splitter.required_columns:\n        if column not in dataset_df:\n            warnings.warn(\n                f\"column: '{column}' is required by the dataset splitter with params: {split_params}, but '{column}' \"\n                f\"is not present in the `dataset_df` with columns: {dataset_df.columns}. This is acceptable during \"\n                \"serving setting where dataset splitting is irrelevant. You may see this warning if, for example, the \"\n                \"model was trained with a configuration that used a stratified split on the target column, but for \"\n                \"live predictions, a value for the target column is not to be provided.\"\n            )\n            continue\n        proc_cols[column] = dataset_df[column]\n\n    # TODO pyarrow: this is needed for caching to work with pyarrow. if removed, the following error is raised:\n    # \"pyarrow.lib.ArrowInvalid: Can only convert 1-dimensional array values\". The data is reshaped when loaded\n    # by the batcher in the RayDataset class (see _prepare_batch).\n    if not skip_save_processed_input and backend.cache.data_format == \"parquet\":\n        for feature in features:\n            name = feature[NAME]\n            proc_column = feature[PROC_COLUMN]\n            reshape = metadata[name].get(\"reshape\")\n            if reshape is not None:\n                proc_cols[proc_column] = backend.df_engine.map_objects(proc_cols[proc_column], lambda x: x.reshape(-1))\n\n    # Implements an outer join of proc_cols\n    dataset = backend.df_engine.df_like(dataset_df, proc_cols)\n\n    # At this point, there should be no missing values left in the dataframe, unless\n    # the DROP_ROW preprocessing option was selected, in which case we need to drop those\n    # rows.\n    len_dataset_before_drop_rows = len(dataset)\n    dataset = dataset.dropna()\n    len_dataset_after_drop_rows = len(dataset)\n\n    if len_dataset_before_drop_rows != len_dataset_after_drop_rows:\n        logger.warning(\n            f\"Dropped a total of {len_dataset_before_drop_rows - len_dataset_after_drop_rows} rows out of \"\n            f\"{len_dataset_before_drop_rows} due to missing values\"\n        )\n\n    # NaNs introduced by outer join change dtype of dataset cols (upcast to float64), so we need to cast them back.\n    col_name_to_dtype = {}\n    for col_name, col in proc_cols.items():\n        # if col is a list of list-like objects, we assume the internal dtype of each col[i] remains unchanged.\n        if isinstance(col, list) and isinstance(col[0], (list, np.ndarray, torch.Tensor)):\n            continue\n        dtype = col.dtype\n        # Skip non-numpy extension dtypes (e.g. TensorDtype from Ray, ArrowDtype from PyArrow)\n        # as they cannot be used with DataFrame.astype() reliably.\n        if not isinstance(dtype, np.dtype):\n            continue\n        col_name_to_dtype[col_name] = dtype\n    dataset = dataset.astype(col_name_to_dtype)\n\n    # Persist the completed dataset with no NaNs\n    dataset = backend.df_engine.persist(dataset)\n\n    # Remove partitions that are empty after removing NaNs\n    dataset = backend.df_engine.remove_empty_partitions(dataset)\n\n    # Embed features with fixed encoders\n    dataset = embed_fixed_features(dataset, feature_configs, metadata, backend)\n\n    return dataset, metadata\n\n\ndef embed_fixed_features(\n    dataset: DataFrame, feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict, backend: Backend\n) -> DataFrame:\n    \"\"\"Transforms every input feature with cacheable encoder embeddings into its encoded form and updates\n    metadata.\"\"\"\n    # Encode features in bulk at the end\n    features_to_encode = get_features_with_cacheable_fixed_embeddings(feature_configs, metadata)\n    if not features_to_encode:\n        return dataset\n\n    logger.info(f\"Cache encoder embeddings for features: {[f[NAME] for f in features_to_encode]}\")\n    for feature in features_to_encode:\n        # Temporarily set to False to ensure proper encoding\n        metadata[feature[NAME]][PREPROCESSING][\"cache_encoder_embeddings\"] = False\n\n    batch_size = backend.tune_batch_size(create_embed_batch_size_evaluator(features_to_encode, metadata), len(dataset))\n    transform_fn = create_embed_transform_fn(features_to_encode, metadata)\n    results = backend.batch_transform(dataset, batch_size, transform_fn, name=\"Caching encoder embeddings\")\n\n    for feature in features_to_encode:\n        # Set metadata so we know to skip encoding the feature\n        metadata[feature[NAME]][PREPROCESSING][\"cache_encoder_embeddings\"] = True\n\n    return results\n\n\ndef _get_sampled_dataset_df(dataset_df, df_engine, sample_ratio, sample_size, random_seed):\n    df_len = len(dataset_df)\n    if sample_ratio < 1.0:\n        if not df_engine.partitioned and df_len * sample_ratio < 1:\n            raise ValueError(\n                f\"sample_ratio {sample_ratio} is too small for dataset of length {df_len}. \"\n                f\"Please increase sample_ratio or use a larger dataset.\"\n            )\n\n        logger.debug(f\"sample {sample_ratio} of data\")\n        dataset_df = dataset_df.sample(frac=sample_ratio, random_state=random_seed)\n\n    if sample_size:\n        if sample_size < df_len:\n            # Cannot use 'n' parameter when using dask DataFrames -- only 'frac' is supported\n            sample_ratio = sample_size / df_len\n            dataset_df = dataset_df.sample(frac=sample_ratio, random_state=random_seed)\n        else:\n            logger.warning(\"sample_size is larger than dataset size, ignoring sample_size\")\n\n    return dataset_df\n\n\ndef get_features_with_cacheable_fixed_embeddings(\n    feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict\n) -> list[FeatureConfigDict]:\n    \"\"\"Returns list of features with `cache_encoder_embeddings=True` set in the preprocessing config.\"\"\"\n    features_to_encode = []\n    for feature_config in feature_configs:\n        # deal with encoders that have fixed preprocessing\n        if ENCODER in feature_config:\n            encoder_params = feature_config[ENCODER]\n            if TYPE in encoder_params:\n                preprocessing = metadata[feature_config[NAME]][PREPROCESSING]\n                if preprocessing.get(\"cache_encoder_embeddings\"):\n                    # TODO(travis): passing in MODEL_ECD is a hack here that can be removed once we move to using\n                    # the config object everywhere in preprocessing. Then we won't need to do the lookup on the\n                    # encoder schema at all.\n                    encoder_class = get_encoder_cls(MODEL_ECD, feature_config[TYPE], encoder_params[TYPE])\n                    encoder = encoder_class.from_dict(encoder_params)\n                    if not encoder.can_cache_embeddings():\n                        raise ValueError(\n                            f\"Set `cache_encoder_embeddings=True` for feature {feature_config[NAME]} with \"\n                            f\"encoder {encoder_params[TYPE]}, but encoder embeddings are not static.\"\n                        )\n\n                    # Convert to Ray Datasets, map batches to encode, then convert back to Dask\n                    features_to_encode.append(feature_config)\n\n    return features_to_encode\n\n\ndef cast_columns(dataset_cols, features, backend) -> None:\n    \"\"\"Casts columns based on their feature type.\"\"\"\n    for feature in features:\n        # todo figure out if additional parameters are needed\n        #  for the cast_column function\n        try:\n            dataset_cols[feature[COLUMN]] = get_from_registry(feature[TYPE], get_base_type_registry()).cast_column(\n                dataset_cols[feature[COLUMN]], backend\n            )\n        except KeyError as e:\n            raise KeyError(\n                f\"Feature name {e} specified in the config was not found in dataset with columns: \"  # noqa: E713\n                + f\"{list(dataset_cols.keys())}\"\n            )\n\n\ndef merge_preprocessing(\n    feature_config: FeatureConfigDict, global_preprocessing_parameters: PreprocessingConfigDict\n) -> FeatureConfigDict:\n    if PREPROCESSING not in feature_config:\n        return global_preprocessing_parameters[feature_config[TYPE]]\n\n    return merge_dict(global_preprocessing_parameters[feature_config[TYPE]], feature_config[PREPROCESSING])\n\n\ndef build_preprocessing_parameters(\n    dataset_cols: dict[str, Series],\n    feature_configs: list[FeatureConfigDict],\n    global_preprocessing_parameters: PreprocessingConfigDict,\n    backend: Backend,\n    metadata: TrainingSetMetadataDict | None = None,\n) -> PreprocessingConfigDict:\n    if metadata is None:\n        metadata = {}\n\n    feature_name_to_preprocessing_parameters = {}\n    for feature_config in feature_configs:\n        feature_name = feature_config[NAME]\n\n        # if metadata already exists, we can use it to get preprocessing parameters\n        if feature_name in metadata:\n            feature_name_to_preprocessing_parameters[feature_name] = metadata[feature_name][PREPROCESSING]\n            continue\n\n        preprocessing_parameters = feature_config[PREPROCESSING]\n        missing_value_strategy = preprocessing_parameters[\"missing_value_strategy\"]\n        fill_value = precompute_fill_value(\n            dataset_cols, feature_config, missing_value_strategy, preprocessing_parameters, backend\n        )\n        if fill_value is not None:\n            preprocessing_parameters.update({\"computed_fill_value\": fill_value})\n\n        # Handle outlier replacement\n        outlier_strategy = preprocessing_parameters.get(\"outlier_strategy\")\n        if outlier_strategy is not None:\n            if outlier_strategy != missing_value_strategy:\n                outlier_fill_value = precompute_fill_value(\n                    dataset_cols, feature_config, outlier_strategy, preprocessing_parameters, backend\n                )\n            else:\n                # Use fill value from missing_value_strategy to avoid redundant computation\n                outlier_fill_value = fill_value\n\n            if outlier_fill_value is not None:\n                preprocessing_parameters.update({\"computed_outlier_fill_value\": outlier_fill_value})\n\n        feature_name_to_preprocessing_parameters[feature_name] = preprocessing_parameters\n\n    return feature_name_to_preprocessing_parameters\n\n\ndef is_input_feature(feature_config: FeatureConfigDict) -> bool:\n    \"\"\"Utility function to check for the presence of encoder in the feature config to determine if the feature is\n    an input feature or output feature.\"\"\"\n    return ENCODER in feature_config\n\n\ndef build_metadata(\n    config: ModelConfigDict,\n    metadata: TrainingSetMetadataDict,\n    feature_name_to_preprocessing_parameters: dict[str, PreprocessingConfigDict],\n    dataset_cols: dict[str, Series],\n    feature_configs: list[FeatureConfigDict],\n    backend: Backend,\n) -> TrainingSetMetadataDict:\n    for feature_config in feature_configs:\n        feature_name = feature_config[NAME]\n        if feature_name in metadata:\n            continue\n\n        preprocessing_parameters = feature_name_to_preprocessing_parameters[feature_name]\n\n        column = dataset_cols[feature_config[COLUMN]]\n        metadata[feature_name] = get_from_registry(feature_config[TYPE], get_base_type_registry()).get_feature_meta(\n            config, column, preprocessing_parameters, backend, is_input_feature(feature_config)\n        )\n\n        metadata[feature_name][PREPROCESSING] = preprocessing_parameters\n\n    return metadata\n\n\ndef build_data(\n    input_cols: DataFrame,\n    feature_configs: list[dict],\n    training_set_metadata: dict,\n    backend: Backend,\n    skip_save_processed_input: bool,\n) -> dict[str, DataFrame]:\n    \"\"\"Preprocesses the input dataframe columns, handles missing values, and potentially adds metadata to\n    training_set_metadata.\n\n    Args:\n        input_cols: Input dataframe to be processed.\n        feature_configs: List of feature configs.\n        training_set_metadata: Training set metadata. Additional fields may be added.\n        backend: Backend for data processing.\n        skip_save_processed_input: (bool) Whether to skip saving the processed input.\n\n    Returns:\n        Dictionary of (feature name) -> (processed data).\n    \"\"\"\n    proc_cols = {}\n    for feature_config in feature_configs:\n        # TODO(travis): instead of using raw dictionary, this should be loaded into a proper PreprocessingConfig\n        #  object, so we don't need to hackily check for the presence of added keys.\n        preprocessing_parameters = training_set_metadata[feature_config[NAME]][PREPROCESSING]\n\n        # Need to run this again here as cast_columns may have introduced new missing values\n        handle_missing_values(input_cols, feature_config, preprocessing_parameters, backend)\n\n        # For features that support it, we perform outlier removal here using metadata computed on the full dataset\n        handle_outliers(\n            input_cols, feature_config, preprocessing_parameters, training_set_metadata[feature_config[NAME]], backend\n        )\n\n        get_from_registry(feature_config[TYPE], get_base_type_registry()).add_feature_data(\n            feature_config,\n            input_cols,\n            proc_cols,\n            training_set_metadata,\n            preprocessing_parameters,\n            backend,\n            skip_save_processed_input,\n        )\n\n    return proc_cols\n\n\ndef balance_data(\n    dataset_df: DataFrame,\n    output_features: list[dict],\n    preprocessing_parameters: dict,\n    backend: Backend,\n    random_seed: int,\n):\n    \"\"\"The purpose of this function is to balance the training dataset using either over-sampling or under-\n    sampling.\n\n    Args:\n        dataset_df: Input dataframe to be over-sampled or under-sampled.\n        output_features: List of feature configs.\n        preprocessing_parameters: Dictionary of the global preprocessing parameters.\n        backend: Backend for data processing.\n        random_seed: Integer to seed the random sampling to ensure determinism.\n\n    Returns: An over-sampled or under-sampled training dataset.\n    \"\"\"\n    target = output_features[0][PROC_COLUMN]\n\n    if backend.df_engine.partitioned:\n        majority_class = backend.df_engine.compute(dataset_df[target].value_counts()).idxmax()\n        minority_class = backend.df_engine.compute(dataset_df[target].value_counts()).idxmin()\n    else:\n        majority_class = dataset_df[target].value_counts().idxmax()\n        minority_class = dataset_df[target].value_counts().idxmin()\n    majority_df = dataset_df[dataset_df[target] == majority_class]\n    minority_df = dataset_df[dataset_df[target] == minority_class]\n\n    if preprocessing_parameters[\"oversample_minority\"]:\n        sample_fraction = (len(majority_df) * preprocessing_parameters[\"oversample_minority\"]) / len(minority_df)\n        minority_df = minority_df.sample(frac=sample_fraction, replace=True, random_state=random_seed)\n    elif preprocessing_parameters[\"undersample_majority\"]:\n        sample_fraction = int(len(minority_df) / preprocessing_parameters[\"undersample_majority\"]) / len(majority_df)\n        majority_df = majority_df.sample(frac=sample_fraction, replace=False, random_state=random_seed)\n\n    balanced_df = backend.df_engine.concat([minority_df, majority_df])\n\n    return balanced_df\n\n\ndef precompute_fill_value(\n    dataset_cols, feature, missing_value_strategy: str, preprocessing_parameters: PreprocessingConfigDict, backend\n):\n    \"\"\"Precomputes the fill value for a feature.\n\n    NOTE: this is called before NaNs are removed from the dataset. Modifications here must handle NaNs gracefully.\n    NOTE: this is called before columns are cast. Modifications here must handle dtype conversion gracefully.\n    \"\"\"\n    if missing_value_strategy == FILL_WITH_CONST:\n        return preprocessing_parameters[\"fill_value\"]\n    elif missing_value_strategy == FILL_WITH_MODE:\n        # Requires separate handling if Dask since Dask has lazy evaluation\n        # Otherwise, dask returns a Dask index structure instead of a value to use as a fill value\n        return (\n            dataset_cols[feature[COLUMN]].value_counts().index.compute()[0]\n            if is_dask_series_or_df(dataset_cols[feature[COLUMN]], backend)\n            else dataset_cols[feature[COLUMN]].value_counts().index[0]\n        )\n    elif missing_value_strategy == FILL_WITH_MEAN:\n        if feature[TYPE] != NUMBER:\n            raise ValueError(\n                f\"Filling missing values with mean is supported \"\n                f\"only for number types, not for type {feature[TYPE]}.\",\n            )\n        return backend.df_engine.compute(dataset_cols[feature[COLUMN]].astype(float).mean())\n    elif missing_value_strategy in {FILL_WITH_FALSE, FILL_WITH_TRUE}:\n        distinct_values = backend.df_engine.compute(\n            dataset_cols[feature[COLUMN]].drop_duplicates().dropna()\n        ).values.tolist()\n        if len(distinct_values) > 2:\n            raise ValueError(\n                f\"Missing value strategy `{missing_value_strategy}` \"\n                f\"for column {feature[COLUMN]} expects 2 distinct values, \"\n                f\"found: {len(distinct_values)} (ex: {distinct_values[:10]})\"\n            )\n\n        fill_to_bool_value = {FILL_WITH_FALSE: False, FILL_WITH_TRUE: True}\n        bool_needed = fill_to_bool_value[missing_value_strategy]\n\n        # Determine the False label.\n        # Distinct values are sorted in reverse to mirror the selection of the default fallback_true_label (in\n        # binary_feature.get_feature_meta) for binary columns with unconventional boolean values, \"human\"/\"bot\".\n        for v in sorted(distinct_values, reverse=True):\n            fallback_true_label = (\n                preprocessing_parameters[\"fallback_true_label\"]\n                # By default, preprocessing_parameters.fallback_true_label is None.\n                if preprocessing_parameters[\"fallback_true_label\"]\n                else \"true\"\n            )\n            if strings_utils.str2bool(v, fallback_true_label) is bool_needed:\n                return v\n        raise ValueError(\n            f\"Unable to determine {bool_needed} value for column {feature[COLUMN]} \"\n            f\"with distinct values: {distinct_values}.\"\n        )\n    # Otherwise, we cannot precompute the fill value for this dataset\n    return None\n\n\n@DeveloperAPI\ndef handle_missing_values(dataset_cols, feature, preprocessing_parameters: PreprocessingConfigDict, backend):\n    missing_value_strategy = preprocessing_parameters[\"missing_value_strategy\"]\n    computed_fill_value = preprocessing_parameters.get(\"computed_fill_value\")\n    _handle_missing_values(dataset_cols, feature, missing_value_strategy, computed_fill_value, backend)\n\n\n@DeveloperAPI\ndef handle_outliers(dataset_cols, feature, preprocessing_parameters: PreprocessingConfigDict, metadata, backend):\n    outlier_strategy = preprocessing_parameters.get(\"outlier_strategy\")\n    if outlier_strategy is None:\n        return\n\n    outlier_threshold = preprocessing_parameters[\"outlier_threshold\"]\n    computed_fill_value = preprocessing_parameters.get(\"computed_outlier_fill_value\")\n\n    # Identify all outliers and set them to NA so they can be removed\n    series = dataset_cols[feature[COLUMN]]\n    dataset_cols[feature[COLUMN]] = series.mask(\n        series.sub(metadata[\"mean\"]).div(metadata[\"std\"]).abs().gt(outlier_threshold)\n    )\n\n    _handle_missing_values(dataset_cols, feature, outlier_strategy, computed_fill_value, backend)\n\n\ndef _handle_missing_values(\n    dataset_cols, feature, missing_value_strategy: str, computed_fill_value: float | None, backend\n):\n    if (\n        missing_value_strategy in {FILL_WITH_CONST, FILL_WITH_MODE, FILL_WITH_MEAN, FILL_WITH_FALSE, FILL_WITH_TRUE}\n        and computed_fill_value is not None\n    ):\n        dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].fillna(\n            computed_fill_value,\n        )\n    elif missing_value_strategy in {BFILL, FFILL}:\n        if missing_value_strategy == BFILL:\n            dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].bfill()\n        else:\n            dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].ffill()\n\n        # 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\n        # applied. This causes downstream errors with Dask (https://github.com/ludwig-ai/ludwig/issues/2452)\n        # To get around this issue, apply the primary missing value strategy (say bfill) first, and then follow it\n        # up with the other missing value strategy (ffill) to ensure all NaNs are filled\n        if backend.df_engine.compute(dataset_cols[feature[COLUMN]].isna().sum()) > 0:\n            if missing_value_strategy == FFILL:\n                dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].bfill()\n            else:\n                dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].ffill()\n    elif missing_value_strategy == DROP_ROW:\n        # Here we only drop from this series, but after preprocessing we'll do a second\n        # round of dropping NA values from the entire output dataframe, which will\n        # result in the removal of the rows.\n        len_before_dropped_rows = len(dataset_cols[feature[COLUMN]])\n        dataset_cols[feature[COLUMN]] = dataset_cols[feature[COLUMN]].dropna()\n        len_after_dropped_rows = len(dataset_cols[feature[COLUMN]])\n\n        if len_before_dropped_rows != len_after_dropped_rows:\n            logger.warning(\n                f\"DROP_ROW missing value strategy applied. Dropped {len_before_dropped_rows - len_after_dropped_rows} \"\n                f\"samples out of {len_before_dropped_rows} from column {feature[COLUMN]}. The rows containing these \"\n                f\"samples will ultimately be dropped from the dataset.\"\n            )\n    else:\n        raise ValueError(f\"Invalid missing value strategy {missing_value_strategy}\")\n\n\ndef handle_features_with_prompt_config(\n    config: ModelConfigDict,\n    dataset_df: DataFrame,\n    features: list[FeatureConfigDict],\n    backend: Backend,\n    split_col: Series | None = None,\n) -> dict[str, Series]:\n    \"\"\"Updates (in-place) dataset columns with prompt configurations containing a non-None task parameter.\n\n    Dataset columns that are updated here are enriched to have prompts as specified by the prompt configuration.\n\n    Args:\n        config: Model configuration.\n        dataset_df (DataFrame): Input dataset.\n        features (List[FeatureConfigDict]): List of feature configurations.\n        df_engine (DataFrameEngine): Dataframe engine.\n        split_col (Optional[Series], optional): Split column. Defaults to None.\n\n    Returns:\n        Dict[str, Series]: Modified dataset columns.\n    \"\"\"\n    dataset_cols = {}\n    input_features, output_features = get_input_and_output_features(features)\n    for input_feature_config in input_features:\n        prompt_config = _get_prompt_config(config, input_feature_config)\n        if prompt_config is None:\n            continue\n\n        input_col_name = input_feature_config[COLUMN]\n        if prompt_config[\"retrieval\"][\"type\"] is not None:\n            # Ensure that the output features are in the dataset columns saved as part of the index\n            # so that they can be retrieved later at lookup time.\n            output_feature_col_names = [output_feature_config[COLUMN] for output_feature_config in output_features]\n            input_and_output_col_names = set([input_col_name] + output_feature_col_names)\n            input_and_output_cols = {\n                feature[NAME]: dataset_df[feature[COLUMN]]\n                for feature in features\n                if feature[NAME] in input_and_output_col_names\n            }\n            retrieval_model, index_name = index_column(\n                prompt_config[\"retrieval\"],\n                col_name=input_col_name,\n                dataset_cols=input_and_output_cols,\n                backend=backend,\n                split_col=split_col,\n            )\n            k = prompt_config[\"retrieval\"][\"k\"]\n\n            # NOTE: after indexing the input column, we update the index_name in the prompt config IN PLACE.\n            # This ensures that the preprocessing parameters for this feature have an up-to-date index_name\n            # when the training set metadata is saved.\n            prompt_config[\"retrieval\"][\"index_name\"] = index_name\n        else:\n            retrieval_model = None\n            k = -1\n\n        dataset_cols[input_col_name] = format_input_with_prompt(\n            input_col_name,\n            dataset_df,\n            backend,\n            prompt_config[\"task\"],\n            retrieval_model=retrieval_model,\n            k=k,\n            template=prompt_config[\"template\"],\n        )\n\n    return dataset_cols\n\n\ndef _get_prompt_config(config: ModelConfigDict, input_feature_config: dict) -> dict:\n    if input_feature_config[TYPE] != TEXT:\n        # Prompt config is only applied to text features\n        return None\n\n    preprocessing = input_feature_config[\"preprocessing\"]\n    if _has_prompt_section(preprocessing):\n        return preprocessing[\"prompt\"]\n\n    if _has_prompt_section(config):\n        return config[\"prompt\"]\n\n    return None\n\n\ndef _has_prompt_section(config: dict) -> bool:\n    return \"prompt\" in config and (config[\"prompt\"][\"template\"] is not None or config[\"prompt\"][\"task\"] is not None)\n\n\ndef load_hdf5(hdf5_file_path, preprocessing_params, backend, split_data=True, shuffle_training=False):\n    # TODO dask: this needs to work with DataFrames\n    logger.info(f\"Loading data from: {hdf5_file_path}\")\n\n    def shuffle(df):\n        return df.sample(frac=1).reset_index(drop=True)\n\n    dataset = data_utils.load_hdf5(hdf5_file_path)\n    if not split_data:\n        if shuffle_training:\n            dataset = shuffle(dataset)\n        return dataset\n\n    training_set, validation_set, test_set = split_dataset(dataset, preprocessing_params, backend)\n\n    if shuffle_training:\n        training_set = shuffle(training_set)\n\n    return training_set, test_set, validation_set\n\n\ndef load_metadata(metadata_file_path: str) -> TrainingSetMetadataDict:\n    logger.info(f\"Loading metadata from: {metadata_file_path}\")\n    training_set_metadata = data_utils.load_json(metadata_file_path)\n    # TODO(travis): decouple config from training_set_metadata so we don't need to\n    #  upgrade it over time.\n    training_set_metadata = upgrade_metadata(training_set_metadata)\n    return training_set_metadata\n\n\ndef drop_extra_cols(features, dfs):\n    retain_cols = list({feature[PROC_COLUMN]: True for feature in features}.keys())\n    return tuple(df[retain_cols] if df is not None else df for df in dfs)\n\n\ndef preprocess_for_training(\n    config,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    training_set_metadata=None,\n    data_format=None,\n    skip_save_processed_input=False,\n    preprocessing_params=default_training_preprocessing_parameters,\n    backend=LOCAL_BACKEND,\n    random_seed=default_random_seed,\n    callbacks=None,\n) -> tuple[Dataset, Dataset, Dataset, TrainingSetMetadataDict]:\n    \"\"\"Returns training, val and test datasets with training set metadata.\"\"\"\n\n    # sanity check to make sure some data source is provided\n    if dataset is None and training_set is None:\n        raise ValueError(\"No training data is provided!\")\n\n    # preload ludwig and HF datasets\n    dataset, training_set, validation_set, test_set = load_dataset_uris(\n        dataset, training_set, validation_set, test_set, backend\n    )\n\n    # determine data format if not provided or auto\n    if not data_format or data_format == \"auto\":\n        data_format = figure_data_format(dataset, training_set, validation_set, test_set)\n\n    # Wrap dataset into a form we can use to manage within the cache\n    dataset = wrap(dataset)\n    training_set = wrap(training_set)\n    validation_set = wrap(validation_set)\n    test_set = wrap(test_set)\n\n    try:\n        lock_path = backend.cache.get_cache_directory(dataset)\n    except (TypeError, ValueError):\n        lock_path = None\n    with file_lock(lock_path, lock_file=\".lock_preprocessing\"):\n        # if training_set_metadata is a string, assume it's a path to load the json\n        training_set_metadata = training_set_metadata or {}\n        if training_set_metadata and isinstance(training_set_metadata, str):\n            training_set_metadata = load_metadata(training_set_metadata)\n\n        # setup\n        features = config[\"input_features\"] + config[\"output_features\"]\n\n        # in case data_format is one of the cacheable formats,\n        # check if there's a cached hdf5 file with the same name,\n        # and in case move on with the hdf5 branch.\n        cached = False\n        cache = backend.cache.get_dataset_cache(config, dataset, training_set, test_set, validation_set)\n\n        # Unwrap dataset into the form used for preprocessing\n        dataset = dataset.unwrap() if dataset is not None else None\n        training_set = training_set.unwrap() if training_set is not None else None\n        validation_set = validation_set.unwrap() if validation_set is not None else None\n        test_set = test_set.unwrap() if test_set is not None else None\n\n        if data_format in CACHEABLE_FORMATS:\n            with backend.storage.cache.use_credentials():\n                # cache.get() returns valid indicating if the checksum for the current config\n                # is equal to that from the cached training set metadata, as well as the paths to the\n                # cached training set metadata, training set, validation_set, test set\n                cache_results = cache.get()\n                if cache_results is not None:\n                    valid, *cache_values = cache_results\n                    if valid:\n                        logger.info(_get_cache_hit_message(cache))\n                        training_set_metadata, training_set, test_set, validation_set = cache_values\n                        config[\"data_hdf5_fp\"] = training_set\n                        data_format = backend.cache.data_format\n                        cached = True\n                        dataset = None\n                    else:\n                        logger.info(\n                            \"Found cached dataset and meta.json with the same filename \"\n                            \"of the dataset, but checksums don't match, \"\n                            \"if saving of processed input is not skipped \"\n                            \"they will be overridden\"\n                        )\n                        cache.delete()\n                else:\n                    logger.info(\n                        f\"No cached dataset found at {cache.get_cached_obj_path('training')}. \"\n                        \"Preprocessing the dataset.\"\n                    )\n\n        training_set_metadata[CHECKSUM] = cache.checksum\n        data_format_processor = get_from_registry(data_format, data_format_preprocessor_registry)\n\n        if cached or data_format == \"hdf5\":\n            with backend.storage.cache.use_credentials():\n                # Always interpret hdf5 files as preprocessed, even if missing from the cache\n                processed = data_format_processor.prepare_processed_data(\n                    features,\n                    dataset=dataset,\n                    training_set=training_set,\n                    validation_set=validation_set,\n                    test_set=test_set,\n                    training_set_metadata=training_set_metadata,\n                    skip_save_processed_input=skip_save_processed_input,\n                    preprocessing_params=preprocessing_params,\n                    backend=backend,\n                    random_seed=random_seed,\n                )\n                training_set, test_set, validation_set, training_set_metadata = processed\n        else:\n            processed = data_format_processor.preprocess_for_training(\n                config,\n                features,\n                dataset=dataset,\n                training_set=training_set,\n                validation_set=validation_set,\n                test_set=test_set,\n                training_set_metadata=training_set_metadata,\n                skip_save_processed_input=skip_save_processed_input,\n                preprocessing_params=preprocessing_params,\n                backend=backend,\n                random_seed=random_seed,\n                callbacks=callbacks,\n            )\n            training_set, test_set, validation_set, training_set_metadata = processed\n            processed = (training_set, test_set, validation_set, training_set_metadata)\n\n            # cache the dataset\n            if backend.cache.can_cache(skip_save_processed_input):\n                with backend.storage.cache.use_credentials():\n                    logger.debug(\"cache processed data\")\n                    processed = cache.put(*processed)\n                    # set cached=True to ensure credentials are used correctly below\n                    cached = True\n            training_set, test_set, validation_set, training_set_metadata = processed\n\n        with backend.storage.cache.use_credentials() if cached else contextlib.nullcontext():\n            logger.debug(\"create training dataset\")\n            training_dataset = backend.dataset_manager.create(training_set, config, training_set_metadata)\n            training_set_size = len(training_dataset)\n            if training_set_size == 0:\n                raise ValueError(\"Training data is empty following preprocessing.\")\n            elif training_set_size < MIN_DATASET_SPLIT_ROWS:\n                raise ValueError(\n                    f\"Training dataset has only {training_set_size} rows following preprocessing, need\"\n                    f\" at least {MIN_DATASET_SPLIT_ROWS} to compute metrics.\"\n                )\n\n            validation_dataset = None\n            if validation_set is not None:\n                logger.debug(\"create validation dataset\")\n                validation_dataset = backend.dataset_manager.create(validation_set, config, training_set_metadata)\n                validation_set_size = len(validation_dataset)\n                if validation_set_size == 0:\n                    logger.warning(\n                        \"Validation set empty. If this is unintentional, please check the preprocessing configuration.\"\n                    )\n                    validation_dataset = None\n                elif validation_set_size < MIN_DATASET_SPLIT_ROWS:\n                    logger.warning(\n                        f\"Validation set too small to compute metrics. Need at least {MIN_DATASET_SPLIT_ROWS} rows, got\"\n                        f\" {validation_set_size} after preprocessing.\"\n                    )\n\n            test_dataset = None\n            if test_set is not None:\n                logger.debug(\"create test dataset\")\n                test_dataset = backend.dataset_manager.create(test_set, config, training_set_metadata)\n                test_set_size = len(test_dataset)\n                if test_set_size == 0:\n                    logger.warning(\n                        \"Test set empty. If this is unintentional, please check the preprocessing configuration.\"\n                    )\n                    test_dataset = None\n                elif test_set_size < MIN_DATASET_SPLIT_ROWS:\n                    logger.warning(\n                        f\"Test set too small to compute metrics. Need at least {MIN_DATASET_SPLIT_ROWS} rows, got\"\n                        f\" {test_set_size} after preprocessing.\"\n                    )\n\n        return (training_dataset, validation_dataset, test_dataset, training_set_metadata)\n\n\ndef _preprocess_file_for_training(\n    config,\n    features,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    training_set_metadata=None,\n    read_fn=read_csv,\n    skip_save_processed_input=False,\n    preprocessing_params=default_training_preprocessing_parameters,\n    backend=LOCAL_BACKEND,\n    random_seed=default_random_seed,\n    callbacks=None,\n):\n    \"\"\"Method to pre-process csv data.\n\n    :param features: list of all features (input + output)\n    :param dataset: path to the data\n    :param training_set: training data\n    :param validation_set: validation data\n    :param test_set: test data\n    :param training_set_metadata: train set metadata\n    :param skip_save_processed_input: if False, the pre-processed data is saved as .hdf5 files in the same location as\n        the csv files with the same names.\n    :param preprocessing_params: preprocessing parameters\n    :param random_seed: random seed\n    :return: training, test, validation datasets, training metadata\n    \"\"\"\n    if dataset:\n        # Use data and ignore _train, _validation and _test.\n        # Also ignore data and train set metadata needs preprocessing\n        logger.info(\"Using full raw dataset, no hdf5 and json file \" \"with the same name have been found\")\n        logger.info(\"Building dataset (it may take a while)\")\n\n        dataset_df = read_fn(dataset, backend.df_engine.df_lib)\n        training_set_metadata[SRC] = dataset\n\n        data, training_set_metadata = build_dataset(\n            config,\n            dataset_df,\n            features,\n            preprocessing_params,\n            mode=\"training\",\n            metadata=training_set_metadata,\n            backend=backend,\n            random_seed=random_seed,\n            skip_save_processed_input=skip_save_processed_input,\n            callbacks=callbacks,\n        )\n\n    elif training_set:\n        # use data_train (including _validation and _test if they are present)\n        # and ignore data and train set metadata\n        # needs preprocessing\n        logger.info(\"Using training raw csv, no hdf5 and json \" \"file with the same name have been found\")\n        logger.info(\"Building dataset (it may take a while)\")\n\n        concatenated_df = concatenate_files(training_set, validation_set, test_set, read_fn, backend)\n        training_set_metadata[SRC] = training_set\n\n        # Data is pre-split.\n        preprocessing_params = set_fixed_split(preprocessing_params)\n\n        data, training_set_metadata = build_dataset(\n            config,\n            concatenated_df,\n            features,\n            preprocessing_params,\n            mode=\"training\",\n            metadata=training_set_metadata,\n            backend=backend,\n            random_seed=random_seed,\n            callbacks=callbacks,\n        )\n\n    else:\n        raise ValueError(\"either data or data_train have to be not None\")\n\n    logger.debug(\"split train-val-test\")\n    training_data, validation_data, test_data = drop_extra_cols(\n        features, split_dataset(data, preprocessing_params, backend, random_seed)\n    )\n\n    if dataset and backend.is_coordinator() and not skip_save_processed_input:\n        logger.debug(\"writing split file\")\n        splits_df = concatenate_splits(training_data, validation_data, test_data, backend)\n        split_fp = get_split_path(dataset or training_set)\n        try:\n            backend.df_engine.to_parquet(splits_df, split_fp, index=True)\n        except Exception as e:\n            logger.warning(\n                f\"Encountered error: '{e}' while writing data to parquet during saving preprocessed data. \"\n                \"Skipping saving processed data.\"\n            )\n\n    logger.info(\"Building dataset: DONE\")\n    if preprocessing_params[\"oversample_minority\"] or preprocessing_params[\"undersample_majority\"]:\n        training_data = balance_data(\n            training_data, config[\"output_features\"], preprocessing_params, backend, random_seed\n        )\n\n    return training_data, test_data, validation_data, training_set_metadata\n\n\ndef _preprocess_df_for_training(\n    config,\n    features,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    training_set_metadata=None,\n    preprocessing_params=default_training_preprocessing_parameters,\n    backend=LOCAL_BACKEND,\n    random_seed=default_random_seed,\n    callbacks=None,\n):\n    \"\"\"Method to pre-process dataframes.\n\n    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\n    be processed in memory\n    \"\"\"\n    if dataset is not None:\n        # needs preprocessing\n        logger.info(\"Using full dataframe\")\n    elif training_set is not None:\n        # needs preprocessing\n        logger.info(\"Using training dataframe\")\n        dataset = concatenate_df(training_set, validation_set, test_set, backend)\n\n        # Data is pre-split.\n        preprocessing_params = set_fixed_split(preprocessing_params)\n\n    logger.info(\"Building dataset (it may take a while)\")\n\n    data, training_set_metadata = build_dataset(\n        config,\n        dataset,\n        features,\n        preprocessing_params,\n        mode=\"training\",\n        metadata=training_set_metadata,\n        random_seed=random_seed,\n        backend=backend,\n        callbacks=callbacks,\n    )\n\n    logger.debug(\"split train-val-test\")\n    training_set, validation_set, test_set = drop_extra_cols(\n        features, split_dataset(data, preprocessing_params, backend, random_seed)\n    )\n\n    logger.info(\"Building dataset: DONE\")\n    if preprocessing_params[\"oversample_minority\"] or preprocessing_params[\"undersample_majority\"]:\n        training_set = balance_data(training_set, config[\"output_features\"], preprocessing_params, backend, random_seed)\n\n    return training_set, test_set, validation_set, training_set_metadata\n\n\ndef preprocess_for_prediction(\n    config,\n    dataset,\n    training_set_metadata=None,\n    data_format=None,\n    split=FULL,\n    include_outputs=True,\n    backend=LOCAL_BACKEND,\n    callbacks=None,\n):\n    \"\"\"Preprocesses the dataset to parse it into a format that is usable by the Ludwig core.\n\n    Args:\n        config: Config dictionary corresponding to Ludwig Model\n        dataset: Dataset to be processed\n        training_set_metadata: Train set metadata for the input features\n        data_format: Format of the data\n        split: The split of dataset to return\n        include_outputs: Whether to include outputs\n        backend: Type of backend to use for preprocessing\n        callbacks: Any callbacks passed in\n\n    Returns:\n        Processed dataset along with updated training set metadata\n    \"\"\"\n    # Sanity Check to make sure some data source is provided\n    if dataset is None:\n        raise ValueError(\"No training data is provided!\")\n\n    if isinstance(dataset, Dataset):\n        return dataset, training_set_metadata\n\n    # preload ludwig and HF datasets\n    dataset, _, _, _ = load_dataset_uris(dataset, None, None, None, backend)\n\n    # determine data format if not provided or auto\n    if not data_format or data_format == \"auto\":\n        data_format = figure_data_format(dataset)\n\n    # manage the in_memory parameter\n    if data_format not in HDF5_FORMATS:\n        num_overrides = override_in_memory_flag(config[\"input_features\"], True)\n        if num_overrides > 0:\n            logger.warning(\"Using in_memory = False is not supported \" \"with {} data format.\".format(data_format))\n\n    preprocessing_params = {}\n    config_defaults = config.get(DEFAULTS, {})\n    for feature_type in config_defaults:\n        preprocessing_params[feature_type] = config_defaults[feature_type].get(PREPROCESSING, {})\n    preprocessing_params[SPLIT] = config.get(PREPROCESSING, {}).get(SPLIT, {})\n\n    preprocessing_params = merge_dict(default_prediction_preprocessing_parameters, preprocessing_params)\n\n    # if training_set_metadata is a string, assume it's a path to load the json\n    if training_set_metadata and isinstance(training_set_metadata, str):\n        training_set_metadata = load_metadata(training_set_metadata)\n\n    # setup\n    output_features = []\n    if include_outputs:\n        output_features += config[\"output_features\"]\n    features = config[\"input_features\"] + output_features\n\n    # Check the cache for an already preprocessed dataset. This only\n    # applies to scenarios where the user wishes to predict on a split\n    # of the full dataset, where we preprocess the whole dataset together\n    # during training. If the user wishes to predict on the full dataset,\n    # it is assumed they are predicting on unseen data. This is done\n    # because the cached data is stored in its split form, and would be\n    # expensive to recombine, requiring further caching.\n    cached = False\n\n    dataset = wrap(dataset)\n    cache = backend.cache.get_dataset_cache(config, dataset)\n    dataset = dataset.unwrap()\n\n    training_set = test_set = validation_set = None\n    if data_format in CACHEABLE_FORMATS and split != FULL:\n        with backend.storage.cache.use_credentials():\n            cache_results = cache.get()\n            if cache_results is not None:\n                valid, *cache_values = cache_results\n                if valid:\n                    logger.info(_get_cache_hit_message(cache))\n                    training_set_metadata, training_set, test_set, validation_set = cache_values\n                    config[\"data_hdf5_fp\"] = training_set\n                    data_format = backend.cache.data_format\n                    cached = True\n\n    data_format_processor = get_from_registry(data_format, data_format_preprocessor_registry)\n    if cached:\n        with backend.storage.cache.use_credentials():\n            processed = data_format_processor.prepare_processed_data(\n                features,\n                dataset=dataset,\n                training_set=training_set,\n                validation_set=validation_set,\n                test_set=test_set,\n                training_set_metadata=training_set_metadata,\n                preprocessing_params=preprocessing_params,\n                backend=backend,\n            )\n            training_set, test_set, validation_set, training_set_metadata = processed\n    else:\n        processed = data_format_processor.preprocess_for_prediction(\n            config, dataset, features, preprocessing_params, training_set_metadata, backend, callbacks\n        )\n        dataset, training_set_metadata, new_hdf5_fp = processed\n        training_set_metadata = training_set_metadata.copy()\n\n        if new_hdf5_fp:\n            training_set_metadata[DATA_TRAIN_HDF5_FP] = new_hdf5_fp\n\n        if split != FULL:\n            logger.debug(\"split train-val-test\")\n            training_set, validation_set, test_set = drop_extra_cols(\n                features, split_dataset(dataset, preprocessing_params, backend)\n            )\n\n    if split == TRAINING:\n        dataset = training_set\n    elif split == VALIDATION:\n        dataset = validation_set\n    elif split == TEST:\n        dataset = test_set\n\n    config = {\n        **config,\n        \"output_features\": output_features,\n    }\n\n    with backend.storage.cache.use_credentials() if cached else contextlib.nullcontext():\n        dataset = backend.dataset_manager.create(\n            dataset,\n            config,\n            training_set_metadata,\n        )\n\n    return dataset, training_set_metadata\n\n\ndef _get_cache_hit_message(cache: DatasetCache) -> str:\n    return (\n        \"Found cached dataset and meta.json with the same filename of the dataset.\\n\"\n        \"Using cached values instead of preprocessing the dataset again.\\n\"\n        f\"- Cached training set metadata path: {cache.get_cached_obj_path(META)}\\n\"\n        f\"- Cached training set path: {cache.get_cached_obj_path(TRAINING)}\\n\"\n        f\"- Cached validation set path: {cache.get_cached_obj_path(VALIDATION)}\\n\"\n        f\"- Cached test set path: {cache.get_cached_obj_path(TEST)}\"\n    )\n"
  },
  {
    "path": "ludwig/data/prompt.py",
    "content": "import json\nimport logging\nimport os\nimport string\nfrom typing import Any, TYPE_CHECKING\n\nimport pandas as pd\n\nif TYPE_CHECKING:\n    from ludwig.backend.base import Backend\n\nfrom ludwig.models.retrieval import df_checksum, get_retrieval_model, RetrievalModel\nfrom ludwig.utils.fs_utils import get_default_cache_location, makedirs, path_exists\nfrom ludwig.utils.types import DataFrame, Series\n\nlogger = logging.getLogger(__name__)\n\nCONTEXT = \"__context__\"\nSAMPLE = \"__sample__\"\nTASK = \"__task__\"\n\nDEFAULT_ZERO_SHOT_PROMPT_TEMPLATE = \"\"\"SAMPLE INPUT: {__sample__}\n\nUSER: Complete the following task: {__task__}\n\nASSISTANT:\n\"\"\"\n\n\nDEFAULT_FEW_SHOT_PROMPT_TEMPLATE = \"\"\"Below is relevant context:\n\nCONTEXT: {__context__}\n\nCONTEXT is comprised of labeled samples whose embeddings were similar to that of the sample input. The labels in\nthese samples could aid you in your final prediction. Given this and no prior knowledge, follow the instructions\nbelow.\n\nSAMPLE INPUT: {__sample__}\n\nUSER: Complete the following task: {__task__}\n\nASSISTANT:\n\"\"\"\n\n\ndef index_column(\n    retrieval_config: dict[str, Any],\n    col_name: str,\n    dataset_cols: dict[str, Series],\n    backend: \"Backend\",\n    split_col: Series | None = None,\n) -> tuple[RetrievalModel, str]:\n    \"\"\"Indexes a column for sample retrieval via embedding index lookup.\n\n    This function indexes a column and saves the index artifact to disk. If an index name is provided as part of the\n    `retrieval_config`, then the index in the ludwig cache with the corresponding name will be loaded instead of being\n    built from scratch.\n\n    To prevent data leakage, a split column must be provided. This ensures that the retrieval model only ever fetches\n    samples from the training set.\n\n    To ensure that the index is usable even if the original DataFrame is not available, the columns used to build the\n    index are stored as part of the index.\n\n    All operations in this function are performed on pandas objects, which means that you may run out of memory if your\n    dataset is large.\n\n    Args:\n        retrieval_config (Dict[str, Any]): The retrieval config from the config object.\n        col_name (str): The name of the column to index.\n        dataset_cols (Dict[str, Series]): A dictionary mapping column names to their corresponding Series. `col_name`\n            must be a key in this dictionary. These columns are stored as part of the index to ensure that the index\n            is usable even if the original DataFrame is not available.\n        df_engine (DataFrameEngine): The engine used to compute the columns into pandas objects.\n        split_col (Optional[Series]): A column that indicates whether a sample is part of the training set. A sample\n            is in the training set if the value in this column is 0.\n    Returns:\n        Tuple[RetrievalModel, str]: A tuple containing the retrieval model and the name of the index.\n    \"\"\"\n    retrieval_model = get_retrieval_model(\n        retrieval_config[\"type\"],\n        model_name=retrieval_config[\"model_name\"],\n    )\n\n    index_name = retrieval_config[\"index_name\"]\n    index_cache_directory = os.path.join(get_default_cache_location(), \"index\")\n    if not path_exists(index_cache_directory):\n        makedirs(index_cache_directory, exist_ok=True)\n\n    if index_name is None:\n        if split_col is None:\n            raise ValueError(\"split column must be provided if using retrieval\")\n        split_col = backend.df_engine.compute(split_col).astype(int)\n\n        # TODO(geoffrey): add support for Dask DataFrames\n        df = pd.DataFrame({name: backend.df_engine.compute(col) for name, col in dataset_cols.items()})\n        df = df[split_col == 0]  # Ensures that the index is only built on the training set\n\n        # Even if index name is not provided, we still want to check if an index for this df already exists in cache\n        # If it does, load it and return immediately\n        index_hash = df_checksum(df)\n        index_name = f\"embedding_index_{index_hash}\"\n        if path_exists(os.path.join(index_cache_directory, index_name)):\n            logger.info(\n                f\"Index for this DataFrame with name '{index_name}' already exists. \"\n                f\"Loading index from '{index_cache_directory}'\"\n            )\n            retrieval_model.load_index(index_name, cache_directory=index_cache_directory)\n            return retrieval_model, index_name\n\n        # Build index if index name is not provided and index for this df does not already exist in cache\n        retrieval_model.create_dataset_index(df, backend, columns_to_index=[col_name])\n        logger.info(f\"Saving index to cache directory '{index_cache_directory}' with name '{index_name}'\")\n        retrieval_model.save_index(index_name, cache_directory=index_cache_directory)\n    else:\n        logger.info(f\"Loading index from cache directory '{index_cache_directory}' with name '{index_name}'\")\n        retrieval_model.load_index(index_name, cache_directory=index_cache_directory)\n    return retrieval_model, index_name\n\n\ndef format_input_with_prompt(\n    input_col_name: str,\n    dataset_df: DataFrame,\n    backend: \"Backend\",\n    task_str: str,\n    retrieval_model: RetrievalModel | None = None,\n    k: int = -1,\n    template: str | None = None,\n) -> Series:\n    \"\"\"Returns a new Series with the input column data formatted with the prompt.\n\n    A prompt can either be zero-shot or few-shot. A zero-shot prompt is comprised of some (unlabeled) input and a task\n    to be completed given the input. A few-shot prompt additionally includes some dynamically retrieved context, which\n    is retrieved using the `retrieval_model.search` function.\n\n    A template can be provided to customize the prompt. The template must be a string with the following fields:\n        - __sample__ or at least one column from the input dataset: The input sample.\n        - __context__: The context retrieved by the `search_fn` function. Only required if `search_fn` is provided.\n        - __task__: The task to be completed given the input. Only required if `task` is set in the prompt config.\n\n    Zero-shot example:\n\n    Before formatting:\n\n        input_col = [\"I am happy\"]\n        task_str = \"sentiment analysis\"\n\n    After formatting:\n\n        input_col = [\"SAMPLE INPUT: I am happy\\n\\nUSER: Complete the following task: sentiment analysis\\n\\nASSISTANT:\"]\n\n    Args:\n        input_col_name (str): The name of the input column.\n        dataset_df (DataFrame): The input dataset.\n        backend (Backend): The backend used for map operations.\n        task_str (str): The task to be completed given the input.\n        retrieval_model (Optional[RetrievalModel]): The retrieval model used to retrieve context. If provided, the\n            prompt will be few-shot. If not provided, the prompt will be zero-shot.\n        k (int): The number of samples to retrieve. Only required if `retrieval_model` is provided.\n        template (Optional[str]): The template to use for the prompt. If not provided, the default will be used.\n\n    Returns:\n        Series: A new Series with the input column data formatted with the prompt.\n    \"\"\"\n    # determine if this is a few-shot or zero-shot prompt\n    # few-shot prompts require a search function that returns samples from some dataset\n    is_few_shot = retrieval_model is not None\n\n    # if no template is provided, use the default template\n    if template is None:\n        if is_few_shot:\n            template = DEFAULT_FEW_SHOT_PROMPT_TEMPLATE\n        else:\n            template = DEFAULT_ZERO_SHOT_PROMPT_TEMPLATE\n\n    # ensure that the prompt template has all required fields\n    template_fields, field_to_dtype = _get_template_fields(template)\n    try:\n        _validate_prompt_template(template_fields, task_str, is_few_shot, dataset_df.columns, input_col_name)\n    except ValueError as e:\n        raise ValueError(f\"template invalid for {'few-shot' if is_few_shot else 'zero-shot'} prompt: {e}\")\n\n    def generate_prompt(df: pd.DataFrame):\n        if CONTEXT in template_fields:\n            df[CONTEXT] = retrieval_model.search(df, backend, k=k, return_data=True)\n        if SAMPLE in template_fields:\n            # During preprocessing, we're inserting quotes that change the token IDs completely if we\n            # don't remove the \" from the string. For parity with expected user output, we need to get rid of them.\n            # TODO(Arnav): see if there's a way to only remove them if the entry does't have quotes. This currently\n            # removes all \" from the string (even those not added by json.dumps), which is not ideal.\n            df[SAMPLE] = df[input_col_name].map(lambda entry: json.dumps(entry, indent=2).strip('\"'))\n        if TASK in template_fields:\n            df[TASK] = task_str\n\n        def generate_prompt_for_row(row):\n            kwargs = {col: field_to_dtype[col](row[col]) for col in template_fields}\n            return template.format(**kwargs)\n\n        return df.apply(generate_prompt_for_row, axis=1)\n\n    result = backend.df_engine.map_partitions(dataset_df, generate_prompt, meta=(input_col_name, \"object\"))\n    result = backend.df_engine.persist(result)  # persist to prevent re-computation\n    return result\n\n\ndef _validate_prompt_template(\n    template_fields: set[str], task: str | None, is_few_shot: bool, columns: list[str], input_col_name: str\n):\n    \"\"\"Validates that the template contains the necessary fields for the prompt.\"\"\"\n    if is_few_shot and CONTEXT not in template_fields:\n        raise ValueError(f\"Prompt template must contain the '{CONTEXT}' field for few-shot learning\")\n\n    if task is not None and TASK not in template_fields:\n        raise ValueError(f\"Prompt template must contain the '{TASK}' field if a task is provided\")\n\n    if SAMPLE in template_fields:\n        if input_col_name not in columns:\n            raise ValueError(\n                f\"Prompt template contains the '{SAMPLE}' field, \"\n                f\"but the input column '{input_col_name}' is not in the dataset\"\n            )\n    elif not any(col in template_fields for col in columns):\n        raise ValueError(\n            f\"Prompt template must contain either the '{SAMPLE}' field or one of the columns from the dataset\"\n        )\n\n\ndef _get_template_fields(template: str) -> tuple[set[str], dict[str, type]]:\n    \"\"\"Returns the fields in the template.\"\"\"\n    parsed = [t for t in string.Formatter().parse(template) if t[1] is not None]\n    field_set = {field for _, field, _, _ in parsed}\n    dtype_map = {field: _get_dtype(format_spec) for _, field, format_spec, _ in parsed}\n    return field_set, dtype_map\n\n\ndef _get_dtype(format_spec: str) -> type:\n    # We need to prepare data in the row for different formatting options.\n    # 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\n    # number is represented as a string in the DF. So we need to cast it to a float before formatting.\n    if not format_spec:\n        return str\n    if \"f\" in format_spec:\n        return float\n    raise ValueError(f\"Unsupported template format spec: {format_spec}\")\n"
  },
  {
    "path": "ludwig/data/sampler.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport math\n\nimport numpy as np\n\nfrom ludwig.distributed import DistributedStrategy\nfrom ludwig.utils.defaults import default_random_seed\n\n\nclass DistributedSampler:\n    \"\"\"Adapted from `torch.utils.data.distributed.DistributedSampler`.\"\"\"\n\n    def __init__(\n        self,\n        dataset_size: int,\n        shuffle: bool = True,\n        random_seed: int = default_random_seed,\n        distributed: DistributedStrategy = None,\n    ):\n        self.dataset_size = dataset_size\n        self.num_replicas = distributed.size() if distributed else 1\n        self.rank = distributed.rank() if distributed else 0\n        self.epoch = 0\n        self.num_samples = int(math.ceil(self.dataset_size * 1.0 / self.num_replicas))\n        self.total_size = self.num_samples * self.num_replicas\n        self.shuffle = shuffle\n        self.random_seed = random_seed\n\n    def __iter__(self):\n        if self.shuffle:\n            # deterministically shuffle based on epoch and seed\n            indices = np.random.RandomState(seed=self.random_seed + self.epoch).permutation(self.dataset_size).tolist()\n        else:\n            indices = list(range(self.dataset_size))\n\n        # add extra samples to make it evenly divisible\n        indices += indices[: (self.total_size - len(indices))]\n        assert len(indices) == self.total_size\n\n        # subsample\n        indices = indices[self.rank : self.total_size : self.num_replicas]\n        assert len(indices) == self.num_samples\n\n        return iter(indices)\n\n    def __len__(self):\n        return self.num_samples\n\n    def set_epoch(self, epoch):\n        \"\"\"Sets the epoch for this sampler.\n\n        When `shuffle=True`, this ensures all replicas use a different random ordering for each epoch. Otherwise, the\n        next iteration of this sampler will yield the same ordering.\n\n        :param epoch: (int) epoch number\n        \"\"\"\n        self.epoch = epoch\n"
  },
  {
    "path": "ludwig/data/split.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING\nfrom zlib import crc32\n\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.backend.base import Backend\nfrom ludwig.constants import BINARY, CATEGORY, DATE, MIN_DATASET_SPLIT_ROWS, SPLIT\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.split import (\n    DateTimeSplitConfig,\n    FixedSplitConfig,\n    HashSplitConfig,\n    RandomSplitConfig,\n    StratifySplitConfig,\n)\nfrom ludwig.types import ModelConfigDict, PreprocessingConfigDict\nfrom ludwig.utils.data_utils import hash_dict, split_dataset_ttv\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.registry import Registry\nfrom ludwig.utils.types import DataFrame\n\nif TYPE_CHECKING:\n    from ludwig.schema.model_config import ModelConfig\n\nsplit_registry = Registry()\nlogger = logging.getLogger(__name__)\n\nTMP_SPLIT_COL = \"__SPLIT__\"\nDEFAULT_PROBABILITIES = (0.7, 0.1, 0.2)\n\n\nclass Splitter(ABC):\n    @abstractmethod\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: int = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        pass\n\n    def validate(self, config: ModelConfigDict):\n        pass\n\n    def has_split(self, split_index: int) -> bool:\n        return True\n\n    @property\n    def required_columns(self) -> list[str]:\n        \"\"\"Returns the list of columns that are required for splitting.\"\"\"\n        return []\n\n\ndef _make_divisions_ensure_minimum_rows(\n    divisions: list[int],\n    n_examples: int,\n    min_val_rows: int = MIN_DATASET_SPLIT_ROWS,\n    min_test_rows: int = MIN_DATASET_SPLIT_ROWS,\n) -> list[int]:\n    \"\"\"Revises divisions to ensure no dataset split has too few examples.\"\"\"\n    result = list(divisions)\n    n = [dn - dm for dm, dn in zip((0,) + divisions, divisions + (n_examples,))]  # Number of examples in each split.\n    if 0 < n[2] < min_test_rows and n[0] > 0:\n        # Test set is nonempty but too small, take examples from training set.\n        shift = min(min_test_rows - n[2], n[0])\n        result = [d - shift for d in result]\n    if 0 < n[1] < min_val_rows and n[0] > 0:\n        # Validation set is nonempty but too small, take examples from training set.\n        result[0] -= min(min_val_rows - n[1], result[0])\n    return result\n\n\ndef _split_divisions_with_min_rows(n_rows: int, probabilities: list[float]) -> list[int]:\n    \"\"\"Generates splits for a dataset of n_rows into train, validation, and test sets according to split\n    probabilities, also ensuring that at least min_val_rows or min_test_rows are present in each nonempty split.\n\n    Returns division indices to split on.\n    \"\"\"\n    d1 = int(np.ceil(probabilities[0] * n_rows))\n    if probabilities[-1] > 0:\n        n2 = int(probabilities[1] * n_rows)\n        d2 = d1 + n2\n    else:\n        # If the last probability is 0, then use the entire remaining dataset for validation.\n        d2 = n_rows\n    return _make_divisions_ensure_minimum_rows((d1, d2), n_rows)\n\n\n@split_registry.register(\"random\", default=True)\nclass RandomSplitter(Splitter):\n    def __init__(self, probabilities: list[float] = DEFAULT_PROBABILITIES, **kwargs):\n        self.probabilities = probabilities\n\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        probabilities = self.probabilities\n        if not backend.df_engine.partitioned:\n            divisions = _split_divisions_with_min_rows(len(df), probabilities)\n            shuffled_df = df.sample(frac=1, random_state=random_seed)\n            return (\n                shuffled_df.iloc[: divisions[0]],  # Train\n                shuffled_df.iloc[divisions[0] : divisions[1]],  # Validation\n                shuffled_df.iloc[divisions[1] :],  # Test\n            )\n\n        # The above approach is very inefficient for partitioned backends, which can split by partition.\n        # This does not give exact guarantees on split size but is much more efficient for large datasets.\n        return df.random_split(self.probabilities, random_state=random_seed)\n\n    def has_split(self, split_index: int) -> bool:\n        return self.probabilities[split_index] > 0\n\n    @staticmethod\n    def get_schema_cls():\n        return RandomSplitConfig\n\n\n@split_registry.register(\"fixed\")\nclass FixedSplitter(Splitter):\n    def __init__(self, column: str = SPLIT, **kwargs):\n        self.column = column\n\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        df[self.column] = df[self.column].astype(np.int8)\n        dfs = split_dataset_ttv(df, self.column)\n        train, test, val = tuple(df.drop(columns=self.column) if df is not None else None for df in dfs)\n        return train, val, test\n\n    @property\n    def required_columns(self) -> list[str]:\n        return [self.column]\n\n    @staticmethod\n    def get_schema_cls():\n        return FixedSplitConfig\n\n\ndef stratify_split_dataframe(\n    df: DataFrame, column: str, probabilities: list[float], backend: Backend, random_seed: float\n) -> tuple[DataFrame, DataFrame, DataFrame]:\n    \"\"\"Splits a dataframe into train, validation, and test sets based on the values of a column.\n\n    The column must be categorical (including binary). The split is stratified, meaning that the proportion of each\n    category in each split is the same as in the original dataset.\n    \"\"\"\n\n    frac_train, frac_val, frac_test = probabilities\n\n    def _safe_stratify(df, column, test_size):\n        # Get the examples with cardinality of 1\n        df_cadinalities = df.groupby(column)[column].size()\n        low_cardinality_elems = df_cadinalities.loc[lambda x: x == 1]\n        df_low_card = df[df[column].isin(low_cardinality_elems.index)]\n        df = df[~df[column].isin(low_cardinality_elems.index)]\n        y = df[[column]]\n\n        df_train, df_temp, _, _ = train_test_split(df, y, stratify=y, test_size=test_size, random_state=random_seed)\n\n        # concat the examples with cardinality of 1 to the training DF.\n        if len(df_low_card.index) > 0:\n            df_train = backend.df_engine.concat([df_train, df_low_card])\n\n        return df_train, df_temp\n\n    df_train, df_temp = _safe_stratify(df, column, 1.0 - frac_train)\n\n    relative_frac_test = frac_test / (frac_val + frac_test)\n    df_val, df_test = _safe_stratify(df_temp, column, relative_frac_test)\n\n    return df_train, df_val, df_test\n\n\n@split_registry.register(\"stratify\")\nclass StratifySplitter(Splitter):\n    def __init__(self, column: str, probabilities: list[float] = DEFAULT_PROBABILITIES, **kwargs):\n        self.column = column\n        self.probabilities = probabilities\n\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        if not backend.df_engine.partitioned:\n            return stratify_split_dataframe(df, self.column, self.probabilities, backend, random_seed)\n\n        # For a partitioned dataset, we can stratify split each partition individually\n        # to obtain a global stratified split.\n\n        def split_partition(partition: DataFrame) -> DataFrame:\n            \"\"\"Splits a single partition into train, val, test.\n\n            Returns a single DataFrame with the split column populated. Assumes that the split column is already present\n            in the partition and has a default value of 0 (train).\n            \"\"\"\n            partition = partition.copy()\n            _, val, test = stratify_split_dataframe(partition, self.column, self.probabilities, backend, random_seed)\n            # Split column defaults to train, so only need to update val and test\n            partition.loc[val.index, TMP_SPLIT_COL] = 1\n            partition.loc[test.index, TMP_SPLIT_COL] = 2\n            return partition\n\n        df[TMP_SPLIT_COL] = 0\n        df = backend.df_engine.map_partitions(df, split_partition, meta=df)\n\n        df_train = df[df[TMP_SPLIT_COL] == 0].drop(columns=TMP_SPLIT_COL)\n        df_val = df[df[TMP_SPLIT_COL] == 1].drop(columns=TMP_SPLIT_COL)\n        df_test = df[df[TMP_SPLIT_COL] == 2].drop(columns=TMP_SPLIT_COL)\n\n        return df_train, df_val, df_test\n\n    def validate(self, config: \"ModelConfig\"):  # noqa: F821\n        features = [f for f in config.input_features] + [f for f in config.output_features]\n        feature_cols = {f.column for f in features}\n        if self.column not in feature_cols:\n            logging.info(\n                f\"Stratify column {self.column} is not among the features. \"\n                f\"Cannot establish if it is a binary or category feature.\"\n            )\n        elif [f for f in features if f.column == self.column][0].type not in {BINARY, CATEGORY}:\n            raise ConfigValidationError(f\"Feature for stratify column {self.column} must be binary or category\")\n\n    def has_split(self, split_index: int) -> bool:\n        return self.probabilities[split_index] > 0\n\n    @property\n    def required_columns(self) -> list[str]:\n        return [self.column]\n\n    @staticmethod\n    def get_schema_cls():\n        return StratifySplitConfig\n\n\n@split_registry.register(\"datetime\")\nclass DatetimeSplitter(Splitter):\n    def __init__(\n        self,\n        column: str,\n        probabilities: list[float] = DEFAULT_PROBABILITIES,\n        datetime_format: str | None = None,\n        fill_value: str = \"\",\n        **kwargs,\n    ):\n        self.column = column\n        self.probabilities = probabilities\n        self.datetime_format = datetime_format\n        self.fill_value = fill_value\n\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        # In case the split column was preprocessed by Ludwig into a list, convert it back to a\n        # datetime string for the sort and split\n        def list_to_date_str(x):\n            if not isinstance(x, list):\n                if not isinstance(x, str):\n                    # Convert timestamps, etc. to strings and return so it can direct cast to epoch time\n                    return str(x)\n\n                if len(x) != 9:\n                    # Strings not in the expected format, so assume it's a formatted datetime and return\n                    return x\n\n            return f\"{x[0]}-{x[1]}-{x[2]} {x[5]}:{x[6]}:{x[7]}\"\n\n        df[TMP_SPLIT_COL] = backend.df_engine.map_objects(df[self.column], list_to_date_str)\n\n        # Convert datetime to int64 to workaround Dask limitation\n        # https://github.com/dask/dask/issues/9003\n        df[TMP_SPLIT_COL] = backend.df_engine.df_lib.to_datetime(df[TMP_SPLIT_COL]).values.astype(\"int64\")\n\n        # Sort by ascending datetime and drop the temporary column\n        df = df.sort_values(TMP_SPLIT_COL).drop(columns=TMP_SPLIT_COL)\n\n        # Split using different methods based on the underlying df engine.\n        # For Pandas, split by row index.\n        # For Dask, split by partition, as splitting by row is very inefficient.\n        return tuple(backend.df_engine.split(df, self.probabilities))\n\n    def validate(self, config: \"ModelConfig\"):  # noqa: F821\n        features = [f for f in config.input_features] + [f for f in config.output_features]\n        feature_cols = {f.column for f in features}\n        if self.column not in feature_cols:\n            logging.info(\n                f\"Datetime split column {self.column} is not among the features. \"\n                f\"Cannot establish if it is a valid datetime.\"\n            )\n        elif [f for f in features if f.column == self.column][0].type not in {DATE}:\n            raise ConfigValidationError(f\"Feature for datetime split column {self.column} must be a datetime\")\n\n    def has_split(self, split_index: int) -> bool:\n        return self.probabilities[split_index] > 0\n\n    @property\n    def required_columns(self) -> list[str]:\n        return [self.column]\n\n    @staticmethod\n    def get_schema_cls():\n        return DateTimeSplitConfig\n\n\n@split_registry.register(\"hash\")\nclass HashSplitter(Splitter):\n    def __init__(\n        self,\n        column: str,\n        probabilities: list[float] = DEFAULT_PROBABILITIES,\n        **kwargs,\n    ):\n        self.column = column\n        self.probabilities = probabilities\n\n    def split(\n        self, df: DataFrame, backend: Backend, random_seed: float = default_random_seed\n    ) -> tuple[DataFrame, DataFrame, DataFrame]:\n        # Maximum value of the hash function crc32\n        max_value = 2**32\n        thresholds = [v * max_value for v in self.probabilities]\n\n        def hash_column(x):\n            value = hash_dict({\"value\": x}, max_length=None)\n            hash_value = crc32(value)\n            if hash_value < thresholds[0]:\n                return 0\n            elif hash_value < (thresholds[0] + thresholds[1]):\n                return 1\n            else:\n                return 2\n\n        df[TMP_SPLIT_COL] = backend.df_engine.map_objects(df[self.column], hash_column).astype(np.int8)\n        dfs = split_dataset_ttv(df, TMP_SPLIT_COL)\n        train, test, val = tuple(df.drop(columns=TMP_SPLIT_COL) if df is not None else None for df in dfs)\n        return train, val, test\n\n    def has_split(self, split_index: int) -> bool:\n        return self.probabilities[split_index] > 0\n\n    @property\n    def required_columns(self) -> list[str]:\n        return [self.column]\n\n    @staticmethod\n    def get_schema_cls():\n        return HashSplitConfig\n\n\n@DeveloperAPI\ndef get_splitter(type: str | None = None, **kwargs) -> Splitter:\n    splitter_cls = split_registry.get(type)\n    if splitter_cls is None:\n        return ValueError(f\"Invalid split type: {type}\")\n    return splitter_cls(**kwargs)\n\n\n@DeveloperAPI\ndef split_dataset(\n    df: DataFrame,\n    global_preprocessing_parameters: PreprocessingConfigDict,\n    backend: Backend,\n    random_seed: float = default_random_seed,\n) -> tuple[DataFrame, DataFrame, DataFrame]:\n    splitter = get_splitter(**global_preprocessing_parameters.get(SPLIT, {}))\n    datasets: tuple[DataFrame, DataFrame, DataFrame] = splitter.split(df, backend, random_seed)\n    if len(datasets[0].columns) == 0:\n        raise ValueError(\n            \"Encountered an empty training set while splitting data. Please double check the preprocessing split \"\n            \"configuration.\"\n        )\n\n    # Remove partitions that are empty after splitting\n    datasets = [None if dataset is None else backend.df_engine.remove_empty_partitions(dataset) for dataset in datasets]\n    return datasets\n"
  },
  {
    "path": "ludwig/data/split_dataset.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport random\n\n\ndef split(input_path, output1, output2, split):\n    with open(input_path) as file:\n        lines = file.readlines()\n\n    random.shuffle(lines)\n    split_idx = int(len(lines) * split)\n\n    with open(output1, \"w\") as f:\n        for line in lines[:split_idx]:\n            line = line if line.endswith(\"\\n\") else line + \"\\n\"\n            f.write(line)\n\n    with open(output2, \"w\") as f:\n        for line in lines[split_idx:]:\n            line = line if line.endswith(\"\\n\") else line + \"\\n\"\n            f.write(line)\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=\"Split a file based on its lines\")\n\n    parser.add_argument(\"-i\", \"--input\", required=True, help=\"input file names\")\n    parser.add_argument(\"-o1\", \"--output1\", required=True, help=\"output 1 file name\")\n    parser.add_argument(\"-o2\", \"--output2\", required=True, help=\"output 2 file name\")\n    parser.add_argument(\"-s\", \"--split\", required=True, type=float, default=0.8, help=\"percentage of the split\")\n\n    args = parser.parse_args()\n\n    split(args.input, args.output1, args.output2, args.split)\n"
  },
  {
    "path": "ludwig/data/utils.py",
    "content": "from typing import Optional\n\nimport numpy as np\n\nfrom ludwig.constants import DECODER, ENCODER, SPLIT\nfrom ludwig.types import FeatureConfigDict, PreprocessingConfigDict\nfrom ludwig.utils.dataframe_utils import is_dask_series_or_df\nfrom ludwig.utils.types import DataFrame\n\n\ndef convert_to_dict(\n    predictions: DataFrame,\n    output_features: dict[str, FeatureConfigDict],\n    backend: Optional[\"Backend\"] = None,  # noqa: F821\n):\n    \"\"\"Convert predictions from DataFrame format to a dictionary.\"\"\"\n    output = {}\n    for of_name, output_feature in output_features.items():\n        feature_keys = {k for k in predictions.columns if k.startswith(of_name)}\n        feature_dict = {}\n        for key in feature_keys:\n            subgroup = key[len(of_name) + 1 :]\n\n            values = predictions[key]\n            if is_dask_series_or_df(values, backend):\n                values = values.compute()\n            try:\n                values = np.stack(values.to_numpy())\n            except ValueError:\n                values = values.to_list()\n\n            feature_dict[subgroup] = values\n        output[of_name] = feature_dict\n    return output\n\n\ndef set_fixed_split(preprocessing_params: PreprocessingConfigDict) -> PreprocessingConfigDict:\n    \"\"\"Sets the split policy explicitly to a fixed split.\n\n    This potentially overrides the split configuration that the user set or what came from schema defaults.\n    \"\"\"\n\n    return {\n        **preprocessing_params,\n        \"split\": {\n            \"type\": \"fixed\",\n            \"column\": SPLIT,\n        },\n    }\n\n\ndef get_input_and_output_features(feature_configs):\n    \"\"\"Returns a tuple (input_features, output_features) where each element is a list of feature configs.\n\n    Determines whether a feature is an input or output feature by checking the presence of the encoder or decoder keys.\n    \"\"\"\n    input_features = []\n    output_features = []\n    for feature in feature_configs:\n        if ENCODER in feature:\n            input_features.append(feature)\n        elif DECODER in feature:\n            output_features.append(feature)\n    return input_features, output_features\n"
  },
  {
    "path": "ludwig/datasets/README.md",
    "content": "## Ludwig Datasets API\n\nThe Ludwig Dataset Zoo provides datasets that can be directly plugged into a Ludwig model. For each dataset, we've also\nincluded an example Ludwig config which should train reasonably fast on a current-generation laptop.\n\nThe simplest way to use a dataset is to import it:\n\n```python\nfrom ludwig.datasets import titanic\n\n# Loads into single dataframe with a 'split' column:\ndataset_df = titanic.load()\n\n# Loads into split dataframes:\ntrain_df, test_df, _ = titanic.load(split=True)\n```\n\nThe `ludwig.datasets` API provides functions to list, describe, and get datasets:\n\n______________________________________________________________________\n\n### list_datasets\n\nGets a list of the names of available datasets.\n\n**Example:**\n\n```python\ndataset_names = ludwig.datasets.list_datasets()\n```\n\n______________________________________________________________________\n\n### get_datasets_output_features\n\nIf a specific dataset name is passed in, then returns the output features associated with that dataset. Otherwise,\nreturns an ordered dictionary with dataset names as keys and dictionaries containing the output features for each\ndataset as values.\n\n**Example:**\n\n```python\noutput_features = ludwig.datasets.get_datasets_output_features(dataset=\"titanic\")\n```\n\n______________________________________________________________________\n\n### describe_dataset\n\nGets a human-readable description string for a dataset\n\n**Example:**\n\n```python\nprint(ludwig.datasets.describe_dataset(\"titanic\"))\n```\n\n______________________________________________________________________\n\n### get_dataset\n\nGet a dataset module by name\n\n**Example:**\n\n```python\ntitanic_dataset = ludwig.datasets.get_dataset(\"titanic\")\n```\n\n______________________________________________________________________\n\n### model_configs_for_dataset\n\nGets a dictionary of model configs for the specified dataset. Keys are the config names, and may\ncontain the special keys:\n\n- `default` - The default config for the dataset. Should train to decent performance under 10 minutes on a typical\n  laptop without GPU.\n- `best` - The best known config for the dataset. Should be replaced when a better config is found. This is a good\n  opportunity for contributions, if you find a better one please check it in and open a PR!\n\n**Example:**\n\n```python\nconfigs = ludwig.datasets.model_configs_for_dataset(\"higgs\")\ndefault_higgs_config = configs[\"default\"]\nbest_higgs_config = configs[\"best\"]\n```\n\n______________________________________________________________________\n\n## Training a model using builtin dataset and config\n\nThis example code trains a model on the Titanic dataset using the default config:\n\n```python\nfrom ludwig.api import LudwigModel\nimport ludwig.datasets\n\ntitanic = ludwig.datasets.get_dataset(\"titanic\")\n\ndataset_df = titanic.load()\n\ntitanic_config = titanic.default_model_config\n\nmodel = LudwigModel(titanic_config)\nmodel.train(dataset_df)\n```\n\nSome datasets are hosted on [Kaggle](https://www.kaggle.com) and require a kaggle account. To use these, you'll need to\n[set up Kaggle credentials](https://www.kaggle.com/docs/api) in your environment. If the dataset is part of a Kaggle\ncompetition, you'll need to accept the terms on the competition page.\n\nTo check programmatically, datasets have an `.is_kaggle_dataset` property.\n\n## Downloading, Processing, and Exporting\n\nDatasets are first downloaded into `LUDWIG_CACHE`, which may be set as an environment variable and defaults to\n`$HOME/.ludwig_cache`.\n\nDatasets are automatically loaded, processed, and re-saved as parquet files. The processed dataset is saved in\nLUDWIG_CACHE.\n\nIf the dataset contains media files including images or audio, media files are saved in subdirectories and referenced by\nrelative paths from the dataset location. To ensure Ludwig can read these files during training, they should be\naccessible from Ludwig's working directory.\n\nTo export the processed dataset, including any media files it depends on, use the `.export` method:\n\n```python\nfrom ludwig.datasets import twitter_bots\n\n# Exports twitter bots dataset and image files to the current working directory.\ntwitter_bots.export(\".\")\n\n# The working directory should now contain:\n# ./twitter_bots.parquet        - The twitter bots dataset\n# ./profile_images              - Account profile image files\n# ./profile_background_images   - Account profile background image files\n```\n"
  },
  {
    "path": "ludwig/datasets/__init__.py",
    "content": "import argparse\nimport importlib\nimport logging\nimport os\nfrom collections import OrderedDict\nfrom functools import lru_cache\nfrom io import BytesIO\nfrom typing import Any, Literal\n\nimport yaml\n\nfrom ludwig.api_annotations import DeveloperAPI, PublicAPI\nfrom ludwig.backend.base import Backend\nfrom ludwig.constants import AUDIO, BINARY, CATEGORY, IMAGE, NUMBER, TEST, TEXT, TRAIN, TYPE, VALIDATION\nfrom ludwig.data.cache.types import CacheableDataframe\nfrom ludwig.datasets import configs\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n# PublicAPI\nfrom ludwig.datasets.utils import model_configs_for_dataset  # noqa\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import print_ludwig\nfrom ludwig.utils.types import DataFrame\n\nURI_PREFIX = \"ludwig://\"\nHF_PREFIX = \"hf://\"\nSPLITS = [TRAIN, VALIDATION, TEST]\n\n\ndef _load_dataset_config(config_filename: str):\n    \"\"\"Loads a dataset config.\"\"\"\n    config_path = os.path.join(os.path.dirname(configs.__file__), config_filename)\n    with open(config_path) as f:\n        return DatasetConfig.from_dict(yaml.safe_load(f))\n\n\n@lru_cache(maxsize=1)\ndef _get_dataset_configs() -> dict[str, DatasetConfig]:\n    \"\"\"Returns all dataset configs indexed by name.\"\"\"\n    import importlib.resources\n\n    config_files = [f.name for f in importlib.resources.files(configs).iterdir() if f.name.endswith(\".yaml\")]\n    config_objects = [_load_dataset_config(f) for f in config_files]\n    return {c.name: c for c in config_objects}\n\n\ndef _get_dataset_config(dataset_name) -> DatasetConfig:\n    \"\"\"Get the config for a dataset.\"\"\"\n    configs = _get_dataset_configs()\n    if dataset_name not in configs:\n        raise AttributeError(f\"No config found for dataset {dataset_name}\")\n    return configs[dataset_name]\n\n\n@PublicAPI\ndef get_dataset(dataset_name, cache_dir=None) -> DatasetLoader:\n    \"\"\"Gets an instance of the dataset loader for a dataset.\"\"\"\n    config = _get_dataset_config(dataset_name)\n    class_name = config.loader.split(\".\")[-1]\n    module_name = \".\" + \".\".join(config.loader.split(\".\")[:-1])\n    loader_module = importlib.import_module(module_name, package=\"ludwig.datasets.loaders\")\n    loader_cls = getattr(loader_module, class_name)\n    if cache_dir:\n        return loader_cls(config, cache_dir=cache_dir)\n    return loader_cls(config)\n\n\n@DeveloperAPI\ndef load_dataset_uris(\n    dataset: str | DataFrame | None,\n    training_set: str | DataFrame | None,\n    validation_set: str | DataFrame | None,\n    test_set: str | DataFrame | None,\n    backend: Backend,\n) -> tuple[\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n]:\n    \"\"\"Loads and returns any Ludwig dataset URIs as CacheableDataframes.\n\n    Returns the input unmodified for any non-Ludwig datasets.\n    \"\"\"\n\n    dataset_out, training_set_out, validation_set_out, test_set_out = dataset, training_set, validation_set, test_set\n    # Check that any of the datasets begin with the `hf://` prefix denoting a Hugging Face dataset URI\n    # Hugging Face datasets should follow the naming convention `hf://<hf_id>--<hf_subsample>`\n    if _is_hf(dataset, training_set):\n        return _load_hf_datasets(dataset, training_set, validation_set, test_set, backend)\n\n    # Check that any of the datasets begin with the `ludwig://` prefix denoting a Ludwig dataset URI\n    if dataset is not None:\n        if isinstance(dataset, str) and dataset.startswith(URI_PREFIX):\n            dataset_out = _load_cacheable_dataset(dataset, backend)\n        return dataset_out, training_set_out, validation_set_out, test_set_out\n    if training_set is not None:\n        train_df = test_df = val_df = None\n        training_set_checksum = None\n        if isinstance(training_set, str) and training_set.startswith(URI_PREFIX):\n            # For the training set, we only want to use the TRAINING split of the dataset\n            dataset_name = training_set[len(URI_PREFIX) :]\n            loader = get_dataset(dataset_name)\n            train_df, test_df, val_df = loader.load(split=True)\n            training_set_checksum = str(loader.get_mtime())\n            train_df = backend.df_engine.from_pandas(train_df)\n            training_set_out = CacheableDataframe(df=train_df, name=training_set, checksum=training_set_checksum)\n\n        if isinstance(validation_set, str) and validation_set.startswith(URI_PREFIX):\n            if validation_set == training_set:\n                # Reuse the loaded DF from the training split\n                val_df = backend.df_engine.from_pandas(val_df)\n                validation_set_out = CacheableDataframe(df=val_df, name=validation_set, checksum=training_set_checksum)\n            else:\n                validation_set_out = _load_cacheable_dataset(validation_set, backend)\n\n        if isinstance(test_set, str) and test_set.startswith(URI_PREFIX):\n            if test_set == training_set:\n                # Reuse the loaded DF from the training split\n                test_df = backend.df_engine.from_pandas(test_df)\n                test_set_out = CacheableDataframe(df=test_df, name=test_set, checksum=training_set_checksum)\n            else:\n                test_set_out = _load_cacheable_dataset(test_set, backend)\n\n        return dataset_out, training_set_out, validation_set_out, test_set_out\n\n\ndef _is_hf(dataset, training_set):\n    dataset_is_hf = dataset is not None and isinstance(dataset, str) and dataset.startswith(HF_PREFIX)\n    training_set_is_hf = (\n        training_set is not None and isinstance(training_set, str) and training_set.startswith(HF_PREFIX)\n    )\n    return dataset_is_hf or training_set_is_hf\n\n\ndef _load_hf_datasets(\n    dataset: str | DataFrame | None,\n    training_set: str | DataFrame | None,\n    validation_set: str | DataFrame | None,\n    test_set: str | DataFrame | None,\n    backend: Backend,\n) -> tuple[\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n    CacheableDataframe | None,\n]:\n    \"\"\"Loads and returns any Hugging Face datasets as CacheableDataframes.\n\n    Returns the input unmodified for any non-HF datasets.\n    \"\"\"\n    dataset_out = dataset\n    training_set_out = training_set\n    validation_set_out = validation_set\n    test_set_out = test_set\n\n    # Check that any of the datasets begin with the `hf://` prefix denoting a Hugging Face dataset URI\n    # Hugging Face datasets should follow the naming convention `hf://<hf_id>--<hf_subsample>`\n    if dataset is not None:\n        if isinstance(dataset, str) and dataset.startswith(HF_PREFIX):\n            dataset_out = _load_cacheable_hf_dataset(dataset, backend)\n        return dataset_out, training_set_out, validation_set_out, test_set_out\n\n    # Because of the conditional logic (_is_hf) in load_dataset_uris, if the above block is not triggered, then\n    # training_set must be a string that starts with HF_PREFIX\n    train_df = test_df = val_df = None\n    loader = get_dataset(\"hugging_face\")\n    hf_id, hf_subsample = _get_hf_dataset_and_subsample(training_set)\n    train_df, val_df, test_df = loader.load(hf_id, hf_subsample, split=True)  # Call hugging_face loader\n    train_df = backend.df_engine.from_pandas(train_df)\n    training_set_out = CacheableDataframe(df=train_df, name=training_set, checksum=None)\n\n    if isinstance(validation_set, str) and validation_set.startswith(HF_PREFIX):\n        if validation_set == training_set:\n            # Reuse the loaded DF from the training split\n            val_df = backend.df_engine.from_pandas(val_df)\n            validation_set_out = CacheableDataframe(df=val_df, name=validation_set, checksum=None)\n        else:  # This handles an edge case -- NOT EXPECTED USER BEHAVIOR\n            logging.warning(\n                \"A Hugging Face validation set has been passed in that is different from the test set. \"\n                \"This is not recommended.\"\n            )\n            validation_set_out = _load_cacheable_hf_dataset(validation_set, backend, split_set=VALIDATION)\n\n    if isinstance(test_set, str) and test_set.startswith(HF_PREFIX):\n        if test_set == training_set:\n            # Reuse the loaded DF from the training split\n            test_df = backend.df_engine.from_pandas(test_df)\n            test_set_out = CacheableDataframe(df=test_df, name=test_set, checksum=None)\n        else:  # This handles an edge case -- NOT EXPECTED USER BEHAVIOR\n            logging.warning(\n                \"A Hugging Face test set has been passed in that is different from the training set. \"\n                \"This is not recommended.\"\n            )\n            test_set_out = _load_cacheable_hf_dataset(test_set, backend, split_set=TEST)\n\n    return dataset_out, training_set_out, validation_set_out, test_set_out\n\n\ndef _load_cacheable_hf_dataset(\n    dataset: str, backend: Backend, split_set: Literal[\"train\", \"validation\", \"test\"] | None = None\n) -> CacheableDataframe:\n    loader = get_dataset(\"hugging_face\")\n    hf_id, hf_subsample = _get_hf_dataset_and_subsample(dataset)\n    if split_set:\n        train_df, validation_df, test_df = loader.load(hf_id, hf_subsample, split=True)\n        df = [train_df, validation_df, test_df][\n            SPLITS.index(split_set)\n        ]  # split_set should be one of TRAIN, VALIDATION, or TEST\n    else:\n        df = loader.load(hf_id, hf_subsample, split=False)\n    df = backend.df_engine.from_pandas(df)\n    return CacheableDataframe(df=df, name=dataset, checksum=None)\n\n\ndef _load_cacheable_dataset(dataset: str, backend: Backend) -> CacheableDataframe:\n    dataset_name = dataset[len(URI_PREFIX) :]\n    loader = get_dataset(dataset_name)\n    df = loader.load(split=False)\n    df = backend.df_engine.from_pandas(df)\n    return CacheableDataframe(df=df, name=dataset, checksum=str(loader.get_mtime()))\n\n\n@PublicAPI\ndef list_datasets() -> list[str]:\n    \"\"\"Returns a list of the names of all available datasets.\"\"\"\n    return sorted(_get_dataset_configs().keys())\n\n\n@PublicAPI\ndef get_datasets_output_features(\n    dataset: str = None, include_competitions: bool = True, include_data_modalities: bool = False\n) -> dict:\n    \"\"\"Returns a dictionary with the output features for each dataset. Optionally, you can pass a dataset name\n    which will then cause the function to return a dictionary with the output features for that dataset.\n\n    Because Hugging Face Datasets are loaded dynamically through a shared connector, they don't have fixed output\n    features. As such, we exclude Hugging Face datasets here.\n\n    :param dataset: (str) name of the dataset\n    :param include_competitions: (bool) whether to include the output features from kaggle competition datasets\n    :param include_data_modalities: (bool) whether to include the data modalities associated with the prediction task\n    :return: (dict) dictionary with the output features for each dataset or a dictionary with the output features for\n        the specified dataset\n    \"\"\"\n    ordered_configs = OrderedDict(sorted(_get_dataset_configs().items()))\n    competition_datasets = []\n    hugging_face_datasets = []\n\n    for name, config in ordered_configs.items():\n        if not include_competitions and config.kaggle_competition:\n            competition_datasets.append(name)\n            continue\n\n        if config.name == \"hugging_face\":\n            # There is no output_features attribute for hugging_face datasets\n            hugging_face_datasets.append(name)\n            continue\n\n        ordered_configs[name] = {\"name\": config.name, \"output_features\": config.output_features}\n\n        if include_data_modalities:\n            column_types = {column[TYPE] for column in config.columns}\n\n            data_modalities = set()\n            if NUMBER in column_types or CATEGORY in column_types or BINARY in column_types:\n                data_modalities.add(\"Tabular\")\n            if TEXT in column_types:\n                data_modalities.add(\"Text\")\n            if IMAGE in column_types:\n                data_modalities.add(\"Image\")\n            if AUDIO in column_types:\n                data_modalities.add(\"Audio\")\n\n            ordered_configs[name][\"data_modalities\"] = data_modalities\n\n    if dataset:\n        return ordered_configs[dataset]\n\n    if not include_competitions:\n        for competition in competition_datasets:\n            del ordered_configs[competition]\n\n    del ordered_configs[\"hugging_face\"]\n\n    return ordered_configs\n\n\n@PublicAPI\ndef describe_dataset(dataset_name: str) -> str:\n    \"\"\"Returns the description of the dataset.\"\"\"\n    return _get_dataset_configs()[dataset_name].description\n\n\n@PublicAPI\ndef download_dataset(dataset_name: str, output_dir: str = \".\"):\n    \"\"\"Downloads the dataset to the specified directory.\"\"\"\n    output_dir = os.path.expanduser(os.path.normpath(output_dir))\n    dataset = get_dataset(dataset_name)\n    dataset.export(output_dir)\n\n\n@DeveloperAPI\ndef get_buffer(dataset_name: str, kaggle_username: str = None, kaggle_key: str = None) -> BytesIO:\n    \"\"\"Returns a byte buffer for the specified dataset.\"\"\"\n    try:\n        if dataset_name.startswith(HF_PREFIX):\n            hf_id, hf_subsample = _get_hf_dataset_and_subsample(dataset_name)\n            dataset = get_dataset(\"hugging_face\").load(hf_id, hf_subsample)\n        else:\n            dataset = get_dataset(dataset_name).load(kaggle_username=kaggle_username, kaggle_key=kaggle_key)\n        buffer = BytesIO(dataset.to_parquet())\n        return buffer\n    except Exception as e:\n        logging.error(logging.ERROR, f\"Failed to upload dataset {dataset_name}: {e}\")\n\n\ndef _get_hf_dataset_and_subsample(dataset_name: str) -> tuple[str, str | None]:\n    \"\"\"Returns the Hugging Face ID and subsample name from the dataset name.\n\n    The dataset name should follow the format \"{HF_PREFIX}{hf_id}--{hf_subsample}\"\n\n    Examples (Dataset Name --> HF ID; HF subsample): \"hf://wikisql\" --> \"wikisql\"; None \"hf://ColumbiaNLP/FLUTE\" -->\n    \"ColumbiaNLP/FLUTE\"; None \"hf://mstz/adult--income\" --> \"mstz/adult\"; \"income\"\n    \"\"\"\n    dataset_name = dataset_name[len(HF_PREFIX) :]\n    dataset_name = dataset_name.split(\"--\")\n    if len(dataset_name) == 1:\n        return dataset_name[0], None\n    return dataset_name[0], dataset_name[1]\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This command downloads and lists Ludwig-ready datasets.\",\n        prog=\"ludwig datasets\",\n        usage=\"%(prog)s [options]\",\n    )\n    sub_parsers = parser.add_subparsers(dest=\"command\", help=\"download and list datasets\")\n\n    parser_download = sub_parsers.add_parser(\"download\", help=\"download a dataset\")\n    parser_download.add_argument(\"dataset\", help=\"dataset to download\")\n    parser_download.add_argument(\n        \"-o\",\n        \"--output_dir\",\n        type=str,\n        default=\".\",\n        help=\"output directory to download into\",\n        required=False,\n    )\n\n    sub_parsers.add_parser(\"list\", help=\"list datasets\")\n\n    parser_describe = sub_parsers.add_parser(\"describe\", help=\"describe datasets\")\n    parser_describe.add_argument(\"dataset\", help=\"dataset to describe\")\n\n    args = parser.parse_args(sys_argv)\n    print_ludwig(f\"Datasets {args.command}\", LUDWIG_VERSION)\n\n    if args.command == \"list\":\n        datasets = list_datasets()\n        for ds in datasets:\n            print(ds)\n    elif args.command == \"describe\":\n        print(describe_dataset(args.dataset))\n    elif args.command == \"download\":\n        download_dataset(args.dataset, args.output_dir)\n    else:\n        raise ValueError(f\"Unrecognized command: {args.command}\")\n\n\ndef __getattr__(name: str) -> Any:\n    \"\"\"Module-level __getattr__ allows us to return an instance of a class.  For example:\n\n         from ludwig.datasets import titanic\n\n    returns an instance of DatasetLoader configured to load titanic.\n\n    If you want to download a dataset in a non-default ludwig cache directory, there are two options:\n        1. set the LUDWIG_CACHE environment variable to your desired path before importing the dataset\n        2. Use ludwig.datasets.get_dataset(dataset_name, cache_dir=<CACHE_DIR>)\n    \"\"\"\n    public_methods = {\n        \"list_datasets\",\n        \"describe_dataset\",\n        \"download_dataset\",\n        \"cli\",\n        \"get_dataset\",\n        \"model_configs_for_dataset\",\n    }\n    if name in public_methods:\n        return globals()[name]\n    return get_dataset(name)\n"
  },
  {
    "path": "ludwig/datasets/archives.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport gzip\nimport logging\nimport os\nimport shutil\nimport tarfile\nfrom enum import Enum\nfrom zipfile import ZipFile\n\nfrom ludwig.utils.fs_utils import upload_output_directory\n\nlogger = logging.getLogger(__name__)\n\n\nclass ArchiveType(str, Enum):\n    \"\"\"The type of file archive.\"\"\"\n\n    UNKNOWN = \"unknown\"\n    ZIP = \"zip\"\n    GZIP = \"gz\"\n    TAR = \"tar\"\n    TAR_ZIP = \"tar.z\"\n    TAR_BZ2 = \"tar.bz2\"\n    TAR_GZ = \"tar.gz\"\n\n\ndef infer_archive_type(archive_path):\n    \"\"\"Try to infer archive type from file extension.\"\"\"\n    # Get the path extension including multiple extensions, ex. \".tar.gz\"\n    extension = \".\".join([\"\", *os.path.basename(archive_path).split(\".\")[1:]])\n    extension = extension.lower()\n    if extension.endswith(\".tar.z\") or extension.endswith(\".tar.zip\"):\n        return ArchiveType.TAR_ZIP\n    elif extension.endswith(\".tar.bz2\") or extension.endswith(\".tbz2\"):\n        return ArchiveType.TAR_BZ2\n    elif extension.endswith(\".tar.gz\") or extension.endswith(\".tgz\"):\n        return ArchiveType.TAR_GZ\n    elif extension.endswith(\".tar\"):\n        return ArchiveType.TAR\n    elif extension.endswith(\".zip\") or extension.endswith(\".zipx\"):\n        return ArchiveType.ZIP\n    elif extension.endswith(\".gz\") or extension.endswith(\".gzip\"):\n        return ArchiveType.GZIP\n    else:\n        return ArchiveType.UNKNOWN\n\n\ndef is_archive(path):\n    \"\"\"Does this path a supported archive type.\"\"\"\n    return infer_archive_type(path) != ArchiveType.UNKNOWN\n\n\ndef list_archive(archive_path, archive_type: ArchiveType | None = None) -> list[str]:\n    \"\"\"Return list of files extracted in an archive (without extracting them).\"\"\"\n    if archive_type is None:\n        archive_type = infer_archive_type(archive_path)\n    if archive_type == ArchiveType.UNKNOWN:\n        logger.error(\n            f\"Could not infer type of archive {archive_path}.  May be an unsupported archive type.\"\n            \"Specify archive_type in the dataset config if this file has an unknown file extension.\"\n        )\n        return []\n    if archive_type == ArchiveType.ZIP:\n        with ZipFile(archive_path) as zfile:\n            return zfile.namelist()\n    elif archive_type == ArchiveType.GZIP:\n        return [\".\".join(archive_path.split(\".\")[:-1])]  # Path minus the .gz extension\n    elif archive_type in {ArchiveType.TAR, ArchiveType.TAR_ZIP, ArchiveType.TAR_BZ2, ArchiveType.TAR_GZ}:\n        with tarfile.open(archive_path) as tar_file:\n            return tar_file.getnames()\n    else:\n        logger.error(f\"Unsupported archive: {archive_path}\")\n    return []\n\n\ndef extract_archive(archive_path: str, archive_type: ArchiveType | None = None) -> list[str]:\n    \"\"\"Extracts files from archive (into the same directory), returns a list of extracted files.\n\n    Args:\n        archive_path - The full path to the archive.\n\n    Returns A list of the files extracted.\n    \"\"\"\n    if archive_type is None:\n        archive_type = infer_archive_type(archive_path)\n    if archive_type == ArchiveType.UNKNOWN:\n        logger.error(\n            f\"Could not infer type of archive {archive_path}.  May be an unsupported archive type.\"\n            \"Specify archive_type in the dataset config if this file has an unknown file extension.\"\n        )\n        return []\n    archive_directory = os.path.dirname(archive_path)\n    directory_contents_before = os.listdir(archive_directory)\n    with upload_output_directory(archive_directory) as (tmpdir, _):\n        if archive_type == ArchiveType.ZIP:\n            with ZipFile(archive_path) as zfile:\n                zfile.extractall(tmpdir)\n        elif archive_type == ArchiveType.GZIP:\n            gzip_content_file = \".\".join(archive_path.split(\".\")[:-1])  # Path minus the .gz extension\n            with gzip.open(archive_path) as gzfile:\n                with open(os.path.join(tmpdir, gzip_content_file), \"wb\") as output:\n                    shutil.copyfileobj(gzfile, output)\n        elif archive_type in {ArchiveType.TAR, ArchiveType.TAR_ZIP, ArchiveType.TAR_BZ2, ArchiveType.TAR_GZ}:\n            with tarfile.open(archive_path) as tar_file:\n\n                def is_within_directory(directory, target):\n                    abs_directory = os.path.abspath(directory)\n                    abs_target = os.path.abspath(target)\n\n                    prefix = os.path.commonprefix([abs_directory, abs_target])\n\n                    return prefix == abs_directory\n\n                def safe_extract(tar, path=\".\", members=None, *, numeric_owner=False):\n                    for member in tar.getmembers():\n                        member_path = os.path.join(path, member.name)\n                        if not is_within_directory(path, member_path):\n                            raise Exception(\"Attempted Path Traversal in Tar File\")\n\n                    tar.extractall(path, members, numeric_owner=numeric_owner)\n\n                safe_extract(tar_file, path=tmpdir)\n        else:\n            logger.error(f\"Unsupported archive: {archive_path}\")\n    directory_contents_after = set(os.listdir(archive_directory))\n    return directory_contents_after.difference(directory_contents_before)\n"
  },
  {
    "path": "ludwig/datasets/configs/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/datasets/configs/adult_census_income.yaml",
    "content": "version: 1.0\nname: adult_census_income\ndownload_urls:\n  - https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data\n  - https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.test\ntrain_filenames: adult.data\ntest_filenames: adult.test\nsha256:\n    adult.data: 5b00264637dbfec36bdeaab5676b0b309ff9eb788d63554ca0a249491c86603d\n    adult.test: a2a9044bc167a35b2361efbabec64e89d69ce82d9790d2980119aac5fd7e9c05\nloader: adult_census_income.AdultCensusIncomeLoader\ndescription: |\n  Predict whether income exceeds $50K/yr based on census data\n  https://archive.ics.uci.edu/ml/datasets/adult\ncolumns:\n  - name: age\n    type: number\n  - name: workclass\n    type: category\n  - name: fnlwgt\n    type: category\n  - name: education\n    type: category\n  - name: education-num\n    type: category\n  - name: marital-status\n    type: category\n  - name: occupation\n    type: category\n  - name: relationship\n    type: category\n  - name: race\n    type: category\n  - name: sex\n    type: category\n  - name: capital-gain\n    type: number\n  - name: capital-loss\n    type: number\n  - name: hours-per-week\n    type: number\n  - name: native-country\n    type: category\n  - name: income\n    type: category\noutput_features:\n    - name: income\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/ae_price_prediction.yaml",
    "content": "version: 1.0\nname: ae_price_prediction\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/ae_price_prediction/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/ae_price_prediction/test.pq\nsha256:\n    test.pq: d05242580e011f3ac5a1a8f0069fd7788ceeacd6b2fb00ca7f409991f998c95e\n    train.pq: 181cfebbedd5c6e2bdc6261706103edddfc6eeb4604b8c6ffdc3d084a6e09a4e\ntrain_filenames: train.pq\ntest_filenames: test.pq\ndescription: |\n  Innerwear Data from Victoria's Secret and Others\n  600,000+ innerwear product data extracted from popular retail sites\n  https://www.kaggle.com/PromptCloudHQ/innerwear-data-from-victorias-secret-and-others\ncolumns:\n  - name: product_name\n    type: category\n  - name: mrp\n    type: category\n  - name: price\n    type: number\n  - name: pdp_url\n    type: category\n  - name: brand_name\n    type: category\n  - name: product_category\n    type: category\n  - name: retailer\n    type: category\n  - name: description\n    type: text\n  - name: rating\n    type: number\n  - name: review_count\n    type: number\n  - name: style_attributes\n    type: set\n  - name: total_sizes\n    type: set\n  - name: available_size\n    type: set\n  - name: color\n    type: category\noutput_features:\n  - name: price\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/agnews.yaml",
    "content": "version: 1.0\nname: agnews\ndownload_urls:\n  - https://raw.githubusercontent.com/mhjabreel/CharCnn_Keras/master/data/ag_news_csv/train.csv\n  - https://raw.githubusercontent.com/mhjabreel/CharCnn_Keras/master/data/ag_news_csv/test.csv\ntrain_filenames: train.csv\ntest_filenames: test.csv\nsha256:\n  test.csv: 521465c2428ed7f02f8d6db6ffdd4b5447c1c701962353eb2c40d548c3c85699\n  train.csv: 76a0a2d2f92b286371fe4d4044640910a04a803fdd2538e0f3f29a5c6f6b672e\nloader: agnews.AGNewsLoader\ndescription: |\n  News articles categorized as \"World\", \"Sports\", \"Business\", and \"Science\".\ncolumns:\n  - name: class_index\n    type: category\n  - name: title\n    type: text\n  - name: description\n    type: text\noutput_features:\n  - name: class_index\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/allstate_claims_severity.yaml",
    "content": "version: 1.0\nname: allstate_claims_severity\nkaggle_competition: allstate-claims-severity\narchive_filenames: allstate-claims-severity.zip\nsha256:\n  allstate-claims-severity.zip: 165f7b4bc5ed40f43656dc958da6572143a7e126e2d37bcd41f1299bfbaa68e2\ntrain_filenames: train.csv\ntest_filenames: test.csv\nloader: allstate_claims_severity.AllstateClaimsSeverityLoader\ndescription: |\n  Allstate Claims Severity.\n  https://www.kaggle.com/c/allstate-claims-severity/overview\noutput_features:\n  - name: loss\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/alpaca.yaml",
    "content": "version: 1.0\nname: alpaca\ndownload_urls: https://raw.githubusercontent.com/tatsu-lab/stanford_alpaca/main/alpaca_data.json\ndataset_filenames: alpaca_data.json\ndescription: |\n  Stanford Alpaca instruction-tuning dataset (https://github.com/tatsu-lab/stanford_alpaca) for LLM fine-tuning.\ncolumns:\n  - name: instruction\n    type: text\n  - name: input\n    type: text\n  - name: output\n    type: text\noutput_features:\n  - name: output\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/amazon_employee_access_challenge.yaml",
    "content": "version: 1.0\nname: amazon_employee_access_challenge\nkaggle_competition: amazon-employee-access-challenge\narchive_filenames: amazon-employee-access-challenge.zip\ntrain_filenames: train.csv\ntest_filenames: test.csv\nsha256:\n  amazon-employee-access-challenge.zip: bba1cf24bc01f390e7faf3f9cdbebd6267c875d51a36a2c625ce66e0c3e71db7\ndescription: |\n  There is a considerable amount of data regarding an employee’s role within an organization and the resources to which\n  they have access. Given the data related to current employees and their provisioned access, models can be built that\n  automatically determine access privileges as employees enter and leave roles within a company.\n  https://www.kaggle.com/c/amazon-employee-access-challenge\noutput_features:\n    - name: ACTION\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/amazon_review_polarity.yaml",
    "content": "version: 1.0\nname: amazon_review_polarity\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/amazon_review_polarity_csv.tgz\ntrain_filenames: amazon_review_polarity_csv/train.csv\ntest_filenames: amazon_review_polarity_csv/test.csv\nsha256:\n  amazon_review_polarity_csv.tgz: d2a3ee7a214497a5d1b8eaed7c8d7ba2737de00ada3b0ec46243983efa100361\ndescription: |\n  The Amazon Reviews Polarity dataset\n    Details:\n        34,686,770 Amazon reviews from 6,643,669 users on 2,441,053\n        products, from the Stanford Network Analysis Project (SNAP).\n        This dataset contains 600,000 training samples and 130,000\n        testing samples in each class.\n    Dataset source:\n        Character-level Convolutional Networks for Text Classification\n        Xiang Zhang et al., 2015\ncolumns:\n  - name: label\n    type: binary\n  - name: review_title\n    type: text\n  - name: review_text\n    type: text\noutput_features:\n  - name: label\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/amazon_reviews.yaml",
    "content": "version: 1.0\nname: amazon_reviews\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/amazon_review_full_csv.tgz\ntrain_filenames: amazon_review_full_csv/train.csv\ntest_filenames: amazon_review_full_csv/test.csv\nsha256:\n  amazon_review_full_csv.tgz: 4af62eeee139d0142e0747340b68646d23483d9475c33ea0641ee9175b423443\ndescription: |\n  The Amazon Reviews dataset\n  Details:\n    34,686,770 Amazon reviews from 6,643,669 users on 2,441,053\n    products, from the Stanford Network Analysis Project (SNAP).\n    This dataset contains 600,000 training samples and 130,000\n    testing samples in each class.\n  Dataset source:\n    Character-level Convolutional Networks for Text Classification\n    Xiang Zhang et al., 2015\ncolumns:\n  - name: label\n    type: category\n  - name: review_title\n    type: text\n  - name: review_text\n    type: text\noutput_features:\n  - name: label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/ames_housing.yaml",
    "content": "version: 1.0\nname: ames_housing\nkaggle_competition: house-prices-advanced-regression-techniques\narchive_filenames: house-prices-advanced-regression-techniques.zip\ntrain_filenames: train.csv\ntest_filenames: test.csv\nsha256:\n  house-prices-advanced-regression-techniques.zip: 65f769a9157a2581671957ed08da8a8162d53e67b4e9970ee856b634deb11d9f\ndescription: |\n  The Ames Housing dataset.\n  https://www.kaggle.com/c/house-prices-advanced-regression-techniques\noutput_features:\n  - name: SalePrice\n    type: number\nfallback_mirrors:\n  - name: predibase\n    download_paths: s3://ludwig-tests/ludwig_backup/house-prices-advanced-regression-techniques.zip\n"
  },
  {
    "path": "ludwig/datasets/configs/bbcnews.yaml",
    "content": "version: 1.0\nname: bbcnews\nkaggle_competition: learn-ai-bbc\narchive_filenames: learn-ai-bbc.zip\ntrain_filenames: \"BBC News Train.csv\"\ntest_filenames: \"BBC News Test.csv\"\nsha256:\n  learn-ai-bbc.zip: 450dd79c6654248af15d91d94c269fe7e8001effd89389f93c7184aac6699e62\ndescription: |\n  BBC News Classification from Kaggle.\n  https://www.kaggle.com/competitions/learn-ai-bbc/overview\noutput_features:\n    - name: Category\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/bnp_claims_management.yaml",
    "content": "version: 1.0\nname: bnp_claims_management\nkaggle_competition: bnp-paribas-cardif-claims-management\narchive_filenames: bnp-paribas-cardif-claims-management.zip\ntrain_filenames: train.csv\ntest_filenames: test.csv\nsha256:\n  bnp-paribas-cardif-claims-management.zip: c01a11ceae565bc95ec30a1ef4c9ffe4aa27e07d6e433776e90a4d5474f3e95d\ndescription: |\n  The BNP Paribas Cardif Claims Management dataset.\n  https://www.kaggle.com/c/bnp-paribas-cardif-claims-management\noutput_features:\n    - name: target\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/bookprice_prediction.yaml",
    "content": "version: 1.0\nname: bookprice_prediction\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_price_of_books/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_price_of_books/test.csv\nsha256:\n  test.csv: 75bcc853efe734a53764127428e005bb9eb7585ad3dc1dce2eb284fa04313c1b\n  train.csv: dd978b591e623f9c5d4f9ade0f237200597afcad2c6417eb1e764698f1afcfcf\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Here we explore a database of books of different genres, from thousands of authors.\n  In this challenge, participants are required to use the dataset to build a\n  Machine Learning model to predict the price of books based on a given set of features.\n  https://machinehack.com/hackathons/predict_the_price_of_books/overview\ncolumns:\n  - name: Title\n    type: category\n  - name: Author\n    type: category\n  - name: Edition\n    type: category\n  - name: Reviews\n    type: number\n  - name: Ratings\n    type: number\n  - name: Synopsis\n    type: text\n  - name: Genre\n    type: category\n  - name: BookCategory\n    type: category\n  - name: Price\n    type: number\noutput_features:\n  - name: Price\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/california_house_price.yaml",
    "content": "version: 1.0\nname: california_house_price\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/kaggle-california-house-prices/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/kaggle-california-house-prices/test.csv\nsha256:\n  test.csv: b5bb9ed6e56cbdd0a410e186a19c6fe137c2ffbb50ba6b0808540434a8123dc6\n  train.csv: 907d45804e622fb136a9d55bde97269f421fb9b8f7c9f34416672cf7078ee94b\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Predict house sale prices based on the house information, such as # of bedrooms,\n  living areas, locations, near-by schools, and the seller summary. The data consist\n  of houses sold in California in 2020, with houses in the test dataset sold after\n  the ones in the training dataset.\n  https://www.kaggle.com/c/california-house-prices\ncolumns:\n  - name: Address\n    type: category\n  - name: Sold Price\n    type: number\n  - name: Summary\n    type: text\n  - name: Type\n    type: category\n  - name: Year built\n    type: number\n  - name: Heating\n    type: category\n  - name: Cooling\n    type: category\n  - name: Parking\n    type: category\n  - name: Lot\n    type: number\n  - name: Bedrooms\n    type: number\n  - name: Bathrooms\n    type: number\n  - name: Full bathrooms\n    type: number\n  - name: Total interior livable area\n    type: number\n  - name: Total spaces\n    type: number\n  - name: Garage spaces\n    type: number\n  - name: Region\n    type: category\n  - name: Elementary School\n    type: category\n  - name: Elementary School Score\n    type: number\n  - name: Elementary School Distance\n    type: number\n  - name: Middle School\n    type: category\n  - name: Middle School Score\n    type: number\n  - name: Middle School Distance\n    type: number\n  - name: High School\n    type: category\n  - name: High School Score\n    type: number\n  - name: High School Distance\n    type: number\n  - name: Flooring\n    type: set\n  - name: Heating features\n    type: set\n  - name: Cooling features\n    type: set\n  - name: Appliances included\n    type: set\n  - name: Laundry features\n    type: set\n  - name: Parking features\n    type: set\n  - name: Tax assessed value\n    type: number\n  - name: Annual tax amount\n    type: number\n  - name: Listed On\n    type: date\n  - name: Listed Price\n    type: number\n  - name: Last Sold On\n    type: date\n  - name: Last Sold Price\n    type: number\n  - name: City\n    type: category\n  - name: Zip\n    type: category\n  - name: State\n    type: category\noutput_features:\n  - name: Sold Price\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/camseq.yaml",
    "content": "version: 1.0\nname: camseq\nkaggle_dataset_id: carlolepelaars/camseq-semantic-segmentation\narchive_filenames: camseq-semantic-segmentation.zip\nsha256:\n   camseq-semantic-segmentation.zip: ea3aeba2661d9b3e3ea406668e7d9240cb2ba0c7e374914bb6d866147faff502\nloader: camseq.CamseqLoader\npreserve_paths:\n  - images\n  - masks\ndescription: |\n  CamSeq01 Cambridge Labeled Objects in Video\n  https://www.kaggle.com/datasets/carlolepelaars/camseq-semantic-segmentation\ncolumns:\n  - name: image_path\n    type: image\n  - name: mask_path\n    type: image\noutput_features:\n  - name: mask_path\n    type: image\n"
  },
  {
    "path": "ludwig/datasets/configs/code_alpaca.yaml",
    "content": "version: 1.0\nname: code_alpaca\ndownload_urls: https://raw.githubusercontent.com/sahil280114/codealpaca/master/data/code_alpaca_20k.json\ntrain_filenames: code_alpaca_20k.json\nloader: code_alpaca_loader.CodeAlpacaLoader\ndescription: |\n  This dataset, created by sahil280114, aims to build and share an instruction-following LLaMA model for code generation. The repo containing\n  this dataset is fully based on Stanford Alpaca, and only changes the data used for training.\ncolumns:\n  - name: instruction\n    type: text\n  - name: input\n    type: text\n  - name: output\n    type: text\noutput_features:\n  - name: output\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/connect4.yaml",
    "content": "version: 1.0\nname: connect4\nkaggle_dataset_id: tbrewer/connect-4\narchive_filenames: connect-4.zip\ndataset_filenames: c4_game_database.csv\nsha256:\n  connect-4.zip: 46c33c47f2664948a4abe53bafee92a602773f31db615bc8bd239e1f98a3d2cf\ndescription: |\n  Each row represents the end results of a Connect-4 game.\n  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.\n  Column 43 marks the winner of the game : -1, 1, and 0 for tie games.\ncolumns:\n  - name: pos_01\n    type: number\n  - name: pos_02\n    type: number\n  - name: pos_03\n    type: number\n  - name: pos_04\n    type: number\n  - name: pos_05\n    type: number\n  - name: pos_06\n    type: number\n  - name: pos_07\n    type: number\n  - name: pos_08\n    type: number\n  - name: pos_09\n    type: number\n  - name: pos_10\n    type: number\n  - name: pos_11\n    type: number\n  - name: pos_12\n    type: number\n  - name: pos_13\n    type: number\n  - name: pos_14\n    type: number\n  - name: pos_15\n    type: number\n  - name: pos_16\n    type: number\n  - name: pos_17\n    type: number\n  - name: pos_18\n    type: number\n  - name: pos_19\n    type: number\n  - name: pos_20\n    type: number\n  - name: pos_21\n    type: number\n  - name: pos_22\n    type: number\n  - name: pos_23\n    type: number\n  - name: pos_24\n    type: number\n  - name: pos_25\n    type: number\n  - name: pos_26\n    type: number\n  - name: pos_27\n    type: number\n  - name: pos_28\n    type: number\n  - name: pos_29\n    type: number\n  - name: pos_30\n    type: number\n  - name: pos_31\n    type: number\n  - name: pos_32\n    type: number\n  - name: pos_33\n    type: number\n  - name: pos_34\n    type: number\n  - name: pos_35\n    type: number\n  - name: pos_36\n    type: number\n  - name: pos_37\n    type: number\n  - name: pos_38\n    type: number\n  - name: pos_39\n    type: number\n  - name: pos_40\n    type: number\n  - name: pos_41\n    type: number\n  - name: pos_42\n    type: number\n  - name: winner\n    type: number\noutput_features:\n  - name: winner\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/consumer_complaints.yaml",
    "content": "version: 1.0\nname: consumer_complaints\nkaggle_dataset_id: selener/consumer-complaint-database\narchive_filenames: consumer-complaint-database.zip\ndataset_filenames: rows.csv\nloader: consumer_complaints_loader.ConsumerComplaintsLoader\ndescription: |\n  The dataset contains different information of complaints that customers have made about a multiple products and\n  services in the financial sector, such us Credit Reports, Student Loans, Money Transfer, etc. The date of each\n  complaint ranges from November 2011 to May 2019.\ncolumns:\n  - name: Date received\n    type: Date\n  - name: Product\n    type: text\n  - name: Sub-product\n    type: text\n  - name: Issue\n    type: text\n  - name: Sub-issue\n    type: text\n  - name: Consumer complaint narrative\n    type: text\n  - name: Company public response\n    type: text\n  - name: Company\n    type: text\n  - name: State\n    type: category\n  - name: ZIP code\n    type: category\n  - name: Tags\n    type: category\n  - name: Consumer consent provided?\n    type: text\n  - name: Submitted via\n    type: category\n  - name: Date sent to company\n    type: date\n  - name: Company response to consumer\n    type: text\n  - name: Timely response?\n    type: binary\n  - name: Consumer disputed?\n    type: binary\n  - name: Complaint ID\n    type: number\noutput_features:\n  - name: Issue\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/consumer_complaints_generation.yaml",
    "content": "version: 1.0\nname: consumer_complaints_generation\ndownload_urls: https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/datasets/consumer_complaints_gen_tutorial.csv\ntrain_filenames: consumer_complaints_gen_tutorial.csv\ndescription: |\n  The dataset contains different information of complaints that customers have made about a multiple products and\n  services in the financial sector, such us Credit Reports, Student Loans, Money Transfer, etc. The date of each\n  complaint ranges from November 2011 to May 2019. The dataset has been modified to be used for text generation.\n  We have added a structured JSON field that contains a company generated response to the raised complaint. The idea\n  is to fine-tune an LLM to generate this output JSON field.\ncolumns:\n  - name: Complaint ID\n    type: number\n  - name: Date received\n    type: Date\n  - name: Product\n    type: text\n  - name: Issue\n    type: text\n  - name: Complaint\n    type: text\n  - name: Company Response\n    type: text\n  - name: Structured JSON Output\n    type: text\noutput_features:\n  - name: Structured JSON Output\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/creditcard_fraud.yaml",
    "content": "version: 1.0\nname: creditcard_fraud\nkaggle_dataset_id: mlg-ulb/creditcardfraud\narchive_filenames: creditcardfraud.zip\nsha256:\n  creditcardfraud.zip: a0360ce715992212e9ac72d8ccdca97f4be87dc1fdf2bed011358f7ab409a28a\nloader: creditcard_fraud.CreditCardFraudLoader\ndescription: |\n  The Machine Learning Group ULB Dataset\n  https://www.kaggle.com/datasets/mlg-ulb/creditcardfraud\ncolumns:\n  - name: Time\n    type: number\n  - name: V1\n    type: number\n  - name: V2\n    type: number\n  - name: V3\n    type: number\n  - name: V4\n    type: number\n  - name: V5\n    type: number\n  - name: V6\n    type: number\n  - name: V7\n    type: number\n  - name: V8\n    type: number\n  - name: V9\n    type: number\n  - name: V10\n    type: number\n  - name: V11\n    type: number\n  - name: V12\n    type: number\n  - name: V13\n    type: number\n  - name: V14\n    type: number\n  - name: V15\n    type: number\n  - name: V16\n    type: number\n  - name: V17\n    type: number\n  - name: V18\n    type: number\n  - name: V19\n    type: number\n  - name: V20\n    type: number\n  - name: V21\n    type: number\n  - name: V22\n    type: number\n  - name: V23\n    type: number\n  - name: V24\n    type: number\n  - name: V25\n    type: number\n  - name: V26\n    type: number\n  - name: V27\n    type: number\n  - name: V28\n    type: number\n  - name: Amount\n    type: number\n  - name: Class\n    type: number\noutput_features:\n  - name: Class\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/customer_churn_prediction.yaml",
    "content": "version: 1.0\nname: customer_churn_prediction\nkaggle_competition: customer-churn-prediction-2020\narchive_filenames: customer-churn-prediction-2020.zip\ntrain_filenames: train.csv\ntest_filenames: test.csv\nsha256:\n  customer-churn-prediction-2020.zip: fb5cbc787081a6a559592230c657a0520a181447da6eb2adc34a3aebbe8ed9ca\ndescription: |\n  Dataset from a Kaggle competition that is about predicting whether a customer will change\n  telecommunications provider, something known as \"churning\".\n  https://www.kaggle.com/c/customer-churn-prediction-2020\noutput_features:\n  - name: churn\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/data_scientist_salary.yaml",
    "content": "version: 1.0\nname: data_scientist_salary\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_data_scientists_salary_in_india_hackathon/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_competitions/predict_the_data_scientists_salary_in_india_hackathon/test.csv\nsha256:\n  test.csv: 244c215f4a03cae4b107e76c7fe94269728450cabf44c943415211ce7d6437df\n  train.csv: 99d6aa80505ac1311e97f402d5723996119e859c7f3fce261350462148debe3d\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  The training data and test data comprise of 19802 samples and of 6601 samples each from the\n  Analytics India Annual Salary Study.\n  https://machinehack.com/hackathons/predict_the_data_scientists_salary_in_india_hackathon/overview\ncolumns:\n  - name: experience\n    type: category\n  - name: job_description\n    type: text\n  - name: job_desig\n    type: category\n  - name: job_type\n    type: category\n  - name: key_skills\n    type: set\n  - name: location\n    type: category\n  - name: salary\n    type: category\noutput_features:\n  - name: salary\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/dbpedia.yaml",
    "content": "version: 1.0\nname: dbpedia\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/dbpedia_csv.tgz\ntrain_filenames: dbpedia_csv/train.csv\ntest_filenames: dbpedia_csv/test.csv\nsha256:\n  dbpedia_csv.tgz: 42db5221ddedddb673a4cabcc5f3a7d869714c878bcfe4ba94b29d14aa38e417\ndescription: |\n  The DBPedia Ontology dataset.\n\n  Details:\n      40,000 training samples and 5,000 testing samples from 14\n      nonoverlapping classes from DBpedia 2014.\n  Dataset source:\n      Character-level Convolutional Networks for Text Classification\n      Xiang Zhang et al., 2015\ncolumns:\n  - name: label\n    type: category\n  - name: title\n    type: category\n  - name: content\n    type: text\noutput_features:\n  - name: label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/electricity.yaml",
    "content": "version: 1.0\nname: electricity\ndownload_urls: https://raw.githubusercontent.com/nimz/electricity_demand/master/elecdemand.csv\nsha256:\n  elecdemand.csv: 4fd3c8a4b8168f34703b55313c5341f8e8385810a54f1a1cdf6987c1904c9698\ndescription: |\n  Electricity demand dataset. Half-hourly electricity demand in Victoria, Australia during 2014, along with\n  Melbourne temperatures.\n\n  Source textbook:\n  Forecasting: Principles and Practice\n      Rob J Hyndman and George Athanasopoulos\ncolumns:\n    - name: Demand\n      type: number\n    - name: WorkDay\n      type: binary\n    - name: Temperature\n      type: number\noutput_features:\n  - name: Demand\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/ethos_binary.yaml",
    "content": "version: 1.1\nname: ethos_binary\ndownload_urls:\n  - https://raw.githubusercontent.com/intelligence-csd-auth-gr/Ethos-Hate-Speech-Dataset/master/ethos/ethos_data/Ethos_Dataset_Binary.csv\nsha256:\n  Ethos_Dataset_Binary.csv: 0cd0050c2592afcb5eca5876df485ca15cda9d7d16fe32c269857260fd10d96c\nloader: ethos_binary.EthosBinaryLoader\ndescription: |\n  The Ethos Hate Speech Dataset.\n\n  Source Paper:\n      ETHOS: an Online Hate Speech Detection Dataset\n          Ioannis Mollas and Zoe Chrysopoulou and Stamatis Karlos and\n          Grigorios Tsoumakas\ncolumns:\n  - name: comment\n    type: text\n  - name: isHate\n    type: binary\noutput_features:\n  - name: isHate\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/fake_job_postings2.yaml",
    "content": "version: 1.0\nname: fake_job_postings2\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/fake_job_postings2/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/fake_job_postings2/test.csv\nsha256:\n  test.csv: a5296f49129d440434e6274bb892a1320fe1dd4c26d5a1b085786d5ea1133dd8\n  train.csv: b6568e415ad49cb7bd23848dfbb8d381f9de590e133a5075abbf4c1a7c7c1711\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  This dataset contains 18K job descriptions out of which about 800 are fake.\n  The data consists of both textual information and meta-information about the jobs.\n  This dataset is \"fake_job_postings2\" in the AutoGluon paper.\n  https://www.kaggle.com/datasets/shivamb/real-or-fake-fake-jobposting-prediction\ncolumns:\n  - name: title\n    type: category\n  - name: salary_range\n    type: category\n  - name: description\n    type: text\n  - name: required_experience\n    type: category\n  - name: required_education\n    type: category\n  - name: fraudulent\n    type: binary\noutput_features:\n  - name: fraudulent\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/fever.yaml",
    "content": "version: 1.0\nname: fever\ndownload_urls:\n  - https://fever.ai/download/fever/train.jsonl\n  - https://fever.ai/download/fever/paper_dev.jsonl\n  - https://fever.ai/download/fever/paper_test.jsonl\nsha256:\n  train.jsonl: eba7e8f87076753f8494718b9a857827af7bf73e76c9e4b75420207d26e588b6\n  paper_test.jsonl: fb7b0280a0adc2302bbb29bfb7af37274fa585de3171bcf908f180642d11d88e\n  paper_dev.jsonl: 41158707810008747946bf23471e82df53e77a513524b9e3ec1c2e674ef5ef8c\ntrain_filenames: train.jsonl\ntest_filenames: paper_test.jsonl\nvalidation_filenames: paper_dev.jsonl\ncolumn_types:\n  evidence: str\ndescription: |\n  FEVER: a Large-scale Dataset for Fact Extraction and VERification\ncolumns:\n  - name: id\n    type: category\n  - name: verifiable\n    type: category\n  - name: label\n    type: category\n  - name: label\n    type: category\n  - name: claim\n    type: text\n  - name: evidence\n    type: category\n  - name: label\n    type: category\noutput_features:\n  - name: label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/flickr8k.yaml",
    "content": "version: 1.0\nname: flickr8k\ndownload_urls:\n  - https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_Dataset.zip\n  - https://github.com/jbrownlee/Datasets/releases/download/Flickr8k/Flickr8k_text.zip\ndataset_filenames: flickr8k_dataset.csv\npreserve_paths: Flicker8k_Dataset\nsha256:\n  Flickr8k_Dataset.zip: 61e4b111d32b24a55b69dafd91f4c3aec07391b7b9217face15dd35d517fe6de\n  Flickr8k_text.zip: 4992ddc8110e9aa49da5bf698522b0c8f11c448814a488584ee6bf040e5137e7\nloader: flickr8k.Flickr8kLoader\ndescription: |\n  A new benchmark collection for sentence-based image description and search,\n  consisting of 8,000 images that are each paired with five different\n  captions which provide clear descriptions of the salient entities and\n  events. The images were chosen from six different Flickr groups, and tend\n  not to contain any well-known people or locations, but were manually\n  selected to depict a variety of scenes and situations.\noutput_features:\n  - name: caption0\n    type: text\n  - name: caption1\n    type: text\n  - name: caption2\n    type: text\n  - name: caption3\n    type: text\n  - name: caption4\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/forest_cover.yaml",
    "content": "version: 1.0\nname: forest_cover\ndownload_urls: https://archive.ics.uci.edu/ml/machine-learning-databases/covtype/covtype.data.gz\nsha256:\n  covtype.data.gz: 614360d0257557dd1792834a85a1cdebfadc3c4f30b011d56afee7ffb5b15771\ndataset_filenames: covtype.data\nloader: forest_cover.ForestCoverLoader\ndescription: |\n  The Forest Cover Type dataset.\n  Predicting forest cover type from cartographic variables only.\n  https://archive.ics.uci.edu/ml/datasets/covertype\ncolumns:\n  - name: Elevation\n    type: number\n  - name: Aspect\n    type: number\n  - name: Slope\n    type: number\n  - name: Horizontal_Distance_To_Hydrology\n    type: number\n  - name: Vertical_Distance_To_Hydrology\n    type: number\n  - name: Horizontal_Distance_To_Roadways\n    type: number\n  - name: Hillshade_9am\n    type: number\n  - name: Hillshade_Noon\n    type: number\n  - name: Hillshade_3pm\n    type: number\n  - name: Horizontal_Distance_To_Fire_Points\n    type: number\n  - name: Wilderness_Area\n    type: category\n  - name: Soil_Type\n    type: category\n  - name: Cover_Type\n    type: category\noutput_features:\n  - name: Cover_Type\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/goemotions.yaml",
    "content": "version: 1.0\nname: goemotions\ndownload_urls:\n  - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/train.tsv\n  - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/dev.tsv\n  - https://raw.githubusercontent.com/google-research/google-research/master/goemotions/data/test.tsv\ntrain_filenames: train.tsv\nvalidation_filenames: dev.tsv\ntest_filenames: test.tsv\nsha256:\n  train.tsv: 1c254a142be5c00e80d819b9ae1bbd36d94b2eeb8f4b1271846508d57e57d9c5\n  dev.tsv: 575489c079c9de1097062a01738f998590d6b7ead66dd1c9fd1d2ba01fd8bc62\n  test.tsv: 0587b2dd8b27b97352adbfc3fb083d46005c8946657fdc2b1ca8b1cc7f1f8be4\nloader: goemotions.GoEmotionsLoader\ndescription: |\n  GoEmotions: A Dataset for Fine-Grained Emotion Classification.\n  https://ai.googleblog.com/2021/10/goemotions-dataset-for-fine-grained.html\ncolumns:\n  - name: text\n    type: text\n  - name: emotion_ids\n    type: category\n  - name: comment_id\n    type: category\noutput_features:\n  - name: emotion_ids\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/goodbooks_books.yaml",
    "content": "version: 1.0\nname: goodbooks_books\ndownload_urls:\n  - https://github.com/zygmuntz/goodbooks-10k/releases/download/v1.0/goodbooks-10k.zip\nsha256:\n  goodbooks-10k.zip: 261b97b56db61f3fb2ce5aadbb13704d30179fcc986c17ace665a0af9ed00731\ndataset_filenames: books.csv\ndescription: |\n  goodbooks_books is a multimodal dataset of 10K books, taken from the goodreads dataset.\n  The Goodbooks-10K dataset contains six million ratings for ten thousand most popular (with most ratings) books.\n  The dataset also contains:\n    books marked to read by the users\n    book metadata (author, year, etc.)\n    tags/shelves/genres\n  https://github.com/zygmuntz/goodbooks-10k\ncolumns:\n  - name: book_id\n    type: category\n  - name: goodreads_book_id\n    type: category\n  - name: best_book_id\n    type: category\n  - name: work_id\n    type: category\n  - name: books_count\n    type: number\n  - name: isbn\n    type: category\n  - name: isbn13\n    type: category\n  - name: authors\n    type: category\n  - name: original_publication_year\n    type: category\n  - name: original_title\n    type: category\n  - name: title\n    type: category\n  - name: language_code\n    type: category\n  - name: average_rating\n    type: number\n  - name: ratings_count\n    type: number\n  - name: work_ratings_count\n    type: number\n  - name: work_text_reviews_count\n    type: number\n  - name: ratings_1\n    type: number\n  - name: ratings_2\n    type: number\n  - name: ratings_3\n    type: number\n  - name: ratings_4\n    type: number\n  - name: ratings_5\n    type: number\n  - name: image_url\n    type: image\n  - name: small_image_url\n    type: image\noutput_features:\n  - name: average_rating\n    type: number\n  - name: ratings_1\n    type: number\n  - name: ratings_2\n    type: number\n  - name: ratings_3\n    type: number\n  - name: ratings_4\n    type: number\n  - name: ratings_5\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/google_qa_answer_type_reason_explanation.yaml",
    "content": "version: 1.0\nname: google_qa_answer_type_reason_explanation\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq\nsha256:\n  train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a\n  dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84\ntrain_filenames: train.pq\ntest_filenames: dev.pq\ndescription: |\n  Google QUEST Q&A Labeling\n  Improving automated understanding of complex question answer content.\n  The data for this competition includes questions and answers from various StackExchange properties.\n  https://www.kaggle.com/c/google-quest-challenge/data\n  Note: this is the same dataset as `google_quest_qa`. It is duplicated here to have a one-to-one mapping\n  with the benchmarking datasets in https://arxiv.org/pdf/2111.02705.pdf\n  In this paper, the column `answer_type_reason_explanation` is used as the output feature.\ncolumns:\n  - name: qa_id\n    type: category\n  - name: question_title\n    type: text\n  - name: question_body\n    type: text\n  - name: question_user_name\n    type: category\n  - name: question_user_page\n    type: category\n  - name: answer\n    type: text\n  - name: answer_user_name\n    type: category\n  - name: answer_user_page\n    type: category\n  - name: url\n    type: category\n  - name: category\n    type: category\n  - name: host\n    type: category\n  - name: question_asker_intent_understanding\n    type: number\n  - name: question_body_critical\n    type: number\n  - name: question_conversational\n    type: number\n  - name: question_expect_short_answer\n    type: number\n  - name: question_fact_seeking\n    type: number\n  - name: question_has_commonly_accepted_answer\n    type: number\n  - name: question_interestingness_others\n    type: number\n  - name: question_interestingness_self\n    type: number\n  - name: question_multi_intent\n    type: number\n  - name: question_not_really_a_question\n    type: number\n  - name: question_opinion_seeking\n    type: number\n  - name: question_type_choice\n    type: number\n  - name: question_type_compare\n    type: number\n  - name: question_type_consequence\n    type: number\n  - name: question_type_definition\n    type: number\n  - name: question_type_entity\n    type: number\n  - name: question_type_instructions\n    type: number\n  - name: question_type_procedure\n    type: number\n  - name: question_type_reason_explanation\n    type: number\n  - name: question_type_spelling\n    type: number\n  - name: question_well_written\n    type: number\n  - name: answer_helpful\n    type: number\n  - name: answer_level_of_information\n    type: number\n  - name: answer_plausible\n    type: number\n  - name: answer_relevance\n    type: number\n  - name: answer_satisfaction\n    type: number\n  - name: answer_type_instructions\n    type: number\n  - name: answer_type_procedure\n    type: number\n  - name: answer_type_reason_explanation\n    type: number\n  - name: answer_well_written\n    type: number\noutput_features:\n  - name: answer_type_reason_explanation\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/google_qa_question_type_reason_explanation.yaml",
    "content": "version: 1.0\nname: google_qa_question_type_reason_explanation\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq\nsha256:\n  train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a\n  dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84\ntrain_filenames: train.pq\ntest_filenames: dev.pq\ndescription: |\n  Google QUEST Q&A Labeling\n  Improving automated understanding of complex question answer content.\n  The data for this competition includes questions and answers from various StackExchange properties.\n  https://www.kaggle.com/c/google-quest-challenge/data\n  Note: this is the same dataset as `google_quest_qa`. It is duplicated here to have a one-to-one mapping\n  with the benchmarking datasets in https://arxiv.org/pdf/2111.02705.pdf\n  In this paper, the column `question_type_reason_explanation` is used as the output feature.\ncolumns:\n  - name: qa_id\n    type: category\n  - name: question_title\n    type: text\n  - name: question_body\n    type: text\n  - name: question_user_name\n    type: category\n  - name: question_user_page\n    type: category\n  - name: answer\n    type: text\n  - name: answer_user_name\n    type: category\n  - name: answer_user_page\n    type: category\n  - name: url\n    type: category\n  - name: category\n    type: category\n  - name: host\n    type: category\n  - name: question_asker_intent_understanding\n    type: number\n  - name: question_body_critical\n    type: number\n  - name: question_conversational\n    type: number\n  - name: question_expect_short_answer\n    type: number\n  - name: question_fact_seeking\n    type: number\n  - name: question_has_commonly_accepted_answer\n    type: number\n  - name: question_interestingness_others\n    type: number\n  - name: question_interestingness_self\n    type: number\n  - name: question_multi_intent\n    type: number\n  - name: question_not_really_a_question\n    type: number\n  - name: question_opinion_seeking\n    type: number\n  - name: question_type_choice\n    type: number\n  - name: question_type_compare\n    type: number\n  - name: question_type_consequence\n    type: number\n  - name: question_type_definition\n    type: number\n  - name: question_type_entity\n    type: number\n  - name: question_type_instructions\n    type: number\n  - name: question_type_procedure\n    type: number\n  - name: question_type_reason_explanation\n    type: number\n  - name: question_type_spelling\n    type: number\n  - name: question_well_written\n    type: number\n  - name: answer_helpful\n    type: number\n  - name: answer_level_of_information\n    type: number\n  - name: answer_plausible\n    type: number\n  - name: answer_relevance\n    type: number\n  - name: answer_satisfaction\n    type: number\n  - name: answer_type_instructions\n    type: number\n  - name: answer_type_procedure\n    type: number\n  - name: answer_type_reason_explanation\n    type: number\n  - name: answer_well_written\n    type: number\noutput_features:\n  - name: question_type_reason_explanation\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/google_quest_qa.yaml",
    "content": "version: 1.0\nname: google_quest_qa\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/dev.pq\n  - https://automl-mm-bench.s3.amazonaws.com/google_quest_qa/test.pq\nsha256:\n  test.pq: cb1bb5f32374d83ad4ef7feb4e443c9376cdd919cda40057732ef500e9a4ecf3\n  train.pq: 92274286ffb759c96bfca77001c10eb323b3531db3a0e178813db9b82e80a12a\n  dev.pq: 2e66450215b94dc404eadc7dde83a1eabad9640d946863c298aa2d42c998ed84\ntrain_filenames: train.pq\nvalidation_filenames: dev.pq\ntest_filenames: test.pq\ndescription: |\n  Google QUEST Q&A Labeling\n  Improving automated understanding of complex question answer content.\n  The data for this competition includes questions and answers from various StackExchange properties.\n  https://www.kaggle.com/c/google-quest-challenge/data\ncolumns:\n  - name: qa_id\n    type: category\n  - name: question_title\n    type: text\n  - name: question_body\n    type: text\n  - name: question_user_name\n    type: category\n  - name: question_user_page\n    type: category\n  - name: answer\n    type: text\n  - name: answer_user_name\n    type: category\n  - name: answer_user_page\n    type: category\n  - name: url\n    type: category\n  - name: category\n    type: category\n  - name: host\n    type: category\n  - name: question_asker_intent_understanding\n    type: number\n  - name: question_body_critical\n    type: number\n  - name: question_conversational\n    type: number\n  - name: question_expect_short_answer\n    type: number\n  - name: question_fact_seeking\n    type: number\n  - name: question_has_commonly_accepted_answer\n    type: number\n  - name: question_interestingness_others\n    type: number\n  - name: question_interestingness_self\n    type: number\n  - name: question_multi_intent\n    type: number\n  - name: question_not_really_a_question\n    type: number\n  - name: question_opinion_seeking\n    type: number\n  - name: question_type_choice\n    type: number\n  - name: question_type_compare\n    type: number\n  - name: question_type_consequence\n    type: number\n  - name: question_type_definition\n    type: number\n  - name: question_type_entity\n    type: number\n  - name: question_type_instructions\n    type: number\n  - name: question_type_procedure\n    type: number\n  - name: question_type_reason_explanation\n    type: number\n  - name: question_type_spelling\n    type: number\n  - name: question_well_written\n    type: number\n  - name: answer_helpful\n    type: number\n  - name: answer_level_of_information\n    type: number\n  - name: answer_plausible\n    type: number\n  - name: answer_relevance\n    type: number\n  - name: answer_satisfaction\n    type: number\n  - name: answer_type_instructions\n    type: number\n  - name: answer_type_procedure\n    type: number\n  - name: answer_type_reason_explanation\n    type: number\n  - name: answer_well_written\n    type: number\noutput_features:\n  - name: question_type_reason_explanation\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/higgs.yaml",
    "content": "version: 1.0\nname: higgs\ndownload_urls: https://archive.ics.uci.edu/ml/machine-learning-databases/00280/HIGGS.csv.gz\nsha256:\n  HIGGS.csv.gz: ea302c18164d4e3d916a1e2e83a9a8d07069fa6ebc7771e4c0540d54e593b698\ncolumn_types:\n  label: int32\nloader: higgs.HiggsLoader\ndescription: |\n  The Higgs Boson dataset.\n\n  This is a classification problem to distinguish between a signal process\n  which produces Higgs bosons and a background process which does not.\n\n  https://archive.ics.uci.edu/ml/datasets/HIGGS\ncolumns:\n  - name: label\n    type: binary\n  - name: lepton_pT\n    type: number\n  - name: lepton_eta\n    type: number\n  - name: lepton_phi\n    type: number\n  - name: missing_energy_magnitude\n    type: number\n  - name: missing_energy_phi\n    type: number\n  - name: jet_1_pt\n    type: number\n  - name: jet_1_eta\n    type: number\n  - name: jet_1_phi\n    type: number\n  - name: jet_1_b-tag\n    type: number\n  - name: jet_2_pt\n    type: number\n  - name: jet_2_eta\n    type: number\n  - name: jet_2_phi\n    type: number\n  - name: jet_2_b-tag\n    type: number\n  - name: jet_3_pt\n    type: number\n  - name: jet_3_eta\n    type: number\n  - name: jet_3_phi\n    type: number\n  - name: jet_3_b-tag\n    type: number\n  - name: jet_4_pt\n    type: number\n  - name: jet_4_eta\n    type: number\n  - name: jet_4_phi\n    type: number\n  - name: jet_4_b-tag\n    type: number\n  - name: m_jj\n    type: number\n  - name: m_jjj\n    type: number\n  - name: m_lv\n    type: number\n  - name: m_jlv\n    type: number\n  - name: m_bb\n    type: number\n  - name: m_wbb\n    type: number\n  - name: m_wwbb\n    type: number\noutput_features:\n  - name: label\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/hugging_face.yaml",
    "content": "version: 1.0\nname: hugging_face\nloader: hugging_face.HFLoader\ndescription: |\n  Hugging Face Datasets\n"
  },
  {
    "path": "ludwig/datasets/configs/ieee_fraud.yaml",
    "content": "version: 1.0\nname: ieee_fraud\nkaggle_competition: ieee-fraud-detection\narchive_filenames: ieee-fraud-detection.zip\nsha256:\n  ieee-fraud-detection.zip: 4cc646da09d0a9b265983ffed775b1f9ee15af5266586df610e04d6adae0b829\ntrain_filenames:\n  - train_identity.csv\n  - train_transaction.csv\ntest_filenames:\n  - test_identity.csv\n  - test_transaction.csv\nloader: ieee_fraud.IEEEFraudLoader\ndescription: |\n  The IEEE-CIS Fraud Detection Dataset\n  https://www.kaggle.com/c/ieee-fraud-detection/overview.\noutput_features:\n  - name: isFraud\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/imbalanced_insurance.yaml",
    "content": "version: 1.0\nname: imbalaced_insurance\nkaggle_dataset_id: arashnic/imbalanced-data-practice\narchive_filenames: imbalanced-data-practice.zip\nsha256:\n  imbalanced-data-practice.zip: 33c7d15cbdb7cc151c1d5e920a8a613b015c19222f90d4eac04ca8cfc5416847\ndataset_filenames: aug_train.csv\nloader: split_loaders.RandomSplitLoader\ndescription: |\n  Health Insurance Cross Sell Prediction\n  Predict Health Insurance Owners' who will be interested in Vehicle Insurance\n  https://www.kaggle.com/datasets/arashnic/imbalanced-data-practice\ncolumns:\n  - name: id\n    type: category\n  - name: Gender\n    type: binary\n  - name: Age\n    type: number\n  - name: Driving_License\n    type: binary\n  - name: Region_Code\n    type: category\n  - name: Previously_Insured\n    type: binary\n  - name: Vehicle_Age\n    type: category\n  - name: Vehicle_Damage\n    type: binary\n  - name: Annual_Premium\n    type: number\n  - name: Policy_Sales_Channel\n    type:\n  - name: Vintage\n    type:\n  - name: Response\n    type:\noutput_features:\n  - name: Response\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/imdb.yaml",
    "content": "version: 1.0\nname: imdb\nkaggle_dataset_id: lakshmi25npathi/imdb-dataset-of-50k-movie-reviews\narchive_filenames: imdb-dataset-of-50k-movie-reviews.zip\nsha256:\n  imdb-dataset-of-50k-movie-reviews.zip: 73a235bc5fc4df57bb5d517afa480fe6bfd4e2afc25dc5e5867fc87f2d25614d\ndescription: |\n  IMDB dataset having 50K movie reviews for natural language processing or Text analytics.\n  https://www.kaggle.com/datasets/lakshmi25npathi/imdb-dataset-of-50k-movie-reviews\ncolumns:\n  - name: review\n    type: text\n  - name: sentiment\n    type: category\noutput_features:\n  - name: sentiment\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/imdb_genre_prediction.yaml",
    "content": "version: 1.0\nname: imdb_genre_prediction\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/imdb_genre_prediction/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/imdb_genre_prediction/test.csv\nsha256:\n  test.csv: 5bca7b6ca34f4057e2a4920d6034f481055bd03061bb0128c87d6c99a6b4661f\n  train.csv: b63f1f6fcad17f644d9266891a01d0f0e1187c277ccf6eecb80af72b92b0b621\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  A data set of 1,000 most popular movies on IMDB in the last 10 years. The data points included are:\n  Title, Genre, Description, Director, Actors, Year, Runtime, Rating, Votes, Revenue, Metascrore\n  https://www.kaggle.com/PromptCloudHQ/imdb-data\ncolumns:\n  - name: Rank\n    type: number\n  - name: Title\n    type: category\n  - name: Description\n    type: text\n  - name: Director\n    type: category\n  - name: Actors\n    type: set\n  - name: Year\n    type: category\n  - name: Runtime (Minutes)\n    type: number\n  - name: Rating\n    type: Number\n  - name: Votes\n    type: number\n  - name: Revenue (Millions)\n    type: number\n  - name: Metascore\n    type: number\n  - name: Genre_is_Drama\n    type: binary\noutput_features:\n- name: Genre_is_Drama\n  type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/insurance_lite.yaml",
    "content": "version: 1.0\nname: insurance_lite\nkaggle_dataset_id: infernape/fast-furious-and-insured\narchive_filenames: fast-furious-and-insured.zip\nsha256:\n  fast-furious-and-insured.zip: 3b88ada517aa88d9c9187121d7ef42f4b5539808677a2b0827b989ca0fa19600\ndataset_filenames: Fast_Furious_Insured/train.csv\npreserve_paths: Fast_Furious_Insured\nloader: insurance_lite.InsuranceLiteLoader\ndescription: |\n  The dataset consists of parameters such as the images of damaged cars,\n  the price of the cars and their insurance claim, and the like.\n  Predict the insurance claim for the cars that are provided in the dataset.\ncolumns:\n    - name: image_path\n      type: image\n    - name: insurance_company\n      type: category\n    - name: cost_of_vehicle\n      type: number\n    - name: min_coverage\n      type: number\n    - name: expiry_date\n      type: date\n    - name: max_coverage\n      type: number\n    - name: condition\n      type: binary\n    - name: amount\n      type: number\noutput_features:\n  - name: amount\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/iris.yaml",
    "content": "version: 1.0\nname: iris\ndownload_urls: https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data\nsha256:\n  iris.data: 6f608b71a7317216319b4d27b4d9bc84e6abd734eda7872b71a458569e2656c0\ndescription: |\n  Iris Dataset\n  https://archive.ics.uci.edu/ml/datasets/Iris\ncolumns:\n  - name: sepal_length_cm\n    type: number\n  - name: sepal_width_cm\n    type: number\n  - name: petal_length_cm\n    type: number\n  - name: petal_width_cm\n    type: number\n  - name: class\n    type: category\noutput_features:\n  - name: class\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/irony.yaml",
    "content": "version: 1.0\nname: irony\ndownload_urls: https://raw.githubusercontent.com/bwallace/ACL-2014-irony/master/irony-labeled.csv\nsha256:\n  irony-labeled.csv: 11f4d0964bd9c5c8363de2920612f5d926a4e6b3a8ab9187da2c33cfc0fdd02b\ndescription: |\n  The Reddit Irony dataset.\n  Source Paper: Humans Require Context to Infer Ironic Intent (so Computers Probably do, too)\n  Byron C Wallace, Do Kook Choe, Laura Kertz, and Eugene Charniak\ncolumns:\n  - name: comment_text\n    type: text\n  - name: label\n    type: binary\noutput_features:\n  - name: label\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/jc_penney_products.yaml",
    "content": "version: 1.0\nname: jc_penney_products\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/jc_penney_products/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/jc_penney_products/test.csv\nsha256:\n  test.csv: 458fb13b07701897fbc0d88481823b90e884e92a42e65eeba816cdf3523b2e85\n  train.csv: e9e3d3da627dc544d01f4c27b1d023288c68e55ce2db2593fb7b2268a6b9b020\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  JCPenney products\n  20,000 product listings from JCPenney\n  https://www.kaggle.com/PromptCloudHQ/all-jc-penny-products\ncolumns:\n  - name: name_title\n    type: category\n  - name: description\n    type: text\n  - name: sale_price\n    type: number\n  - name: average_product_rating\n    type: number\n  - name: brand\n    type: category\n  - name: total_number_reviews\n    type: number\noutput_features:\n- name: sale_price\n  type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/jigsaw_unintended_bias.yaml",
    "content": "version: 1.0\nname: jigsaw_unintended_bias\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/dev.pq\n  - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias/test.pq\nsha256:\n  test.pq: e9f3fd6fa83ddea2af8d21e93eb677b2fa5686c9b8ae38e6293f7c3306f66fad\n  train.pq: 30bedd5bbd5b2277b8bffa4ed3a02ce6ef7c838aa5c1338908b5ad599a6a9888\n  dev.pq: 57e1e3a06733fb83ad9ca46839ed8afd7d670e5e5f5c7f0026b748d760457d57\ntrain_filenames: train.pq\nvalidation_filenames: dev.pq\ntest_filenames: test.pq\ndescription: |\n  A dataset labeled for identity mentions and optimizing a metric designed to measure unintended bias.\n  Disclaimer: The dataset for this competition contains text that may be considered profane, vulgar, or offensive.\n  https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification\ncolumns:\n  - name: id\n    type: category\n  - name: target\n    type: binary\n  - name: comment_text\n    type: text\n  - name: severe_toxicity\n    type: number\n  - name: obscene\n    type: number\n  - name: identity_attack\n    type: number\n  - name: insult\n    type: number\n  - name: threat\n    type: number\n  - name: asian\n    type: number\n  - name: atheist\n    type: number\n  - name: bisexual\n    type: number\n  - name: black\n    type: number\n  - name: buddhist\n    type: number\n  - name: christian\n    type: number\n  - name: female\n    type: number\n  - name: heterosexual\n    type: number\n  - name: hindu\n    type: number\n  - name: homosexual_gay_or_lesbian\n    type: number\n  - name: intellectual_or_learning_disability\n    type: number\n  - name: jewish\n    type: number\n  - name: latino\n    type: number\n  - name: male\n    type: number\n  - name: muslim\n    type: number\n  - name: other_disability\n    type: number\n  - name: other_gender\n    type: number\n  - name: other_race_or_ethnicity\n    type: number\n  - name: other_religion\n    type: number\n  - name: other_sexual_orientation\n    type: number\n  - name: physical_disability\n    type: number\n  - name: psychiatric_or_mental_illness\n    type: number\n  - name: transgender\n    type: number\n  - name: white\n    type: number\n  - name: created_date\n    type: date\n  - name: publication_id\n    type: category\n  - name: parent_id\n    type: category\n  - name: article_id\n    type: category\n  - name: rating\n    type: category\n  - name: funny\n    type: number\n  - name: wow\n    type: number\n  - name: sad\n    type: number\n  - name: likes\n    type: number\n  - name: disagree\n    type: number\n  - name: sexual_explicit\n    type: number\n  - name: identity_annotator_count\n    type: number\n  - name: toxicity_annotator_count\n    type: number\noutput_features:\n- name: target\n  type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/jigsaw_unintended_bias100k.yaml",
    "content": "version: 1.0\nname: jigsaw_unintended_bias100K\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias100K/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/jigsaw_unintended_bias100K/test.pq\nsha256:\n  test.pq: f7a0ec60ac89ffdb94919bf95e514057588a444c90ebdcb8ac90dfb0bfec3d48\n  train.pq: 48916c037b0a20167f6e9176cc1eedcb0e6ef942beeedb7dc02f19dfebac0229\ntrain_filenames: train.pq\ntest_filenames: test.pq\ndescription: |\n  A dataset labeled for identity mentions and optimizing a metric designed to measure unintended bias.\n  Disclaimer: The dataset for this competition contains text that may be considered profane, vulgar, or offensive.\n  https://www.kaggle.com/c/jigsaw-unintended-bias-in-toxicity-classification\ncolumns:\n  - name: id\n    type: category\n  - name: target\n    type: binary\n  - name: comment_text\n    type: text\n  - name: severe_toxicity\n    type: number\n  - name: obscene\n    type: number\n  - name: identity_attack\n    type: number\n  - name: insult\n    type: number\n  - name: threat\n    type: number\n  - name: asian\n    type: number\n  - name: atheist\n    type: number\n  - name: bisexual\n    type: number\n  - name: black\n    type: number\n  - name: buddhist\n    type: number\n  - name: christian\n    type: number\n  - name: female\n    type: number\n  - name: heterosexual\n    type: number\n  - name: hindu\n    type: number\n  - name: homosexual_gay_or_lesbian\n    type: number\n  - name: intellectual_or_learning_disability\n    type: number\n  - name: jewish\n    type: number\n  - name: latino\n    type: number\n  - name: male\n    type: number\n  - name: muslim\n    type: number\n  - name: other_disability\n    type: number\n  - name: other_gender\n    type: number\n  - name: other_race_or_ethnicity\n    type: number\n  - name: other_religion\n    type: number\n  - name: other_sexual_orientation\n    type: number\n  - name: physical_disability\n    type: number\n  - name: psychiatric_or_mental_illness\n    type: number\n  - name: transgender\n    type: number\n  - name: white\n    type: number\n  - name: created_date\n    type: date\n  - name: publication_id\n    type: category\n  - name: parent_id\n    type: category\n  - name: article_id\n    type: category\n  - name: rating\n    type: category\n  - name: funny\n    type: number\n  - name: wow\n    type: number\n  - name: sad\n    type: number\n  - name: likes\n    type: number\n  - name: disagree\n    type: number\n  - name: sexual_explicit\n    type: number\n  - name: identity_annotator_count\n    type: number\n  - name: toxicity_annotator_count\n    type: number\noutput_features:\n- name: target\n  type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/kdd_appetency.yaml",
    "content": "version: 1.0\nname: kdd_appetency\ndownload_urls:\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_appetency.labels\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/appetency/stratified_train_idx_appetency.txt\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/appetency/stratified_test_idx_appetency.txt\nsha256:\n  orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53\n  orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040\n  orange_small_train_appetency.labels: edbfa40e7513804cf25c3f8b3c8f4a6cf5c77116cffc2f87ef770351250a963c\n  stratified_train_idx_appetency.txt: 9c6bf7da6209653e13d9a1d2ef90e4afafe0ecac0eb843c8025816a445c625d9\n  stratified_test_idx_appetency.txt: b80fb8dcf43cd028f4b8affeab65299d580a7e5432ebbe639527dc8177f8764a\ndataset_filenames: orange_small_train.data\nloader: kdd_loader.KDDAppetencyLoader\ndescription: |\n  The KDD Cup 2009 Appetency dataset.\n  https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\ncolumns:\n  - name: Var1\n    type: number\n  - name: Var2\n    type: number\n  - name: Var3\n    type: number\n  - name: Var4\n    type: number\n  - name: Var5\n    type: number\n  - name: Var6\n    type: number\n  - name: Var7\n    type: number\n  - name: Var8\n    type: number\n  - name: Var9\n    type: number\n  - name: Var10\n    type: number\n  - name: Var11\n    type: number\n  - name: Var12\n    type: number\n  - name: Var13\n    type: number\n  - name: Var14\n    type: number\n  - name: Var15\n    type: number\n  - name: Var16\n    type: number\n  - name: Var17\n    type: number\n  - name: Var18\n    type: number\n  - name: Var19\n    type: number\n  - name: Var20\n    type: number\n  - name: Var21\n    type: number\n  - name: Var22\n    type: number\n  - name: Var23\n    type: number\n  - name: Var24\n    type: number\n  - name: Var25\n    type: number\n  - name: Var26\n    type: number\n  - name: Var27\n    type: number\n  - name: Var28\n    type: number\n  - name: Var29\n    type: number\n  - name: Var30\n    type: number\n  - name: Var31\n    type: number\n  - name: Var32\n    type: number\n  - name: Var33\n    type: number\n  - name: Var34\n    type: number\n  - name: Var35\n    type: number\n  - name: Var36\n    type: number\n  - name: Var37\n    type: number\n  - name: Var38\n    type: number\n  - name: Var39\n    type: number\n  - name: Var40\n    type: number\n  - name: Var41\n    type: number\n  - name: Var42\n    type: number\n  - name: Var43\n    type: number\n  - name: Var44\n    type: number\n  - name: Var45\n    type: number\n  - name: Var46\n    type: number\n  - name: Var47\n    type: number\n  - name: Var48\n    type: number\n  - name: Var49\n    type: number\n  - name: Var50\n    type: number\n  - name: Var51\n    type: number\n  - name: Var52\n    type: number\n  - name: Var53\n    type: number\n  - name: Var54\n    type: number\n  - name: Var55\n    type: number\n  - name: Var56\n    type: number\n  - name: Var57\n    type: number\n  - name: Var58\n    type: number\n  - name: Var59\n    type: number\n  - name: Var60\n    type: number\n  - name: Var61\n    type: number\n  - name: Var62\n    type: number\n  - name: Var63\n    type: number\n  - name: Var64\n    type: number\n  - name: Var65\n    type: number\n  - name: Var66\n    type: number\n  - name: Var67\n    type: number\n  - name: Var68\n    type: number\n  - name: Var69\n    type: number\n  - name: Var70\n    type: number\n  - name: Var71\n    type: number\n  - name: Var72\n    type: number\n  - name: Var73\n    type: number\n  - name: Var74\n    type: number\n  - name: Var75\n    type: number\n  - name: Var76\n    type: number\n  - name: Var77\n    type: number\n  - name: Var78\n    type: number\n  - name: Var79\n    type: number\n  - name: Var80\n    type: number\n  - name: Var81\n    type: number\n  - name: Var82\n    type: number\n  - name: Var83\n    type: number\n  - name: Var84\n    type: number\n  - name: Var85\n    type: number\n  - name: Var86\n    type: number\n  - name: Var87\n    type: number\n  - name: Var88\n    type: number\n  - name: Var89\n    type: number\n  - name: Var90\n    type: number\n  - name: Var91\n    type: number\n  - name: Var92\n    type: number\n  - name: Var93\n    type: number\n  - name: Var94\n    type: number\n  - name: Var95\n    type: number\n  - name: Var96\n    type: number\n  - name: Var97\n    type: number\n  - name: Var98\n    type: number\n  - name: Var99\n    type: number\n  - name: Var100\n    type: number\n  - name: Var101\n    type: number\n  - name: Var102\n    type: number\n  - name: Var103\n    type: number\n  - name: Var104\n    type: number\n  - name: Var105\n    type: number\n  - name: Var106\n    type: number\n  - name: Var107\n    type: number\n  - name: Var108\n    type: number\n  - name: Var109\n    type: number\n  - name: Var110\n    type: number\n  - name: Var111\n    type: number\n  - name: Var112\n    type: number\n  - name: Var113\n    type: number\n  - name: Var114\n    type: number\n  - name: Var115\n    type: number\n  - name: Var116\n    type: number\n  - name: Var117\n    type: number\n  - name: Var118\n    type: number\n  - name: Var119\n    type: number\n  - name: Var120\n    type: number\n  - name: Var121\n    type: number\n  - name: Var122\n    type: number\n  - name: Var123\n    type: number\n  - name: Var124\n    type: number\n  - name: Var125\n    type: number\n  - name: Var126\n    type: number\n  - name: Var127\n    type: number\n  - name: Var128\n    type: number\n  - name: Var129\n    type: number\n  - name: Var130\n    type: number\n  - name: Var131\n    type: number\n  - name: Var132\n    type: number\n  - name: Var133\n    type: number\n  - name: Var134\n    type: number\n  - name: Var135\n    type: number\n  - name: Var136\n    type: number\n  - name: Var137\n    type: number\n  - name: Var138\n    type: number\n  - name: Var139\n    type: number\n  - name: Var140\n    type: number\n  - name: Var141\n    type: number\n  - name: Var142\n    type: number\n  - name: Var143\n    type: number\n  - name: Var144\n    type: number\n  - name: Var145\n    type: number\n  - name: Var146\n    type: number\n  - name: Var147\n    type: number\n  - name: Var148\n    type: number\n  - name: Var149\n    type: number\n  - name: Var150\n    type: number\n  - name: Var151\n    type: number\n  - name: Var152\n    type: number\n  - name: Var153\n    type: number\n  - name: Var154\n    type: number\n  - name: Var155\n    type: number\n  - name: Var156\n    type: number\n  - name: Var157\n    type: number\n  - name: Var158\n    type: number\n  - name: Var159\n    type: number\n  - name: Var160\n    type: number\n  - name: Var161\n    type: number\n  - name: Var162\n    type: number\n  - name: Var163\n    type: number\n  - name: Var164\n    type: number\n  - name: Var165\n    type: number\n  - name: Var166\n    type: number\n  - name: Var167\n    type: number\n  - name: Var168\n    type: number\n  - name: Var169\n    type: number\n  - name: Var170\n    type: number\n  - name: Var171\n    type: number\n  - name: Var172\n    type: number\n  - name: Var173\n    type: number\n  - name: Var174\n    type: number\n  - name: Var175\n    type: number\n  - name: Var176\n    type: number\n  - name: Var177\n    type: number\n  - name: Var178\n    type: number\n  - name: Var179\n    type: number\n  - name: Var180\n    type: number\n  - name: Var181\n    type: number\n  - name: Var182\n    type: number\n  - name: Var183\n    type: number\n  - name: Var184\n    type: number\n  - name: Var185\n    type: number\n  - name: Var186\n    type: number\n  - name: Var187\n    type: number\n  - name: Var188\n    type: number\n  - name: Var189\n    type: number\n  - name: Var190\n    type: number\n  - name: Var191\n    type: category\n  - name: Var192\n    type: category\n  - name: Var193\n    type: category\n  - name: Var194\n    type: category\n  - name: Var195\n    type: category\n  - name: Var196\n    type: category\n  - name: Var197\n    type: category\n  - name: Var198\n    type: category\n  - name: Var199\n    type: category\n  - name: Var200\n    type: category\n  - name: Var201\n    type: category\n  - name: Var202\n    type: category\n  - name: Var203\n    type: category\n  - name: Var204\n    type: category\n  - name: Var205\n    type: category\n  - name: Var206\n    type: category\n  - name: Var207\n    type: category\n  - name: Var208\n    type: category\n  - name: Var209\n    type: number\n  - name: Var210\n    type: category\n  - name: Var211\n    type: category\n  - name: Var212\n    type: category\n  - name: Var213\n    type: category\n  - name: Var214\n    type: category\n  - name: Var215\n    type: category\n  - name: Var216\n    type: category\n  - name: Var217\n    type: category\n  - name: Var218\n    type: category\n  - name: Var219\n    type: category\n  - name: Var220\n    type: category\n  - name: Var221\n    type: category\n  - name: Var222\n    type: category\n  - name: Var223\n    type: category\n  - name: Var224\n    type: category\n  - name: Var225\n    type: category\n  - name: Var226\n    type: category\n  - name: Var227\n    type: category\n  - name: Var228\n    type: category\n  - name: Var229\n    type: category\n  - name: Var230\n    type: number\n  - name: target\n    type: binary\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/kdd_churn.yaml",
    "content": "version: 1.0\nname: kdd_churn\ndownload_urls:\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_churn.labels\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/churn/stratified_train_idx_churn.txt\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/churn/stratified_test_idx_churn.txt\nsha256:\n  orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53\n  orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040\n  orange_small_train_churn.labels: fe8891cc574bd55a214514e522a5bed1eec2c3f347a49a36e51620009e7b6f5b\n  stratified_train_idx_churn.txt: 34f9880959ced6f668b25f879fdd388b3826efeca0df03f5a2a5494ce6795406\n  stratified_test_idx_churn.txt: 1675a62cd49c43535eedee3b746f65f8c6a4ebd7f4d0da04e442fd658a408042\ndataset_filenames: orange_small_train.data\nloader: kdd_loader.KDDChurnLoader\ndescription: |\n  The KDD Cup 2009 Churn dataset.\n  https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\ncolumns:\n  - name: Var1\n    type: number\n  - name: Var2\n    type: number\n  - name: Var3\n    type: number\n  - name: Var4\n    type: number\n  - name: Var5\n    type: number\n  - name: Var6\n    type: number\n  - name: Var7\n    type: number\n  - name: Var8\n    type: number\n  - name: Var9\n    type: number\n  - name: Var10\n    type: number\n  - name: Var11\n    type: number\n  - name: Var12\n    type: number\n  - name: Var13\n    type: number\n  - name: Var14\n    type: number\n  - name: Var15\n    type: number\n  - name: Var16\n    type: number\n  - name: Var17\n    type: number\n  - name: Var18\n    type: number\n  - name: Var19\n    type: number\n  - name: Var20\n    type: number\n  - name: Var21\n    type: number\n  - name: Var22\n    type: number\n  - name: Var23\n    type: number\n  - name: Var24\n    type: number\n  - name: Var25\n    type: number\n  - name: Var26\n    type: number\n  - name: Var27\n    type: number\n  - name: Var28\n    type: number\n  - name: Var29\n    type: number\n  - name: Var30\n    type: number\n  - name: Var31\n    type: number\n  - name: Var32\n    type: number\n  - name: Var33\n    type: number\n  - name: Var34\n    type: number\n  - name: Var35\n    type: number\n  - name: Var36\n    type: number\n  - name: Var37\n    type: number\n  - name: Var38\n    type: number\n  - name: Var39\n    type: number\n  - name: Var40\n    type: number\n  - name: Var41\n    type: number\n  - name: Var42\n    type: number\n  - name: Var43\n    type: number\n  - name: Var44\n    type: number\n  - name: Var45\n    type: number\n  - name: Var46\n    type: number\n  - name: Var47\n    type: number\n  - name: Var48\n    type: number\n  - name: Var49\n    type: number\n  - name: Var50\n    type: number\n  - name: Var51\n    type: number\n  - name: Var52\n    type: number\n  - name: Var53\n    type: number\n  - name: Var54\n    type: number\n  - name: Var55\n    type: number\n  - name: Var56\n    type: number\n  - name: Var57\n    type: number\n  - name: Var58\n    type: number\n  - name: Var59\n    type: number\n  - name: Var60\n    type: number\n  - name: Var61\n    type: number\n  - name: Var62\n    type: number\n  - name: Var63\n    type: number\n  - name: Var64\n    type: number\n  - name: Var65\n    type: number\n  - name: Var66\n    type: number\n  - name: Var67\n    type: number\n  - name: Var68\n    type: number\n  - name: Var69\n    type: number\n  - name: Var70\n    type: number\n  - name: Var71\n    type: number\n  - name: Var72\n    type: number\n  - name: Var73\n    type: number\n  - name: Var74\n    type: number\n  - name: Var75\n    type: number\n  - name: Var76\n    type: number\n  - name: Var77\n    type: number\n  - name: Var78\n    type: number\n  - name: Var79\n    type: number\n  - name: Var80\n    type: number\n  - name: Var81\n    type: number\n  - name: Var82\n    type: number\n  - name: Var83\n    type: number\n  - name: Var84\n    type: number\n  - name: Var85\n    type: number\n  - name: Var86\n    type: number\n  - name: Var87\n    type: number\n  - name: Var88\n    type: number\n  - name: Var89\n    type: number\n  - name: Var90\n    type: number\n  - name: Var91\n    type: number\n  - name: Var92\n    type: number\n  - name: Var93\n    type: number\n  - name: Var94\n    type: number\n  - name: Var95\n    type: number\n  - name: Var96\n    type: number\n  - name: Var97\n    type: number\n  - name: Var98\n    type: number\n  - name: Var99\n    type: number\n  - name: Var100\n    type: number\n  - name: Var101\n    type: number\n  - name: Var102\n    type: number\n  - name: Var103\n    type: number\n  - name: Var104\n    type: number\n  - name: Var105\n    type: number\n  - name: Var106\n    type: number\n  - name: Var107\n    type: number\n  - name: Var108\n    type: number\n  - name: Var109\n    type: number\n  - name: Var110\n    type: number\n  - name: Var111\n    type: number\n  - name: Var112\n    type: number\n  - name: Var113\n    type: number\n  - name: Var114\n    type: number\n  - name: Var115\n    type: number\n  - name: Var116\n    type: number\n  - name: Var117\n    type: number\n  - name: Var118\n    type: number\n  - name: Var119\n    type: number\n  - name: Var120\n    type: number\n  - name: Var121\n    type: number\n  - name: Var122\n    type: number\n  - name: Var123\n    type: number\n  - name: Var124\n    type: number\n  - name: Var125\n    type: number\n  - name: Var126\n    type: number\n  - name: Var127\n    type: number\n  - name: Var128\n    type: number\n  - name: Var129\n    type: number\n  - name: Var130\n    type: number\n  - name: Var131\n    type: number\n  - name: Var132\n    type: number\n  - name: Var133\n    type: number\n  - name: Var134\n    type: number\n  - name: Var135\n    type: number\n  - name: Var136\n    type: number\n  - name: Var137\n    type: number\n  - name: Var138\n    type: number\n  - name: Var139\n    type: number\n  - name: Var140\n    type: number\n  - name: Var141\n    type: number\n  - name: Var142\n    type: number\n  - name: Var143\n    type: number\n  - name: Var144\n    type: number\n  - name: Var145\n    type: number\n  - name: Var146\n    type: number\n  - name: Var147\n    type: number\n  - name: Var148\n    type: number\n  - name: Var149\n    type: number\n  - name: Var150\n    type: number\n  - name: Var151\n    type: number\n  - name: Var152\n    type: number\n  - name: Var153\n    type: number\n  - name: Var154\n    type: number\n  - name: Var155\n    type: number\n  - name: Var156\n    type: number\n  - name: Var157\n    type: number\n  - name: Var158\n    type: number\n  - name: Var159\n    type: number\n  - name: Var160\n    type: number\n  - name: Var161\n    type: number\n  - name: Var162\n    type: number\n  - name: Var163\n    type: number\n  - name: Var164\n    type: number\n  - name: Var165\n    type: number\n  - name: Var166\n    type: number\n  - name: Var167\n    type: number\n  - name: Var168\n    type: number\n  - name: Var169\n    type: number\n  - name: Var170\n    type: number\n  - name: Var171\n    type: number\n  - name: Var172\n    type: number\n  - name: Var173\n    type: number\n  - name: Var174\n    type: number\n  - name: Var175\n    type: number\n  - name: Var176\n    type: number\n  - name: Var177\n    type: number\n  - name: Var178\n    type: number\n  - name: Var179\n    type: number\n  - name: Var180\n    type: number\n  - name: Var181\n    type: number\n  - name: Var182\n    type: number\n  - name: Var183\n    type: number\n  - name: Var184\n    type: number\n  - name: Var185\n    type: number\n  - name: Var186\n    type: number\n  - name: Var187\n    type: number\n  - name: Var188\n    type: number\n  - name: Var189\n    type: number\n  - name: Var190\n    type: number\n  - name: Var191\n    type: category\n  - name: Var192\n    type: category\n  - name: Var193\n    type: category\n  - name: Var194\n    type: category\n  - name: Var195\n    type: category\n  - name: Var196\n    type: category\n  - name: Var197\n    type: category\n  - name: Var198\n    type: category\n  - name: Var199\n    type: category\n  - name: Var200\n    type: category\n  - name: Var201\n    type: category\n  - name: Var202\n    type: category\n  - name: Var203\n    type: category\n  - name: Var204\n    type: category\n  - name: Var205\n    type: category\n  - name: Var206\n    type: category\n  - name: Var207\n    type: category\n  - name: Var208\n    type: category\n  - name: Var209\n    type: number\n  - name: Var210\n    type: category\n  - name: Var211\n    type: category\n  - name: Var212\n    type: category\n  - name: Var213\n    type: category\n  - name: Var214\n    type: category\n  - name: Var215\n    type: category\n  - name: Var216\n    type: category\n  - name: Var217\n    type: category\n  - name: Var218\n    type: category\n  - name: Var219\n    type: category\n  - name: Var220\n    type: category\n  - name: Var221\n    type: category\n  - name: Var222\n    type: category\n  - name: Var223\n    type: category\n  - name: Var224\n    type: category\n  - name: Var225\n    type: category\n  - name: Var226\n    type: category\n  - name: Var227\n    type: category\n  - name: Var228\n    type: category\n  - name: Var229\n    type: category\n  - name: Var230\n    type: number\n  - name: target\n    type: binary\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/kdd_upselling.yaml",
    "content": "version: 1.0\nname: kdd_upselling\ndownload_urls:\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_test.data.zip\n  - https://kdd.org/cupfiles/KDDCupData/2009/orange_small_train_upselling.labels\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/upselling/stratified_train_idx_upselling.txt\n  - https://raw.githubusercontent.com/catboost/benchmarks/master/quality_benchmarks/prepare_appetency_churn_upselling/upselling/stratified_test_idx_upselling.txt\nsha256:\n  orange_small_test.data.zip: 440ac8a350144c14f4d6947c096ad675ee84aa27b4b742071662696e333cec53\n  orange_small_train.data.zip: 31ccb810bdbb71c16e079326443166dc3dfbf73cd358fc4a4ce7440fb1bc6040\n  orange_small_train_upselling.labels: 86effe68394fe1ab21c2d855f74adf70f442990aa95dfe5c97340fc924440e68\n  stratified_train_idx_upselling.txt: 659060717872177d607fbb157e8d2142c719912771d1716da11ccdd6ff915a05\n  stratified_test_idx_upselling.txt: 64cb66ef559b4ccff096e0d7c150c7d019321ffd6cef2362c195a56c56effcb7\ndataset_filenames: orange_small_train.data\nloader: kdd_loader.KDDUpsellingLoader\ndescription: |\n  The KDD Cup 2009 Upselling dataset.\n  https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\ncolumns:\n  - name: Var1\n    type: number\n  - name: Var2\n    type: number\n  - name: Var3\n    type: number\n  - name: Var4\n    type: number\n  - name: Var5\n    type: number\n  - name: Var6\n    type: number\n  - name: Var7\n    type: number\n  - name: Var8\n    type: number\n  - name: Var9\n    type: number\n  - name: Var10\n    type: number\n  - name: Var11\n    type: number\n  - name: Var12\n    type: number\n  - name: Var13\n    type: number\n  - name: Var14\n    type: number\n  - name: Var15\n    type: number\n  - name: Var16\n    type: number\n  - name: Var17\n    type: number\n  - name: Var18\n    type: number\n  - name: Var19\n    type: number\n  - name: Var20\n    type: number\n  - name: Var21\n    type: number\n  - name: Var22\n    type: number\n  - name: Var23\n    type: number\n  - name: Var24\n    type: number\n  - name: Var25\n    type: number\n  - name: Var26\n    type: number\n  - name: Var27\n    type: number\n  - name: Var28\n    type: number\n  - name: Var29\n    type: number\n  - name: Var30\n    type: number\n  - name: Var31\n    type: number\n  - name: Var32\n    type: number\n  - name: Var33\n    type: number\n  - name: Var34\n    type: number\n  - name: Var35\n    type: number\n  - name: Var36\n    type: number\n  - name: Var37\n    type: number\n  - name: Var38\n    type: number\n  - name: Var39\n    type: number\n  - name: Var40\n    type: number\n  - name: Var41\n    type: number\n  - name: Var42\n    type: number\n  - name: Var43\n    type: number\n  - name: Var44\n    type: number\n  - name: Var45\n    type: number\n  - name: Var46\n    type: number\n  - name: Var47\n    type: number\n  - name: Var48\n    type: number\n  - name: Var49\n    type: number\n  - name: Var50\n    type: number\n  - name: Var51\n    type: number\n  - name: Var52\n    type: number\n  - name: Var53\n    type: number\n  - name: Var54\n    type: number\n  - name: Var55\n    type: number\n  - name: Var56\n    type: number\n  - name: Var57\n    type: number\n  - name: Var58\n    type: number\n  - name: Var59\n    type: number\n  - name: Var60\n    type: number\n  - name: Var61\n    type: number\n  - name: Var62\n    type: number\n  - name: Var63\n    type: number\n  - name: Var64\n    type: number\n  - name: Var65\n    type: number\n  - name: Var66\n    type: number\n  - name: Var67\n    type: number\n  - name: Var68\n    type: number\n  - name: Var69\n    type: number\n  - name: Var70\n    type: number\n  - name: Var71\n    type: number\n  - name: Var72\n    type: number\n  - name: Var73\n    type: number\n  - name: Var74\n    type: number\n  - name: Var75\n    type: number\n  - name: Var76\n    type: number\n  - name: Var77\n    type: number\n  - name: Var78\n    type: number\n  - name: Var79\n    type: number\n  - name: Var80\n    type: number\n  - name: Var81\n    type: number\n  - name: Var82\n    type: number\n  - name: Var83\n    type: number\n  - name: Var84\n    type: number\n  - name: Var85\n    type: number\n  - name: Var86\n    type: number\n  - name: Var87\n    type: number\n  - name: Var88\n    type: number\n  - name: Var89\n    type: number\n  - name: Var90\n    type: number\n  - name: Var91\n    type: number\n  - name: Var92\n    type: number\n  - name: Var93\n    type: number\n  - name: Var94\n    type: number\n  - name: Var95\n    type: number\n  - name: Var96\n    type: number\n  - name: Var97\n    type: number\n  - name: Var98\n    type: number\n  - name: Var99\n    type: number\n  - name: Var100\n    type: number\n  - name: Var101\n    type: number\n  - name: Var102\n    type: number\n  - name: Var103\n    type: number\n  - name: Var104\n    type: number\n  - name: Var105\n    type: number\n  - name: Var106\n    type: number\n  - name: Var107\n    type: number\n  - name: Var108\n    type: number\n  - name: Var109\n    type: number\n  - name: Var110\n    type: number\n  - name: Var111\n    type: number\n  - name: Var112\n    type: number\n  - name: Var113\n    type: number\n  - name: Var114\n    type: number\n  - name: Var115\n    type: number\n  - name: Var116\n    type: number\n  - name: Var117\n    type: number\n  - name: Var118\n    type: number\n  - name: Var119\n    type: number\n  - name: Var120\n    type: number\n  - name: Var121\n    type: number\n  - name: Var122\n    type: number\n  - name: Var123\n    type: number\n  - name: Var124\n    type: number\n  - name: Var125\n    type: number\n  - name: Var126\n    type: number\n  - name: Var127\n    type: number\n  - name: Var128\n    type: number\n  - name: Var129\n    type: number\n  - name: Var130\n    type: number\n  - name: Var131\n    type: number\n  - name: Var132\n    type: number\n  - name: Var133\n    type: number\n  - name: Var134\n    type: number\n  - name: Var135\n    type: number\n  - name: Var136\n    type: number\n  - name: Var137\n    type: number\n  - name: Var138\n    type: number\n  - name: Var139\n    type: number\n  - name: Var140\n    type: number\n  - name: Var141\n    type: number\n  - name: Var142\n    type: number\n  - name: Var143\n    type: number\n  - name: Var144\n    type: number\n  - name: Var145\n    type: number\n  - name: Var146\n    type: number\n  - name: Var147\n    type: number\n  - name: Var148\n    type: number\n  - name: Var149\n    type: number\n  - name: Var150\n    type: number\n  - name: Var151\n    type: number\n  - name: Var152\n    type: number\n  - name: Var153\n    type: number\n  - name: Var154\n    type: number\n  - name: Var155\n    type: number\n  - name: Var156\n    type: number\n  - name: Var157\n    type: number\n  - name: Var158\n    type: number\n  - name: Var159\n    type: number\n  - name: Var160\n    type: number\n  - name: Var161\n    type: number\n  - name: Var162\n    type: number\n  - name: Var163\n    type: number\n  - name: Var164\n    type: number\n  - name: Var165\n    type: number\n  - name: Var166\n    type: number\n  - name: Var167\n    type: number\n  - name: Var168\n    type: number\n  - name: Var169\n    type: number\n  - name: Var170\n    type: number\n  - name: Var171\n    type: number\n  - name: Var172\n    type: number\n  - name: Var173\n    type: number\n  - name: Var174\n    type: number\n  - name: Var175\n    type: number\n  - name: Var176\n    type: number\n  - name: Var177\n    type: number\n  - name: Var178\n    type: number\n  - name: Var179\n    type: number\n  - name: Var180\n    type: number\n  - name: Var181\n    type: number\n  - name: Var182\n    type: number\n  - name: Var183\n    type: number\n  - name: Var184\n    type: number\n  - name: Var185\n    type: number\n  - name: Var186\n    type: number\n  - name: Var187\n    type: number\n  - name: Var188\n    type: number\n  - name: Var189\n    type: number\n  - name: Var190\n    type: number\n  - name: Var191\n    type: category\n  - name: Var192\n    type: category\n  - name: Var193\n    type: category\n  - name: Var194\n    type: category\n  - name: Var195\n    type: category\n  - name: Var196\n    type: category\n  - name: Var197\n    type: category\n  - name: Var198\n    type: category\n  - name: Var199\n    type: category\n  - name: Var200\n    type: category\n  - name: Var201\n    type: category\n  - name: Var202\n    type: category\n  - name: Var203\n    type: category\n  - name: Var204\n    type: category\n  - name: Var205\n    type: category\n  - name: Var206\n    type: category\n  - name: Var207\n    type: category\n  - name: Var208\n    type: category\n  - name: Var209\n    type: number\n  - name: Var210\n    type: category\n  - name: Var211\n    type: category\n  - name: Var212\n    type: category\n  - name: Var213\n    type: category\n  - name: Var214\n    type: category\n  - name: Var215\n    type: category\n  - name: Var216\n    type: category\n  - name: Var217\n    type: category\n  - name: Var218\n    type: category\n  - name: Var219\n    type: category\n  - name: Var220\n    type: category\n  - name: Var221\n    type: category\n  - name: Var222\n    type: category\n  - name: Var223\n    type: category\n  - name: Var224\n    type: category\n  - name: Var225\n    type: category\n  - name: Var226\n    type: category\n  - name: Var227\n    type: category\n  - name: Var228\n    type: category\n  - name: Var229\n    type: category\n  - name: Var230\n    type: number\n  - name: target\n    type: binary\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/kick_starter_funding.yaml",
    "content": "version: 1.0\nname: kick_starter_funding\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/kick_starter_funding/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/kick_starter_funding/test.csv\nsha256:\n  test.csv: 13c2d4b74ac8d1e258659b5f5fa74526b9d27e305f6c29ad7e853dfeeb01983c\n  train.csv: 3120b69f30bbc08c68940ab9e5d85d6cc2fbc9a65e8a24c66739179b6a60150e\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Funding Successful Projects on Kickstarter\n  Predict if a project will get successfully funded or not using labeled data\n  https://www.kaggle.com/codename007/funding-successful-projects\ncolumns:\n  - name: name\n    type: category\n  - name: desc\n    type: text\n  - name: goal\n    type: number\n  - name: keywords\n    type: category\n  - name: disable_communication\n    type: binary\n  - name: country\n    type: category\n  - name: currency\n    type: category\n  - name: deadline\n    type: number\n  - name: created_at\n    type: number\n  - name: final_status\n    type: binary\noutput_features:\n  - name: final_status\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/melbourne_airbnb.yaml",
    "content": "version: 1.0\nname: melbourne_airbnb\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/airbnb_melbourne/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/airbnb_melbourne/test.pq\nsha256:\n  test.pq: 9fe965cfdbd24ee9af7a004a7dc8c4e535a7ffceb722dce00f8ea90a54f95aa9\n  train.pq: c158c0f497ef355ba9d5de0de7556f6eb7f9bc343a67c4c681b014f6c7412e48\ntrain_filenames: train.pq\ntest_filenames: test.pq\ndescription: |\n  Melbourne Airbnb Open Data\n  Detailed and summarized data of Airbnb activity in Melbourne, VIC, Australia\n  https://www.kaggle.com/tylerx/melbourne-airbnb-open-data\ncolumns:\n  - name: id\n    type: number\n  - name: listing_url\n    type: category\n  - name: scrape_id\n    type: number\n  - name: last_scraped\n    type: date\n  - name: text\n    type: category\n  - name: summary\n    type: text\n  - name: space\n    type: text\n  - name: description\n    type: text\n  - name: neighborhood_overview\n    type: text\n  - name: notes\n    type: text\n  - name: transit\n    type: text\n  - name: access\n    type: text\n  - name: interaction\n    type: text\n  - name: house_rules\n    type: text\n  - name: picture_url\n    type: category\n  - name: host_id\n    type: category\n  - name: host_url\n    type: category\n  - name: host_name\n    type: category\n  - name: host_since\n    type: date\n  - name: host_location\n    type: category\n  - name: host_about\n    type: text\n  - name: host_response_time\n    type: category\n  - name: host_response_rate\n    type: category\n  - name: host_is_superhost\n    type: binary\n  - name: host_thumbnail_url\n    type: category\n  - name: host_picture_url\n    type: category\n  - name: host_neighborhood\n    type: category\n  - name: host_verifications\n    type: set\n  - name: host_has_profile_pic\n    type: binary\n  - name: host_identity_verified\n    type: binary\n  - name: street\n    type: category\n  - name: neighborhood\n    type: category\n  - name: city\n    type: category\n  - name: suburb\n    type: category\n  - name: state\n    type: category\n  - name: zipcode\n    type: category\n  - name: smart_location\n    type: category\n  - name: country_code\n    type: category\n  - name: country\n    type: category\n  - name: latitude\n    type: number\n  - name: longitude\n    type: number\n  - name: is_location_exact\n    type: binary\n  - name: property_type\n    type: category\n  - name: room_type\n    type: category\n  - name: accommodates\n    type: number\n  - name: bathrooms\n    type: number\n  - name: bedrooms\n    type: number\n  - name: beds\n    type: number\n  - name: bed_type\n    type: category\n  - name: amenities\n    type: set\n  - name: price\n    type: number\n  - name: weekly_price\n    type: number\n  - name: monthly_price\n    type: number\n  - name: security_deposit\n    type: number\n  - name: cleaning_fee\n    type: number\n  - name: guests_included\n    type: number\n  - name: extra_people\n    type: number\n  - name: minimum_nights\n    type: number\n  - name: maximum_nights\n    type: number\n  - name: calendar_updated\n    type: category\n  - name: has_availability\n    type: binary\n  - name: availability_30\n    type: number\n  - name: availability_60\n    type: number\n  - name: availability_90\n    type: number\n  - name: availability_365\n    type: number\n  - name: calendar_last_scraped\n    type: date\n  - name: number_of_reviews\n    type: number\n  - name: first_review\n    type: date\n  - name: last_review\n    type: date\n  - name: review_scores_rating\n    type: number\n  - name: review_scores_accuracy\n    type: number\n  - name: review_scores_cleanliness\n    type: number\n  - name: review_scores_checkin\n    type: number\n  - name: review_scores_communication\n    type: number\n  - name: review_scores_location\n    type: number\n  - name: review_scores_value\n    type: number\n  - name: requires_license\n    type: binary\n  - name: license\n    type: category\n  - name: instant_bookable\n    type: binary\n  - name: cancellation_policy\n    type: category\n  - name: require_guest_profile_picture\n    type: binary\n  - name: require_guest_phone_verification\n    type: binary\n  - name: calculated_host_listings_count\n    type: number\n  - name: reviews_per_month\n    type: number\n  - name: price_label\n    type: number\n  - name: host_verifications_jumio\n    type: binary\n  - name: host_verifications_government_id\n    type: binary\n  - name: host_verifications_kba\n    type: binary\n  - name: host_verifications_zhima_selfie\n    type: binary\n  - name: host_verifications_facebook\n    type: binary\n  - name: host_verifications_work_email\n    type: binary\n  - name: host_verifications_google\n    type: binary\n  - name: host_verifications_sesame\n    type: binary\n  - name: host_verifications_manual_online\n    type: binary\n  - name: host_verifications_manual_offline\n    type: binary\n  - name: host_verifications_offline_government_id\n    type: binary\n  - name: host_verifications_selfie\n    type: binary\n  - name: host_verifications_reviews\n    type: binary\n  - name: host_verifications_identity_manual\n    type: binary\n  - name: host_verifications_sesame_offline\n    type: binary\n  - name: host_verifications_weibo\n    type: binary\n  - name: host_verifications_email\n    type: binary\n  - name: host_verifications_sent_id\n    type: binary\n  - name: host_verifications_phone\n    type: binary\noutput_features:\n  - name: price_label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/mercari_price_suggestion.yaml",
    "content": "version: 1.0\nname: mercari_price_suggestion\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/dev.pq\n  - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion/test.pq\nsha256:\n  test.pq: 05fed940f5545e6a470ca595d014a02b173fd3362ca5bc5c458d02640b892a57\n  train.pq: a0613b77714ebb9f8927cf6bff2092af8143f4a66a64e45e9c3bf9d18604cfe3\n  dev.pq: f7284b86adde0354f30ee2c2b7a7a55dc895d202b4291138e807c8f3eaacb6b0\ntrain_filenames: train.pq\nvalidation_filenames: dev.pq\ntest_filenames: test.pq\ndescription: |\n  Predict product price based on details like product category name, brand name, and item condition.\n  We have converted price to log price by log(1 + price).\n  https://www.kaggle.com/c/mercari-price-suggestion-challenge\ncolumns:\n  - name: train_id\n    type: category\n  - name: name\n    type: category\n  - name: item_condition_id\n    type: category\n  - name: category_name\n    type: category\n  - name: brand_name\n    type: category\n  - name: price\n    type: number\n  - name: shipping\n    type: binary\n  - name: item_description\n    type: text\n  - name: log_price\n    type: number\n  - name: cat1\n    type: category\n  - name: cat2\n    type: category\n  - name: cat3\n    type: category\noutput_features:\n  - name: log_price\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/mercari_price_suggestion100K.yaml",
    "content": "version: 1.0\nname: mercari_price_suggestion100K\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion100K/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/mercari_price_suggestion100K/test.pq\nsha256:\n  test.pq: 60431577bd6cb433bae287ced2edc7a557497b66b1fe90e2fbec6ffc24bf35eb\n  train.pq: f60063847d9b828f1e9366eb69fa53774771b53291586d1cce506c931b7173f4\ntrain_filenames: train.pq\ntest_filenames: test.pq\ndescription: |\n  Predict product price based on details like product category name, brand name, and item condition.\n  We have converted price to log price by log(1 + price).\n  https://www.kaggle.com/c/mercari-price-suggestion-challenge\ncolumns:\n  - name: train_id\n    type: category\n  - name: name\n    type: category\n  - name: item_condition_id\n    type: category\n  - name: category_name\n    type: category\n  - name: brand_name\n    type: category\n  - name: price\n    type: number\n  - name: shipping\n    type: binary\n  - name: item_description\n    type: text\n  - name: log_price\n    type: number\n  - name: cat1\n    type: category\n  - name: cat2\n    type: category\n  - name: cat3\n    type: category\noutput_features:\n  - name: log_price\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/mercedes_benz_greener.yaml",
    "content": "version: 1.0\nname: mercedes_benz_greener\nkaggle_competition: mercedes-benz-greener-manufacturing\narchive_filenames: mercedes-benz-greener-manufacturing.zip\nsha256:\n  mercedes-benz-greener-manufacturing.zip: 91143716085345a84dc4991b8eb1d5ff80d8aa134930de946b3b24be0f2e5d1a\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  The Mercedes-Benz Greener Manufacturing dataset.\n  https://www.kaggle.com/c/mercedes-benz-greener-manufacturing\noutput_features:\n  - name: y\n    type: number\nfallback_mirrors:\n  - name: predibase\n    download_paths: s3://ludwig-tests/ludwig_backup/mercedes-benz-greener-manufacturing.zip\n"
  },
  {
    "path": "ludwig/datasets/configs/mnist.yaml",
    "content": "version: 1.0\nname: mnist\ndownload_urls:\n  - https://ossci-datasets.s3.amazonaws.com/mnist/train-images-idx3-ubyte.gz\n  - https://ossci-datasets.s3.amazonaws.com/mnist/train-labels-idx1-ubyte.gz\n  - https://ossci-datasets.s3.amazonaws.com/mnist/t10k-images-idx3-ubyte.gz\n  - https://ossci-datasets.s3.amazonaws.com/mnist/t10k-labels-idx1-ubyte.gz\nsha256:\n  t10k-images-idx3-ubyte.gz: 8d422c7b0a1c1c79245a5bcf07fe86e33eeafee792b84584aec276f5a2dbc4e6\n  train-images-idx3-ubyte.gz: 440fcabf73cc546fa21475e81ea370265605f56be210a4024d2ca8f203523609\n  train-labels-idx1-ubyte.gz: 3552534a0a558bbed6aed32b30c495cca23d567ec52cac8be1a0730e8010255c\n  t10k-labels-idx1-ubyte.gz: f7ae60f92e00ec6debd23a6088c31dbd2371eca3ffa0defaefb259924204aec6\npreserve_paths:\n  - training\n  - testing\nloader: mnist.MNISTLoader\ndescription: |\n  The MNIST database of handwritten digits, available from this page,\n  has a training set of 60,000 examples, and a test set of 10,000 examples.\n  It is a subset of a larger set available from NIST. The digits have been\n  size-normalized and centered in a fixed-size image.\n  It is a good database for people who want to try learning techniques and\n  pattern recognition methods on real-world data while spending minimal\n  efforts on preprocessing and formatting.\n  http://yann.lecun.com/exdb/mnist/\ncolumns:\n  - name: image_path\n    type: image\n  - name: label\n    type: category\noutput_features:\n    - name: label\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/mushroom_edibility.yaml",
    "content": "version: 1.0\nname: mushroom_edibility\ndownload_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/mushroom/agaricus-lepiota.data\nsha256:\n  agaricus-lepiota.data: e65d082030501a3ebcbcd7c9f7c71aa9d28fdfff463bf4cf4716a3fe13ac360e\ntrain_filenames: agaricus-lepiota.data\ndescription: |\n  This data set includes descriptions of hypothetical samples corresponding\n  to 23 species of gilled mushrooms in the Agaricus and Lepiota Family (pp. 500-525).\n  Each species is identified as definitely edible, definitely poisonous,\n  or of unknown edibility and not recommended. This latter class was combined with\n  the poisonous one.\ncolumns:\n  - name: class\n    type: category\n  - name: cap-shape\n    type: category\n  - name: cap-surface\n    type: category\n  - name: cap-color\n    type: category\n  - name: bruises?\n    type: category\n  - name: odor\n    type: category\n  - name: gill-attachment\n    type: category\n  - name: gill-spacing\n    type: category\n  - name: gill-size\n    type: category\n  - name: gill-color\n    type: category\n  - name: stalk-shape\n    type: category\n  - name: stalk-root\n    type: category\n  - name: stalk-surface-above-ring\n    type: category\n  - name: stalk-surface-below-ring\n    type: category\n  - name: stalk-color-above-ring\n    type: category\n  - name: stalk-color-below-ring\n    type: category\n  - name: veil-type\n    type: category\n  - name: veil-color\n    type: category\n  - name: ring-number\n    type: category\n  - name: ring-type\n    type: category\n  - name: spore-print-color\n    type: category\n  - name: population\n    type: category\n  - name: habitat\n    type: category\noutput_features:\n  - name: class\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/naval.yaml",
    "content": "version: 1.0\nname: naval\ndownload_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/00316/UCI%20CBM%20Dataset.zip\nsha256:\n  UCI%20CBM%20Dataset.zip: 91a3815da80b5ab7e2d5b82ac82f1c2cbf89182c7a65bcdf240db1e014423cb9\ndataset_filenames: UCI CBM Dataset/data.txt\nloader: naval.NavalLoader\ndescription: |\n  Condition Based Maintenance of Naval Propulsion Plants Data Set\n  http://archive.ics.uci.edu/ml/datasets/condition+based+maintenance+of+naval+propulsion+plants\ncolumns:\n  - name: lp\n    type: number\n  - name: v\n    type: number\n  - name: gtt\n    type: number\n  - name: gtn\n    type: number\n  - name: ggn\n    type: number\n  - name: ts\n    type: number\n  - name: tp\n    type: number\n  - name: t48\n    type: number\n  - name: t1\n    type: number\n  - name: t2\n    type: number\n  - name: p48\n    type: number\n  - name: p1\n    type: number\n  - name: p2\n    type: number\n  - name: pexh\n    type: number\n  - name: tic\n    type: number\n  - name: mf\n    type: number\n  - name: gtcdsc\n    type: number\n  - name: gttdsc\n    type: number\noutput_features:\n- name: gtcdsc\n  type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/news_channel.yaml",
    "content": "version: 1.0\nname: news_channel\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/news_channel/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/news_channel/test.csv\nsha256:\n  test.csv: d48e7261dce69964eb1163c89e05261b8732c676b10de9b40339b2d95559c9c3\n  train.csv: 46e433fcf070ec684cfaf30bada482a73637e8dd954edc3e1fe860de8e661055\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Online News Popularity Data Set\n  This dataset summarizes a heterogeneous set of features about articles\n  published by Mashable in a period of two years. The goal is to predict\n  the number of shares in social networks (popularity).\n  https://archive.ics.uci.edu/ml/datasets/online+news+popularity\ncolumns:  # Most lot of these columns have a leading space\n  - name:  n_tokens_content\n    type: number\n  - name:  n_unique_tokens\n    type: number\n  - name:  n_non_stop_words\n    type: number\n  - name:  n_non_stop_unique_tokens\n    type: number\n  - name:  num_hrefs\n    type: number\n  - name:  num_self_hrefs\n    type: number\n  - name:  num_imgs\n    type: number\n  - name:  num_videos\n    type: number\n  - name:  average_token_length\n    type: number\n  - name:  num_keywords\n    type: number\n  - name:  global_subjectivity\n    type: number\n  - name:  global_sentiment_polarity\n    type: number\n  - name:  global_rate_positive_words\n    type: number\n  - name:  global_rate_negative_words\n    type: number\n  - name:  rate_positive_words\n    type: number\n  - name:  rate_negative_words\n    type: number\n  - name: article_title\n    type: text\n  - name: channel\n    type: category\noutput_features:\n- name: channel\n  type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/news_popularity2.yaml",
    "content": "version: 1.0\nname: news_popularity2\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/news_popularity2/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/news_popularity2/test.csv\nsha256:\n  test.csv: 276effa981456e187fb1fc07abd8556d240e1a110fc5c096f2ad75a4082d1ccb\n  train.csv: 3673a07b87dbe09a9073e5ab83241681f561984269a9dc5411018fd9bca70b71\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Online News Popularity Data Set\n  This dataset summarizes a heterogeneous set of features about articles\n  published by Mashable in a period of two years. The goal is to predict\n  the number of shares in social networks (popularity).\n  https://archive.ics.uci.edu/ml/datasets/online+news+popularity\ncolumns:\n  - name: n_tokens_content\n    type: number\n  - name: average_token_length\n    type: number\n  - name: num_keywords\n    type: number\n  - name: log_shares\n    type: number\n  - name: article_title\n    type: text\noutput_features:\n- name: log_shares\n  type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/noshow_appointments.yaml",
    "content": "version: 1.0\nname: noshow_appointments\nkaggle_dataset_id: joniarroba/noshowappointments\narchive_filenames: noshowappointments.zip\nsha256:\n  noshowappointments.zip: 4b4f258837029bd4e61ed4c9bab2ce8a3b8a299d1a4f5bdabcc98967d5e29a43\nloader: split_loaders.RandomSplitLoader\ndescription: |\n  110.527 medical appointments its 14 associated variables (characteristics).\n  The most important one if the patient show-up or no-show to the appointment.\n  https://www.kaggle.com/datasets/joniarroba/noshowappointments\ncolumns:\n  - name: PatientId\n    type: category\n  - name: AppointmentID\n    type: category\n  - name: Gender\n    type: binary\n  - name: ScheduledDay\n    type: date\n  - name: AppointmentDay\n    type: date\n  - name: Age\n    type: number\n  - name: Neighbourhood\n    type: category\n  - name: Scholarship\n    type: binary\n  - name: Hipertension\n    type: binary\n  - name: Diabetes\n    type: binary\n  - name: Alcoholism\n    type: binary\n  - name: Handcap\n    type: binary\n  - name: SMS_received\n    type: binary\n  - name: No-show\n    type: binary\noutput_features:\n  - name: No-show\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/numerai28pt6.yaml",
    "content": "version: 1.0\nname: numerai28pt6\nkaggle_dataset_id: numerai/encrypted-stock-market-data-from-numerai\narchive_filenames: encrypted-stock-market-data-from-numerai.zip\nsha256:\n  encrypted-stock-market-data-from-numerai.zip: cc0714c5f4c8ac6b212f7569641c5110bd2296547af434cba77184ebb03f304b\ndescription: |\n  Encrypted Stock Market Data from Numerai dataset from Kaggle.\ncolumns:\n  - name: feature1\n    type: number\n  - name: feature2\n    type: number\n  - name: feature3\n    type: number\n  - name: feature4\n    type: number\n  - name: feature5\n    type: number\n  - name: feature6\n    type: number\n  - name: feature7\n    type: number\n  - name: feature8\n    type: number\n  - name: feature9\n    type: number\n  - name: feature10\n    type: number\n  - name: feature11\n    type: number\n  - name: feature12\n    type: number\n  - name: feature13\n    type: number\n  - name: feature14\n    type: number\n  - name: feature15\n    type: number\n  - name: feature16\n    type: number\n  - name: feature17\n    type: number\n  - name: feature18\n    type: number\n  - name: feature19\n    type: number\n  - name: feature20\n    type: number\n  - name: feature21\n    type: number\n  - name: target\n    type: binary\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/ohsumed_7400.yaml",
    "content": "version: 1.0\nname: ohsumed_7400\nkaggle_dataset_id: weipengfei/ohr8r52\narchive_filenames: ohr8r52.zip\nsha256:\n  ohr8r52.zip: 93c7a8817a32b994d93267506ad766281764ba9382e3f4f9d978544cebab6ca4\ntrain_filenames: oh/oh-train-stemmed.csv\nvalidation_filenames: oh/oh-dev-stemmed.csv\ntest_filenames: oh/oh-test-stemmed.csv\ndescription: |\n  Ohsumed corpus is extracted from MEDLINE database. MEDLINE is designed for multi-label classification, we remove the\n  text with two or more labels.\n  https://www.kaggle.com/datasets/weipengfei/ohr8r52\ncolumns:\n    - name: text\n      type: text\n    - name: edge\n      type: text\n    - name: intent\n      type: category\noutput_features:\n    - name: intent\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/ohsumed_cmu.yaml",
    "content": "version: 1.0\nname: ohsumed_cmu\ndownload_urls: http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/ohsumed-allcats-6.zip\nsha256:\n  ohsumed-allcats-6.zip: 3f2f6c4e27faaac1c8dc179a121bed92d6adbdf91a1e11d2d124f7bd963798da\ndescription: |\n  OHSUMED is a well-known medical abstracts dataset. It contains 348,566 references,\n  and is still used for research and development.\n\n  This is a subset of OHSUMED containing 6 categories, from this CMU course:\n  http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/\ncolumns:\n    - name: text\n      type: text\n    - name: class\n      type: category\noutput_features:\n  - name: class\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/otto_group_product.yaml",
    "content": "version: 1.0\nname: otto_group_product\nkaggle_competition: otto-group-product-classification-challenge\narchive_filenames: otto-group-product-classification-challenge.zip\nsha256:\n  otto-group-product-classification-challenge.zip: 81d1fa5805036772b7a2a2425311fdc7b1568af4fbb42f0ec8f9661d0d21ce42\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  The Otto Group Product Classification Challenge\n  https://www.kaggle.com/c/otto-group-product-classification-challenge/overview\noutput_features:\n  - name: target\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/poker_hand.yaml",
    "content": "version: 1.0\nname: poker_hand\ndownload_urls:\n  - http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-training-true.data\n  - http://archive.ics.uci.edu/ml/machine-learning-databases/poker/poker-hand-testing.data\ntrain_filenames: poker-hand-training-true.data\ntest_filenames: poker-hand-testing.data\nsha256:\n  poker-hand-testing.data: 3cd75958e19dd321ed5ca3f7f154c0f6aad544aab9f37731ac545b5f66b232c7\n  poker-hand-training-true.data: 37becdf87d5f8cbf2b91d6471e965a25b86cb4a6d878c0f94a4025969fca464f\ndescription: |\n  Each record is an example of a hand consisting of five playing cards\n  drawn from a standard deck of 52. Each card is described using two\n  attributes (suit and rank), for a total of 10 predictive attributes.\n  There is one Class attribute that describes the \"Poker Hand\". The\n  order of cards is important, which is why there are 480 possible\n  Royal Flush hands as compared to 4.\n  https://archive.ics.uci.edu/ml/datasets/Poker+Hand\ncolumns:\n  - name: S1\n    type: number\n  - name: C1\n    type: number\n  - name: S2\n    type: number\n  - name: C2\n    type: number\n  - name: S3\n    type: number\n  - name: C3\n    type: number\n  - name: S4\n    type: number\n  - name: C4\n    type: number\n  - name: S5\n    type: number\n  - name: C5\n    type: number\n  - name: hand\n    type: category\noutput_features:\n  - name: hand\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/porto_seguro_safe_driver.yaml",
    "content": "version: 1.0\nname: porto_seguro_safe_driver\nkaggle_competition: porto-seguro-safe-driver-prediction\narchive_filenames: porto-seguro-safe-driver-prediction.zip\nsha256:\n  porto-seguro-safe-driver-prediction.zip: 53dd7b67b9b3df088c4e0814cba7317d3bc8f76094c726471c8f91e84f61ccdc\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Predict the probability that an auto insurance policy holder files a claim.\n  https://www.kaggle.com/competitions/porto-seguro-safe-driver-prediction\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/product_sentiment_machine_hack.yaml",
    "content": "version: 1.0\nname: product_sentiment_machine_hack\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_product_sentiment/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/machine_hack_product_sentiment/dev.csv\nsha256:\n  dev.csv: 33adff4dba7d9322397b398900c20f678d3fffc5d87b0ea825d9aa497a343150\n  train.csv: 85a229e162b6d8c4839d1b27f834c36ae5e244fd027534fe62a888d4f536f0ef\ntrain_filenames: train.csv\ntest_filenames: dev.csv\ndescription: |\n  We challenge the machinehackers community to develop a machine learning model\n  to accurately classify various products into 4 different classes of sentiments\n  based on the raw text review provided by the user.\n  https://www.machinehack.com/hackathons/product_sentiment_classification_weekend_hackathon_19/overview\ncolumns:\n  - name: Text_ID\n    type: category\n  - name: Product_Description\n    type: text\n  - name: Product_Type\n    type: category\n  - name: Sentiment\n    type: category\noutput_features:\n- name: Sentiment\n  type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/protein.yaml",
    "content": "version: 1.0\nname: protein\ndownload_urls: http://archive.ics.uci.edu/ml/machine-learning-databases/00265/CASP.csv\nsha256:\n  CASP.csv: 4277cfcb4e91a181746cbc654f001b57951c9e6a80f4f795fdb5c807e0848f40\ndescription: |\n  Physicochemical Properties of Protein Tertiary Structure Data Set.\n  https://archive.ics.uci.edu/ml/datasets/Physicochemical+Properties+of+Protein+Tertiary+Structure\ncolumns:\n  - name: RMSD\n    type: number\n  - name: F1\n    type: number\n  - name: F2\n    type: number\n  - name: F3\n    type: number\n  - name: F4\n    type: number\n  - name: F5\n    type: number\n  - name: F6\n    type: number\n  - name: F7\n    type: number\n  - name: F8\n    type: number\n  - name: F9\n    type: number\noutput_features:\n  - name: RMSD\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/reuters_cmu.yaml",
    "content": "version: 1.0\nname: reuters_cmu\ndownload_urls: http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/reuters-allcats-6.zip\nsha256:\n  reuters-allcats-6.zip: 304ae223f9ca35f7ce9066c9d31558c06ed5c72cd91faa885f82b928b2aa6f34\ndescription: |\n  Reuters-21578 is a well-known newswire dataset containing 21,578 documents.\n\n  This is a subset of Reuters-21578 using only 6 categories, from this CMU course:\n  http://boston.lti.cs.cmu.edu/classes/95-865-K/HW/HW2/\ncolumns:\n    - name: text\n      type: text\n    - name: class\n      type: category\noutput_features:\n  - name: class\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/reuters_r8.yaml",
    "content": "version: 1.0\nname: reuters_r8\nkaggle_dataset_id: weipengfei/ohr8r52\narchive_filenames: ohr8r52.zip\nsha256:\n  ohr8r52.zip: 93c7a8817a32b994d93267506ad766281764ba9382e3f4f9d978544cebab6ca4\ntrain_filenames: r8/r8-train-stemmed.csv\nvalidation_filenames: r8/r8-dev-stemmed.csv\ntest_filenames: r8/r8-test-stemmed.csv\ndescription: |\n  Reuters R8 subset of Reuters 21578 dataset from Kaggle.\ncolumns:\n    - name: text\n      type: text\n    - name: edge\n      type: text\n    - name: intent\n      type: category\noutput_features:\n    - name: intent\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/rossman_store_sales.yaml",
    "content": "version: 1.0\nname: rossman_store_sales\nkaggle_competition: rossmann-store-sales\narchive_filenames: rossmann-store-sales.zip\nsha256:\n  rossmann-store-sales.zip: 52ce715e02dc70cac16b14548580d656997f5d43ce3544220d5e574d26483cf3\nloader: rossman_store_sales.RossmanStoreSalesLoader\ndescription: |\n  The Rossmann Store Sales dataset.\n  Using the time split from the catboost benchmark\n  https://github.com/catboost/benchmarks/tree/master/kaggle/rossmann-store-sales\n  that is used in the TabNet paper,\n  because the test set does not contain sales ground truth.\noutput_features:\n  - name: Sales\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/santander_customer_satisfaction.yaml",
    "content": "version: 1.0\nname: santander_customer_satisfaction\nkaggle_competition: santander-customer-satisfaction\narchive_filenames: santander-customer-satisfaction.zip\nsha256:\n  santander-customer-satisfaction.zip: d4c2d068d8041af168d82d0eef7ad0b53ddd1d7fca9aba4e5d88fa1f957ee594\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Santander Customer Satisfaction Prediction.\n  https://www.kaggle.com/c/santander-customer-satisfaction/overview\noutput_features:\n  - name: TARGET\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/santander_customer_transaction.yaml",
    "content": "version: 1.0\nname: santander_customer_transaction\nkaggle_competition: santander-customer-transaction-prediction\narchive_filenames: santander-customer-transaction-prediction.zip\nsha256:\n  santander-customer-transaction-prediction.zip: b3a56d036b493a9cf0695018c968baba1ba7ef8c39d842cc5626e72f13c0ec69\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Santander Customer Transaction Prediction.\n  https://www.kaggle.com/c/santander-customer-transaction-prediction/overview\noutput_features:\n  - name: target\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/santander_value_prediction.yaml",
    "content": "version: 1.0\nname: santander_value_prediction\nkaggle_competition: santander-value-prediction-challenge\narchive_filenames: santander-value-prediction-challenge.zip\nsha256:\n  santander-value-prediction-challenge.zip: a8b44a0403bff6ab42f2bd1da8d9cbaf98f1fd4b9ea7a86e47491ac996384bf4\ntrain_filenames: train.csv\nloader: santander_value_prediction.SantanderValuePredictionLoader\ndescription: |\n  The Santander Value Prediction Challenge dataset.\n  https://www.kaggle.com/c/santander-value-prediction-challenge\noutput_features:\n  - name: target\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/sarcastic_headlines.yaml",
    "content": "version: 1.0\nname: sarcastic_headlines\ntrain_filenames: Sarcasm_Headlines_Dataset.json\narchive_filenames: news-headlines-dataset-for-sarcasm-detection.zip\nsha256:\n  news-headlines-dataset-for-sarcasm-detection.zip: 3728f0fbce563536c3c67ab92e343e3ebcdc5cf1feaf4980c3abd4e54109eb51\nkaggle_dataset_id: rmisra/news-headlines-dataset-for-sarcasm-detection\ndescription: A dataset to determine if a news headline is sarcastic or serious.\nloader: sarcastic_headlines.SarcasticHeadlinesLoader\ncolumns:\n    - name: article_link\n      type: category\n    - name: headline\n      type: text\n    - name: is_sarcastic\n      type: binary\noutput_features:\n  - name: is_sarcastic\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/sarcos.yaml",
    "content": "version: 1.0\nname: sarcos\ndownload_urls:\n  - http://www.gaussianprocess.org/gpml/data/sarcos_inv.mat\n  - http://www.gaussianprocess.org/gpml/data/sarcos_inv_test.mat\nsha256:\n  sarcos_inv_test.mat: 161a59b5c3b4f4b404584323f181607b2acbe620eb134dc720760dc3f38f5cec\n  sarcos_inv.mat: b8a249733253ba6097372fedee7696833fcf30de42037d5b4a7227f21a6d1d97\ntrain_filenames: sarcos_inv.mat\ntest_filenames: sarcos_inv_test.mat\nloader: sarcos.SarcosLoader\ndescription: |\n  The data relates to an inverse dynamics problem for a seven\n  degrees-of-freedom SARCOS anthropomorphic robot arm.\n  The task is to map from a 21-dimensional input space\n  (7 joint positions, 7 joint velocities, 7 joint accelerations)\n  to the corresponding 7 joint torques.\n  http://gaussianprocess.org/gpml/data/\ncolumns:\n  - name: position_1\n    type: number\n  - name: position_2\n    type: number\n  - name: position_3\n    type: number\n  - name: position_4\n    type: number\n  - name: position_5\n    type: number\n  - name: position_6\n    type: number\n  - name: position_7\n    type: number\n  - name: velocity_1\n    type: number\n  - name: velocity_2\n    type: number\n  - name: velocity_3\n    type: number\n  - name: velocity_4\n    type: number\n  - name: velocity_5\n    type: number\n  - name: velocity_6\n    type: number\n  - name: velocity_7\n    type: number\n  - name: acceleration_1\n    type: number\n  - name: acceleration_2\n    type: number\n  - name: acceleration_3\n    type: number\n  - name: acceleration_4\n    type: number\n  - name: acceleration_5\n    type: number\n  - name: acceleration_6\n    type: number\n  - name: acceleration_7\n    type: number\n  - name: torque_1\n    type: number\n  - name: torque_2\n    type: number\n  - name: torque_3\n    type: number\n  - name: torque_4\n    type: number\n  - name: torque_5\n    type: number\n  - name: torque_6\n    type: number\n  - name: torque_7\n    type: number\noutput_features:\n- name: torque_1\n  type: number\nfallback_mirrors:\n  - name: predibase\n    download_paths:\n      - s3://ludwig-tests/ludwig_backup/sarcos_inv.mat\n      - s3://ludwig-tests/ludwig_backup/sarcos_inv_test.mat\n"
  },
  {
    "path": "ludwig/datasets/configs/sst2.yaml",
    "content": "version: 1.0\nname: sst2\ndownload_urls: https://nlp.stanford.edu/~socherr/stanfordSentimentTreebank.zip\nsha256:\n  stanfordSentimentTreebank.zip: 3f5209483b46bbf129cacbbbe6ae02fe780407034f61cf6342b7833257c3f1db\ntrain_filenames: train.csv\nvalidation_filenames: dev.csv\ntest_filenames: test.csv\nloader: sst.SST2Loader\ndescription: |\n  The SST2 dataset.\n\n  This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n  This dataset contains binary labels (positive or negative) for each sample.\n\n  The original dataset specified 5 labels:\n  very negative, negative, neutral, positive, very positive with\n  the following cutoffs:\n  [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0]\n\n  In the construction of this dataset, we remove all neutral phrases\n  and assign a negative label if the original rating falls\n  into the following range: [0, 0.4] and a positive label\n  if the original rating is between (0.6, 1.0].\ncolumns:\n  - name: sentence\n    type: text\n  - name: label\n    type: binary\noutput_features:\n    - name: label\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/sst3.yaml",
    "content": "version: 1.0\nname: sst3\ndownload_urls: https://nlp.stanford.edu/~socherr/stanfordSentimentTreebank.zip\nsha256:\n  stanfordSentimentTreebank.zip: 3f5209483b46bbf129cacbbbe6ae02fe780407034f61cf6342b7833257c3f1db\ntrain_filenames: train.csv\nvalidation_filenames: dev.csv\ntest_filenames: test.csv\nloader: sst.SST3Loader\ndescription: |\n  The SST3 dataset.\n\n  This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n  The original dataset contains five labels (very negative, negative, neutral,\n  positive, very positive) for each sample.\n\n  In this dataset, the 3 labels negative, neutral, positive have the following cutoffs:\n  [0, 0.4], (0.4, 0.6], (0.6, 1.0]\ncolumns:\n  - name: sentence\n    type: text\n  - name: label\n    type: category\noutput_features:\n    - name: label\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/sst5.yaml",
    "content": "version: 1.0\nname: sst5\ndownload_urls: https://nlp.stanford.edu/~socherr/stanfordSentimentTreebank.zip\nsha256:\n  stanfordSentimentTreebank.zip: 3f5209483b46bbf129cacbbbe6ae02fe780407034f61cf6342b7833257c3f1db\ntrain_filenames: train.csv\nvalidation_filenames: dev.csv\ntest_filenames: test.csv\nloader: sst.SST5Loader\ndescription: |\n  The SST5 dataset.\n\n  This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n  This dataset contains five labels (very negative, negative, neutral,\n  positive, very positive) for each sample.\n\n  In the original dataset, the  5 labels: very negative, negative, neutral, positive,\n  and very positive have the following cutoffs:\n  [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0]\ncolumns:\n  - name: sentence\n    type: text\n  - name: label\n    type: category\noutput_features:\n    - name: label\n      type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/synthetic_fraud.yaml",
    "content": "version: 1.0\nname: synthetic_fraud\nkaggle_dataset_id: ealaxi/paysim1\narchive_filenames: paysim1.zip\nsha256:\n  paysim1.zip: f7eef9ffad5cfa64a034143a5c9b30491d189420b273d5ad5723ca40b596613d\ndescription: |\n  The Synthetic Financial Datasets For Fraud Detection dataset.\n  https://www.kaggle.com/ealaxi/paysim1\ncolumns:\n  - name: step\n    type: category\n  - name: type\n    type: category\n  - name: amount\n    type: number\n  - name: nameOrig\n    type: category\n  - name: oldbalanceOrg\n    type: number\n  - name: newbalanceOrig\n    type: number\n  - name: nameDest\n    type: category\n  - name: oldbalanceDest\n    type: number\n  - name: newbalanceDest\n    type: number\n  - name: isFraud\n    type: binary\n  - name: isFlaggedFraud\n    type: binary\noutput_features:\n  - name: isFraud\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/talkingdata_adtrack_fraud.yaml",
    "content": "version: 1.0\nname: talkingdata_adtrack_fraud_detection\nkaggle_competition: talkingdata-adtracking-fraud-detection\narchive_filenames: talkingdata-adtracking-fraud-detection.zip\nsha256:\n  talkingdata-adtracking-fraud-detection.zip: 4441bea984e936db153aba30627b222cb1685021efb887bd22d78771fb793735\ntrain_filenames: train.csv\ndescription: |\n  TalkingData AdTracking Fraud Detection Challenge.\n  https://www.kaggle.com/competitions/talkingdata-adtracking-fraud-detection/overview\noutput_features:\n  - name: is_attributed\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/telco_customer_churn.yaml",
    "content": "version: 1.0\nname: telco_customer_churn\nkaggle_dataset_id: blastchar/telco-customer-churn\narchive_filenames: telco-customer-churn.zip\ndataset_filenames: WA_Fn-UseC_-Telco-Customer-Churn.csv\nsha256:\n  telco-customer-churn.zip: cf7e6dcd8a238ecaa841a7d133142525453992d8d5e3ef6d1e5f0d359e7bf444\ndescription: |\n  The Telco customer churn data contains information about a fictional telco company\n  that provided home phone and Internet services to customers. Each row represents a\n  customer, each column contains customer’s attributes described on the column Metadata.\n  https://www.kaggle.com/datasets/blastchar/telco-customer-churn\ncolumns:\n  - name: customerID\n    type: category\n  - name: gender\n    type: binary\n  - name: SeniorCitizen\n    type: binary\n  - name: Partner\n    type: binary\n  - name: Dependents\n    type: binary\n  - name: tenure\n    type: number\n  - name: PhoneService\n    type: binary\n  - name: MultipleLines\n    type: category\n  - name: InternetService\n    type: category\n  - name: OnlineSecurity\n    type: category\n  - name: OnlineBackup\n    type: category\n  - name: DeviceProtection\n    type: category\n  - name: TechSupport\n    type: category\n  - name: StreamingTV\n    type: category\n  - name: StreamingMovies\n    type: category\n  - name: Contract\n    type: category\n  - name: PaperlessBilling\n    type: binary\n  - name: PaymentMethod\n    type: category\n  - name: MonthlyCharges\n    type: number\n  - name: TotalCharges\n    type: number\n  - name: Churn\n    type: binary\noutput_features:\n    - name: Churn\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/temperature.yaml",
    "content": "version: 1.0\nname: temperature\nkaggle_dataset_id: selfishgene/historical-hourly-weather-data\narchive_filenames: historical-hourly-weather-data.zip\nsha256:\n  historical-hourly-weather-data.zip: db40ffce67318f366115b82a6f693d6dc82c808f23514e2ddae56c0434f606d7\ndataset_filenames: temperature.csv\ndescription: |\n  Hourly temperature dataset from Kaggle\n    https://www.kaggle.com/selfishgene/historical-hourly-weather-data\ncolumns:\n  - name: datetime\n    type: date\n  - name: Vancouver\n    type: number\n  - name: Portland\n    type: number\n  - name: San Francisco\n    type: number\n  - name: Seattle\n    type: number\n  - name: Los Angeles\n    type: number\n  - name: San Diego\n    type: number\n  - name: Las Vegas\n    type: number\n  - name: Phoenix\n    type: number\n  - name: Albuquerque\n    type: number\n  - name: Denver\n    type: number\n  - name: San Antonio\n    type: number\n  - name: Dallas\n    type: number\n  - name: Houston\n    type: number\n  - name: Kansas City\n    type: number\n  - name: Minneapolis\n    type: number\n  - name: Saint Louis\n    type: number\n  - name: Chicago\n    type: number\n  - name: Nashville\n    type: number\n  - name: Indianapolis\n    type: number\n  - name: Atlanta\n    type: number\n  - name: Detroit\n    type: number\n  - name: Jacksonville\n    type: number\n  - name: Charlotte\n    type: number\n  - name: Miami\n    type: number\n  - name: Pittsburgh\n    type: number\n  - name: Toronto\n    type: number\n  - name: Philadelphia\n    type: number\n  - name: New York\n    type: number\n  - name: Montreal\n    type: number\n  - name: Boston\n    type: number\n  - name: Beersheba\n    type: number\n  - name: Tel Aviv District\n    type: number\n  - name: Eilat\n    type: number\n  - name: Haifa\n    type: number\n  - name: Nahariyya\n    type: number\n  - name: Jerusalem\n    type: number\noutput_features:\n  - name: San Francisco\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/titanic.yaml",
    "content": "version: 1.0\nname: titanic\nkaggle_competition: titanic\narchive_filenames: titanic.zip\nsha256:\n  titanic.zip: bb1bda464cc6819d412b41d34be69fd89d26b372dc24c09421c3dbca1b0dbe9f\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  The Titanic dataset: use machine learning to create a model\n  that predicts which passengers survived the Titanic shipwreck.\n    https://www.kaggle.com/c/titanic\noutput_features:\n    - name: Survived\n      type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/twitter_bots.yaml",
    "content": "version: 1.0\nname: twitter_bots\nkaggle_dataset_id: danieltreiman/twitter-human-bots-dataset\narchive_filenames: twitter-human-bots-dataset.zip\ndataset_filenames: twitter_human_bots_dataset.csv\nsha256:\n  twitter-human-bots-dataset.zip: 16ffaad719ebb9688231844a80f92901c5efb1ff96eafeb869dc5de07b323cdd\npreserve_paths:\n  - profile_images\n  - profile_background_images\ndescription: |\n  A dataset for Twitter Bot account detection.\n  https://www.kaggle.com/datasets/davidmartngutirrez/twitter-bots-accounts\ncolumns:\n  - name: created_at\n    type: date\n  - name: default_profile\n    type: binary\n  - name: default_profile_image\n    type: binary\n  - name: description\n    type: text\n  - name: favourites_count\n    type: number\n  - name: followers_count\n    type: number\n  - name: friends_count\n    type: number\n  - name: geo_enabled\n    type: binary\n  - name: id\n    type: category\n  - name: lang\n    type: category\n  - name: location\n    type: category\n  - name: profile_background_image_url\n    type: category\n  - name: profile_image_url\n    type: category\n  - name: screen_name\n    type: category\n  - name: statuses_count\n    type: number\n  - name: verified\n    type: binary\n  - name: average_tweets_per_day\n    type: number\n  - name: account_age_days\n    type: number\n  - name: account_type\n    type: category\n  - name: profile_image_path\n    type: image\n  - name: profile_background_image_path\n    type: image\noutput_features:\n  - name: account_type\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/walmart_recruiting.yaml",
    "content": "version: 1.0\nname: walmart_recruiting\nkaggle_competition: walmart-recruiting-trip-type-classification\narchive_filenames: walmart-recruiting-trip-type-classification.zip\nsha256:\n  walmart-recruiting-trip-type-classification.zip: 4c0ad71034d0b907e018adcb00c7b2835d2c30abe770fde5ce8719d7b89d4de6\ntrain_filenames: train.csv\ndescription: |\n  Walmart Recruiting: Trip Type Classification\n  https://www.kaggle.com/c/walmart-recruiting-trip-type-classification\noutput_features:\n  - name: TripType\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/wine_reviews.yaml",
    "content": "version: 1.0\nname: wine_reviews\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/wine_reviews/train.csv\n  - https://automl-mm-bench.s3.amazonaws.com/wine_reviews/test.csv\nsha256:\n  test.csv: c862d1af572659406ab39356a25c7d5e9b7c8570a89e069311fca1abb6bf1849\n  train.csv: c54101bb07571a3df0723e93a5f7c48123dd792b316396db4404a04bcf1809cb\ntrain_filenames: train.csv\ntest_filenames: test.csv\ndescription: |\n  Wine Reviews\n  130k wine reviews with variety, location, winery, price, and description\n  https://www.kaggle.com/datasets/zynicide/wine-reviews\ncolumns:\n  - name: country\n    type: category\n  - name: description\n    type: text\n  - name: points\n    type: number\n  - name: price\n    type: number\n  - name: province\n    type: category\n  - name: variety\n    type: category\noutput_features:\n  - name: points\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/wmt15.yaml",
    "content": "version: 1.0\nname: wmt15\nkaggle_dataset_id: dhruvildave/en-fr-translation-dataset\narchive_filenames: en-fr-translation-dataset.zip\nsha256:\n  en-fr-translation-dataset.zip: 5fb911b327f2f36ea32315b4754f6aef95e6830562eec7054d31d614dd53d93c\ndescription: |\n  French/English parallel texts for training translation models.\n  Over 22.5 million sentences in French and English.\n  https://www.kaggle.com/dhruvildave/en-fr-translation-dataset\noutput_features:\n  - name: en\n    type: text\n"
  },
  {
    "path": "ludwig/datasets/configs/women_clothing_review.yaml",
    "content": "version: 1.0\nname: women_clothing_review\ndownload_urls:\n  - https://automl-mm-bench.s3.amazonaws.com/women_clothing_review/train.pq\n  - https://automl-mm-bench.s3.amazonaws.com/women_clothing_review/test.pq\nsha256:\n  test.pq: 477de72fe7e672ef87e1eca00de312f55ba884a9b80fbd04fa79c0d0159e5593\n  train.pq: 1b3d248397cee76a6ccff814560f29ae3d66eeb26a6e97ac0837e021629bc740\ntrain_filenames: train.pq\ntest_filenames: test.pq\ndescription: |\n  Women's E-Commerce Clothing Reviews\n  23,000 Customer Reviews and Ratings\n  https://www.kaggle.com/nicapotato/womens-ecommerce-clothing-reviews\ncolumns:\n  - name: Clothing ID\n    type: category\n  - name: Age\n    type: number\n  - name: Title\n    type: text\n  - name: Review Text\n    type: text\n  - name: Rating\n    type: number\n  - name: Recommended IND\n    type: binary\n  - name: Positive Feedback Count\n    type: number\n  - name: Division Name\n    type: category\n  - name: Department Name\n    type: category\n  - name: Class Name\n    type: category\noutput_features:\n  - name: Rating\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/configs/yahoo_answers.yaml",
    "content": "version: 1.0\nname: yahoo_answers\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/yahoo_answers_csv.tgz\nsha256:\n  yahoo_answers_csv.tgz: 2d4277855faf8b35259009425fa8f7fe1888b5644b47165508942d000f4c96ae\ntrain_filenames: yahoo_answers_csv/train.csv\ntest_filenames: yahoo_answers_csv/test.csv\ndescription: |\n  The Yahoo Answers dataset\n  Details:\n      The 10 largest main categories from the Yahoo! Answers \\\n      Comprehensive Questions and Answers version 1.0 dataset. \\\n      Each class contains 140,000 training samples and 5,000 \\\n      testing samples.\n  Dataset source:\n      Character-level Convolutional Networks for Text Classification\n      Xiang Zhang et al., 2015\n        https://arxiv.org/abs/1509.01626\ncolumns:\n  - name: label\n    type: category\n  - name: question_title\n    type: text\n  - name: question\n    type: text\n  - name: best_answer\n    type: text\noutput_features:\n  - name: label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/yelp_review_polarity.yaml",
    "content": "version: 1.0\nname: yelp_review_polarity\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/yelp_review_polarity_csv.tgz\nsha256:\n  yelp_review_polarity_csv.tgz: 528f22e286cad085948acbc3bea7e58188416546b0e364d0ae4ca0ce666abe35\ntrain_filenames: yelp_review_polarity_csv/train.csv\ntest_filenames: yelp_review_polarity_csv/test.csv\ndescription: |\n  The Yelp Polarity dataset\n  Details:\n      1,569,264 samples from the Yelp Dataset Challenge 2015.\n      This subset has 280,000 training samples and 19,000 test samples\n      in each polarity.\n  Dataset source:\n      Character-level Convolutional Networks for Text Classification\n      Xiang Zhang et al., 2015\ncolumns:\n  - name: label\n    type: binary\n  - name: text\n    type: text\noutput_features:\n  - name: label\n    type: binary\n"
  },
  {
    "path": "ludwig/datasets/configs/yelp_reviews.yaml",
    "content": "version: 1.0\nname: yelp_reviews\ndownload_urls: https://s3.amazonaws.com/fast-ai-nlp/yelp_review_full_csv.tgz\nsha256:\n  yelp_review_full_csv.tgz: 56006b0a17a370f1e366504b1f2c3e3754e4a3dda17d3e718a885c552869a559\ntrain_filenames: yelp_review_full_csv/train.csv\ntest_filenames: yelp_review_full_csv/test.csv\ndescription: |\n  The Yelp Reviews dataset\n  Details:\n      1,569,264 samples from the Yelp Dataset Challenge 2015.\n      This subset has 130,000 training samples and 10,000\n      testing samples in each star rating.\n  Dataset source:\n      Character-level Convolutional Networks for Text Classification\n      Xiang Zhang et al., 2015\ncolumns:\n  - name: label\n    type: category\n  - name: text\n    type: text\noutput_features:\n  - name: label\n    type: category\n"
  },
  {
    "path": "ludwig/datasets/configs/yosemite.yaml",
    "content": "version: 1.0\nname: yosemite\ndownload_urls: https://raw.githubusercontent.com/ourownstory/neuralprophet-data/main/datasets_raw/yosemite_temps.csv\nsha256:\n  yosemite_temps.csv: c0ec9f2cb4bbf0bc53f7bfd2e39f88ae21e43b7b8912b2d1eb8185055f9510e2\ndescription: |\n    Yosemite temperatures dataset.\n    As found in https://github.com/ourownstory/neural_prophet\ncolumns:\n    - name: ds\n      type: date\n    - name: y\n      type: number\noutput_features:\n  - name: y\n    type: number\n"
  },
  {
    "path": "ludwig/datasets/dataset_config.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom dataclasses import dataclass, field\n\nfrom dataclasses_json import dataclass_json\n\n\n@dataclass_json\n@dataclass\nclass DatasetFallbackMirror:\n    # Name of the mirror\n    name: str\n\n    # List of paths to download from. Must map 1:1 to DatasetConfig.download_urls or to the archive_filenames\n    # that we get from Kaggle.\n    download_paths: str | list[str]\n\n\n@dataclass_json\n@dataclass\nclass DatasetConfig:\n    \"\"\"The configuration of a Ludwig dataset.\"\"\"\n\n    # The version of the dataset.\n    version: str\n\n    # The name of the dataset. Make this a valid python module name, should not contain spaces or dashes.\n    name: str\n\n    # The readable description of the dataset\n    description: str = \"\"\n\n    # Fallback mirrors. Paths must be in local/remote filesystems.\n    fallback_mirrors: list[DatasetFallbackMirror] | None = None\n\n    # Optional. The (suggested) output features for this dataset. Helps users discover new datasets and filter for\n    # relevance to a specific machine learning setting.\n    output_features: list[dict] = field(default_factory=list)\n\n    # The kaggle competition this dataset belongs to, or None if this dataset is not hosted by a Kaggle competition.\n    kaggle_competition: str | None = None\n\n    # The kaggle dataset ID, or None if this dataset if not hosted by Kaggle.\n    kaggle_dataset_id: str | None = None\n\n    # The list of URLs to download.\n    download_urls: str | list[str] = field(default_factory=list)\n\n    # The list of file archives which will be downloaded. If download_urls contains a filename with extension, for\n    # example https://domain.com/archive.zip, then archive_filenames does not need to be specified.\n    archive_filenames: str | list[str] = field(default_factory=list)\n\n    # The names of files in the dataset (after extraction). Glob-style patterns are supported, see\n    # https://docs.python.org/3/library/glob.html\n    dataset_filenames: str | list[str] = field(default_factory=list)\n\n    # If the dataset contains separate files for training, testing, or validation. Glob-style patterns are supported,\n    # see https://docs.python.org/3/library/glob.html\n    train_filenames: str | list[str] = field(default_factory=list)\n    validation_filenames: str | list[str] = field(default_factory=list)\n    test_filenames: str | list[str] = field(default_factory=list)\n\n    # If the dataset contains additional referenced files or directories (ex. images or audio) list them here and they\n    # will be copied to the same location as the processed dataset. Glob-style patterns are supported,\n    # see https://docs.python.org/3/library/glob.html\n    preserve_paths: str | list[str] = field(default_factory=list)\n\n    # Optionally verify integrity of the dataset by providing sha256 checksums for important files. Maps filename to\n    # sha256 digest.  Use `sha256sum <filename>` on linux, `shasum -a 256 <filename>` on Mac to get checksums.\n    # If verification fails, loading the dataset will fail with a ValueError.\n    # If no sha256 digests are in the config, a warning is logged and the dataset will load without verification.\n    sha256: dict[str, str] = field(default_factory=dict)\n\n    # List of column names, for datasets which do not have column names. If specified, will override the column names\n    # already present in the dataset.\n    columns: list[dict] = field(default_factory=list)\n\n    # Optional dictionary which maps column name to column type. Column's will be converted to the requested type, or\n    # will be inferred from the dataset by default.\n    column_types: dict[str, str] = field(default_factory=dict)\n\n    # The loader module and class to use, relative to ludwig.datasets.loaders. Only change this if the dataset requires\n    # processing which is not handled by the default loader.\n    loader: str = \"dataset_loader.DatasetLoader\"\n"
  },
  {
    "path": "ludwig/datasets/kaggle.py",
    "content": "import os\nfrom contextlib import contextmanager\n\nfrom ludwig.utils.fs_utils import upload_output_directory\n\n\ndef create_kaggle_client():\n    # Need to import here to prevent Kaggle from authenticating on import\n    from kaggle import api\n\n    return api\n\n\n@contextmanager\ndef update_env(**kwargs):\n    override_env = {k: v for k, v in kwargs.items() if v is not None}\n    old = os.environ.copy()\n    try:\n        os.environ.update(override_env)\n        yield\n    finally:\n        os.environ = old\n\n\ndef download_kaggle_dataset(\n    download_directory: str,\n    kaggle_dataset_id: str | None = None,\n    kaggle_competition: str | None = None,\n    kaggle_username: str | None = None,\n    kaggle_key: str | None = None,\n):\n    \"\"\"Download all files in a kaggle dataset. One of kaggle_dataset_id,\n\n    If the user has not specified creds in the kaggle.json file we lookup the passed in username and the api key and\n    perform authentication.\n    \"\"\"\n    with update_env(KAGGLE_USERNAME=kaggle_username, KAGGLE_KEY=kaggle_key):\n        # Call authenticate explicitly to pick up new credentials if necessary\n        api = create_kaggle_client()\n        api.authenticate()\n    with upload_output_directory(download_directory) as (tmpdir, _):\n        if kaggle_competition:\n            api.competition_download_files(kaggle_competition, path=tmpdir)\n        else:\n            api.dataset_download_files(kaggle_dataset_id, path=tmpdir)\n    return [os.path.join(download_directory, f) for f in os.listdir(download_directory)]\n"
  },
  {
    "path": "ludwig/datasets/loaders/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/datasets/loaders/adult_census_income.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass AdultCensusIncomeLoader(DatasetLoader):\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        if file_path.endswith(\".test\"):\n            # The test file contains the line \"|1x3 Cross validator\" before the CSV content.\n            return pd.read_csv(file_path, skiprows=1)\n        return super().load_file_to_dataframe(file_path)\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        processed_df[\"income\"] = processed_df[\"income\"].str.rstrip(\".\")\n        processed_df[\"income\"] = processed_df[\"income\"].str.strip()\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/agnews.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass AGNewsLoader(DatasetLoader):\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        # Maps class_index to class name.\n        class_names = [\"\", \"world\", \"sports\", \"business\", \"sci_tech\"]\n        # Adds new column 'class' by mapping class indexes to strings.\n        processed_df[\"class\"] = processed_df.class_index.apply(lambda i: class_names[i])\n        # Agnews has no validation split, only train and test (0, 2). For convenience, we'll designate the first 5% of\n        # each class from the training set as the validation set.\n        val_set_n = int((len(processed_df) * 0.05) // len(class_names))  # rows from each class in validation set.\n        for ci in range(1, 5):\n            # For each class, reassign the first val_set_n rows of the training set to validation set.\n            train_rows = processed_df[(processed_df.split == 0) & (processed_df.class_index == ci)].index\n            processed_df.loc[train_rows[:val_set_n], \"split\"] = 1\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/allstate_claims_severity.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass AllstateClaimsSeverityLoader(DatasetLoader):\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        if os.path.basename(file_path) == \"train.csv\":\n            # train.csv has been updated with quoted test rows at the end; don't load these, only load the original\n            # training set.\n            return pd.read_csv(file_path, nrows=188319)\n        if os.path.basename(file_path) == \"test.csv\":\n            # we limit the loaded rows for the same reason as the training set.\n            return pd.read_csv(file_path, nrows=125547)\n        super().load_file_to_dataframe(file_path)\n"
  },
  {
    "path": "ludwig/datasets/loaders/camseq.py",
    "content": "# Copyright (c) 2023 Aizen Corp.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\nfrom ludwig.utils.fs_utils import makedirs\n\n\nclass CamseqLoader(DatasetLoader):\n    def transform_files(self, file_paths: list[str]) -> list[str]:\n        if not os.path.exists(self.processed_dataset_dir):\n            os.makedirs(self.processed_dataset_dir)\n\n        # move images and masks into separate directories\n        source_dir = self.raw_dataset_dir\n        images_dir = os.path.join(source_dir, \"images\")\n        masks_dir = os.path.join(source_dir, \"masks\")\n        makedirs(images_dir, exist_ok=True)\n        makedirs(masks_dir, exist_ok=True)\n\n        data_files = []\n        for f in os.listdir(source_dir):\n            if f.endswith(\"_L.png\"):  # masks\n                dest_file = os.path.join(masks_dir, f)\n            elif f.endswith(\".png\"):  # images\n                dest_file = os.path.join(images_dir, f)\n            else:\n                continue\n            source_file = os.path.join(source_dir, f)\n            os.replace(source_file, dest_file)\n            data_files.append(dest_file)\n\n        return super().transform_files(data_files)\n\n    def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame:\n        \"\"\"Creates a dataframe of image paths and mask paths.\"\"\"\n        images_dir = os.path.join(self.processed_dataset_dir, \"images\")\n        masks_dir = os.path.join(self.processed_dataset_dir, \"masks\")\n        images = []\n        masks = []\n        for f in os.listdir(images_dir):\n            images.append(os.path.join(images_dir, f))\n            mask_f = f[:-4] + \"_L.png\"\n            masks.append(os.path.join(masks_dir, mask_f))\n\n        return pd.DataFrame({\"image_path\": images, \"mask_path\": masks})\n"
  },
  {
    "path": "ludwig/datasets/loaders/code_alpaca_loader.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass CodeAlpacaLoader(DatasetLoader):\n    \"\"\"The Code Alpaca dataset.\"\"\"\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        df = pd.read_json(file_path)\n        return df\n"
  },
  {
    "path": "ludwig/datasets/loaders/consumer_complaints_loader.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass ConsumerComplaintsLoader(DatasetLoader):\n    \"\"\"The Consumer Complaints dataset.\"\"\"\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n\n        consumer_complaints_df = pd.read_csv(file_path)\n        consumer_complaints_df = preprocess_df(consumer_complaints_df)\n\n        return consumer_complaints_df\n\n\ndef preprocess_df(df):\n    \"\"\"Preprocesses the dataframe.\n\n        - Remove all rows with missing values in the following columns:\n            - Consumer complaint narrative\n            - Issue\n            - Product\n\n    Args:\n        df (pd.DataFrame): The dataframe to preprocess.\n\n    Returns:\n        pd.DataFrame: The preprocessed dataframe.\n    \"\"\"\n    return df.dropna(subset=[\"Consumer complaint narrative\", \"Issue\", \"Product\"])\n"
  },
  {
    "path": "ludwig/datasets/loaders/creditcard_fraud.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass CreditCardFraudLoader(DatasetLoader):\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        # Train/Test split like https://www.kaggle.com/competitions/1056lab-fraud-detection-in-credit-card/overview\n        processed_df = processed_df.sort_values(by=[\"Time\"])\n        processed_df.loc[:198365, \"split\"] = 0\n        processed_df.loc[198365:, \"split\"] = 2\n        processed_df.split = processed_df.split.astype(int)\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/dataset_loader.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom __future__ import annotations\n\nimport glob\nimport hashlib\nimport logging\nimport os\nimport shutil\nimport urllib\nfrom enum import Enum\nfrom urllib.parse import urlparse\n\nimport pandas as pd\nfrom tqdm import tqdm\n\nfrom ludwig.api_annotations import DeveloperAPI, PublicAPI\nfrom ludwig.constants import SPLIT\nfrom ludwig.datasets.archives import extract_archive, is_archive, list_archive\nfrom ludwig.datasets.dataset_config import DatasetConfig, DatasetFallbackMirror\nfrom ludwig.datasets.kaggle import download_kaggle_dataset\nfrom ludwig.datasets.utils import model_configs_for_dataset\nfrom ludwig.utils.fs_utils import get_default_cache_location, get_fs_and_path\nfrom ludwig.utils.strings_utils import make_safe_filename\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\nclass TqdmUpTo(tqdm):\n    \"\"\"Provides progress bar for `urlretrieve`.\n\n    Taken from: https://gist.github.com/leimao/37ff6e990b3226c2c9670a2cd1e4a6f5\n    \"\"\"\n\n    def update_to(self, b=1, bsize=1, tsize=None):\n        \"\"\"\n        b  : int, optional\n            Number of blocks transferred so far [default: 1].\n        bsize  : int, optional\n            Size of each block (in tqdm units) [default: 1].\n        tsize  : int, optional\n            Total size (in tqdm units). If [default: None] remains unchanged.\n        \"\"\"\n        if tsize is not None:\n            self.total = tsize  # noqa W0201\n        self.update(b * bsize - self.n)  # will also set self.n = b * bsize\n\n\ndef _list_of_strings(list_or_string: str | list[str]) -> list[str]:\n    \"\"\"Helper function to accept single string or lists in config.\"\"\"\n    return [list_or_string] if isinstance(list_or_string, str) else list_or_string\n\n\ndef _glob_multiple(pathnames: list[str], root_dir: str = None, recursive: bool = True) -> set[str]:\n    \"\"\"Recursive glob multiple patterns, returns set of matches.\n\n    Note: glob's root_dir argument was added in python 3.10, not using it for compatibility.\n    \"\"\"\n    if root_dir:\n        pathnames = [os.path.join(root_dir, p) for p in pathnames]\n    return set().union(*[glob.glob(p, recursive=recursive) for p in pathnames])\n\n\ndef _sha256_digest(file_path) -> str:\n    \"\"\"Returns the sha256 digest for the specified file.\"\"\"\n    hash = hashlib.sha256()\n    buffer = bytearray(hash.block_size * 1024)  # Attempts to read in multiples of the hash block size (64KB).\n    mv = memoryview(buffer)\n    with open(file_path, \"rb\", buffering=0) as f:\n        for bytes_read in iter(lambda: f.readinto(mv), 0):\n            hash.update(mv[:bytes_read])\n    return hash.hexdigest()\n\n\n@PublicAPI\nclass DatasetState(int, Enum):\n    \"\"\"The state of the dataset.\"\"\"\n\n    NOT_LOADED = 0\n    DOWNLOADED = 1\n    EXTRACTED = 2\n    TRANSFORMED = 3\n\n\n@PublicAPI\nclass DatasetLoader:\n    \"\"\"Base class that defines the default pipeline for loading a ludwig dataset.\n\n    Clients will typically call load(), which processes the dataset according to the config.\n\n    A dataset is processed in 4 phases:\n        1. Download       - The dataset files are downloaded to the cache.\n        2. Verify         - Hashes of downloaded files are verified.\n        3. Extract        - The dataset files are extracted from an archive (may be a no-op if data is not archived).\n        4. Transform      - The dataset is transformed into a format usable for training and is ready to load.\n            a. Transform Files      (Files -> Files)\n            b. Load Dataframe       (Files -> DataFrame)\n            c. Transform Dataframe  (DataFrame -> DataFrame)\n            d. Save Processed       (DataFrame -> File)\n\n    The download and extract phases are run for each URL based on the URL type and file extension. After extraction, the\n    full set of downloaded and extracted files are collected and passed as a list to the transform stage.\n\n    The transform phase offers customization points for datasets which require preprocessing before they are usable for\n    training.\n    \"\"\"\n\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None):\n        \"\"\"Constructor.\"\"\"\n        self.config = config\n        self.cache_dir = cache_dir if cache_dir else get_default_cache_location()\n\n    @property\n    def name(self):\n        \"\"\"The name of the dataset.\"\"\"\n        return self.config.name\n\n    @property\n    def version(self):\n        \"\"\"The version of the dataset.\"\"\"\n        return self.config.version\n\n    @property\n    def is_kaggle_dataset(self) -> bool:\n        return self.config.kaggle_dataset_id or self.config.kaggle_competition\n\n    @property\n    def download_dir(self) -> str:\n        \"\"\"Directory where all dataset artifacts are saved.\"\"\"\n        return os.path.join(self.cache_dir, f\"{self.name}_{self.version}\")\n\n    @property\n    def raw_dataset_dir(self) -> str:\n        \"\"\"Save path for raw data downloaded from the web.\"\"\"\n        return os.path.join(self.download_dir, \"raw\")\n\n    @property\n    def processed_dataset_dir(self) -> str:\n        \"\"\"Save path for processed data.\"\"\"\n        return os.path.join(self.download_dir, \"processed\")\n\n    @property\n    def processed_dataset_filename(self) -> str:\n        \"\"\"Filename for processed data.\"\"\"\n        return f\"{make_safe_filename(self.config.name)}.parquet\"\n\n    @property\n    def processed_dataset_path(self) -> str:\n        \"\"\"Save path to processed dataset file.\"\"\"\n        return os.path.join(self.processed_dataset_dir, self.processed_dataset_filename)\n\n    @property\n    def processed_temp_dir(self) -> str:\n        \"\"\"Save path for processed temp data.\"\"\"\n        return os.path.join(self.download_dir, \"_processed\")\n\n    @property\n    def state(self) -> DatasetState:\n        \"\"\"Dataset state.\"\"\"\n        if os.path.exists(self.processed_dataset_path):\n            return DatasetState.TRANSFORMED\n        if all([os.path.exists(os.path.join(self.raw_dataset_dir, filename)) for filename in self.download_filenames]):\n            archive_filenames = [f for f in self.download_filenames if is_archive(f)]\n            if archive_filenames:\n                # Check to see if archive has been extracted.\n                extracted_files = [\n                    f for a in archive_filenames for f in list_archive(os.path.join(self.raw_dataset_dir, a))\n                ]\n                if all(os.path.exists(os.path.join(self.raw_dataset_dir, ef)) for ef in extracted_files):\n                    return DatasetState.EXTRACTED\n                else:\n                    return DatasetState.DOWNLOADED\n            # If none of the dataset download files are archives, skip extraction phase.\n            return DatasetState.EXTRACTED\n        return DatasetState.NOT_LOADED\n\n    @property\n    def download_urls(self) -> list[str]:\n        return _list_of_strings(self.config.download_urls)\n\n    @property\n    def download_filenames(self) -> list[str]:\n        \"\"\"Filenames for downloaded files inferred from download_urls.\"\"\"\n        if self.config.archive_filenames:\n            return _list_of_strings(self.config.archive_filenames)\n        return [os.path.basename(urlparse(url).path) for url in self.download_urls]\n\n    @staticmethod\n    def get_mirror_download_paths(mirror: DatasetFallbackMirror):\n        \"\"\"Filenames for downloaded files inferred from mirror download_paths.\"\"\"\n        return _list_of_strings(mirror.download_paths)\n\n    def get_mirror_download_filenames(self, mirror: DatasetFallbackMirror):\n        \"\"\"Filenames for downloaded files inferred from mirror download_paths.\"\"\"\n        if self.config.archive_filenames:\n            return _list_of_strings(self.config.archive_filenames)\n        return [os.path.basename(path) for path in mirror.download_paths]\n\n    def description(self) -> str:\n        \"\"\"Returns human-readable description of the dataset.\"\"\"\n        return f\"{self.config.name} {self.config.version}\\n{self.config.description}\"\n\n    @property\n    def model_configs(self) -> dict[str, dict]:\n        \"\"\"Returns a dictionary of built-in model configs for this dataset.\"\"\"\n        return model_configs_for_dataset(self.config.name)\n\n    @property\n    def best_model_config(self) -> dict | None:\n        \"\"\"Returns the best built-in model config for this dataset, or None.\"\"\"\n        return self.model_configs.get(\"best\")\n\n    @property\n    def default_model_config(self) -> dict | None:\n        \"\"\"Returns the default built-in model config for this dataset.\n\n        This is a good first model which should train in under 10m on a current laptop without GPU acceleration.\n        \"\"\"\n        return self.model_configs.get(\"default\")\n\n    def _get_preserved_paths(self, root_dir=None):\n        \"\"\"Gets list of files to preserve when exporting dataset, not including self.processed_dataset_path.\n\n        Returns paths relative to the dataset root directory.\n        \"\"\"\n        root_dir = root_dir if root_dir else self.processed_dataset_dir\n        preserved_paths = _glob_multiple(_list_of_strings(self.config.preserve_paths), root_dir=root_dir)\n        return [os.path.relpath(p, start=root_dir) for p in preserved_paths]\n\n    def export(self, output_directory: str) -> None:\n        \"\"\"Exports the dataset (and any files required by it) into the specified directory.\"\"\"\n        self._download_and_process()\n        os.makedirs(output_directory, exist_ok=True)\n        shutil.copy2(self.processed_dataset_path, os.path.join(output_directory, self.processed_dataset_filename))\n        preserve_paths = self._get_preserved_paths()\n        for relative_path in preserve_paths:\n            source = os.path.join(self.processed_dataset_dir, relative_path)\n            destination = os.path.join(output_directory, relative_path)\n            if os.path.isdir(source):\n                shutil.copytree(source, destination, symlinks=False, dirs_exist_ok=True)\n            else:\n                shutil.copy2(source, destination)\n\n    def _download_and_process(self, kaggle_username: str | None = None, kaggle_key: str | None = None):\n        \"\"\"Loads the dataset, downloaded and processing it if needed.\n\n        If dataset is already processed, does nothing.\n        \"\"\"\n        if self.state == DatasetState.NOT_LOADED:\n            try:\n                self.download(kaggle_username=kaggle_username, kaggle_key=kaggle_key)\n            except Exception as e:\n                logger.warning(\n                    f\"Finding fallback mirrors to download the dataset. Downloading from \"\n                    f\"the original source failed with the following error {e}.\"\n                )\n                if not self.config.fallback_mirrors:\n                    logger.exception(f\"No fallback mirror found. Failed to download dataset {self.config.name}.\")\n                else:\n                    self.download_from_fallback_mirrors()\n        self.verify()\n        if self.state == DatasetState.DOWNLOADED:\n            # Extract dataset\n            try:\n                self.extract()\n            except Exception:\n                logger.exception(\"Failed to extract dataset\")\n        if self.state == DatasetState.EXTRACTED:\n            # Transform dataset\n            try:\n                self.transform()\n            except Exception:\n                logger.exception(\"Failed to transform dataset\")\n\n    def load(\n        self, kaggle_username: str | None = None, kaggle_key: str | None = None, split: bool = False\n    ) -> pd.DataFrame | list[pd.DataFrame, pd.DataFrame, pd.DataFrame]:\n        \"\"\"Loads the dataset, downloaded and processing it if needed.\n\n        Note: This method is also responsible for splitting the data, returning a single dataframe if split=False, and a\n        3-tuple of train, val, test if split=True.\n\n        :param kaggle_username: (str) username on Kaggle platform\n        :param kaggle_key: (str) dataset key on Kaggle platform\n        :param split: (bool) splits dataset along 'split' column if present. The split column should always have values\n            0: train, 1: validation, 2: test.\n        \"\"\"\n        self._download_and_process(kaggle_username=kaggle_username, kaggle_key=kaggle_key)\n        if self.state == DatasetState.TRANSFORMED:\n            dataset_df = self.load_transformed_dataset()\n            if split:\n                return self.split(dataset_df)\n            else:\n                return dataset_df\n\n    def download(self, kaggle_username: str | None = None, kaggle_key: str | None = None) -> list[str]:\n        if not os.path.exists(self.raw_dataset_dir):\n            os.makedirs(self.raw_dataset_dir)\n        if self.is_kaggle_dataset:\n            return download_kaggle_dataset(\n                self.raw_dataset_dir,\n                kaggle_dataset_id=self.config.kaggle_dataset_id,\n                kaggle_competition=self.config.kaggle_competition,\n                kaggle_username=kaggle_username,\n                kaggle_key=kaggle_key,\n            )\n        else:\n            for url, filename in zip(self.download_urls, self.download_filenames):\n                downloaded_file_path = os.path.join(self.raw_dataset_dir, filename)\n                with TqdmUpTo(unit=\"B\", unit_scale=True, unit_divisor=1024, miniters=1, desc=filename) as t:\n                    urllib.request.urlretrieve(url, downloaded_file_path, t.update_to)\n\n    def download_from_fallback_mirrors(self):\n        for mirror in self.config.fallback_mirrors:\n            logger.info(f\"Attempting download from mirror {mirror.name}.\")\n            try:\n                download_paths = self.get_mirror_download_paths(mirror)\n                filenames = self.get_mirror_download_filenames(mirror)\n                for path, filename in zip(download_paths, filenames):\n                    downloaded_file_path = os.path.join(self.raw_dataset_dir, filename)\n                    with TqdmUpTo(unit=\"B\", unit_scale=True, unit_divisor=1024, miniters=1, desc=filename):\n                        fs, path = get_fs_and_path(path)\n                        fs.get(path, downloaded_file_path)\n                return\n            except Exception:\n                logger.exception(f\"Download from mirror `{mirror.name}` failed.\")\n\n    def verify(self) -> None:\n        \"\"\"Verifies checksums for dataset.\"\"\"\n        for filename, sha256sum in self.config.sha256.items():\n            digest = _sha256_digest(os.path.join(self.raw_dataset_dir, filename))\n            if digest != sha256sum:\n                raise ValueError(f\"Checksum mismatch for file {filename} of {self.config.name} dataset\")\n        if not self.config.sha256:\n            logger.warning(f\"No sha256 digest provided for dataset {self.config.name}, cannot verify.\")\n            logger.info(\"Contents:\")\n            for filename in os.listdir(self.raw_dataset_dir):\n                path = os.path.join(self.raw_dataset_dir, filename)\n                if not os.path.isdir(path):\n                    digest = _sha256_digest(path)\n                    logger.info(f\"    {filename}: {digest}\")\n\n    def extract(self) -> list[str]:\n        extracted_files = set()\n        for download_filename in self.download_filenames:\n            download_path = os.path.join(self.raw_dataset_dir, download_filename)\n            if is_archive(download_path):\n                extracted_files.update(extract_archive(download_path))\n            # If the archive contains archives, extract those too. For example, bnp_claims_management.\n            archive_contents = extracted_files.copy()\n            for extracted_file in archive_contents:\n                extracted_path = os.path.join(self.raw_dataset_dir, extracted_file)\n                if is_archive(extracted_path):\n                    try:\n                        extracted_files.update(extract_archive(extracted_path))\n                    except RuntimeError as e:\n                        logger.warning(f\"Error extracting {extracted_file}\" + str(e))\n        return list(extracted_files)\n\n    def transform(self) -> None:\n        data_filenames = [\n            os.path.join(self.raw_dataset_dir, f) for f in os.listdir(self.raw_dataset_dir) if not is_archive(f)\n        ]\n        transformed_files = self.transform_files(data_filenames)\n        unprocessed_dataframe = self.load_unprocessed_dataframe(transformed_files)\n        transformed_dataframe = self.transform_dataframe(unprocessed_dataframe)\n        self.save_processed(transformed_dataframe)\n\n    def transform_files(self, file_paths: list[str]) -> list[str]:\n        \"\"\"Transform data files before loading to dataframe.\n\n        Subclasses should override this method to process files before loading dataframe, calling the base class\n        implementation after transformation if the results of transformation are needed by preserve_paths.\n        \"\"\"\n        data_files = [p for p in file_paths if not os.path.isdir(p)]\n        if not os.path.exists(self.processed_dataset_dir):\n            os.makedirs(self.processed_dataset_dir)\n        # Moves any preserved paths (ex. image directories) into processed directory to avoid unnecessary copy.\n        for rel_path in self._get_preserved_paths(self.raw_dataset_dir):\n            source_path = os.path.join(self.raw_dataset_dir, rel_path)\n            dest_path = os.path.join(self.processed_dataset_dir, rel_path)\n            if os.path.exists(source_path) and not os.path.exists(dest_path):\n                os.replace(source_path, dest_path)\n        return data_files\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\n\n        Subclasses may override this method to support other input formats (json, jsonl, tsv, csv, parquet)\n        \"\"\"\n        file_extension = os.path.splitext(file_path)[-1].lower()\n        if file_extension == \".json\":\n            return pd.read_json(file_path)\n        elif file_extension == \".jsonl\":\n            return pd.read_json(file_path, lines=True)\n        elif file_extension == \".tsv\":\n            return pd.read_table(file_path)\n        elif file_extension in {\".csv\", \".data\"}:\n            return pd.read_csv(file_path)\n        elif file_extension in {\".parquet\", \".pq\", \".pqt\"}:\n            return pd.read_parquet(file_path)\n        else:\n            raise ValueError(f\"Unsupported dataset file type: {file_extension}\")\n\n    def load_files_to_dataframe(self, file_paths: list[str], root_dir=None) -> pd.DataFrame:\n        \"\"\"Loads a file or list of files and returns a dataframe.\n\n        Subclasses may override this method to change the loader's behavior for groups of files.\n        \"\"\"\n        if root_dir:\n            file_paths = [os.path.join(root_dir, path) for path in file_paths]\n        dataframes = [self.load_file_to_dataframe(path) for path in file_paths]\n        try:\n            if self.config.columns:\n                column_names = [column[\"name\"] for column in self.config.columns]\n\n                set_cols_dfs = []\n                for df in dataframes:\n                    # Split column is not included in configs, add in if pre-set split is present\n                    if SPLIT in df.columns:\n                        column_names.append(SPLIT)\n\n                    # If the number of columns in the dataframe does not match the number of columns in the config,\n                    # then the dataframe likely has an extra column that we don't want - i.e. \"Unnamed: 0\".\n                    if len(column_names) != len(df.columns):\n                        df = df[column_names]\n                    set_cols_dfs.append(df.set_axis(column_names, axis=1))\n                return pd.concat(set_cols_dfs, ignore_index=True)\n            else:\n                return pd.concat(dataframes, ignore_index=True)\n        except ValueError as e:\n            logger.warning(f\"Error setting column names: {e}\")\n            return pd.concat(dataframes, ignore_index=True)\n\n    def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame:\n        \"\"\"Load dataset files into a dataframe.\n\n        Will use the list of data files in the dataset directory as a default if all of config's dataset_filenames,\n        train_filenames, validation_filenames, test_filenames are empty.\n        \"\"\"\n        dataset_paths = _glob_multiple(_list_of_strings(self.config.dataset_filenames), root_dir=self.raw_dataset_dir)\n        train_paths = _glob_multiple(_list_of_strings(self.config.train_filenames), root_dir=self.raw_dataset_dir)\n        validation_paths = _glob_multiple(\n            _list_of_strings(self.config.validation_filenames), root_dir=self.raw_dataset_dir\n        )\n        test_paths = _glob_multiple(_list_of_strings(self.config.test_filenames), root_dir=self.raw_dataset_dir)\n        if self.config.name == \"hugging_face\":\n            dataframes = self._get_dataframe_with_fixed_splits_from_hf()\n        else:\n            dataframes = self._get_dataframe_with_fixed_splits(\n                train_paths, validation_paths, test_paths, dataset_paths, file_paths\n            )\n        return pd.concat(dataframes, ignore_index=True)\n\n    def _get_dataframe_with_fixed_splits_from_hf(self):\n        dataframes = []\n        splits = [\"train\", \"validation\", \"test\"]\n        data_dict = self.load_hf_to_dict(\n            self.config.huggingface_dataset_id, self.config.huggingface_subset\n        )  # This function is defined in the Hugging Face dataloader\n        for split_type in splits:\n            if split_type in data_dict:\n                # We don't have to do anything if split not in data_dict because we just concatenate the dataframes\n                # in the end anyway.\n                data_dict[split_type][SPLIT] = splits.index(split_type)  # Add \"split\" column (0, 1, or 2)\n                dataframes.append(data_dict[split_type])\n        return dataframes\n\n    def _get_dataframe_with_fixed_splits(self, train_paths, validation_paths, test_paths, dataset_paths, file_paths):\n        dataframes = []\n        if len(train_paths) > 0:\n            train_df = self.load_files_to_dataframe(train_paths)\n            train_df[SPLIT] = 0\n            dataframes.append(train_df)\n        if len(validation_paths) > 0:\n            validation_df = self.load_files_to_dataframe(validation_paths)\n            validation_df[SPLIT] = 1\n            dataframes.append(validation_df)\n        if len(test_paths) > 0:\n            test_df = self.load_files_to_dataframe(test_paths)\n            test_df[SPLIT] = 2\n            dataframes.append(test_df)\n        # If we have neither train/validation/test files nor dataset_paths in the config,\n        # use data files in root dir.\n        if len(dataset_paths) == len(dataframes) == 0:\n            dataset_paths = file_paths\n        if len(dataset_paths) > 0:\n            dataframes.append(self.load_files_to_dataframe(dataset_paths))\n        return dataframes\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        \"\"\"Transforms a dataframe of the entire dataset.\n\n        Subclasses should override this method if transformation of the dataframe is needed.\n        \"\"\"\n        for column_name, type in self.config.column_types.items():\n            dataframe[column_name] = dataframe[column_name].astype(type)\n        return dataframe\n\n    def save_processed(self, dataframe: pd.DataFrame) -> None:\n        \"\"\"Saves transformed dataframe as a flat file ludwig can load for training.\"\"\"\n        if not os.path.exists(self.processed_dataset_dir):\n            os.makedirs(self.processed_dataset_dir)\n        dataframe.to_parquet(self.processed_dataset_path, engine=\"pyarrow\")\n\n    def load_transformed_dataset(self) -> pd.DataFrame:\n        \"\"\"Load processed dataset into a dataframe.\"\"\"\n        return pd.read_parquet(self.processed_dataset_path)\n\n    def get_mtime(self) -> float:\n        \"\"\"Last modified time of the processed dataset after downloading successfully.\"\"\"\n        return os.path.getmtime(self.processed_dataset_path)\n\n    @staticmethod\n    def split(dataset: pd.DataFrame) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:\n        if SPLIT in dataset:\n            dataset[SPLIT] = pd.to_numeric(dataset[SPLIT])\n            training_set = dataset[dataset[SPLIT] == 0].drop(columns=[SPLIT])\n            val_set = dataset[dataset[SPLIT] == 1].drop(columns=[SPLIT])\n            test_set = dataset[dataset[SPLIT] == 2].drop(columns=[SPLIT])\n            return training_set, test_set, val_set\n        else:\n            raise ValueError(f\"The dataset does not a '{SPLIT}' column, load with `split=False`\")\n"
  },
  {
    "path": "ludwig/datasets/loaders/ethos_binary.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass EthosBinaryLoader(DatasetLoader):\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        # This dataset uses ; seperator instead of ,\n        return pd.read_csv(file_path, sep=\";\")\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        # convert float labels (0.0, 1.0) to binary labels\n        processed_df[\"isHate\"] = processed_df[\"isHate\"] >= 0.5\n        processed_df[\"isHate\"] = processed_df[\"isHate\"].astype(int)\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/flickr8k.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\nimport re\nfrom collections import defaultdict\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass Flickr8kLoader(DatasetLoader):\n    def transform_files(self, file_paths: list[str]) -> list[str]:\n        # create a dictionary matching image_path --> list of captions\n        image_to_caption = defaultdict(list)\n        with open(f\"{self.raw_dataset_dir}/Flickr8k.token.txt\") as captions_file:\n            image_to_caption = defaultdict(list)\n            for line in captions_file:\n                line = line.split(\"#\")\n                # the regex is to format the string to fit properly in a csv\n                line[1] = line[1].strip(\"\\n01234.\\t \")\n                line[1] = re.sub('\"', '\"\"', line[1])\n                line[1] = '\"' + line[1] + '\"'\n                image_to_caption[line[0]].append(line[1])\n        # create csv file with 7 columns: image_path, 5 captions, and split\n        with open(os.path.join(self.raw_dataset_dir, \"flickr8k_dataset.csv\"), \"w\") as output_file:\n            output_file.write(\"image_path,caption0,caption1,caption2,\")\n            output_file.write(\"caption3,caption4,split\\n\")\n            splits = [\"train\", \"dev\", \"test\"]\n            for i in range(len(splits)):\n                split = splits[i]\n                with open(f\"{self.raw_dataset_dir}/Flickr_8k.{split}Images.txt\") as split_file:\n                    for image_name in split_file:\n                        image_name = image_name.strip(\"\\n\")\n                        if image_name in image_to_caption:\n                            output_file.write(\n                                \"{},{},{},{},{},{},{}\\n\".format(\n                                    # Note: image folder is named Flicker8k_Dataset\n                                    f\"{self.raw_dataset_dir}/Flicker8k_Dataset/{image_name}\",\n                                    *image_to_caption[image_name],\n                                    i,\n                                )\n                            )\n        return super().transform_files(file_paths)\n"
  },
  {
    "path": "ludwig/datasets/loaders/forest_cover.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass ForestCoverLoader(DatasetLoader):\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, use_tabnet_split=True):\n        super().__init__(config, cache_dir=cache_dir)\n        self.use_tabnet_split = use_tabnet_split\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        df = super().transform_dataframe(dataframe)\n        # Elevation                               quantitative    meters                       Elevation in meters\n        # Aspect                                  quantitative    azimuth                      Aspect in degrees azimuth\n        # Slope                                   quantitative    degrees                      Slope in degrees\n        # Horizontal_Distance_To_Hydrology        quantitative    meters                       Horz Dist to nearest surface water features      # noqa: E501\n        # Vertical_Distance_To_Hydrology          quantitative    meters                       Vert Dist to nearest surface water features      # noqa: E501\n        # Horizontal_Distance_To_Roadways         quantitative    meters                       Horz Dist to nearest roadway                     # noqa: E501\n        # Hillshade_9am                           quantitative    0 to 255 index               Hillshade index at 9am, summer solstice          # noqa: E501\n        # Hillshade_Noon                          quantitative    0 to 255 index               Hillshade index at noon, summer soltice          # noqa: E501\n        # Hillshade_3pm                           quantitative    0 to 255 index               Hillshade index at 3pm, summer solstice          # noqa: E501\n        # Horizontal_Distance_To_Fire_Points      quantitative    meters                       Horz Dist to nearest wildfire ignition points    # noqa: E501\n        # Wilderness_Area (4 binary columns)      qualitative     0 (absence) or 1 (presence)  Wilderness area designation                      # noqa: E501\n        # Soil_Type (40 binary columns)           qualitative     0 (absence) or 1 (presence)  Soil Type designation\n        # Cover_Type (7 types)                    integer         1 to 7                       Forest Cover Type designation                    # noqa: E501\n\n        # Map the 40 soil types to a single integer instead of 40 binary columns\n        st_cols = [\n            \"Soil_Type_1\",\n            \"Soil_Type_2\",\n            \"Soil_Type_3\",\n            \"Soil_Type_4\",\n            \"Soil_Type_5\",\n            \"Soil_Type_6\",\n            \"Soil_Type_7\",\n            \"Soil_Type_8\",\n            \"Soil_Type_9\",\n            \"Soil_Type_10\",\n            \"Soil_Type_11\",\n            \"Soil_Type_12\",\n            \"Soil_Type_13\",\n            \"Soil_Type_14\",\n            \"Soil_Type_15\",\n            \"Soil_Type_16\",\n            \"Soil_Type_17\",\n            \"Soil_Type_18\",\n            \"Soil_Type_19\",\n            \"Soil_Type_20\",\n            \"Soil_Type_21\",\n            \"Soil_Type_22\",\n            \"Soil_Type_23\",\n            \"Soil_Type_24\",\n            \"Soil_Type_25\",\n            \"Soil_Type_26\",\n            \"Soil_Type_27\",\n            \"Soil_Type_28\",\n            \"Soil_Type_29\",\n            \"Soil_Type_30\",\n            \"Soil_Type_31\",\n            \"Soil_Type_32\",\n            \"Soil_Type_33\",\n            \"Soil_Type_34\",\n            \"Soil_Type_35\",\n            \"Soil_Type_36\",\n            \"Soil_Type_37\",\n            \"Soil_Type_38\",\n            \"Soil_Type_39\",\n            \"Soil_Type_40\",\n        ]\n        st_vals = []\n        for _, row in df[st_cols].iterrows():\n            st_vals.append(row.to_numpy().nonzero()[0].item(0))\n        df = df.drop(columns=st_cols)\n        df[\"Soil_Type\"] = st_vals\n\n        # Map the 4 wilderness areas to a single integer\n        # instead of 4 binary columns\n        wa_cols = [\"Wilderness_Area_1\", \"Wilderness_Area_2\", \"Wilderness_Area_3\", \"Wilderness_Area_4\"]\n        wa_vals = []\n        for _, row in df[wa_cols].iterrows():\n            wa_vals.append(row.to_numpy().nonzero()[0].item(0))\n        df = df.drop(columns=wa_cols)\n        df[\"Wilderness_Area\"] = wa_vals\n\n        if not self.use_tabnet_split:\n            # first 11340 records used for training data subset\n            # next 3780 records used for validation data subset\n            # last 565892 records used for testing data subset\n            df[\"split\"] = [0] * 11340 + [1] * 3780 + [2] * 565892\n        else:\n            # Split used in the tabNet paper\n            # https://github.com/google-research/google-research/blob/master/tabnet/download_prepare_covertype.py\n            train_val_indices, test_indices = train_test_split(range(len(df)), test_size=0.2, random_state=0)\n            train_indices, val_indices = train_test_split(train_val_indices, test_size=0.2 / 0.6, random_state=0)\n\n            df[\"split\"] = 0\n            df.loc[val_indices, \"split\"] = 1\n            df.loc[test_indices, \"split\"] = 2\n\n        return df\n"
  },
  {
    "path": "ludwig/datasets/loaders/goemotions.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass GoEmotionsLoader(DatasetLoader):\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        # Format emotion IDs as space-delimited string (Set).\n        processed_df[\"emotion_ids\"] = processed_df[\"emotion_ids\"].apply(lambda e_id: \" \".join(e_id.split(\",\")))\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/higgs.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport pandas as pd\n\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass HiggsLoader(DatasetLoader):\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, add_validation_set=True):\n        super().__init__(config, cache_dir)\n        self.add_validation_set = add_validation_set\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        return pd.read_csv(file_path, header=None)\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        if self.add_validation_set:\n            processed_df[\"split\"] = [0] * 10000000 + [1] * 500000 + [2] * 500000\n        else:\n            processed_df[\"split\"] = [0] * 10500000 + [2] * 500000\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/hugging_face.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom __future__ import annotations\n\nimport logging\n\nimport datasets\nimport pandas as pd\n\nfrom ludwig.constants import TEST, TRAIN, VALIDATION\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\nSPLITS = [TRAIN, VALIDATION, TEST]\nlogger = logging.getLogger(__name__)\n\n\nclass HFLoader(DatasetLoader):\n    \"\"\"HFLoader differs from all other DatasetLoaders because of how it loads data through the Hugging Face\n    datasets API instead of saving any files to the cache.\n\n    The config for HFLoader contains two unique parameters, huggingface_dataset_id and huggingface_subsample, that\n    identify which dataset and which subsample of that dataset to load in.\n    \"\"\"\n\n    @staticmethod\n    def load_hf_to_dict(hf_id: str | None = None, hf_subsample: str | None = None) -> dict[str, pd.DataFrame]:\n        \"\"\"Returns a map of split -> pd.DataFrame for the given HF dataset.\n\n        :param hf_id: (str) path to dataset on HuggingFace platform\n        :param hf_subsample: (str) name of dataset configuration on HuggingFace platform\n        \"\"\"\n        dataset_dict: dict[str, datasets.Dataset] = datasets.load_dataset(path=hf_id, name=hf_subsample)\n        pandas_dict = {}\n        for split in dataset_dict:\n            # Convert from HF DatasetDict type to a dictionary of pandas dataframes\n            pandas_dict[split] = dataset_dict[split].to_pandas()\n        return pandas_dict\n\n    # TODO(Alex): Standardize load() signature as interface method in DatasetLoader and adhere to it in all subclasses.\n    def load(\n        self, hf_id: str | None = None, hf_subsample: str | None = None, split: bool = False\n    ) -> pd.DataFrame | list[pd.DataFrame, pd.DataFrame, pd.DataFrame]:\n        \"\"\"When load() is called, HFLoader calls the datasets API to return all of the data in a HuggingFace\n        DatasetDict, converts it to a dictionary of pandas dataframes, and returns either three dataframes\n        containing train, validation, and test data or one dataframe that is the concatenation of all three\n        depending on whether `split` is set to True or False.\n\n        :param split: (bool) directive for how to interpret if dataset contains validation or test set (see below)\n\n        Note that some datasets may not provide a validation set or a test set. In this case:\n        - If split is True, the DataFrames corresponding to the missing sets are initialized to be empty\n        - If split is False, the \"split\" column in the resulting DataFrame will reflect the fact that there is no\n          validation/test split (i.e., there will be no 1s/2s)\n\n        A train set should always be provided by Hugging Face.\n\n        :param hf_id: (str) path to dataset on HuggingFace platform\n        :param hf_subsample: (str) name of dataset configuration on HuggingFace platform\n        \"\"\"\n        self.config.huggingface_dataset_id = hf_id\n        self.config.huggingface_subsample = hf_subsample\n        pandas_dict = self.load_hf_to_dict(\n            hf_id=hf_id,\n            hf_subsample=hf_subsample,\n        )\n        if split:  # For each split, either return the appropriate dataframe or an empty dataframe\n            for spl in SPLITS:\n                if spl not in pandas_dict:\n                    logger.warning(f\"No {spl} set found in provided Hugging Face dataset. Skipping {spl} set.\")\n            train_df = pandas_dict[TRAIN] if TRAIN in pandas_dict else pd.DataFrame()\n            validation_df = pandas_dict[VALIDATION] if VALIDATION in pandas_dict else pd.DataFrame()\n            test_df = pandas_dict[TEST] if TEST in pandas_dict else pd.DataFrame()\n\n            return train_df, validation_df, test_df\n        else:\n            dataset_list = []\n            for spl in pandas_dict:\n                pandas_dict[spl][\"split\"] = SPLITS.index(spl)  # Add a column containing 0s, 1s, and 2s denoting splits\n                dataset_list.append(pandas_dict[spl])\n            return pd.concat(dataset_list)\n"
  },
  {
    "path": "ludwig/datasets/loaders/ieee_fraud.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass IEEEFraudLoader(DatasetLoader):\n    \"\"\"The IEEE-CIS Fraud Detection Dataset https://www.kaggle.com/c/ieee-fraud-detection/overview.\"\"\"\n\n    def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame:\n        \"\"\"Load dataset files into a dataframe.\"\"\"\n        train_files = {\"train_identity.csv\", \"train_transaction.csv\"}\n        test_files = {\"test_identity.csv\", \"test_transaction.csv\"}\n\n        train_dfs, test_dfs = {}, {}\n\n        for filename in train_files.union(test_files):\n            split_name = os.path.splitext(filename)[0]\n            file_df = self.load_file_to_dataframe(os.path.join(self.raw_dataset_dir, filename))\n            if filename in train_files:\n                train_dfs[split_name] = file_df\n            elif filename in test_files:\n                test_dfs[split_name] = file_df\n\n        # Merge on TransactionID\n        final_train = pd.merge(\n            train_dfs[\"train_transaction\"], train_dfs[\"train_identity\"], on=\"TransactionID\", how=\"left\"\n        )\n        return final_train\n"
  },
  {
    "path": "ludwig/datasets/loaders/insurance_lite.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass InsuranceLiteLoader(DatasetLoader):\n    \"\"\"Health Insurance Cross Sell Prediction Predict Health Insurance Owners' who will be interested in Vehicle\n    Insurance https://www.kaggle.com/datasets/arashnic/imbalanced-data-practice.\"\"\"\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        df = super().transform_dataframe(dataframe)\n        # Make image paths relative to dataset root directory\n        df[\"image_path\"] = df[\"image_path\"].apply(\n            lambda x: os.path.join(\"Fast_Furious_Insured\", \"trainImages\", os.path.basename(x))\n        )\n        return df\n"
  },
  {
    "path": "ludwig/datasets/loaders/kdd_loader.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass KDDCup2009Loader(DatasetLoader):\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, task_name=\"\", include_test_download=False):\n        super().__init__(config, cache_dir=cache_dir)\n        self.task_name = task_name\n        self.include_test_download = include_test_download\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        return pd.read_csv(file_path, sep=\"\\t\")\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        train_df = super().transform_dataframe(dataframe)\n        train_df = process_categorical_features(train_df, categorical_features)\n        train_df = process_number_features(train_df, categorical_features)\n\n        targets = (\n            pd.read_csv(os.path.join(self.raw_dataset_dir, f\"orange_small_train_{self.task_name}.labels\"), header=None)[\n                0\n            ]\n            .astype(str)\n            .apply(lambda x: \"true\" if x == \"1\" else \"false\")\n        )\n\n        train_idcs = pd.read_csv(\n            os.path.join(self.raw_dataset_dir, f\"stratified_train_idx_{self.task_name}.txt\"), header=None\n        )[0]\n\n        val_idcs = pd.read_csv(\n            os.path.join(self.raw_dataset_dir, f\"stratified_test_idx_{self.task_name}.txt\"), header=None\n        )[0]\n\n        processed_train_df = train_df.iloc[train_idcs].copy()\n        processed_train_df[\"target\"] = targets.iloc[train_idcs]\n        processed_train_df[\"split\"] = 0\n\n        processed_val_df = train_df.iloc[val_idcs].copy()\n        processed_val_df[\"target\"] = targets.iloc[val_idcs]\n        processed_val_df[\"split\"] = 1\n\n        if self.include_test_download:\n            test_df = self.load_file_to_dataframe(os.path.join(self.raw_dataset_dir, \"orange_small_test.data\"))\n            test_df[\"target\"] = \"\"  # no ground truth labels for test download\n            test_df[\"split\"] = 2\n            df = pd.concat([processed_train_df, processed_val_df, test_df])\n        else:\n            df = pd.concat([processed_train_df, processed_val_df])\n\n        return df\n\n\ndef process_categorical_features(df, categorical_features):\n    for i in categorical_features:\n        df.iloc[:, i].fillna(\"\", inplace=True)\n    return df\n\n\ndef process_number_features(df, categorical_features):\n    for i, column in enumerate(df.columns):\n        if i not in categorical_features:\n            df[column].astype(float, copy=False)\n    return df\n\n\ncategorical_features = {\n    190,\n    191,\n    192,\n    193,\n    194,\n    195,\n    196,\n    197,\n    198,\n    199,\n    200,\n    201,\n    202,\n    203,\n    204,\n    205,\n    206,\n    207,\n    209,\n    210,\n    211,\n    212,\n    213,\n    214,\n    215,\n    216,\n    217,\n    218,\n    219,\n    220,\n    221,\n    222,\n    223,\n    224,\n    225,\n    226,\n    227,\n    228,\n}\n\n\nclass KDDAppetencyLoader(KDDCup2009Loader):\n    \"\"\"The KDD Cup 2009 Appetency dataset.\n\n    https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\n    \"\"\"\n\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False):\n        super().__init__(\n            config, cache_dir=cache_dir, task_name=\"appetency\", include_test_download=include_test_download\n        )\n\n\nclass KDDChurnLoader(KDDCup2009Loader):\n    \"\"\"The KDD Cup 2009 Churn dataset.\n\n    https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\n    \"\"\"\n\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False):\n        super().__init__(config, cache_dir=cache_dir, task_name=\"churn\", include_test_download=include_test_download)\n\n\nclass KDDUpsellingLoader(KDDCup2009Loader):\n    \"\"\"The KDD Cup 2009 Upselling dataset.\n\n    https://www.kdd.org/kdd-cup/view/kdd-cup-2009/Data\n    \"\"\"\n\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None, include_test_download=False):\n        super().__init__(\n            config, cache_dir=cache_dir, task_name=\"upselling\", include_test_download=include_test_download\n        )\n"
  },
  {
    "path": "ludwig/datasets/loaders/mnist.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\nimport struct\nfrom multiprocessing.pool import ThreadPool\n\nimport numpy as np\nimport pandas as pd\nimport torch\n\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\nfrom ludwig.utils.fs_utils import makedirs\n\nlogger = logging.getLogger(__name__)\nNUM_LABELS = 10\n\n\nclass MNISTLoader(DatasetLoader):\n    def __init__(self, config: DatasetConfig, cache_dir: str | None = None):\n        try:\n            from torchvision.io import write_png\n\n            self.write_png = write_png\n        except ImportError:\n            logger.error(\n                \"torchvision is not installed. \"\n                \"In order to install all image feature dependencies run \"\n                \"pip install ludwig[image]\"\n            )\n            raise\n        super().__init__(config, cache_dir)\n\n    def transform_files(self, file_paths: list[str]) -> list[str]:\n        for dataset in [\"training\", \"testing\"]:\n            labels, images = self.read_source_dataset(dataset, self.raw_dataset_dir)\n            self.write_output_dataset(labels, images, os.path.join(self.raw_dataset_dir, dataset))\n        return super().transform_files(file_paths)\n\n    def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame:\n        \"\"\"Load dataset files into a dataframe.\"\"\"\n        return self.output_training_and_test_data()\n\n    def read_source_dataset(self, dataset=\"training\", path=\".\"):\n        \"\"\"Create a directory for training and test and extract all the images and labels to this destination.\n\n        :args: dataset (str) : the label for the dataset path (str): the raw dataset path\n        :returns: A tuple of the label for the image, the file array, the size and rows and columns for the image\n        \"\"\"\n        if dataset == \"training\":\n            fname_img = os.path.join(path, \"train-images-idx3-ubyte\")\n            fname_lbl = os.path.join(path, \"train-labels-idx1-ubyte\")\n        elif dataset == \"testing\":\n            fname_img = os.path.join(path, \"t10k-images-idx3-ubyte\")\n            fname_lbl = os.path.join(path, \"t10k-labels-idx1-ubyte\")\n        else:\n            raise ValueError(\"dataset must be 'testing' or 'training'\")\n\n        with open(fname_lbl, \"rb\") as flbl:\n            struct.unpack(\">II\", flbl.read(8))\n            lbl = np.frombuffer(flbl.read(), dtype=np.uint8)\n\n        with open(fname_img, \"rb\") as fimg:\n            magic_nr, size, rows, cols = struct.unpack(\">IIII\", fimg.read(16))\n            img = np.frombuffer(fimg.read(), dtype=np.uint8)\n            img = img.reshape((size, rows, cols))\n\n        return lbl, img\n\n    def write_output_dataset(self, labels, images, output_dir):\n        \"\"\"Create output directories where we write out the images.\n\n        :args: labels (str) : the labels for the image data (np.array) : the binary array corresponding to the image\n            output_dir (str) : the output directory that we need to write to path (str): the raw dataset path\n        :returns: A tuple of the label for the image, the file array, the size and rows and columns for the image\n        \"\"\"\n        # create child image output directories\n        output_dirs = [os.path.join(output_dir, str(i)) for i in range(NUM_LABELS)]\n\n        for output_dir in output_dirs:\n            makedirs(output_dir, exist_ok=True)\n\n        def write_processed_image(t):\n            i, label = t\n            output_filename = os.path.join(output_dirs[label], str(i) + \".png\")\n            torch_image = torch.from_numpy(images[i].copy()).view(1, 28, 28)\n            self.write_png(torch_image, output_filename)\n\n        # write out image data\n        tasks = list(enumerate(labels))\n        pool = ThreadPool(NUM_LABELS)\n        pool.map(write_processed_image, tasks)\n        pool.close()\n        pool.join()\n\n    def output_training_and_test_data(self):\n        \"\"\"Creates a combined (training and test) dataframe by iterating through all the images and labels.\"\"\"\n        dataframes = []\n        for name in [\"training\", \"testing\"]:\n            labels = []\n            paths = []\n            splits = []\n            for i in range(NUM_LABELS):\n                label_dir = f\"{name}/{i}\"\n                img_dir = os.path.join(self.processed_dataset_dir, label_dir)\n                for file in os.listdir(img_dir):\n                    if file.endswith(\".png\"):\n                        labels.append(str(i))\n                        paths.append(os.path.join(img_dir, file))\n                        splits.append(0 if name == \"training\" else 2)\n            dataframes.append(pd.DataFrame({\"image_path\": paths, \"label\": labels, \"split\": splits}))\n        return pd.concat(dataframes, ignore_index=True)\n"
  },
  {
    "path": "ludwig/datasets/loaders/naval.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass NavalLoader(DatasetLoader):\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        return pd.read_csv(file_path, header=None, sep=\"   \")\n"
  },
  {
    "path": "ludwig/datasets/loaders/rossman_store_sales.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport calendar\nimport os\n\nimport numpy as np\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass RossmanStoreSalesLoader(DatasetLoader):\n    \"\"\"The Rossmann Store Sales dataset.\"\"\"\n\n    def load_unprocessed_dataframe(self, file_paths: list[str]) -> pd.DataFrame:\n        \"\"\"Load dataset files into a dataframe.\"\"\"\n\n        stores_df = pd.read_csv(os.path.join(self.raw_dataset_dir, \"store.csv\"))\n\n        train_df = pd.read_csv(os.path.join(self.raw_dataset_dir, \"train.csv\"), low_memory=False)\n        train_df = preprocess_df(train_df, stores_df)\n\n        train_df[\"split\"] = -1\n        train_df.loc[train_df[\"Year\"] == 2014, \"split\"] = 0\n        train_df.loc[train_df[\"Year\"] == 2015, \"split\"] = 2\n        train_df.drop(train_df[train_df[\"split\"] == -1].index, inplace=True)\n        return train_df\n\n\ndef preprocess_dates(df):\n    # Make integer Year,Month,Day columns instead of Date\n    dates = np.array([[int(v) for v in s.split(\"-\")] for s in df[\"Date\"]])\n    df = df.drop([\"Date\"], axis=1)\n    df[\"Year\"] = dates[:, 0]\n    df[\"Month\"] = dates[:, 1]\n    df[\"Day\"] = dates[:, 2]\n    return df\n\n\nmonth_abbrs = calendar.month_abbr[1:]\nmonth_abbrs[8] = \"Sept\"\n\n\ndef preprocess_stores(df, stores_df):\n    # join data in df with stores df\n    df = df.join(stores_df, on=\"Store\", rsuffix=\"_right\")\n    df = df.drop([\"Store_right\"], axis=1)\n\n    promo2_start_months = [(s.split(\",\") if not pd.isnull(s) else []) for s in df[\"PromoInterval\"]]\n\n    for month_abbr in month_abbrs:\n        df[\"Promo2Start_\" + month_abbr] = np.array(\n            [(1 if month_abbr in s else 0) for s in promo2_start_months], dtype=np.int8\n        )\n    df = df.drop([\"PromoInterval\"], axis=1)\n\n    return df\n\n\nint_columns = [\n    \"Store\",\n    \"DayOfWeek\",\n    \"Sales\",\n    \"Customers\",\n    \"Open\",\n    \"Promo\",\n    \"SchoolHoliday\",\n    \"Year\",\n    \"Month\",\n    \"Day\",\n    \"CompetitionDistance\",\n    \"CompetitionOpenSinceMonth\",\n    \"CompetitionOpenSinceYear\",\n    \"Promo2\",\n    \"Promo2SinceWeek\",\n    \"Promo2SinceYear\",\n]\n\n\ndef preprocess_df(df, stores_df):\n    df = preprocess_dates(df)\n    df = preprocess_stores(df, stores_df)\n\n    for column in int_columns:\n        df[column] = pd.to_numeric(df[column].fillna(0), downcast=\"integer\")\n\n    df[\"StateHoliday\"] = df[\"StateHoliday\"].astype(str)\n    df.loc[df[\"StateHoliday\"] == \"0\", \"StateHoliday\"] = \"No\"\n\n    return df\n"
  },
  {
    "path": "ludwig/datasets/loaders/santander_value_prediction.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass SantanderValuePredictionLoader(DatasetLoader):\n    \"\"\"The Santander Value Prediction Challenge dataset.\n\n    https://www.kaggle.com/c/santander-value-prediction-challenge\n    \"\"\"\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        # Ensure feature column names are strings (some are numeric); keep special names as is\n        processed_df.columns = [\"C\" + str(col) for col in processed_df.columns]\n        processed_df.rename(columns={\"CID\": \"ID\", \"Ctarget\": \"target\", \"Csplit\": \"split\"}, inplace=True)\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/sarcastic_headlines.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport pandas as pd\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass SarcasticHeadlinesLoader(DatasetLoader):\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        return pd.read_json(file_path, lines=True)\n"
  },
  {
    "path": "ludwig/datasets/loaders/sarcos.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\nfrom scipy.io import loadmat\n\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\nfrom ludwig.utils.fs_utils import open_file\n\n\nclass SarcosLoader(DatasetLoader):\n    \"\"\"The Sarcos dataset.\n\n    Details:\n        The data relates to an inverse dynamics problem for a seven\n        degrees-of-freedom SARCOS anthropomorphic robot arm. The\n        task is to map from a 21-dimensional input space (7 joint\n        positions, 7 joint velocities, 7 joint accelerations) to the\n        corresponding 7 joint torques. There are 44,484 training\n        examples and 4,449 test examples. The first 21 columns are\n        the input variables, and the 22nd column is used as the target\n        variable.\n\n    Dataset source:\n        Locally Weighted Projection RegressionL: An O(n) Algorithm for\n        Incremental Real Time Learning in High Dimensional Space,\n        S. Vijayakumar and S. Schaal, Proc ICML 2000.\n        http://www.gaussianprocess.org/gpml/data/\n    \"\"\"\n\n    def load_file_to_dataframe(self, file_path: str) -> pd.DataFrame:\n        \"\"\"Loads a file into a dataframe.\"\"\"\n        with open_file(file_path) as f:\n            mat = loadmat(f)\n        file_df = pd.DataFrame(mat[os.path.basename(file_path).split(\".\")[0]])\n        return file_df\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        processed_df = super().transform_dataframe(dataframe)\n        columns = []\n        columns += [f\"position_{i}\" for i in range(1, 8)]\n        columns += [f\"velocity_{i}\" for i in range(1, 8)]\n        columns += [f\"acceleration_{i}\" for i in range(1, 8)]\n        columns += [f\"torque_{i}\" for i in range(1, 8)]\n        columns += [\"split\"]\n\n        processed_df.columns = columns\n        return processed_df\n"
  },
  {
    "path": "ludwig/datasets/loaders/split_loaders.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport numpy as np\nimport pandas as pd\n\nfrom ludwig.constants import SPLIT\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass RandomSplitLoader(DatasetLoader):\n    \"\"\"Adds a random split column to the dataset, with fixed proportions of:\n\n    train: 70%\n     validation: 10%\n     test: 20%\n    .\n    \"\"\"\n\n    def transform_dataframe(self, dataframe: pd.DataFrame) -> pd.DataFrame:\n        df = super().transform_dataframe(dataframe)\n        df[SPLIT] = np.random.choice(3, len(df), p=(0.7, 0.1, 0.2)).astype(np.int8)\n        return df\n"
  },
  {
    "path": "ludwig/datasets/loaders/sst.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\n\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n\nclass SSTLoader(DatasetLoader):\n    \"\"\"The SST dataset.\n\n    This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n    This dataset contains binary labels (positive or negative) for each sample.\n\n    The original dataset specified 5 labels:\n    very negative, negative, neutral, positive, very positive with\n    the following cutoffs:\n    [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0]\n    \"\"\"\n\n    def __init__(\n        self,\n        config: DatasetConfig,\n        cache_dir: str | None = None,\n        include_subtrees=False,\n        discard_neutral=False,\n        convert_parentheses=True,\n        remove_duplicates=False,\n    ):\n        super().__init__(config, cache_dir=cache_dir)\n        self.include_subtrees = include_subtrees\n        self.discard_neutral = discard_neutral\n        self.convert_parentheses = convert_parentheses\n        self.remove_duplicates = remove_duplicates\n\n    @staticmethod\n    def get_sentiment_label(id2sent, phrase_id):\n        raise NotImplementedError\n\n    def transform_files(self, file_paths: list[str]) -> list[str]:\n        # maybe this should be\n\n        \"\"\"Load dataset files into a dataframe.\"\"\"\n        sentences_df = pd.read_csv(\n            os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/datasetSentences.txt\"),\n            sep=\"\\t\",\n        )\n\n        sentences_df[\"sentence\"] = sentences_df[\"sentence\"].apply(format_text)\n\n        datasplit_df = pd.read_csv(\n            os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/datasetSplit.txt\"), sep=\",\"\n        )\n\n        phrase2id = {}\n        with open(os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/dictionary.txt\")) as f:\n            Lines = f.readlines()\n            for line in Lines:\n                if line:\n                    split_line = line.split(\"|\")\n                    phrase = split_line[0]\n                    phrase2id[phrase] = int(split_line[1])\n\n        id2sent = {}\n        with open(os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/sentiment_labels.txt\")) as f:\n            Lines = f.readlines()\n            for line in Lines:\n                if line:\n                    split_line = line.split(\"|\")\n                    try:\n                        id2sent[int(split_line[0])] = float(split_line[1])\n                    except ValueError:\n                        pass\n\n        trees_pointers = None\n        trees_phrases = None\n\n        if self.include_subtrees:\n            trees_pointers = []\n            with open(os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/STree.txt\")) as f:\n                Lines = f.readlines()\n                for line in Lines:\n                    if line:\n                        trees_pointers.append([int(s.strip()) for s in line.split(\"|\")])\n\n            trees_phrases = []\n            with open(os.path.join(self.raw_dataset_dir, \"stanfordSentimentTreebank/SOStr.txt\")) as f:\n                Lines = f.readlines()\n                for line in Lines:\n                    if line:\n                        trees_phrases.append([s.strip() for s in line.split(\"|\")])\n\n        splits = {\"train\": 1, \"test\": 2, \"dev\": 3}\n\n        generated_csv_filenames = []\n        for split_name, split_id in splits.items():\n            sentence_idcs = get_sentence_idcs_in_split(datasplit_df, split_id)\n\n            pairs = []\n            if split_name == \"train\" and self.include_subtrees:\n                phrases = []\n                for sentence_idx in sentence_idcs:\n                    # trees_pointers and trees_phrases are 0 indexed\n                    # while sentence_idx starts from 1\n                    # so we need to decrease sentence_idx value\n                    sentence_idx -= 1\n                    subtrees = sentence_subtrees(sentence_idx, trees_pointers, trees_phrases)\n\n                    sentence_idx += 1\n                    sentence_phrase = list(sentences_df[sentences_df[\"sentence_index\"] == sentence_idx][\"sentence\"])[0]\n\n                    sentence_phrase = convert_parentheses(sentence_phrase)\n                    label = self.get_sentiment_label(id2sent, phrase2id[sentence_phrase])\n                    # filter @ sentence level\n                    # For SST-2, check subtrees only if sentence is not neutral\n                    if not self.discard_neutral or label != -1:\n                        for phrase in subtrees:\n                            label = self.get_sentiment_label(id2sent, phrase2id[phrase])\n                            if not self.discard_neutral or label != -1:\n                                if not self.convert_parentheses:\n                                    phrase = convert_parentheses_back(phrase)\n                                    phrase = phrase.replace(\"\\xa0\", \" \")\n                                pairs.append([phrase, label])\n            else:\n                phrases = get_sentences_with_idcs(sentences_df, sentence_idcs)\n                for phrase in phrases:\n                    phrase = convert_parentheses(phrase)\n                    label = self.get_sentiment_label(id2sent, phrase2id[phrase])\n                    if not self.discard_neutral or label != -1:\n                        if not self.convert_parentheses:\n                            phrase = convert_parentheses_back(phrase)\n                            phrase = phrase.replace(\"\\xa0\", \" \")\n                        pairs.append([phrase, label])\n\n            final_csv = pd.DataFrame(pairs)\n            final_csv.columns = [\"sentence\", \"label\"]\n            if self.remove_duplicates:\n                final_csv = final_csv.drop_duplicates(subset=[\"sentence\"])\n            csv_filename = os.path.join(self.raw_dataset_dir, f\"{split_name}.csv\")\n            generated_csv_filenames.append(csv_filename)\n            final_csv.to_csv(csv_filename, index=False)\n\n        return super().transform_files(generated_csv_filenames)\n\n\nclass SST2Loader(SSTLoader):\n    \"\"\"The SST2 dataset.\n\n    This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n    This dataset contains binary labels (positive or negative) for each sample.\n\n    The original dataset specified 5 labels:\n    very negative, negative, neutral, positive, very positive with\n    the following cutoffs:\n    [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0]\n\n    In the construction of this dataset, we remove all neutral phrases\n    and assign a negative label if the original rating falls\n    into the following range: [0, 0.4] and a positive label\n    if the original rating is between (0.6, 1.0].\n    \"\"\"\n\n    def __init__(\n        self,\n        config: DatasetConfig,\n        cache_dir: str | None = None,\n        include_subtrees=False,\n        convert_parentheses=True,\n        remove_duplicates=False,\n    ):\n        super().__init__(\n            config,\n            cache_dir=cache_dir,\n            include_subtrees=include_subtrees,\n            discard_neutral=True,\n            convert_parentheses=convert_parentheses,\n            remove_duplicates=remove_duplicates,\n        )\n\n    def get_sentiment_label(self, id2sent, phrase_id):\n        sentiment = id2sent[phrase_id]\n        if sentiment <= 0.4:  # negative\n            return 0\n        elif sentiment > 0.6:  # positive\n            return 1\n        return -1  # neutral\n\n\nclass SST3Loader(SSTLoader):\n    \"\"\"The SST3 dataset.\n\n    This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n    This dataset contains five labels (very negative, negative, neutral,\n    positive, very positive) for each sample.\n\n    In the original dataset, the  5 labels: very negative, negative, neutral, positive,\n    and very positive have the following cutoffs:\n    [0, 0.4], (0.4, 0.6], (0.6, 1.0]\n\n    This class pulls in an array of mixins for different types of functionality\n    which belongs in the workflow for ingesting and transforming\n    training data into a destination dataframe that can be use by Ludwig.\n    \"\"\"\n\n    def __init__(\n        self,\n        config: DatasetConfig,\n        cache_dir: str | None = None,\n        include_subtrees=False,\n        convert_parentheses=True,\n        remove_duplicates=False,\n    ):\n        super().__init__(\n            config,\n            cache_dir=cache_dir,\n            include_subtrees=include_subtrees,\n            convert_parentheses=convert_parentheses,\n            remove_duplicates=remove_duplicates,\n        )\n\n    def get_sentiment_label(self, id2sent, phrase_id):\n        sentiment = id2sent[phrase_id]\n        if sentiment <= 0.4:\n            return \"negative\"\n        elif sentiment <= 0.6:\n            return \"neutral\"\n        elif sentiment <= 1.0:\n            return \"positive\"\n        return \"neutral\"\n\n\nclass SST5Loader(SSTLoader):\n    \"\"\"The SST5 dataset.\n\n    This dataset is constructed using the Stanford Sentiment Treebank Dataset.\n    This dataset contains five labels (very negative, negative, neutral,\n    positive, very positive) for each sample.\n\n    In the original dataset, the  5 labels: very negative, negative, neutral, positive,\n    and very positive have the following cutoffs:\n    [0, 0.2], (0.2, 0.4], (0.4, 0.6], (0.6, 0.8], (0.8, 1.0]\n\n    This class pulls in an array of mixins for different types of functionality\n    which belongs in the workflow for ingesting and transforming\n    training data into a destination dataframe that can be use by Ludwig.\n    \"\"\"\n\n    def __init__(\n        self,\n        config: DatasetConfig,\n        cache_dir: str | None = None,\n        include_subtrees=False,\n        convert_parentheses=True,\n        remove_duplicates=False,\n    ):\n        super().__init__(\n            config,\n            cache_dir=cache_dir,\n            include_subtrees=include_subtrees,\n            convert_parentheses=convert_parentheses,\n            remove_duplicates=remove_duplicates,\n        )\n\n    def get_sentiment_label(self, id2sent, phrase_id):\n        sentiment = id2sent[phrase_id]\n        if sentiment <= 0.2:\n            return \"very_negative\"\n        elif sentiment <= 0.4:\n            return \"negative\"\n        elif sentiment <= 0.6:\n            return \"neutral\"\n        elif sentiment <= 0.8:\n            return \"positive\"\n        elif sentiment <= 1.0:\n            return \"very_positive\"\n        return \"neutral\"\n\n\ndef format_text(text: str):\n    \"\"\"Formats text by decoding into utf-8.\"\"\"\n    return \" \".join([w.encode(\"latin1\").decode(\"utf-8\") for w in text.strip().split(\" \")])\n\n\ndef convert_parentheses(text: str):\n    \"\"\"Replaces -LRB- and -RRB- tokens present in SST with ( and )\"\"\"\n    return text.replace(\"-LRB-\", \"(\").replace(\"-RRB-\", \")\")\n\n\ndef convert_parentheses_back(text: str):\n    \"\"\"Replaces ( and ) tokens with -LRB- and -RRB-\"\"\"\n    return text.replace(\"(\", \"-LRB-\").replace(\")\", \"-RRB-\")\n\n\ndef get_sentence_idcs_in_split(datasplit: pd.DataFrame, split_id: int):\n    \"\"\"Given a dataset split is (1 for train, 2 for test, 3 for dev), returns the set of corresponding sentence\n    indices in sentences_df.\"\"\"\n    return set(datasplit[datasplit[\"splitset_label\"] == split_id][\"sentence_index\"])\n\n\ndef get_sentences_with_idcs(sentences: pd.DataFrame, sentences_idcs: set[int]):\n    \"\"\"Given a set of sentence indices, returns the corresponding sentences texts in sentences.\"\"\"\n    criterion = sentences[\"sentence_index\"].map(lambda x: x in sentences_idcs)\n    return sentences[criterion][\"sentence\"].tolist()\n\n\ndef sentence_subtrees(sentence_idx, trees_pointers, trees_phrases):\n    tree_pointers = trees_pointers[sentence_idx]\n    tree_phrases = trees_phrases[sentence_idx]\n    tree = SSTTree(tree_pointers, tree_phrases)\n    return tree.subtrees()\n\n\ndef visit_postorder(node, visit_list):\n    if node:\n        visit_postorder(node.left, visit_list)\n        visit_postorder(node.right, visit_list)\n        visit_list.append(node.val)\n\n\nclass SSTTree:\n    class Node:\n        def __init__(self, key, val=None):\n            self.left = None\n            self.right = None\n            self.key = key\n            self.val = val\n\n    def create_node(self, parent, i):\n        if self.nodes[i] is not None:\n            # already created\n            return\n        self.nodes[i] = self.Node(i)\n\n        if parent[i] == -1:\n            # is root\n            self.root = self.nodes[i]\n            return\n\n        if self.nodes[parent[i]] is None:\n            # parent not yet created\n            self.create_node(parent, parent[i])\n\n        # assign current node to parent\n        parent = self.nodes[parent[i]]\n        if parent.left is None:\n            parent.left = self.nodes[i]\n        else:\n            parent.right = self.nodes[i]\n\n    def create_tree(self, parents, tree_phrases):\n        n = len(parents)\n        self.nodes = [None for i in range(n)]\n        self.root = [None]\n        for i in range(n):\n            self.create_node(parents, i)\n        for i, phrase in enumerate(tree_phrases):\n            self.nodes[i].val = phrase\n        for node in self.nodes:\n            if node.val is None:\n                node.val = \" \".join((node.left.val, node.right.val))\n\n    def __init__(self, tree_pointers, tree_phrases):\n        self.create_tree([int(elem) - 1 for elem in tree_pointers], tree_phrases)\n\n    def subtrees(self):\n        visit_list = []\n        visit_postorder(self.root, visit_list)\n        return visit_list\n"
  },
  {
    "path": "ludwig/datasets/model_configs/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/datasets/model_configs/adult_census_income_default.yaml",
    "content": "output_features:\n  - name: income\n    type: category\ninput_features:\n  - name: age\n    type: number\n  - name: workclass\n    type: category\n  - name: fnlwgt\n    type: number\n  - name: education\n    type: category\n  - name: education-num\n    type: number\n  - name: marital-status\n    type: category\n  - name: occupation\n    type: category\n  - name: relationship\n    type: category\n  - name: race\n    type: category\n  - name: sex\n    type: category\n  - name: capital-gain\n    type: number\n  - name: capital-loss\n    type: number\n  - name: hours-per-week\n    type: number\n  - name: native-country\n    type: category\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n  steps_per_checkpoint: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/allstate_claims_severity_default.yaml",
    "content": "output_features:\n  - name: loss\n    type: number\ninput_features:\n  - name: cat1\n    type: category\n  - name: cat2\n    type: category\n  - name: cat3\n    type: category\n  - name: cat4\n    type: category\n  - name: cat5\n    type: category\n  - name: cat6\n    type: category\n  - name: cat7\n    type: category\n  - name: cat8\n    type: category\n  - name: cat9\n    type: category\n  - name: cat10\n    type: category\n  - name: cat11\n    type: category\n  - name: cat12\n    type: category\n  - name: cat13\n    type: category\n  - name: cat14\n    type: category\n  - name: cat15\n    type: category\n  - name: cat16\n    type: category\n  - name: cat17\n    type: category\n  - name: cat18\n    type: category\n  - name: cat19\n    type: category\n  - name: cat20\n    type: category\n  - name: cat21\n    type: category\n  - name: cat22\n    type: category\n  - name: cat23\n    type: category\n  - name: cat24\n    type: category\n  - name: cat25\n    type: category\n  - name: cat26\n    type: category\n  - name: cat27\n    type: category\n  - name: cat28\n    type: category\n  - name: cat29\n    type: category\n  - name: cat30\n    type: category\n  - name: cat31\n    type: category\n  - name: cat32\n    type: category\n  - name: cat33\n    type: category\n  - name: cat34\n    type: category\n  - name: cat35\n    type: category\n  - name: cat36\n    type: category\n  - name: cat37\n    type: category\n  - name: cat38\n    type: category\n  - name: cat39\n    type: category\n  - name: cat40\n    type: category\n  - name: cat41\n    type: category\n  - name: cat42\n    type: category\n  - name: cat43\n    type: category\n  - name: cat44\n    type: category\n  - name: cat45\n    type: category\n  - name: cat46\n    type: category\n  - name: cat47\n    type: category\n  - name: cat48\n    type: category\n  - name: cat49\n    type: category\n  - name: cat50\n    type: category\n  - name: cat51\n    type: category\n  - name: cat52\n    type: category\n  - name: cat53\n    type: category\n  - name: cat54\n    type: category\n  - name: cat55\n    type: category\n  - name: cat56\n    type: category\n  - name: cat57\n    type: category\n  - name: cat58\n    type: category\n  - name: cat59\n    type: category\n  - name: cat60\n    type: category\n  - name: cat61\n    type: category\n  - name: cat62\n    type: category\n  - name: cat63\n    type: category\n  - name: cat64\n    type: category\n  - name: cat65\n    type: category\n  - name: cat66\n    type: category\n  - name: cat67\n    type: category\n  - name: cat68\n    type: category\n  - name: cat69\n    type: category\n  - name: cat70\n    type: category\n  - name: cat71\n    type: category\n  - name: cat72\n    type: category\n  - name: cat73\n    type: category\n  - name: cat74\n    type: category\n  - name: cat75\n    type: category\n  - name: cat76\n    type: category\n  - name: cat77\n    type: category\n  - name: cat78\n    type: category\n  - name: cat79\n    type: category\n  - name: cat80\n    type: category\n  - name: cat81\n    type: category\n  - name: cat82\n    type: category\n  - name: cat83\n    type: category\n  - name: cat84\n    type: category\n  - name: cat85\n    type: category\n  - name: cat86\n    type: category\n  - name: cat87\n    type: category\n  - name: cat88\n    type: category\n  - name: cat89\n    type: category\n  - name: cat90\n    type: category\n  - name: cat91\n    type: category\n  - name: cat92\n    type: category\n  - name: cat93\n    type: category\n  - name: cat94\n    type: category\n  - name: cat95\n    type: category\n  - name: cat96\n    type: category\n  - name: cat97\n    type: category\n  - name: cat98\n    type: category\n  - name: cat99\n    type: category\n  - name: cat100\n    type: category\n  - name: cat101\n    type: category\n  - name: cat102\n    type: category\n  - name: cat103\n    type: category\n  - name: cat104\n    type: category\n  - name: cat105\n    type: category\n  - name: cat106\n    type: category\n  - name: cat107\n    type: category\n  - name: cat108\n    type: category\n  - name: cat109\n    type: category\n  - name: cat110\n    type: category\n  - name: cat111\n    type: category\n  - name: cat112\n    type: category\n  - name: cat113\n    type: category\n  - name: cat114\n    type: category\n  - name: cat115\n    type: category\n  - name: cat116\n    type: category\n  - name: cont1\n    type: number\n  - name: cont2\n    type: number\n  - name: cont3\n    type: number\n  - name: cont4\n    type: number\n  - name: cont5\n    type: number\n  - name: cont6\n    type: number\n  - name: cont7\n    type: number\n  - name: cont8\n    type: number\n  - name: cont9\n    type: number\n  - name: cont10\n    type: number\n  - name: cont11\n    type: number\n  - name: cont12\n    type: number\n  - name: cont13\n    type: number\n  - name: cont14\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/ames_housing_default.yaml",
    "content": "output_features:\n  - name: SalePrice\n    type: number\ninput_features:\n  - name: MSSubClass\n    type: category\n  - name: MSZoning\n    type: category\n  - name: LotFrontage\n    type: number\n  - name: LotArea\n    type: number\n  - name: Street\n    type: category\n  - name: Alley\n    type: category\n  - name: LotShape\n    type: category\n  - name: LandContour\n    type: category\n  - name: Utilities\n    type: category\n  - name: LotConfig\n    type: category\n  - name: LandSlope\n    type: category\n  - name: Neighborhood\n    type: category\n  - name: Condition1\n    type: category\n  - name: Condition2\n    type: category\n  - name: BldgType\n    type: category\n  - name: HouseStyle\n    type: category\n  - name: OverallQual\n    type: category\n  - name: OverallCond\n    type: category\n  - name: YearBuilt\n    type: number\n  - name: YearRemodAdd\n    type: number\n  - name: RoofStyle\n    type: category\n  - name: RoofMatl\n    type: category\n  - name: Exterior1st\n    type: category\n  - name: Exterior2nd\n    type: category\n  - name: MasVnrType\n    type: category\n  - name: MasVnrArea\n    type: number\n  - name: ExterQual\n    type: category\n  - name: ExterCond\n    type: category\n  - name: Foundation\n    type: category\n  - name: BsmtQual\n    type: category\n  - name: BsmtCond\n    type: category\n  - name: BsmtExposure\n    type: category\n  - name: BsmtFinType1\n    type: category\n  - name: BsmtFinSF1\n    type: number\n  - name: BsmtFinType2\n    type: category\n  - name: BsmtFinSF2\n    type: number\n  - name: BsmtUnfSF\n    type: number\n  - name: TotalBsmtSF\n    type: number\n  - name: Heating\n    type: category\n  - name: HeatingQC\n    type: category\n  - name: CentralAir\n    type: binary\n  - name: Electrical\n    type: category\n  - name: 1stFlrSF\n    type: number\n  - name: 2ndFlrSF\n    type: number\n  - name: LowQualFinSF\n    type: number\n  - name: GrLivArea\n    type: number\n  - name: BsmtFullBath\n    type: number\n  - name: BsmtHalfBath\n    type: number\n  - name: FullBath\n    type: number\n  - name: HalfBath\n    type: number\n  - name: BedroomAbvGr\n    type: number\n  - name: KitchenAbvGr\n    type: number\n  - name: KitchenQual\n    type: category\n  - name: TotRmsAbvGrd\n    type: number\n  - name: Functional\n    type: category\n  - name: Fireplaces\n    type: number\n  - name: FireplaceQu\n    type: category\n  - name: GarageType\n    type: category\n  - name: GarageYrBlt\n    type: number\n  - name: GarageFinish\n    type: category\n  - name: GarageCars\n    type: number\n  - name: GarageArea\n    type: number\n  - name: GarageQual\n    type: category\n  - name: GarageCond\n    type: category\n  - name: PavedDrive\n    type: category\n  - name: WoodDeckSF\n    type: number\n  - name: OpenPorchSF\n    type: number\n  - name: EnclosedPorch\n    type: number\n  - name: 3SsnPorch\n    type: number\n  - name: ScreenPorch\n    type: number\n  - name: PoolArea\n    type: number\n  - name: PoolQC\n    type: category\n  - name: Fence\n    type: category\n  - name: MiscFeature\n    type: category\n  - name: MiscVal\n    type: number\n  - name: MoSold\n    type: category\n  - name: YrSold\n    type: number\n  - name: SaleType\n    type: category\n  - name: SaleCondition\n    type: category\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/bnp_claims_management_default.yaml",
    "content": "output_features:\n  - name: target\n    type: binary\ninput_features:\n  - name: v1\n    type: number\n  - name: v2\n    type: number\n  - name: v3\n    type: category\n  - name: v4\n    type: number\n  - name: v5\n    type: number\n  - name: v6\n    type: number\n  - name: v7\n    type: number\n  - name: v8\n    type: number\n  - name: v9\n    type: number\n  - name: v10\n    type: number\n  - name: v11\n    type: number\n  - name: v12\n    type: number\n  - name: v13\n    type: number\n  - name: v14\n    type: number\n  - name: v15\n    type: number\n  - name: v16\n    type: number\n  - name: v17\n    type: number\n  - name: v18\n    type: number\n  - name: v19\n    type: number\n  - name: v20\n    type: number\n  - name: v21\n    type: number\n  - name: v22\n    type: category\n  - name: v23\n    type: number\n  - name: v24\n    type: category\n  - name: v25\n    type: number\n  - name: v26\n    type: number\n  - name: v27\n    type: number\n  - name: v28\n    type: number\n  - name: v29\n    type: number\n  - name: v30\n    type: category\n  - name: v31\n    type: category\n  - name: v32\n    type: number\n  - name: v33\n    type: number\n  - name: v34\n    type: number\n  - name: v35\n    type: number\n  - name: v36\n    type: number\n  - name: v37\n    type: number\n  - name: v38\n    type: number\n  - name: v39\n    type: number\n  - name: v40\n    type: number\n  - name: v41\n    type: number\n  - name: v42\n    type: number\n  - name: v43\n    type: number\n  - name: v44\n    type: number\n  - name: v45\n    type: number\n  - name: v46\n    type: number\n  - name: v47\n    type: category\n  - name: v48\n    type: number\n  - name: v49\n    type: number\n  - name: v50\n    type: number\n  - name: v51\n    type: number\n  - name: v52\n    type: category\n  - name: v53\n    type: number\n  - name: v54\n    type: number\n  - name: v55\n    type: number\n  - name: v56\n    type: category\n  - name: v57\n    type: number\n  - name: v58\n    type: number\n  - name: v59\n    type: number\n  - name: v60\n    type: number\n  - name: v61\n    type: number\n  - name: v62\n    type: number\n  - name: v63\n    type: number\n  - name: v64\n    type: number\n  - name: v65\n    type: number\n  - name: v66\n    type: category\n  - name: v67\n    type: number\n  - name: v68\n    type: number\n  - name: v69\n    type: number\n  - name: v70\n    type: number\n  - name: v71\n    type: category\n  - name: v72\n    type: number\n  - name: v73\n    type: number\n  - name: v74\n    type: category\n  - name: v75\n    type: category\n  - name: v76\n    type: number\n  - name: v77\n    type: number\n  - name: v78\n    type: number\n  - name: v79\n    type: category\n  - name: v80\n    type: number\n  - name: v81\n    type: number\n  - name: v82\n    type: number\n  - name: v83\n    type: number\n  - name: v84\n    type: number\n  - name: v85\n    type: number\n  - name: v86\n    type: number\n  - name: v87\n    type: number\n  - name: v88\n    type: number\n  - name: v89\n    type: number\n  - name: v90\n    type: number\n  - name: v91\n    type: category\n  - name: v92\n    type: number\n  - name: v93\n    type: number\n  - name: v94\n    type: number\n  - name: v95\n    type: number\n  - name: v96\n    type: number\n  - name: v97\n    type: number\n  - name: v98\n    type: number\n  - name: v99\n    type: number\n  - name: v100\n    type: number\n  - name: v101\n    type: number\n  - name: v102\n    type: number\n  - name: v103\n    type: number\n  - name: v104\n    type: number\n  - name: v105\n    type: number\n  - name: v106\n    type: number\n  - name: v107\n    type: category\n  - name: v108\n    type: number\n  - name: v109\n    type: number\n  - name: v110\n    type: category\n  - name: v111\n    type: number\n  - name: v112\n    type: category\n  - name: v113\n    type: category\n  - name: v114\n    type: number\n  - name: v115\n    type: number\n  - name: v116\n    type: number\n  - name: v117\n    type: number\n  - name: v118\n    type: number\n  - name: v119\n    type: number\n  - name: v120\n    type: number\n  - name: v121\n    type: number\n  - name: v122\n    type: number\n  - name: v123\n    type: number\n  - name: v124\n    type: number\n  - name: v125\n    type: category\n  - name: v126\n    type: number\n  - name: v127\n    type: number\n  - name: v128\n    type: number\n  - name: v129\n    type: number\n  - name: v130\n    type: number\n  - name: v131\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/forest_cover_default.yaml",
    "content": "output_features:\n  - name: Cover_Type\n    type: category\ninput_features:\n  - name: Elevation\n    type: number\n  - name: Aspect\n    type: number\n  - name: Slope\n    type: number\n  - name: Horizontal_Distance_To_Hydrology\n    type: number\n  - name: Vertical_Distance_To_Hydrology\n    type: number\n  - name: Horizontal_Distance_To_Roadways\n    type: number\n  - name: Hillshade_9am\n    type: number\n  - name: Hillshade_Noon\n    type: number\n  - name: Hillshade_3pm\n    type: number\n  - name: Horizontal_Distance_To_Fire_Points\n    type: number\n  - name: Wilderness_Area\n    type: category\n  - name: Soil_Type\n    type: category\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/higgs_best.yaml",
    "content": "output_features:\n  - name: label\n    type: binary\n    weight_regularization: null\ninput_features:\n  - name: lepton_pT\n    type: number\n  - name: lepton_eta\n    type: number\n  - name: lepton_phi\n    type: number\n  - name: missing_energy_magnitude\n    type: number\n  - name: missing_energy_phi\n    type: number\n  - name: jet_1_pt\n    type: number\n  - name: jet_1_eta\n    type: number\n  - name: jet_1_phi\n    type: number\n  - name: jet_1_b-tag\n    type: number\n  - name: jet_2_pt\n    type: number\n  - name: jet_2_eta\n    type: number\n  - name: jet_2_phi\n    type: number\n  - name: jet_2_b-tag\n    type: number\n  - name: jet_3_pt\n    type: number\n  - name: jet_3_eta\n    type: number\n  - name: jet_3_phi\n    type: number\n  - name: jet_3_b-tag\n    type: number\n  - name: jet_4_pt\n    type: number\n  - name: jet_4_eta\n    type: number\n  - name: jet_4_phi\n    type: number\n  - name: jet_4_b-tag\n    type: number\n  - name: m_jj\n    type: number\n  - name: m_jjj\n    type: number\n  - name: m_lv\n    type: number\n  - name: m_jlv\n    type: number\n  - name: m_bb\n    type: number\n  - name: m_wbb\n    type: number\n  - name: m_wwbb\n    type: number\ncombiner:\n  type: tabnet\n  bn_momentum: 0.95\n  bn_virtual_bs: 1024\n  dropout: 0.05252744300130521\n  fc_size: 128\n  num_fc_layers: 3\n  num_steps: 3\n  output_size: 128\n  relaxation_factor: 1.5\n  size: 32\n  sparsity: 0.0001\ntraining:\n  batch_size: 8192\n  learning_rate: 0.01\n  shuffle_buffer_size: 1000000\n  should_shuffle: true\n  eval_batch_size: 500000 #4096 # 65536 131072 262144 524288\n  epochs: 300\n  early_stop: 30\n  optimizer:\n    type: adam\n  learning_rate_scheduler:\n    decay: exponential\n    decay_rate: 0.8\n    decay_steps: 20000\n  regularization_lambda: 1\n  validation_field: label\n"
  },
  {
    "path": "ludwig/datasets/model_configs/higgs_default.yaml",
    "content": "output_features:\n  - name: label\n    type: binary\n    weight_regularization: null\ninput_features:\n  - name: lepton_pT\n    type: number\n  - name: lepton_eta\n    type: number\n  - name: lepton_phi\n    type: number\n  - name: missing_energy_magnitude\n    type: number\n  - name: missing_energy_phi\n    type: number\n  - name: jet_1_pt\n    type: number\n  - name: jet_1_eta\n    type: number\n  - name: jet_1_phi\n    type: number\n  - name: jet_1_b-tag\n    type: number\n  - name: jet_2_pt\n    type: number\n  - name: jet_2_eta\n    type: number\n  - name: jet_2_phi\n    type: number\n  - name: jet_2_b-tag\n    type: number\n  - name: jet_3_pt\n    type: number\n  - name: jet_3_eta\n    type: number\n  - name: jet_3_phi\n    type: number\n  - name: jet_3_b-tag\n    type: number\n  - name: jet_4_pt\n    type: number\n  - name: jet_4_eta\n    type: number\n  - name: jet_4_phi\n    type: number\n  - name: jet_4_b-tag\n    type: number\n  - name: m_jj\n    type: number\n  - name: m_jjj\n    type: number\n  - name: m_lv\n    type: number\n  - name: m_jlv\n    type: number\n  - name: m_bb\n    type: number\n  - name: m_wbb\n    type: number\n  - name: m_wwbb\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/ieee_fraud_default.yaml",
    "content": "output_features:\n  - name: isFraud\n    type: binary\ninput_features:\n  - name: TransactionDT\n    type: number\n  - name: TransactionAmt\n    type: number\n  - name: ProductCD\n    type: category\n  - name: card1\n    type: number\n  - name: card2\n    type: number\n  - name: card3\n    type: number\n  - name: card4\n    type: category\n  - name: card5\n    type: number\n  - name: card6\n    type: category\n  - name: addr1\n    type: number\n  - name: addr2\n    type: number\n  - name: dist1\n    type: number\n  - name: dist2\n    type: number\n  - name: P_emaildomain\n    type: category\n  - name: R_emaildomain\n    type: number\n  - name: C1\n    type: number\n  - name: C2\n    type: number\n  - name: C3\n    type: number\n  - name: C4\n    type: number\n  - name: C5\n    type: number\n  - name: C6\n    type: number\n  - name: C7\n    type: number\n  - name: C8\n    type: number\n  - name: C9\n    type: number\n  - name: C10\n    type: number\n  - name: C11\n    type: number\n  - name: C12\n    type: number\n  - name: C13\n    type: number\n  - name: C14\n    type: number\n  - name: D1\n    type: number\n  - name: D2\n    type: number\n  - name: D3\n    type: number\n  - name: D4\n    type: number\n  - name: D5\n    type: number\n  - name: D6\n    type: number\n  - name: D7\n    type: number\n  - name: D8\n    type: number\n  - name: D9\n    type: number\n  - name: D10\n    type: number\n  - name: D11\n    type: number\n  - name: D12\n    type: number\n  - name: D13\n    type: number\n  - name: D14\n    type: number\n  - name: D15\n    type: number\n  - name: M1\n    type: category\n  - name: M2\n    type: category\n  - name: M3\n    type: category\n  - name: M4\n    type: category\n  - name: M5\n    type: category\n  - name: M6\n    type: category\n  - name: M7\n    type: category\n  - name: M8\n    type: category\n  - name: M9\n    type: category\n  - name: V1\n    type: number\n  - name: V2\n    type: number\n  - name: V3\n    type: number\n  - name: V4\n    type: number\n  - name: V5\n    type: number\n  - name: V6\n    type: number\n  - name: V7\n    type: number\n  - name: V8\n    type: number\n  - name: V9\n    type: number\n  - name: V10\n    type: number\n  - name: V11\n    type: number\n  - name: V12\n    type: number\n  - name: V13\n    type: number\n  - name: V14\n    type: number\n  - name: V15\n    type: number\n  - name: V16\n    type: number\n  - name: V17\n    type: number\n  - name: V18\n    type: number\n  - name: V19\n    type: number\n  - name: V20\n    type: number\n  - name: V21\n    type: number\n  - name: V22\n    type: number\n  - name: V23\n    type: number\n  - name: V24\n    type: number\n  - name: V25\n    type: number\n  - name: V26\n    type: number\n  - name: V27\n    type: number\n  - name: V28\n    type: number\n  - name: V29\n    type: number\n  - name: V30\n    type: number\n  - name: V31\n    type: number\n  - name: V32\n    type: number\n  - name: V33\n    type: number\n  - name: V34\n    type: number\n  - name: V35\n    type: number\n  - name: V36\n    type: number\n  - name: V37\n    type: number\n  - name: V38\n    type: number\n  - name: V39\n    type: number\n  - name: V40\n    type: number\n  - name: V41\n    type: number\n  - name: V42\n    type: number\n  - name: V43\n    type: number\n  - name: V44\n    type: number\n  - name: V45\n    type: number\n  - name: V46\n    type: number\n  - name: V47\n    type: number\n  - name: V48\n    type: number\n  - name: V49\n    type: number\n  - name: V50\n    type: number\n  - name: V51\n    type: number\n  - name: V52\n    type: number\n  - name: V53\n    type: number\n  - name: V54\n    type: number\n  - name: V55\n    type: number\n  - name: V56\n    type: number\n  - name: V57\n    type: number\n  - name: V58\n    type: number\n  - name: V59\n    type: number\n  - name: V60\n    type: number\n  - name: V61\n    type: number\n  - name: V62\n    type: number\n  - name: V63\n    type: number\n  - name: V64\n    type: number\n  - name: V65\n    type: number\n  - name: V66\n    type: number\n  - name: V67\n    type: number\n  - name: V68\n    type: number\n  - name: V69\n    type: number\n  - name: V70\n    type: number\n  - name: V71\n    type: number\n  - name: V72\n    type: number\n  - name: V73\n    type: number\n  - name: V74\n    type: number\n  - name: V75\n    type: number\n  - name: V76\n    type: number\n  - name: V77\n    type: number\n  - name: V78\n    type: number\n  - name: V79\n    type: number\n  - name: V80\n    type: number\n  - name: V81\n    type: number\n  - name: V82\n    type: number\n  - name: V83\n    type: number\n  - name: V84\n    type: number\n  - name: V85\n    type: number\n  - name: V86\n    type: number\n  - name: V87\n    type: number\n  - name: V88\n    type: number\n  - name: V89\n    type: number\n  - name: V90\n    type: number\n  - name: V91\n    type: number\n  - name: V92\n    type: number\n  - name: V93\n    type: number\n  - name: V94\n    type: number\n  - name: V95\n    type: number\n  - name: V96\n    type: number\n  - name: V97\n    type: number\n  - name: V98\n    type: number\n  - name: V99\n    type: number\n  - name: V100\n    type: number\n  - name: V101\n    type: number\n  - name: V102\n    type: number\n  - name: V103\n    type: number\n  - name: V104\n    type: number\n  - name: V105\n    type: number\n  - name: V106\n    type: number\n  - name: V107\n    type: number\n  - name: V108\n    type: number\n  - name: V109\n    type: number\n  - name: V110\n    type: number\n  - name: V111\n    type: number\n  - name: V112\n    type: number\n  - name: V113\n    type: number\n  - name: V114\n    type: number\n  - name: V115\n    type: number\n  - name: V116\n    type: number\n  - name: V117\n    type: number\n  - name: V118\n    type: number\n  - name: V119\n    type: number\n  - name: V120\n    type: number\n  - name: V121\n    type: number\n  - name: V122\n    type: number\n  - name: V123\n    type: number\n  - name: V124\n    type: number\n  - name: V125\n    type: number\n  - name: V126\n    type: number\n  - name: V127\n    type: number\n  - name: V128\n    type: number\n  - name: V129\n    type: number\n  - name: V130\n    type: number\n  - name: V131\n    type: number\n  - name: V132\n    type: number\n  - name: V133\n    type: number\n  - name: V134\n    type: number\n  - name: V135\n    type: number\n  - name: V136\n    type: number\n  - name: V137\n    type: number\n  - name: V138\n    type: number\n  - name: V139\n    type: number\n  - name: V140\n    type: number\n  - name: V141\n    type: number\n  - name: V142\n    type: number\n  - name: V143\n    type: number\n  - name: V144\n    type: number\n  - name: V145\n    type: number\n  - name: V146\n    type: number\n  - name: V147\n    type: number\n  - name: V148\n    type: number\n  - name: V149\n    type: number\n  - name: V150\n    type: number\n  - name: V151\n    type: number\n  - name: V152\n    type: number\n  - name: V153\n    type: number\n  - name: V154\n    type: number\n  - name: V155\n    type: number\n  - name: V156\n    type: number\n  - name: V157\n    type: number\n  - name: V158\n    type: number\n  - name: V159\n    type: number\n  - name: V160\n    type: number\n  - name: V161\n    type: number\n  - name: V162\n    type: number\n  - name: V163\n    type: number\n  - name: V164\n    type: number\n  - name: V165\n    type: number\n  - name: V166\n    type: number\n  - name: V167\n    type: number\n  - name: V168\n    type: number\n  - name: V169\n    type: number\n  - name: V170\n    type: number\n  - name: V171\n    type: number\n  - name: V172\n    type: number\n  - name: V173\n    type: number\n  - name: V174\n    type: number\n  - name: V175\n    type: number\n  - name: V176\n    type: number\n  - name: V177\n    type: number\n  - name: V178\n    type: number\n  - name: V179\n    type: number\n  - name: V180\n    type: number\n  - name: V181\n    type: number\n  - name: V182\n    type: number\n  - name: V183\n    type: number\n  - name: V184\n    type: number\n  - name: V185\n    type: number\n  - name: V186\n    type: number\n  - name: V187\n    type: number\n  - name: V188\n    type: number\n  - name: V189\n    type: number\n  - name: V190\n    type: number\n  - name: V191\n    type: number\n  - name: V192\n    type: number\n  - name: V193\n    type: number\n  - name: V194\n    type: number\n  - name: V195\n    type: number\n  - name: V196\n    type: number\n  - name: V197\n    type: number\n  - name: V198\n    type: number\n  - name: V199\n    type: number\n  - name: V200\n    type: number\n  - name: V201\n    type: number\n  - name: V202\n    type: number\n  - name: V203\n    type: number\n  - name: V204\n    type: number\n  - name: V205\n    type: number\n  - name: V206\n    type: number\n  - name: V207\n    type: number\n  - name: V208\n    type: number\n  - name: V209\n    type: number\n  - name: V210\n    type: number\n  - name: V211\n    type: number\n  - name: V212\n    type: number\n  - name: V213\n    type: number\n  - name: V214\n    type: number\n  - name: V215\n    type: number\n  - name: V216\n    type: number\n  - name: V217\n    type: number\n  - name: V218\n    type: number\n  - name: V219\n    type: number\n  - name: V220\n    type: number\n  - name: V221\n    type: number\n  - name: V222\n    type: number\n  - name: V223\n    type: number\n  - name: V224\n    type: number\n  - name: V225\n    type: number\n  - name: V226\n    type: number\n  - name: V227\n    type: number\n  - name: V228\n    type: number\n  - name: V229\n    type: number\n  - name: V230\n    type: number\n  - name: V231\n    type: number\n  - name: V232\n    type: number\n  - name: V233\n    type: number\n  - name: V234\n    type: number\n  - name: V235\n    type: number\n  - name: V236\n    type: number\n  - name: V237\n    type: number\n  - name: V238\n    type: number\n  - name: V239\n    type: number\n  - name: V240\n    type: number\n  - name: V241\n    type: number\n  - name: V242\n    type: number\n  - name: V243\n    type: number\n  - name: V244\n    type: number\n  - name: V245\n    type: number\n  - name: V246\n    type: number\n  - name: V247\n    type: number\n  - name: V248\n    type: number\n  - name: V249\n    type: number\n  - name: V250\n    type: number\n  - name: V251\n    type: number\n  - name: V252\n    type: number\n  - name: V253\n    type: number\n  - name: V254\n    type: number\n  - name: V255\n    type: number\n  - name: V256\n    type: number\n  - name: V257\n    type: number\n  - name: V258\n    type: number\n  - name: V259\n    type: number\n  - name: V260\n    type: number\n  - name: V261\n    type: number\n  - name: V262\n    type: number\n  - name: V263\n    type: number\n  - name: V264\n    type: number\n  - name: V265\n    type: number\n  - name: V266\n    type: number\n  - name: V267\n    type: number\n  - name: V268\n    type: number\n  - name: V269\n    type: number\n  - name: V270\n    type: number\n  - name: V271\n    type: number\n  - name: V272\n    type: number\n  - name: V273\n    type: number\n  - name: V274\n    type: number\n  - name: V275\n    type: number\n  - name: V276\n    type: number\n  - name: V277\n    type: number\n  - name: V278\n    type: number\n  - name: V279\n    type: number\n  - name: V280\n    type: number\n  - name: V281\n    type: number\n  - name: V282\n    type: number\n  - name: V283\n    type: number\n  - name: V284\n    type: number\n  - name: V285\n    type: number\n  - name: V286\n    type: number\n  - name: V287\n    type: number\n  - name: V288\n    type: number\n  - name: V289\n    type: number\n  - name: V290\n    type: number\n  - name: V291\n    type: number\n  - name: V292\n    type: number\n  - name: V293\n    type: number\n  - name: V294\n    type: number\n  - name: V295\n    type: number\n  - name: V296\n    type: number\n  - name: V297\n    type: number\n  - name: V298\n    type: number\n  - name: V299\n    type: number\n  - name: V300\n    type: number\n  - name: V301\n    type: number\n  - name: V302\n    type: number\n  - name: V303\n    type: number\n  - name: V304\n    type: number\n  - name: V305\n    type: number\n  - name: V306\n    type: number\n  - name: V307\n    type: number\n  - name: V308\n    type: number\n  - name: V309\n    type: number\n  - name: V310\n    type: number\n  - name: V311\n    type: number\n  - name: V312\n    type: number\n  - name: V313\n    type: number\n  - name: V314\n    type: number\n  - name: V315\n    type: number\n  - name: V316\n    type: number\n  - name: V317\n    type: number\n  - name: V318\n    type: number\n  - name: V319\n    type: number\n  - name: V320\n    type: number\n  - name: V321\n    type: number\n  - name: V322\n    type: number\n  - name: V323\n    type: number\n  - name: V324\n    type: number\n  - name: V325\n    type: number\n  - name: V326\n    type: number\n  - name: V327\n    type: number\n  - name: V328\n    type: number\n  - name: V329\n    type: number\n  - name: V330\n    type: number\n  - name: V331\n    type: number\n  - name: V332\n    type: number\n  - name: V333\n    type: number\n  - name: V334\n    type: number\n  - name: V335\n    type: number\n  - name: V336\n    type: number\n  - name: V337\n    type: number\n  - name: V338\n    type: number\n  - name: V339\n    type: number\n  - name: id_01\n    type: number\n  - name: id_02\n    type: number\n  - name: id_03\n    type: number\n  - name: id_04\n    type: number\n  - name: id_05\n    type: number\n  - name: id_06\n    type: number\n  - name: id_07\n    type: number\n  - name: id_08\n    type: number\n  - name: id_09\n    type: number\n  - name: id_10\n    type: number\n  - name: id_11\n    type: number\n  - name: id_12\n    type: number\n  - name: id_13\n    type: number\n  - name: id_14\n    type: number\n  - name: id_15\n    type: number\n  - name: id_16\n    type: number\n  - name: id_17\n    type: number\n  - name: id_18\n    type: number\n  - name: id_19\n    type: number\n  - name: id_20\n    type: number\n  - name: id_21\n    type: number\n  - name: id_22\n    type: number\n  - name: id_23\n    type: number\n  - name: id_24\n    type: number\n  - name: id_25\n    type: number\n  - name: id_26\n    type: number\n  - name: id_27\n    type: number\n  - name: id_28\n    type: number\n  - name: id_29\n    type: number\n  - name: id_30\n    type: number\n  - name: id_31\n    type: number\n  - name: id_32\n    type: number\n  - name: id_33\n    type: number\n  - name: id_34\n    type: number\n  - name: id_35\n    type: number\n  - name: id_36\n    type: number\n  - name: id_37\n    type: number\n  - name: id_38\n    type: number\n  - name: DeviceType\n    type: number\n  - name: DeviceInfo\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/mercedes_benz_greener_default.yaml",
    "content": "output_features:\n  - name: y\n    type: number\ninput_features:\n  - name: X0\n    type: category\n  - name: X1\n    type: category\n  - name: X2\n    type: category\n  - name: X3\n    type: category\n  - name: X4\n    type: category\n  - name: X5\n    type: category\n  - name: X6\n    type: category\n  - name: X8\n    type: category\n  - name: X10\n    type: binary\n  - name: X11\n    type: binary\n  - name: X12\n    type: binary\n  - name: X13\n    type: binary\n  - name: X14\n    type: binary\n  - name: X15\n    type: binary\n  - name: X16\n    type: binary\n  - name: X17\n    type: binary\n  - name: X18\n    type: binary\n  - name: X19\n    type: binary\n  - name: X20\n    type: binary\n  - name: X21\n    type: binary\n  - name: X22\n    type: binary\n  - name: X23\n    type: binary\n  - name: X24\n    type: binary\n  - name: X26\n    type: binary\n  - name: X27\n    type: binary\n  - name: X28\n    type: binary\n  - name: X29\n    type: binary\n  - name: X30\n    type: binary\n  - name: X31\n    type: binary\n  - name: X32\n    type: binary\n  - name: X33\n    type: binary\n  - name: X34\n    type: binary\n  - name: X35\n    type: binary\n  - name: X36\n    type: binary\n  - name: X37\n    type: binary\n  - name: X38\n    type: binary\n  - name: X39\n    type: binary\n  - name: X40\n    type: binary\n  - name: X41\n    type: binary\n  - name: X42\n    type: binary\n  - name: X43\n    type: binary\n  - name: X44\n    type: binary\n  - name: X45\n    type: binary\n  - name: X46\n    type: binary\n  - name: X47\n    type: binary\n  - name: X48\n    type: binary\n  - name: X49\n    type: binary\n  - name: X50\n    type: binary\n  - name: X51\n    type: binary\n  - name: X52\n    type: binary\n  - name: X53\n    type: binary\n  - name: X54\n    type: binary\n  - name: X55\n    type: binary\n  - name: X56\n    type: binary\n  - name: X57\n    type: binary\n  - name: X58\n    type: binary\n  - name: X59\n    type: binary\n  - name: X60\n    type: binary\n  - name: X61\n    type: binary\n  - name: X62\n    type: binary\n  - name: X63\n    type: binary\n  - name: X64\n    type: binary\n  - name: X65\n    type: binary\n  - name: X66\n    type: binary\n  - name: X67\n    type: binary\n  - name: X68\n    type: binary\n  - name: X69\n    type: binary\n  - name: X70\n    type: binary\n  - name: X71\n    type: binary\n  - name: X73\n    type: binary\n  - name: X74\n    type: binary\n  - name: X75\n    type: binary\n  - name: X76\n    type: binary\n  - name: X77\n    type: binary\n  - name: X78\n    type: binary\n  - name: X79\n    type: binary\n  - name: X80\n    type: binary\n  - name: X81\n    type: binary\n  - name: X82\n    type: binary\n  - name: X83\n    type: binary\n  - name: X84\n    type: binary\n  - name: X85\n    type: binary\n  - name: X86\n    type: binary\n  - name: X87\n    type: binary\n  - name: X88\n    type: binary\n  - name: X89\n    type: binary\n  - name: X90\n    type: binary\n  - name: X91\n    type: binary\n  - name: X92\n    type: binary\n  - name: X93\n    type: binary\n  - name: X94\n    type: binary\n  - name: X95\n    type: binary\n  - name: X96\n    type: binary\n  - name: X97\n    type: binary\n  - name: X98\n    type: binary\n  - name: X99\n    type: binary\n  - name: X100\n    type: binary\n  - name: X101\n    type: binary\n  - name: X102\n    type: binary\n  - name: X103\n    type: binary\n  - name: X104\n    type: binary\n  - name: X105\n    type: binary\n  - name: X106\n    type: binary\n  - name: X107\n    type: binary\n  - name: X108\n    type: binary\n  - name: X109\n    type: binary\n  - name: X110\n    type: binary\n  - name: X111\n    type: binary\n  - name: X112\n    type: binary\n  - name: X113\n    type: binary\n  - name: X114\n    type: binary\n  - name: X115\n    type: binary\n  - name: X116\n    type: binary\n  - name: X117\n    type: binary\n  - name: X118\n    type: binary\n  - name: X119\n    type: binary\n  - name: X120\n    type: binary\n  - name: X122\n    type: binary\n  - name: X123\n    type: binary\n  - name: X124\n    type: binary\n  - name: X125\n    type: binary\n  - name: X126\n    type: binary\n  - name: X127\n    type: binary\n  - name: X128\n    type: binary\n  - name: X129\n    type: binary\n  - name: X130\n    type: binary\n  - name: X131\n    type: binary\n  - name: X132\n    type: binary\n  - name: X133\n    type: binary\n  - name: X134\n    type: binary\n  - name: X135\n    type: binary\n  - name: X136\n    type: binary\n  - name: X137\n    type: binary\n  - name: X138\n    type: binary\n  - name: X139\n    type: binary\n  - name: X140\n    type: binary\n  - name: X141\n    type: binary\n  - name: X142\n    type: binary\n  - name: X143\n    type: binary\n  - name: X144\n    type: binary\n  - name: X145\n    type: binary\n  - name: X146\n    type: binary\n  - name: X147\n    type: binary\n  - name: X148\n    type: binary\n  - name: X150\n    type: binary\n  - name: X151\n    type: binary\n  - name: X152\n    type: binary\n  - name: X153\n    type: binary\n  - name: X154\n    type: binary\n  - name: X155\n    type: binary\n  - name: X156\n    type: binary\n  - name: X157\n    type: binary\n  - name: X158\n    type: binary\n  - name: X159\n    type: binary\n  - name: X160\n    type: binary\n  - name: X161\n    type: binary\n  - name: X162\n    type: binary\n  - name: X163\n    type: binary\n  - name: X164\n    type: binary\n  - name: X165\n    type: binary\n  - name: X166\n    type: binary\n  - name: X167\n    type: binary\n  - name: X168\n    type: binary\n  - name: X169\n    type: binary\n  - name: X170\n    type: binary\n  - name: X171\n    type: binary\n  - name: X172\n    type: binary\n  - name: X173\n    type: binary\n  - name: X174\n    type: binary\n  - name: X175\n    type: binary\n  - name: X176\n    type: binary\n  - name: X177\n    type: binary\n  - name: X178\n    type: binary\n  - name: X179\n    type: binary\n  - name: X180\n    type: binary\n  - name: X181\n    type: binary\n  - name: X182\n    type: binary\n  - name: X183\n    type: binary\n  - name: X184\n    type: binary\n  - name: X185\n    type: binary\n  - name: X186\n    type: binary\n  - name: X187\n    type: binary\n  - name: X189\n    type: binary\n  - name: X190\n    type: binary\n  - name: X191\n    type: binary\n  - name: X192\n    type: binary\n  - name: X194\n    type: binary\n  - name: X195\n    type: binary\n  - name: X196\n    type: binary\n  - name: X197\n    type: binary\n  - name: X198\n    type: binary\n  - name: X199\n    type: binary\n  - name: X200\n    type: binary\n  - name: X201\n    type: binary\n  - name: X202\n    type: binary\n  - name: X203\n    type: binary\n  - name: X204\n    type: binary\n  - name: X205\n    type: binary\n  - name: X206\n    type: binary\n  - name: X207\n    type: binary\n  - name: X208\n    type: binary\n  - name: X209\n    type: binary\n  - name: X210\n    type: binary\n  - name: X211\n    type: binary\n  - name: X212\n    type: binary\n  - name: X213\n    type: binary\n  - name: X214\n    type: binary\n  - name: X215\n    type: binary\n  - name: X216\n    type: binary\n  - name: X217\n    type: binary\n  - name: X218\n    type: binary\n  - name: X219\n    type: binary\n  - name: X220\n    type: binary\n  - name: X221\n    type: binary\n  - name: X222\n    type: binary\n  - name: X223\n    type: binary\n  - name: X224\n    type: binary\n  - name: X225\n    type: binary\n  - name: X226\n    type: binary\n  - name: X227\n    type: binary\n  - name: X228\n    type: binary\n  - name: X229\n    type: binary\n  - name: X230\n    type: binary\n  - name: X231\n    type: binary\n  - name: X232\n    type: binary\n  - name: X233\n    type: binary\n  - name: X234\n    type: binary\n  - name: X235\n    type: binary\n  - name: X236\n    type: binary\n  - name: X237\n    type: binary\n  - name: X238\n    type: binary\n  - name: X239\n    type: binary\n  - name: X240\n    type: binary\n  - name: X241\n    type: binary\n  - name: X242\n    type: binary\n  - name: X243\n    type: binary\n  - name: X244\n    type: binary\n  - name: X245\n    type: binary\n  - name: X246\n    type: binary\n  - name: X247\n    type: binary\n  - name: X248\n    type: binary\n  - name: X249\n    type: binary\n  - name: X250\n    type: binary\n  - name: X251\n    type: binary\n  - name: X252\n    type: binary\n  - name: X253\n    type: binary\n  - name: X254\n    type: binary\n  - name: X255\n    type: binary\n  - name: X256\n    type: binary\n  - name: X257\n    type: binary\n  - name: X258\n    type: binary\n  - name: X259\n    type: binary\n  - name: X260\n    type: binary\n  - name: X261\n    type: binary\n  - name: X262\n    type: binary\n  - name: X263\n    type: binary\n  - name: X264\n    type: binary\n  - name: X265\n    type: binary\n  - name: X266\n    type: binary\n  - name: X267\n    type: binary\n  - name: X268\n    type: binary\n  - name: X269\n    type: binary\n  - name: X270\n    type: binary\n  - name: X271\n    type: binary\n  - name: X272\n    type: binary\n  - name: X273\n    type: binary\n  - name: X274\n    type: binary\n  - name: X275\n    type: binary\n  - name: X276\n    type: binary\n  - name: X277\n    type: binary\n  - name: X278\n    type: binary\n  - name: X279\n    type: binary\n  - name: X280\n    type: binary\n  - name: X281\n    type: binary\n  - name: X282\n    type: binary\n  - name: X283\n    type: binary\n  - name: X284\n    type: binary\n  - name: X285\n    type: binary\n  - name: X286\n    type: binary\n  - name: X287\n    type: binary\n  - name: X288\n    type: binary\n  - name: X289\n    type: binary\n  - name: X290\n    type: binary\n  - name: X291\n    type: binary\n  - name: X292\n    type: binary\n  - name: X293\n    type: binary\n  - name: X294\n    type: binary\n  - name: X295\n    type: binary\n  - name: X296\n    type: binary\n  - name: X297\n    type: binary\n  - name: X298\n    type: binary\n  - name: X299\n    type: binary\n  - name: X300\n    type: binary\n  - name: X301\n    type: binary\n  - name: X302\n    type: binary\n  - name: X304\n    type: binary\n  - name: X305\n    type: binary\n  - name: X306\n    type: binary\n  - name: X307\n    type: binary\n  - name: X308\n    type: binary\n  - name: X309\n    type: binary\n  - name: X310\n    type: binary\n  - name: X311\n    type: binary\n  - name: X312\n    type: binary\n  - name: X313\n    type: binary\n  - name: X314\n    type: binary\n  - name: X315\n    type: binary\n  - name: X316\n    type: binary\n  - name: X317\n    type: binary\n  - name: X318\n    type: binary\n  - name: X319\n    type: binary\n  - name: X320\n    type: binary\n  - name: X321\n    type: binary\n  - name: X322\n    type: binary\n  - name: X323\n    type: binary\n  - name: X324\n    type: binary\n  - name: X325\n    type: binary\n  - name: X326\n    type: binary\n  - name: X327\n    type: binary\n  - name: X328\n    type: binary\n  - name: X329\n    type: binary\n  - name: X330\n    type: binary\n  - name: X331\n    type: binary\n  - name: X332\n    type: binary\n  - name: X333\n    type: binary\n  - name: X334\n    type: binary\n  - name: X335\n    type: binary\n  - name: X336\n    type: binary\n  - name: X337\n    type: binary\n  - name: X338\n    type: binary\n  - name: X339\n    type: binary\n  - name: X340\n    type: binary\n  - name: X341\n    type: binary\n  - name: X342\n    type: binary\n  - name: X343\n    type: binary\n  - name: X344\n    type: binary\n  - name: X345\n    type: binary\n  - name: X346\n    type: binary\n  - name: X347\n    type: binary\n  - name: X348\n    type: binary\n  - name: X349\n    type: binary\n  - name: X350\n    type: binary\n  - name: X351\n    type: binary\n  - name: X352\n    type: binary\n  - name: X353\n    type: binary\n  - name: X354\n    type: binary\n  - name: X355\n    type: binary\n  - name: X356\n    type: binary\n  - name: X357\n    type: binary\n  - name: X358\n    type: binary\n  - name: X359\n    type: binary\n  - name: X360\n    type: binary\n  - name: X361\n    type: binary\n  - name: X362\n    type: binary\n  - name: X363\n    type: binary\n  - name: X364\n    type: binary\n  - name: X365\n    type: binary\n  - name: X366\n    type: binary\n  - name: X367\n    type: binary\n  - name: X368\n    type: binary\n  - name: X369\n    type: binary\n  - name: X370\n    type: binary\n  - name: X371\n    type: binary\n  - name: X372\n    type: binary\n  - name: X373\n    type: binary\n  - name: X374\n    type: binary\n  - name: X375\n    type: binary\n  - name: X376\n    type: binary\n  - name: X377\n    type: binary\n  - name: X378\n    type: binary\n  - name: X379\n    type: binary\n  - name: X380\n    type: binary\n  - name: X382\n    type: binary\n  - name: X383\n    type: binary\n  - name: X384\n    type: binary\n  - name: X385\n    type: binary\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/mnist_default.yaml",
    "content": "output_features:\n  - name: label\n    type: category\ninput_features:\n  - name: image_path\n    type: image\n    preprocessing:\n      num_processes: 4\n    encoder: stacked_cnn\n    conv_layers:\n      - num_filters: 32\n        filter_size: 3\n        pool_size: 2\n        pool_stride: 2\n      - num_filters: 64\n        filter_size: 3\n        pool_size: 2\n        pool_stride: 2\n        dropout: 0.4\n    fc_layers:\n      - output_size: 128\n        dropout: 0.4\ntrainer:\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/mushroom_edibility_default.yaml",
    "content": "output_features:\n  - name: class\n    type: category\ninput_features:\n  - name: cap-shape\n    type: category\n  - name: cap-surface\n    type: category\n  - name: cap-color\n    type: category\n  - name: bruises?\n    type: category\n  - name: odor\n    type: category\n  - name: gill-attachment\n    type: category\n  - name: gill-spacing\n    type: category\n  - name: gill-size\n    type: category\n  - name: gill-color\n    type: category\n  - name: stalk-shape\n    type: category\n  - name: stalk-root\n    type: category\n  - name: stalk-surface-above-ring\n    type: category\n  - name: stalk-surface-below-ring\n    type: category\n  - name: stalk-color-above-ring\n    type: category\n  - name: stalk-color-below-ring\n    type: category\n  - name: veil-type\n    type: category\n  - name: veil-color\n    type: category\n  - name: ring-number\n    type: category\n  - name: ring-type\n    type: category\n  - name: spore-print-color\n    type: category\n  - name: population\n    type: category\n  - name: habitat\n    type: category\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/otto_group_product_default.yaml",
    "content": "output_features:\n  - name: target\n    type: category\ninput_features:\n  - name: feat_1\n    type: number\n  - name: feat_2\n    type: number\n  - name: feat_3\n    type: number\n  - name: feat_4\n    type: number\n  - name: feat_5\n    type: number\n  - name: feat_6\n    type: number\n  - name: feat_7\n    type: number\n  - name: feat_8\n    type: number\n  - name: feat_9\n    type: number\n  - name: feat_10\n    type: number\n  - name: feat_11\n    type: number\n  - name: feat_12\n    type: number\n  - name: feat_13\n    type: number\n  - name: feat_14\n    type: number\n  - name: feat_15\n    type: number\n  - name: feat_16\n    type: number\n  - name: feat_17\n    type: number\n  - name: feat_18\n    type: number\n  - name: feat_19\n    type: number\n  - name: feat_20\n    type: number\n  - name: feat_21\n    type: number\n  - name: feat_22\n    type: number\n  - name: feat_23\n    type: number\n  - name: feat_24\n    type: number\n  - name: feat_25\n    type: number\n  - name: feat_26\n    type: number\n  - name: feat_27\n    type: number\n  - name: feat_28\n    type: number\n  - name: feat_29\n    type: number\n  - name: feat_30\n    type: number\n  - name: feat_31\n    type: number\n  - name: feat_32\n    type: number\n  - name: feat_33\n    type: number\n  - name: feat_34\n    type: number\n  - name: feat_35\n    type: number\n  - name: feat_36\n    type: number\n  - name: feat_37\n    type: number\n  - name: feat_38\n    type: number\n  - name: feat_39\n    type: number\n  - name: feat_40\n    type: number\n  - name: feat_41\n    type: number\n  - name: feat_42\n    type: number\n  - name: feat_43\n    type: number\n  - name: feat_44\n    type: number\n  - name: feat_45\n    type: number\n  - name: feat_46\n    type: number\n  - name: feat_47\n    type: number\n  - name: feat_48\n    type: number\n  - name: feat_49\n    type: number\n  - name: feat_50\n    type: number\n  - name: feat_51\n    type: number\n  - name: feat_52\n    type: number\n  - name: feat_53\n    type: number\n  - name: feat_54\n    type: number\n  - name: feat_55\n    type: number\n  - name: feat_56\n    type: number\n  - name: feat_57\n    type: number\n  - name: feat_58\n    type: number\n  - name: feat_59\n    type: number\n  - name: feat_60\n    type: number\n  - name: feat_61\n    type: number\n  - name: feat_62\n    type: number\n  - name: feat_63\n    type: number\n  - name: feat_64\n    type: number\n  - name: feat_65\n    type: number\n  - name: feat_66\n    type: number\n  - name: feat_67\n    type: number\n  - name: feat_68\n    type: number\n  - name: feat_69\n    type: number\n  - name: feat_70\n    type: number\n  - name: feat_71\n    type: number\n  - name: feat_72\n    type: number\n  - name: feat_73\n    type: number\n  - name: feat_74\n    type: number\n  - name: feat_75\n    type: number\n  - name: feat_76\n    type: number\n  - name: feat_77\n    type: number\n  - name: feat_78\n    type: number\n  - name: feat_79\n    type: number\n  - name: feat_80\n    type: number\n  - name: feat_81\n    type: number\n  - name: feat_82\n    type: number\n  - name: feat_83\n    type: number\n  - name: feat_84\n    type: number\n  - name: feat_85\n    type: number\n  - name: feat_86\n    type: number\n  - name: feat_87\n    type: number\n  - name: feat_88\n    type: number\n  - name: feat_89\n    type: number\n  - name: feat_90\n    type: number\n  - name: feat_91\n    type: number\n  - name: feat_92\n    type: number\n  - name: feat_93\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/poker_hand_default.yaml",
    "content": "output_features:\n  - name: hand\n    type: category\ninput_features:\n  - name: S1\n    type: category\n  - name: C1\n    type: category\n  - name: S2\n    type: category\n  - name: C2\n    type: category\n  - name: S3\n    type: category\n  - name: C3\n    type: category\n  - name: S4\n    type: category\n  - name: C4\n    type: category\n  - name: S5\n    type: category\n  - name: C5\n    type: category\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/porto_seguro_safe_driver_default.yaml",
    "content": "output_features:\n  - name: target\n    type: binary\ninput_features:\n  - name: ps_ind_01\n    type: number\n  - name: ps_ind_02_cat\n    type: category\n  - name: ps_ind_03\n    type: number\n  - name: ps_ind_04_cat\n    type: category\n  - name: ps_ind_05_cat\n    type: category\n  - name: ps_ind_06_bin\n    type: binary\n  - name: ps_ind_07_bin\n    type: binary\n  - name: ps_ind_08_bin\n    type: binary\n  - name: ps_ind_09_bin\n    type: binary\n  - name: ps_ind_10_bin\n    type: binary\n  - name: ps_ind_11_bin\n    type: binary\n  - name: ps_ind_12_bin\n    type: binary\n  - name: ps_ind_13_bin\n    type: binary\n  - name: ps_ind_14\n    type: number\n  - name: ps_ind_15\n    type: number\n  - name: ps_ind_16_bin\n    type: binary\n  - name: ps_ind_17_bin\n    type: binary\n  - name: ps_ind_18_bin\n    type: binary\n  - name: ps_reg_01\n    type: number\n  - name: ps_reg_02\n    type: number\n  - name: ps_reg_03\n    type: number\n  - name: ps_car_01_cat\n    type: category\n  - name: ps_car_02_cat\n    type: category\n  - name: ps_car_03_cat\n    type: category\n  - name: ps_car_04_cat\n    type: category\n  - name: ps_car_05_cat\n    type: category\n  - name: ps_car_06_cat\n    type: category\n  - name: ps_car_07_cat\n    type: category\n  - name: ps_car_08_cat\n    type: category\n  - name: ps_car_09_cat\n    type: category\n  - name: ps_car_10_cat\n    type: category\n  - name: ps_car_11_cat\n    type: category\n  - name: ps_car_11\n    type: number\n  - name: ps_car_12\n    type: number\n  - name: ps_car_13\n    type: number\n  - name: ps_car_14\n    type: number\n  - name: ps_car_15\n    type: number\n  - name: ps_calc_01\n    type: number\n  - name: ps_calc_02\n    type: number\n  - name: ps_calc_03\n    type: number\n  - name: ps_calc_04\n    type: number\n  - name: ps_calc_05\n    type: number\n  - name: ps_calc_06\n    type: number\n  - name: ps_calc_07\n    type: number\n  - name: ps_calc_08\n    type: number\n  - name: ps_calc_09\n    type: number\n  - name: ps_calc_10\n    type: number\n  - name: ps_calc_11\n    type: number\n  - name: ps_calc_12\n    type: number\n  - name: ps_calc_13\n    type: number\n  - name: ps_calc_14\n    type: number\n  - name: ps_calc_15_bin\n    type: binary\n  - name: ps_calc_16_bin\n    type: binary\n  - name: ps_calc_17_bin\n    type: binary\n  - name: ps_calc_18_bin\n    type: binary\n  - name: ps_calc_19_bin\n    type: binary\n  - name: ps_calc_20_bin\n    type: binary\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/synthetic_fraud_default.yaml",
    "content": "output_features:\n  - name: isFraud\n    type: binary\ninput_features:\n  - name: step\n    type: number\n  - name: type\n    type: category\n  - name: amount\n    type: number\n  - name: oldbalanceOrg\n    type: number\n  - name: newbalanceOrig\n    type: number\n  - name: oldbalanceDest\n    type: number\n  - name: newbalanceDest\n    type: number\ncombiner:\n  type: concat\n  num_fc_layers: 3\n  fc_size: 128\n  dropout: 0.1\ntraining:\n  batch_size: 256\n  learning_rate: .001\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/model_configs/titanic_default.yaml",
    "content": "output_features:\n  - name: Survived\n    type: binary\ninput_features:\n  - name: Pclass\n    type: category\n  - name: Sex\n    type: category\n  - name: Age\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n  - name: SibSp\n    type: number\n  - name: Parch\n    type: number\n  - name: Fare\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n  - name: Embarked\n    type: category\ntraining:\n  batch_size: 256\n  epochs: 1\n"
  },
  {
    "path": "ludwig/datasets/utils.py",
    "content": "import os\nfrom functools import lru_cache\n\nimport yaml\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.datasets import model_configs\n\n\n@PublicAPI\ndef model_configs_for_dataset(dataset_name: str) -> dict[str, dict]:\n    \"\"\"Returns a dictionary of built-in model configs for the specified dataset.\n\n    Maps config name to ludwig config dict.\n    \"\"\"\n    return _get_model_configs(dataset_name)\n\n\n@lru_cache(maxsize=3)\ndef _get_model_configs(dataset_name: str) -> dict[str, dict]:\n    \"\"\"Returns all model configs for the specified dataset.\n\n    Model configs are named <dataset_name>_<config_name>.yaml\n    \"\"\"\n    import importlib.resources\n\n    config_filenames = [\n        f.name\n        for f in importlib.resources.files(model_configs).iterdir()\n        if f.name.endswith(\".yaml\") and f.name.startswith(dataset_name)\n    ]\n    configs = {}\n    for config_filename in config_filenames:\n        basename = os.path.splitext(config_filename)[0]\n        config_name = basename[len(dataset_name) + 1 :]\n        configs[config_name] = _load_model_config(config_filename)\n    return configs\n\n\ndef _load_model_config(model_config_filename: str):\n    \"\"\"Loads a model config.\"\"\"\n    model_config_path = os.path.join(os.path.dirname(model_configs.__file__), model_config_filename)\n    with open(model_config_path) as f:\n        return yaml.safe_load(f)\n"
  },
  {
    "path": "ludwig/decoders/__init__.py",
    "content": "# register all decoders\nimport ludwig.decoders.generic_decoders  # noqa\nimport ludwig.decoders.image_decoders  # noqa\nimport ludwig.decoders.llm_decoders  # noqa\nimport ludwig.decoders.sequence_decoders  # noqa\nimport ludwig.decoders.sequence_tagger  # noqa\n"
  },
  {
    "path": "ludwig/decoders/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom abc import ABC, abstractmethod\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.torch_utils import LudwigModule\n\n\n@DeveloperAPI\nclass Decoder(LudwigModule, ABC):\n    @abstractmethod\n    def forward(self, inputs, mask=None):\n        raise NotImplementedError\n\n    @property\n    def name(self):\n        return self.__class__.__name__\n"
  },
  {
    "path": "ludwig/decoders/generic_decoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom functools import partial\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, CATEGORY, CATEGORY_DISTRIBUTION, LOSS, NUMBER, SET, TIMESERIES, TYPE, VECTOR\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.schema.decoders.base import ClassifierConfig, PassthroughDecoderConfig, ProjectorConfig, RegressorConfig\nfrom ludwig.utils.torch_utils import Dense, get_activation\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n# TODO(Arnav): Re-enable once we add DotProduct Combiner: https://github.com/ludwig-ai/ludwig/issues/3150\n# @register_decoder(\"passthrough\", [BINARY, CATEGORY, NUMBER, SET, VECTOR, SEQUENCE, TEXT])\nclass PassthroughDecoder(Decoder):\n    def __init__(self, input_size: int = 1, num_classes: int = None, decoder_config=None, **kwargs):\n        super().__init__()\n        self.config = decoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.input_size = input_size\n        self.num_classes = num_classes\n\n    def forward(self, inputs, **kwargs):\n        return inputs\n\n    @staticmethod\n    def get_schema_cls():\n        return PassthroughDecoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.input_shape\n\n\n@DeveloperAPI\n@register_decoder(\"regressor\", [BINARY, NUMBER])\nclass Regressor(Decoder):\n    def __init__(\n        self,\n        input_size,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Dense\")\n\n        self.dense = Dense(\n            input_size=input_size,\n            output_size=1,\n            use_bias=use_bias,\n            weights_initializer=weights_initializer,\n            bias_initializer=bias_initializer,\n        )\n\n    @staticmethod\n    def get_schema_cls():\n        return RegressorConfig\n\n    @property\n    def input_shape(self):\n        return self.dense.input_shape\n\n    def forward(self, inputs, **kwargs):\n        return self.dense(inputs)\n\n\n@DeveloperAPI\n@register_decoder(\"projector\", [VECTOR, TIMESERIES])\nclass Projector(Decoder):\n    def __init__(\n        self,\n        input_size,\n        output_size,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        activation=None,\n        multiplier=1.0,\n        clip=None,\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Dense\")\n        self.dense = Dense(\n            input_size=input_size,\n            output_size=output_size,\n            use_bias=use_bias,\n            weights_initializer=weights_initializer,\n            bias_initializer=bias_initializer,\n        )\n\n        self.activation = get_activation(activation)\n        self.multiplier = multiplier\n\n        if clip is not None:\n            if isinstance(clip, (list, tuple)) and len(clip) == 2:\n                self.clip = partial(torch.clip, min=clip[0], max=clip[1])\n            else:\n                raise ValueError(\n                    \"The clip parameter of {} is {}. \"\n                    \"It must be a list or a tuple of length 2.\".format(self.feature_name, self.clip)\n                )\n        else:\n            self.clip = None\n\n    @staticmethod\n    def get_schema_cls():\n        return ProjectorConfig\n\n    @property\n    def input_shape(self):\n        return self.dense.input_shape\n\n    def forward(self, inputs, **kwargs):\n        values = self.activation(self.dense(inputs)) * self.multiplier\n        if self.clip:\n            values = self.clip(values)\n        return values\n\n\n@DeveloperAPI\n@register_decoder(\"classifier\", [CATEGORY, CATEGORY_DISTRIBUTION, SET])\nclass Classifier(Decoder):\n    def __init__(\n        self,\n        input_size,\n        num_classes,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Dense\")\n        self.num_classes = num_classes\n        self.dense = Dense(\n            input_size=input_size,\n            output_size=num_classes,\n            use_bias=use_bias,\n            weights_initializer=weights_initializer,\n            bias_initializer=bias_initializer,\n        )\n\n        self.sampled_loss = False\n        if LOSS in kwargs and TYPE in kwargs[LOSS] and kwargs[LOSS][TYPE] is not None:\n            self.sampled_loss = kwargs[LOSS][TYPE].startswith(\"sampled\")\n\n    @staticmethod\n    def get_schema_cls():\n        return ClassifierConfig\n\n    @property\n    def input_shape(self):\n        return self.dense.input_shape\n\n    def forward(self, inputs, **kwargs):\n        return self.dense(inputs)\n"
  },
  {
    "path": "ludwig/decoders/image_decoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Aizen Corp.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN, IMAGE, LOGITS, PREDICTIONS\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.modules.convolutional_modules import UNetUpStack\nfrom ludwig.schema.decoders.image_decoders import ImageDecoderConfig, UNetDecoderConfig\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_decoder(\"unet\", IMAGE)\nclass UNetDecoder(Decoder):\n    def __init__(\n        self,\n        input_size: int,\n        height: int,\n        width: int,\n        num_channels: int = 1,\n        num_classes: int = 2,\n        conv_norm: str | None = None,\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n        self.num_classes = num_classes\n\n        logger.debug(f\" {self.name}\")\n        if num_classes < 2:\n            raise ValueError(f\"Invalid `num_classes` {num_classes} for unet decoder\")\n        if height % 16 or width % 16:\n            raise ValueError(f\"Invalid `height` {height} or `width` {width} for unet decoder\")\n\n        self.unet = UNetUpStack(\n            img_height=height,\n            img_width=width,\n            out_channels=num_classes,\n            norm=conv_norm,\n        )\n\n        self.input_reshape = list(self.unet.input_shape)\n        self.input_reshape.insert(0, -1)\n        self._output_shape = (height, width)\n\n    def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor):\n        hidden = combiner_outputs[HIDDEN]\n        skips = combiner_outputs[ENCODER_OUTPUT_STATE]\n\n        # unflatten combiner outputs\n        hidden = hidden.reshape(self.input_reshape)\n\n        logits = self.unet(hidden, skips)\n        predictions = logits.argmax(dim=1).squeeze(1).byte()\n\n        return {LOGITS: logits, PREDICTIONS: predictions}\n\n    def get_prediction_set(self):\n        return {LOGITS, PREDICTIONS}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageDecoderConfig]:\n        return UNetDecoderConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.unet.input_shape\n"
  },
  {
    "path": "ludwig/decoders/llm_decoders.py",
    "content": "import logging\nimport re\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CATEGORY, LOGITS, PREDICTIONS, PROBABILITIES, TEXT\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.decoders.utils import extract_generated_tokens\nfrom ludwig.schema.decoders.llm_decoders import CategoryExtractorDecoderConfig, TextExtractorDecoderConfig\nfrom ludwig.utils.strings_utils import get_tokenizer\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO(Arnav): Refactor to split into strategies like splitters\nclass Matcher:\n    def __init__(self, match: dict[str, dict[str, Any]]):\n        self.match = match\n\n    def contains(self, decoded_input: str, value: str) -> bool:\n        return value in decoded_input\n\n    def regex(self, decoded_input: str, regex_pattern: str) -> bool:\n        \"\"\"Perform a regex match on a given text using a specified regex pattern.\n\n        Parameters:\n        text (str): The text to perform the match on.\n        regex_pattern (str): The regex pattern to use for the match.\n\n        Returns:\n        A list of match objects.\n        \"\"\"\n        # Compile the regex pattern\n        matches = []\n        try:\n            regex = re.compile(regex_pattern)\n            # Perform the match\n            matches = regex.findall(decoded_input)\n        except Exception:\n            logger.warning(f\"Regex pattern {regex_pattern} could not be compiled.\")\n        # If there is a match, matches is a non-empty list, so we can use this\n        # to infer if there was a match or not and return a bool\n        return len(matches) > 0\n\n    def __call__(self, decoded_input: str) -> str | None:\n        # Greedy match on first label that matches the input\n        for label, label_def in self.match.items():\n            label_def_type = label_def[\"type\"]\n            label_def_value = label_def[\"value\"]\n\n            if label_def_type == \"contains\":\n                is_match = self.contains(decoded_input, label_def_value)\n            elif label_def_type == \"regex\":\n                is_match = self.regex(decoded_input, label_def_value)\n            else:\n                raise ValueError(\n                    f\"{label_def_type} is not a valid match `type`. Ludwig \"\n                    \"currently supports `contains` and `regex` match types.\"\n                )\n\n            if is_match:\n                return label\n        return None\n\n\n@DeveloperAPI\n@register_decoder(\"text_extractor\", [TEXT])\nclass TextExtractorDecoder(Decoder):\n    def __init__(\n        self,\n        input_size: int,\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n        self.input_size = input_size\n\n        # Tokenizer\n        self.tokenizer_type = self.config.tokenizer\n        self.pretrained_model_name_or_path = self.config.pretrained_model_name_or_path\n        self.vocab_file = self.config.vocab_file\n\n        # Load tokenizer required for decoding the output from the generate\n        # function of the text input feature for LLMs.\n        self.tokenizer = get_tokenizer(self.tokenizer_type, self.vocab_file, self.pretrained_model_name_or_path)\n        if hasattr(self.tokenizer, \"tokenizer\"):\n            # Transformer Tokenizers\n            self.tokenizer_vocab_size = self.tokenizer.tokenizer.vocab_size\n        else:\n            # TorchText Tokenizers\n            self.tokenizer_vocab_size = len(self.tokenizer.vocab)\n\n        # Maximum number of new tokens that will be generated\n        # TODO(geoffrey): figure out where self.max_sequence_length is used– if not used, we might consider removing it.\n        # It's confusing to have both this and `max_new_tokens` as a mandatory param in the `forward` function.\n        self.max_sequence_length = self.config.max_new_tokens\n\n    @staticmethod\n    def get_schema_cls():\n        return TextExtractorDecoderConfig\n\n    @property\n    def input_shape(self):\n        return self.input_size\n\n    def get_prediction_set(self):\n        return {LOGITS, PREDICTIONS, PROBABILITIES}\n\n    def forward(self, inputs: list[torch.Tensor], input_lengths: list[int], max_new_tokens: int):\n        # Extract the sequences tensor from the LLMs forward pass\n        generated_outputs = extract_generated_tokens(\n            raw_generated_output_sequences=inputs,\n            input_lengths=input_lengths,\n            max_new_tokens=max_new_tokens,\n            pad_sequence=True,\n        )\n        # Stack the predictions for each example in the batch. The padding should ensure they are all the same shape.\n        for output in generated_outputs:\n            if output.shape[0] > max_new_tokens:\n                raise ValueError(\n                    f\"Output {output} is longer than the max_new_tokens {max_new_tokens} during decoding. \"\n                    f\"This should never happen– please file an issue on GitHub.\"\n                )\n\n        generated_outputs = torch.stack(generated_outputs, dim=0)\n        outputs_device = generated_outputs.device\n\n        return {\n            PREDICTIONS: generated_outputs,\n            # TODO(Arnav): Add support for probabilities and logits\n            PROBABILITIES: torch.zeros((len(generated_outputs), max_new_tokens, self.tokenizer_vocab_size)).to(\n                outputs_device\n            ),\n            LOGITS: torch.zeros((len(generated_outputs), max_new_tokens, self.tokenizer_vocab_size)).to(outputs_device),\n        }\n\n\n@DeveloperAPI\n@register_decoder(\"category_extractor\", [CATEGORY])\nclass CategoryExtractorDecoder(Decoder):\n    def __init__(\n        self,\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n\n        self.input_size = self.config.input_size\n        self.fallback_label = self.config.fallback_label\n        self.str2idx = self.config.str2idx\n        self.vocab_size = len(self.config.str2idx)\n\n        # Create Matcher object to perform matching on the decoded output\n        self.matcher = Matcher(self.config.match)\n\n        # Tokenizer\n        self.tokenizer_type = self.config.tokenizer\n        self.pretrained_model_name_or_path = self.config.pretrained_model_name_or_path\n        self.vocab_file = self.config.vocab_file\n\n        # Load tokenizer required for decoding the output from the generate\n        # function of the text input feature for LLMs.\n        self.tokenizer = get_tokenizer(self.tokenizer_type, self.vocab_file, self.pretrained_model_name_or_path)\n\n    @staticmethod\n    def get_schema_cls():\n        return CategoryExtractorDecoderConfig\n\n    @property\n    def input_shape(self):\n        return self.input_size\n\n    def get_prediction_set(self):\n        return {LOGITS, PREDICTIONS, PROBABILITIES}\n\n    def forward(self, inputs: list[torch.Tensor], input_lengths: list[int], max_new_tokens: int):\n        # Extract the sequences tensor from the LLMs forward pass\n        generated_outputs = extract_generated_tokens(\n            raw_generated_output_sequences=inputs,\n            input_lengths=input_lengths,\n            max_new_tokens=max_new_tokens,\n            pad_sequence=False,\n        )\n        outputs_device = generated_outputs[0].device\n\n        # Decode generated outputs from the LLM's generate function.\n        decoded_outputs = self.tokenizer.tokenizer.batch_decode(generated_outputs, skip_special_tokens=True)\n\n        # Parse labels based on matching criteria and return probability vectors\n        matched_labels = []\n        probabilities = []\n        logits = []\n        for output in decoded_outputs:\n            output = output.lower()  # Convert to lowercase for matching\n\n            matched_label = self.matcher(output)\n            idx = self.str2idx[matched_label] if matched_label in self.str2idx else self.str2idx[self.fallback_label]\n\n            # Append the index of the matched label\n            matched_labels.append(idx)\n\n            # Append the probability vector for the matched label\n            probability_vec = [0] * self.vocab_size\n            probability_vec[idx] = 1\n            probabilities.append(probability_vec)\n\n            # TODO(Arnav): Figure out how to compute logits. For now, we return\n            # a tensor of zeros.\n            logits.append([0] * self.vocab_size)\n\n        return {\n            PREDICTIONS: torch.tensor(matched_labels, device=outputs_device),\n            PROBABILITIES: torch.tensor(probabilities, dtype=torch.float32, device=outputs_device),\n            LOGITS: torch.tensor(logits, dtype=torch.float32, device=outputs_device),\n        }\n"
  },
  {
    "path": "ludwig/decoders/registry.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.utils.registry import Registry\n\n_decoder_registry = Registry()\n\n\n@DeveloperAPI\ndef get_decoder_registry() -> Registry:\n    return _decoder_registry\n\n\n@DeveloperAPI\ndef register_decoder(name: str, features: str | list[str]):\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for feature in features:\n            feature_registry = get_decoder_registry().get(feature, {})\n            feature_registry[name] = cls\n            get_decoder_registry()[feature] = feature_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_decoder_cls(feature: str, name: str) -> type[Decoder]:\n    return get_decoder_registry()[feature][name]\n\n\n@DeveloperAPI\ndef get_decoder_classes(feature: str) -> dict[str, type[Decoder]]:\n    return get_decoder_registry()[feature]\n"
  },
  {
    "path": "ludwig/decoders/sequence_decoder_utils.py",
    "content": "\"\"\"Utility functions related to sequence decoders.\"\"\"\n\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN\nfrom ludwig.modules.reduction_modules import SequenceReducer\n\n\ndef repeat_2D_tensor(tensor, k):\n    \"\"\"Repeats a 2D-tensor k times over the first dimension.\n\n    For example:\n    Input: Tensor of [batch_size, state_size], k=2\n    Output: Tensor of [k, batch_size, state_size]\n    \"\"\"\n    if len(tensor.size()) > 2:\n        raise ValueError(\"Cannot repeat a non-2D tensor with this method.\")\n    return tensor.repeat(k, 1, 1)\n\n\ndef get_rnn_init_state(\n    combiner_outputs: dict[str, torch.Tensor], sequence_reducer: SequenceReducer, num_layers: int\n) -> torch.Tensor:\n    \"\"\"Computes the hidden state that the RNN decoder should start with.\n\n    Args:\n        combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features.\n        sequence_reducer: SequenceReducer to reduce rank-3 to rank-2.\n        num_layers: Number of layers the decoder uses.\n\n    Returns:\n        Tensor of [num_layers, batch_size, hidden_size].\n    \"\"\"\n    if ENCODER_OUTPUT_STATE not in combiner_outputs:\n        # Use the combiner's hidden state.\n        encoder_output_state = combiner_outputs[HIDDEN]\n    else:\n        # Use the encoder's output state.\n        encoder_output_state = combiner_outputs[ENCODER_OUTPUT_STATE]\n        if isinstance(encoder_output_state, tuple):\n            if len(encoder_output_state) == 2:\n                # LSTM encoder. Use the hidden state and ignore the cell state.\n                encoder_output_state = encoder_output_state[0]\n            elif len(encoder_output_state) == 4:\n                # Bi-directional LSTM encoder. Use the average of hidden states and ignore cell state.\n                encoder_output_state = torch.mean([encoder_output_state[0], encoder_output_state[2]])\n            else:\n                raise ValueError(\n                    f\"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder \"\n                    + f\"state: {encoder_output_state.size()} that was invalid. Please double check the compatibility \"\n                    + \"of your encoder and decoder.\"\n                )\n\n    if len(encoder_output_state.size()) > 3:\n        raise ValueError(\"Init state for RNN decoders only works for 1d or 2d tensors (encoder_output).\")\n\n    if len(encoder_output_state.size()) == 3:\n        # Reduce to [batch_size, hidden_size].\n        encoder_output_state = sequence_reducer(encoder_output_state)\n\n    return repeat_2D_tensor(encoder_output_state, num_layers)\n\n\ndef get_lstm_init_state(\n    combiner_outputs: dict[str, torch.Tensor], sequence_reducer: SequenceReducer, num_layers: int\n) -> tuple[torch.Tensor, torch.Tensor]:\n    \"\"\"Returns the states that the LSTM decoder should start with.\n\n    Args:\n        combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features.\n        sequence_reducer: SequenceReducer to reduce rank-3 to rank-2.\n        num_layers: Number of layers the decoder uses.\n\n    Returns:\n        Tuple of 2 tensors (decoder hidden state, decoder cell state), each [num_layers, batch_size, hidden_size].\n    \"\"\"\n    if ENCODER_OUTPUT_STATE not in combiner_outputs:\n        # Use the combiner's hidden state.\n        decoder_hidden_state = combiner_outputs[HIDDEN]\n        decoder_cell_state = torch.clone(decoder_hidden_state)\n    else:\n        # Use the encoder's output state.\n        encoder_output_state = combiner_outputs[ENCODER_OUTPUT_STATE]\n        if not isinstance(encoder_output_state, tuple):\n            decoder_hidden_state = encoder_output_state\n            decoder_cell_state = decoder_hidden_state\n        else:\n            if len(encoder_output_state) == 2:\n                # The encoder was probably an LSTM.\n                decoder_hidden_state, decoder_cell_state = encoder_output_state\n            elif len(encoder_output_state) == 4:\n                # The encoder was probably a bi-LSTM.\n                # Use the average of the encoder's hidden states for hidden state.\n                # Use the average of the encoder's cell states for cell state.\n                decoder_hidden_state = torch.mean([encoder_output_state[0], encoder_output_state[2]])\n                decoder_cell_state = torch.mean([encoder_output_state[1], encoder_output_state[3]])\n            else:\n                raise ValueError(\n                    f\"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder \"\n                    + f\"state: {encoder_output_state} that was invalid. Please double check the compatibility of your \"\n                    + \"encoder and decoder.\"\n                )\n\n    # Check rank and reduce if necessary.\n    if len(decoder_hidden_state.size()) > 3 or len(decoder_cell_state.size()) > 3:\n        raise ValueError(\n            f\"Invalid sequence decoder inputs with keys: {combiner_outputs.keys()} with extracted encoder \"\n            + f\"state: {decoder_hidden_state.size()} that was invalid. Please double check the compatibility \"\n            + \"of your encoder and decoder.\"\n        )\n    if len(decoder_hidden_state.size()) == 3:\n        decoder_hidden_state = sequence_reducer(decoder_hidden_state)\n    if len(decoder_cell_state.size()) == 3:\n        decoder_cell_state = sequence_reducer(decoder_cell_state)\n\n    # Repeat over the number of layers.\n    return repeat_2D_tensor(decoder_hidden_state, num_layers), repeat_2D_tensor(decoder_cell_state, num_layers)\n"
  },
  {
    "path": "ludwig/decoders/sequence_decoders.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nimport torch.nn as nn\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import LOGITS, PREDICTIONS, PROBABILITIES, SEQUENCE, TEXT\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.decoders.sequence_decoder_utils import get_lstm_init_state, get_rnn_init_state\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.decoders.sequence_decoders import SequenceGeneratorDecoderConfig\nfrom ludwig.utils import strings_utils\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\nclass RNNDecoder(nn.Module):\n    \"\"\"GRU or RNN-based decoder.\"\"\"\n\n    def __init__(self, hidden_size: int, vocab_size: int, cell_type: str, num_layers: int = 1):\n        super().__init__()\n        self.hidden_size = hidden_size\n        self.vocab_size = vocab_size\n        self.embedding = nn.Embedding(vocab_size, hidden_size)\n        if cell_type == \"gru\":\n            self.rnn = nn.GRU(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)\n        else:\n            self.rnn = nn.RNN(hidden_size, hidden_size, num_layers=num_layers, batch_first=True)\n        self.out = nn.Linear(hidden_size, vocab_size)\n\n        # Have the embedding and projection share weights.\n        # This is a trick used by the Transformer, and seems to attain better loss.\n        # See section 3.4 of https://arxiv.org/pdf/1706.03762.pdf.\n        self.out.weight = self.embedding.weight\n\n    def forward(self, input: torch.Tensor, hidden: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:\n        \"\"\"Runs a single decoding time step.\n\n        Modeled off of https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html.\n\n        Args:\n            input: [batch_size] tensor with the previous step's predicted symbol.\n            hidden: [batch_size, hidden_size] tensor with the previous step's hidden state.\n\n        Returns:\n            Tuple of two tensors:\n            - output: [batch_size, 1, vocab_size] tensor with the logits.\n            - hidden: [num_layers, batch_size, hidden_size] tensor with the hidden state for the next time step.\n        \"\"\"\n        # Unsqueeze predicted tokens.\n        input = input.unsqueeze(1).to(torch.int)\n        output = self.embedding(input)\n        output, hidden = self.rnn(output, hidden)\n        output_logits = self.out(output)\n        return output_logits, hidden\n\n\n@DeveloperAPI\nclass LSTMDecoder(nn.Module):\n    \"\"\"LSTM-based decoder.\"\"\"\n\n    def __init__(self, hidden_size: int, vocab_size: int, num_layers: int = 1):\n        super().__init__()\n        self.hidden_size = hidden_size\n        self.vocab_size = vocab_size\n        self.embedding = nn.Embedding(vocab_size, hidden_size)\n        self.lstm = nn.LSTM(hidden_size, hidden_size, batch_first=True, num_layers=num_layers)\n        self.out = nn.Linear(hidden_size, vocab_size)\n\n        # Have the embedding and projection share weights.\n        # This is a trick used by the Transformer, and seems to attain better loss.\n        # See section 3.4 of https://arxiv.org/pdf/1706.03762.pdf.\n        self.out.weight = self.embedding.weight\n\n    def forward(\n        self, input: torch.Tensor, hidden_state: torch.Tensor, cell_state: torch.Tensor\n    ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:\n        \"\"\"Runs a single decoding time step.\n\n        Modeled off of https://pytorch.org/tutorials/intermediate/seq2seq_translation_tutorial.html.\n\n        Args:\n            input: [batch_size] tensor with the previous step's predicted symbol.\n            hidden_state: [batch_size, hidden_size] tensor with the previous step's hidden state.\n            cell_state: [batch_size, hidden_size] tensor with the previous step's cell state.\n\n        Returns:\n            Tuple of 3 tensors:\n            - output: [batch_size, vocab_size] tensor with the logits.\n            - hidden_state: [batch_size, hidden_size] tensor with the hidden state for the next time step.\n            - cell_state: [batch_size, hidden_size] tensor with the cell state for the next time step.\n        \"\"\"\n        # Unsqueeze predicted tokens.\n        input = input.unsqueeze(1).to(torch.int)\n        output = self.embedding(input)\n        output, (hidden_state, cell_state) = self.lstm(output, (hidden_state, cell_state))\n        output_logits = self.out(output)\n        return output_logits, hidden_state, cell_state\n\n\n@DeveloperAPI\nclass SequenceRNNDecoder(nn.Module):\n    \"\"\"RNN-based decoder over multiple time steps.\"\"\"\n\n    def __init__(\n        self,\n        hidden_size: int,\n        vocab_size: int,\n        max_sequence_length: int,\n        cell_type: str,\n        num_layers: int = 1,\n        reduce_input=\"sum\",\n    ):\n        super().__init__()\n        self.hidden_size = hidden_size\n        self.vocab_size = vocab_size\n        self.rnn_decoder = RNNDecoder(hidden_size, vocab_size, cell_type, num_layers=num_layers)\n        self.max_sequence_length = max_sequence_length\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_input)\n        self.num_layers = num_layers\n\n        self.register_buffer(\"logits\", torch.zeros([max_sequence_length, vocab_size]))\n        self.register_buffer(\"decoder_input\", torch.Tensor([strings_utils.SpecialSymbol.START.value]))\n\n    def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor):\n        \"\"\"Runs max_sequence_length RNN decoding time steps.\n\n        Args:\n            combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features.\n            target: Tensor [batch_size, max_sequence_length] with target symbols.\n\n        Returns:\n            Tensor of logits [batch_size, max_sequence_length, vocab_size].\n        \"\"\"\n        # Prepare the encoder output state.\n        decoder_hidden = get_rnn_init_state(combiner_outputs, self.reduce_sequence, self.num_layers)\n\n        batch_size = decoder_hidden.size()[1]\n\n        # Tensor to store decoder output logits.\n        logits = self.logits.unsqueeze(0).repeat(batch_size, 1, 1)\n\n        # Initialize the decoder with start symbols.\n        decoder_input = self.decoder_input.repeat(batch_size)\n\n        # Unsqueeze to account for extra multilayer dimension.\n        # decoder_hidden = encoder_output_state.unsqueeze(0)\n\n        # Decode until max length.\n        for di in range(self.max_sequence_length):\n            decoder_output, decoder_hidden = self.rnn_decoder(decoder_input, decoder_hidden)\n\n            # decoder_output: [batch_size, 1, vocab_size]\n            # Squeeze out the multilayer dimension and save logits.\n            logits[:, di, :] = decoder_output.squeeze(1)\n\n            # Determine inputs for next time step.\n            # Using teacher forcing causes the model to converge faster but when the trained network is exploited, it\n            # may be unstable: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.378.4095&rep=rep1&type=pdf.\n            # TODO: Use a configurable ratio for how often to use teacher forcing during training.\n            if target is None:\n                _, topi = decoder_output.topk(1)\n                # Squeeze out multilayer and vocabulary dimensions.\n                decoder_input = topi.squeeze(1).squeeze(1).detach()  # detach from history as input\n            else:\n                # Teacher forcing.\n                decoder_input = target[:, di]\n\n        return logits\n\n\n@DeveloperAPI\nclass SequenceLSTMDecoder(nn.Module):\n    \"\"\"LSTM-based decoder over multiple time steps.\"\"\"\n\n    def __init__(\n        self,\n        hidden_size: int,\n        vocab_size: int,\n        max_sequence_length: int,\n        reduce_input: str = \"sum\",\n        num_layers: int = 1,\n    ):\n        super().__init__()\n        self.hidden_size = hidden_size\n        self.vocab_size = vocab_size\n        self.lstm_decoder = LSTMDecoder(hidden_size, vocab_size, num_layers)\n        self.max_sequence_length = max_sequence_length\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_input)\n        self.num_layers = num_layers\n\n        self.register_buffer(\"logits\", torch.zeros([max_sequence_length, vocab_size]))\n        self.register_buffer(\"decoder_input\", torch.Tensor([strings_utils.SpecialSymbol.START.value]))\n\n    def forward(self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor) -> torch.Tensor:\n        \"\"\"Runs max_sequence_length LSTM decoding time steps.\n\n        Args:\n            combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features.\n            target: Tensor [batch_size, max_sequence_length] with target symbols.\n\n        Returns:\n            Tensor of logits [batch_size, max_sequence_length, vocab_size].\n        \"\"\"\n        # Prepare the decoder initial state.\n        decoder_hidden, decoder_cell_state = get_lstm_init_state(\n            combiner_outputs, self.reduce_sequence, self.num_layers\n        )\n        batch_size = decoder_hidden.size()[1]\n\n        # Initialize the decoder with start symbols.\n        decoder_input = self.decoder_input.repeat(batch_size)\n\n        # Tensor to store decoder output logits.\n        logits = self.logits.unsqueeze(0).repeat(batch_size, 1, 1)\n\n        # Decode until max length.\n        for di in range(self.max_sequence_length):\n            decoder_output, decoder_hidden, decoder_cell_state = self.lstm_decoder(\n                decoder_input, decoder_hidden, decoder_cell_state\n            )\n\n            # decoder_output: [batch_size, 1, vocab_size]\n            # Squeeze out the multilayer dimension and save logits.\n            logits[:, di, :] = decoder_output.squeeze(1)\n\n            # Determine inputs for next time step.\n            # Using teacher forcing causes the model to converge faster but when the trained network is exploited, it\n            # may be unstable: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.378.4095&rep=rep1&type=pdf.\n            # TODO: Use a configurable ratio for how often to use teacher forcing during training.\n            if target is None:\n                _, topi = decoder_output.topk(1)\n                # Squeeze out multilayer and vocabulary dimensions.\n                decoder_input = topi.squeeze(1).squeeze(1).detach()  # detach from history as input\n            else:\n                # Teacher forcing.\n                decoder_input = target[:, di]\n\n        return logits\n\n\n@DeveloperAPI\n@register_decoder(\"generator\", [SEQUENCE, TEXT])\nclass SequenceGeneratorDecoder(Decoder):\n    \"\"\"Dispatcher for different sequence generator decoders.\"\"\"\n\n    def __init__(\n        self,\n        vocab_size: int,\n        max_sequence_length: int,\n        cell_type: str = \"gru\",\n        input_size: int = 256,\n        reduce_input: str = \"sum\",\n        num_layers: int = 1,\n        decoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        Args:\n            vocab_size: Vocab size.\n            max_sequence_length: Maximum sequence length.\n            cell_type: Type of RNN cell to use. 'rnn', 'gru', or 'lstm'.\n            input_size: Size of incoming combiner output.\n            reduce_input: Mode with which to reduce incoming combiner output, if needed.\n            num_layers: Number of layers for the RNN deecoders.\n        \"\"\"\n        super().__init__()\n        self.config = decoder_config\n\n        self.vocab_size = vocab_size\n        self.input_size = input_size\n        self.max_sequence_length = max_sequence_length\n        if cell_type == \"lstm\":\n            self.rnn_decoder = SequenceLSTMDecoder(\n                hidden_size=input_size,\n                vocab_size=vocab_size,\n                max_sequence_length=max_sequence_length,\n                reduce_input=reduce_input,\n                num_layers=num_layers,\n            )\n        else:\n            self.rnn_decoder = SequenceRNNDecoder(\n                hidden_size=input_size,\n                vocab_size=vocab_size,\n                max_sequence_length=max_sequence_length,\n                cell_type=cell_type,\n                reduce_input=reduce_input,\n                num_layers=num_layers,\n            )\n\n    def forward(\n        self, combiner_outputs: dict[str, torch.Tensor], target: torch.Tensor = None\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Decodes combiner_outputs into a sequence.\n\n        Args:\n            combiner_outputs: Dictionary of tensors from the outputs of the combiner and other output features.\n            target: Tensor [batch_size, max_sequence_length] with target symbols.\n\n        Returns:\n            Dictionary of tensors of logits [batch_size, max_sequence_length, vocab_size].\n        \"\"\"\n        logits = self.rnn_decoder(combiner_outputs, target)\n        return {LOGITS: logits}\n\n    def get_prediction_set(self):\n        return {LOGITS, PREDICTIONS, PROBABILITIES}\n\n    @staticmethod\n    def get_schema_cls():\n        return SequenceGeneratorDecoderConfig\n\n    @property\n    def input_shape(self):\n        # Dummy implementation.\n        return torch.Size([1])\n\n    @property\n    def output_shape(self):\n        return torch.Size([self.max_sequence_length, self.vocab_size])\n"
  },
  {
    "path": "ludwig/decoders/sequence_tagger.py",
    "content": "import logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import HIDDEN, LOGITS, PREDICTIONS, PROBABILITIES, SEQUENCE, TEXT\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.modules.attention_modules import MultiHeadSelfAttention\nfrom ludwig.schema.decoders.sequence_decoders import SequenceTaggerDecoderConfig\nfrom ludwig.utils.torch_utils import Dense\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_decoder(\"tagger\", [SEQUENCE, TEXT])\nclass SequenceTaggerDecoder(Decoder):\n    def __init__(\n        self,\n        input_size: int,\n        vocab_size: int,\n        max_sequence_length: int,\n        use_attention: bool = False,\n        use_bias: bool = True,\n        attention_embedding_size: int = 256,\n        attention_num_heads: int = 8,\n        decoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = decoder_config\n\n        self.vocab_size = vocab_size\n        self.max_sequence_length = max_sequence_length\n        self.input_size = input_size\n        self.use_attention = use_attention\n        if use_attention:\n            logger.debug(\"  MultiHeadSelfAttention\")\n            self.self_attention = MultiHeadSelfAttention(\n                input_size=input_size, hidden_size=attention_embedding_size, num_heads=attention_num_heads\n            )\n            # Adjust the input size to the final projection layer.\n            input_size = self.self_attention.output_shape[0]\n        self.projection_layer = Dense(input_size=input_size, output_size=vocab_size, use_bias=use_bias)\n\n    def forward(self, inputs: dict[str, torch.Tensor], target: torch.Tensor = None) -> dict[str, torch.Tensor]:\n        \"\"\"Decodes the inputs into a sequence.\n\n        Args:\n            inputs: Dictionary of tensors from the outputs of the combiner and other output features.\n            target: Tensor [batch_size, max_sequence_length] with predictions.\n\n        Returns:\n            Dictionary of tensors with logits [batch_size, max_sequence_length, vocab_size].\n        \"\"\"\n        hidden = inputs[HIDDEN]\n        if len(hidden.size()) != 3:\n            raise ValueError(\n                f\"Decoder inputs rank is {len(hidden.size())}, but should be 3: \"\n                + \"[batch_size x max_sequence_length x hidden_size] in when using a tagger sequential decoder. \"\n                + \"Consider setting reduce_output to None if a sequential encoder / combiner is used.\"\n            )\n        if list(hidden.shape[1:]) != [self.max_sequence_length, self.input_size]:\n            raise ValueError(\n                \"Sequence tagger decoder inputs (hidden) should be [batch_size, self.max_sequence_length, \"\n                + f\"input_size], or [batch_size, {self.max_sequence_length}, {self.input_size}]. However, the \"\n                + f\"inputs (hidden) was instead: {list(hidden.size())}. \"\n                + \"The encoder is not length preserving. Please check its configuration.\"\n            )\n\n        if self.use_attention:\n            hidden = self.self_attention(hidden)\n\n        logits = self.projection_layer(hidden)\n        return {LOGITS: logits}\n\n    def get_prediction_set(self):\n        return {LOGITS, PROBABILITIES, PREDICTIONS}\n\n    @staticmethod\n    def get_schema_cls():\n        return SequenceTaggerDecoderConfig\n\n    @property\n    def input_shape(self):\n        # Dummy implementation.\n        return torch.Size([1])\n\n    @property\n    def output_shape(self):\n        return torch.Size([self.max_sequence_length, self.vocab_size])\n"
  },
  {
    "path": "ludwig/decoders/utils.py",
    "content": "import torch\nfrom torch import Tensor\n\n\ndef extract_generated_tokens(\n    raw_generated_output_sequences: list[Tensor],\n    input_lengths: list[int],\n    max_new_tokens: int,\n    pad_sequence: bool,\n) -> list[Tensor]:\n    \"\"\"Extracts the generated tokens from the raw output sequences of the language model.\n\n    Args:\n        raw_generated_output_sequences: The raw output sequences of the language model.\n            Represented as a list to handle variable length sequences.\n        input_lengths: The length of the inputs to the language model.\n        max_new_tokens: The maximum number of new tokens that were generated. Used to\n            pad the generated sequences to the max_new_tokens.\n        pad_sequence: Whether to pad the generated sequences to the max_new_tokens.\n\n    Returns:\n        The generated tokens.\n    \"\"\"\n    if len(raw_generated_output_sequences) != len(input_lengths):\n        raise ValueError(\n            f\"The number of raw_generated_output_sequences ({len(raw_generated_output_sequences)}) \"\n            f\"must be the same as the number of input_lengths ({len(input_lengths)}).\"\n        )\n\n    generated_outputs = []\n    for idx, input_length in enumerate(input_lengths):\n        # Remove the input sequence from the generated sequence\n        generated_sequence = raw_generated_output_sequences[idx][input_length:]\n\n        # Pad the sequence if it is shorter than the max_new_tokens for downstream metric computation\n        if pad_sequence and generated_sequence.size()[0] < max_new_tokens:\n            generated_sequence = torch.nn.functional.pad(\n                generated_sequence, (0, max_new_tokens - generated_sequence.size()[0]), \"constant\", 0\n            )\n        generated_outputs.append(generated_sequence)\n    return generated_outputs\n"
  },
  {
    "path": "ludwig/distributed/__init__.py",
    "content": "from typing import Any\n\nfrom ludwig.distributed.base import DistributedStrategy, LocalStrategy\n\n\ndef load_ddp():\n    from ludwig.distributed.ddp import DDPStrategy\n\n    return DDPStrategy\n\n\ndef load_fsdp():\n    from ludwig.distributed.fsdp import FSDPStrategy\n\n    return FSDPStrategy\n\n\ndef load_deepspeed():\n    from ludwig.distributed.deepspeed import DeepSpeedStrategy\n\n    return DeepSpeedStrategy\n\n\ndef load_local():\n    return LocalStrategy\n\n\nSTRATEGIES = {\n    \"ddp\": load_ddp,\n    \"fsdp\": load_fsdp,\n    \"deepspeed\": load_deepspeed,\n    \"local\": load_local,\n}\n\n\n_current_strategy: DistributedStrategy = None\n\n\ndef init_dist_strategy(strategy: str | dict[str, Any], **kwargs) -> DistributedStrategy:\n    global _current_strategy\n    if isinstance(strategy, dict):\n        dtype = strategy.pop(\"type\", None)\n        obj = get_dist_strategy(dtype)(**strategy)\n    else:\n        obj = get_dist_strategy(strategy)(**kwargs)\n    _current_strategy = obj\n    return obj\n\n\ndef get_current_dist_strategy() -> DistributedStrategy:\n    if _current_strategy is None:\n        raise RuntimeError(\"Distributed strategy not initialized\")\n    return _current_strategy\n\n\ndef get_dist_strategy(strategy: str | dict[str, Any]) -> type[DistributedStrategy]:\n    name = strategy\n    if isinstance(strategy, dict):\n        name = strategy[\"type\"]\n    return STRATEGIES[name]()\n\n\ndef get_default_strategy_name() -> str:\n    return \"ddp\"\n"
  },
  {
    "path": "ludwig/distributed/base.py",
    "content": "from __future__ import annotations\n\nimport contextlib\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable\nfrom typing import Any, TYPE_CHECKING\n\nimport torch\nfrom torch import nn\nfrom torch.optim import Optimizer\n\nfrom ludwig.modules.optimization_modules import create_optimizer\nfrom ludwig.utils.torch_utils import get_torch_device\n\nif TYPE_CHECKING:\n    from ray.train.backend import BackendConfig\n    from ray.train.data_parallel_trainer import DataParallelTrainer\n\n    from ludwig.models.base import BaseModel\n    from ludwig.modules.lr_scheduler import LRScheduler\n    from ludwig.schema.trainer import ECDTrainerConfig\n    from ludwig.utils.checkpoint_utils import Checkpoint\n\n\nclass DistributedStrategy(ABC):\n    \"\"\"Interface that wraps a distributed training framework (DDP, FSDP, DeepSpeed).\n\n    Distributed strategies modify the model and/or optimizer to coordinate gradient updates among multiple workers\n    running in parallel. In most cases, these are using collective communication libraries pass messages between\n    processes.\n    \"\"\"\n\n    @abstractmethod\n    def prepare(\n        self,\n        model: nn.Module,\n        trainer_config: ECDTrainerConfig,\n        base_learning_rate: float,\n    ) -> tuple[nn.Module, Optimizer]:\n        \"\"\"Modifies the model to support distributed training and creates the optimizer.\n\n        Args:\n            model: The model to wrap for distributed training.\n            trainer_config: The trainer configuration, which includes optimizer params.\n            base_learning_rate: The base learning rate to init the optimizer, which may be scaled by the strategy.\n\n        Returns:\n            A tuple of the wrapped model and the optimizer.\n        \"\"\"\n\n    def prepare_for_inference(self, model: nn.Module) -> nn.Module:\n        return model\n\n    def to_device(self, model: BaseModel, device: torch.device | None = None) -> nn.Module:\n        return model.to_device(device if device is not None else get_torch_device())\n\n    def backward(self, loss: torch.Tensor, model: nn.Module):\n        loss.backward()\n\n    def step(self, optimizer: Optimizer, *args, **kwargs):\n        optimizer.step(*args, **kwargs)\n\n    def zero_grad(self, optimizer: Optimizer):\n        optimizer.zero_grad()\n\n    def set_batch_size(self, model: nn.Module, batch_size: int):\n        pass\n\n    @abstractmethod\n    def size(self) -> int:\n        pass\n\n    @abstractmethod\n    def rank(self) -> int:\n        pass\n\n    @abstractmethod\n    def local_size(self) -> int:\n        pass\n\n    @abstractmethod\n    def local_rank(self) -> int:\n        pass\n\n    def is_coordinator(self) -> bool:\n        return self.rank() == 0\n\n    @abstractmethod\n    def barrier(self):\n        pass\n\n    @abstractmethod\n    def allreduce(self, t: torch.Tensor) -> torch.Tensor:\n        pass\n\n    @abstractmethod\n    def broadcast(self, t: torch.Tensor) -> torch.Tensor:\n        pass\n\n    @abstractmethod\n    def sync_model(self, model: nn.Module):\n        pass\n\n    @abstractmethod\n    def sync_optimizer(self, optimizer: Optimizer):\n        pass\n\n    @abstractmethod\n    def broadcast_object(self, v: Any, name: str | None = None) -> Any:\n        pass\n\n    @abstractmethod\n    def wait_optimizer_synced(self, optimizer: Optimizer):\n        pass\n\n    @abstractmethod\n    @contextlib.contextmanager\n    def prepare_model_update(self, model: nn.Module, should_step: bool):\n        pass\n\n    @abstractmethod\n    @contextlib.contextmanager\n    def prepare_optimizer_update(self, optimizer: Optimizer):\n        pass\n\n    @classmethod\n    @abstractmethod\n    def is_available(cls) -> bool:\n        pass\n\n    @classmethod\n    @abstractmethod\n    def gather_all_tensors_fn(cls) -> Callable | None:\n        pass\n\n    @classmethod\n    @abstractmethod\n    def get_ray_trainer_backend(cls, **kwargs) -> Any | None:\n        pass\n\n    @classmethod\n    @abstractmethod\n    def get_trainer_cls(cls, backend_config: BackendConfig) -> tuple[type[DataParallelTrainer], dict[str, Any]]:\n        pass\n\n    @abstractmethod\n    def shutdown(self):\n        pass\n\n    def return_first(self, fn: Callable) -> Callable:\n        \"\"\"Wraps function so results are only returned by the first (coordinator) rank.\n\n        The purpose of this function is to reduce network overhead.\n        \"\"\"\n\n        def wrapped(*args, **kwargs):\n            res = fn(*args, **kwargs)\n            return res if self.rank() == 0 else None\n\n        return wrapped\n\n    def allow_gradient_accumulation(self) -> bool:\n        return True\n\n    def allow_mixed_precision(self) -> bool:\n        return True\n\n    def allow_clip_gradients(self) -> bool:\n        return True\n\n    def prepare_before_load(self) -> bool:\n        \"\"\"True if we need to call `prepare` again before loading a checkpoint.\"\"\"\n        return False\n\n    @classmethod\n    def is_model_parallel(cls) -> bool:\n        return False\n\n    def create_checkpoint_handle(\n        self,\n        dist_model: nn.Module,\n        model: nn.Module,\n        optimizer: Optimizer | None = None,\n        scheduler: LRScheduler | None = None,\n    ) -> Checkpoint:\n        from ludwig.utils.checkpoint_utils import MultiNodeCheckpoint\n\n        return MultiNodeCheckpoint(self, model, optimizer, scheduler)\n\n    @classmethod\n    def extract_model_for_serialization(cls, model: nn.Module) -> nn.Module | tuple[nn.Module, list[dict]]:\n        return model\n\n    @classmethod\n    def replace_model_from_serialization(cls, state: nn.Module | tuple[nn.Module, list[dict]]) -> nn.Module:\n        assert isinstance(state, nn.Module)\n        return state\n\n\nclass LocalStrategy(DistributedStrategy):\n    def prepare(\n        self,\n        model: nn.Module,\n        trainer_config: ECDTrainerConfig,\n        base_learning_rate: float,\n    ) -> tuple[nn.Module, Optimizer]:\n        return model, create_optimizer(model, trainer_config.optimizer, base_learning_rate)\n\n    def size(self) -> int:\n        return 1\n\n    def rank(self) -> int:\n        return 0\n\n    def local_size(self) -> int:\n        return 0\n\n    def local_rank(self) -> int:\n        return 0\n\n    def barrier(self):\n        pass\n\n    def allreduce(self, t: torch.Tensor) -> torch.Tensor:\n        return t\n\n    def broadcast(self, t: torch.Tensor) -> torch.Tensor:\n        return t\n\n    def sync_model(self, model: nn.Module):\n        pass\n\n    def sync_optimizer(self, optimizer: Optimizer):\n        pass\n\n    def broadcast_object(self, v: Any, name: str | None = None) -> Any:\n        return v\n\n    def wait_optimizer_synced(self, optimizer: Optimizer):\n        pass\n\n    @contextlib.contextmanager\n    def prepare_model_update(self, model: nn.Module, should_step: bool):\n        yield\n\n    @contextlib.contextmanager\n    def prepare_optimizer_update(self, optimizer: Optimizer):\n        yield\n\n    @classmethod\n    def is_available(cls) -> bool:\n        # While this strategy is always an option, it is not \"distributed\" which is the meaning of availability\n        # in this context.\n        return False\n\n    @classmethod\n    def gather_all_tensors_fn(cls) -> Callable | None:\n        return None\n\n    @classmethod\n    def get_ray_trainer_backend(cls, **kwargs) -> Any | None:\n        return None\n\n    @classmethod\n    def get_trainer_cls(cls, backend_config: BackendConfig) -> tuple[type[DataParallelTrainer], dict[str, Any]]:\n        raise ValueError(\"Cannot construct a trainer from a local strategy.\")\n\n    def shutdown(self):\n        pass\n"
  },
  {
    "path": "ludwig/distributed/ddp.py",
    "content": "import contextlib\nimport logging\nimport os\nimport socket\nfrom collections.abc import Callable\nfrom typing import Any, Optional, TYPE_CHECKING, Union\n\nimport torch\nimport torch.distributed as dist\nfrom ray.train.backend import BackendConfig\nfrom ray.train.data_parallel_trainer import DataParallelTrainer\nfrom ray.train.torch import TorchTrainer\nfrom torch import nn\nfrom torch.nn.parallel import DistributedDataParallel as DDP\nfrom torch.optim import Optimizer\nfrom torchmetrics.utilities.distributed import gather_all_tensors\n\nfrom ludwig.distributed.base import DistributedStrategy\nfrom ludwig.modules.optimization_modules import create_optimizer\nfrom ludwig.utils.torch_utils import get_torch_device\n\nif TYPE_CHECKING:\n    from ludwig.models.base import BaseModel\n    from ludwig.modules.lr_scheduler import LRScheduler\n    from ludwig.schema.trainer import ECDTrainerConfig\n    from ludwig.utils.checkpoint_utils import Checkpoint\n\n\nclass DDPStrategy(DistributedStrategy):\n    def __init__(self):\n        self._local_rank, self._local_size = local_rank_and_size()\n        self._log_on_init()\n\n    def _log_on_init(self):\n        logging.info(\"Using DDP strategy\")\n\n    def prepare(\n        self,\n        model: nn.Module,\n        trainer_config: \"ECDTrainerConfig\",\n        base_learning_rate: float,\n    ) -> tuple[nn.Module, Optimizer]:\n        return DDP(model), create_optimizer(model, trainer_config.optimizer, base_learning_rate)\n\n    def size(self) -> int:\n        return dist.get_world_size()\n\n    def rank(self) -> int:\n        return dist.get_rank()\n\n    def local_size(self) -> int:\n        return self._local_size\n\n    def local_rank(self) -> int:\n        return self._local_rank\n\n    def barrier(self):\n        return dist.barrier()\n\n    def allreduce(self, t: torch.Tensor) -> torch.Tensor:\n        dist.all_reduce(t)\n        return t\n\n    def broadcast(self, t: torch.Tensor) -> torch.Tensor:\n        dist.broadcast(t)\n        return t\n\n    def sync_model(self, model: nn.Module):\n        # TODO(travis): open question if this is needed to ensure all workers using same weights\n        pass\n\n    def sync_optimizer(self, optimizer: Optimizer):\n        # TODO(travis): open question if this is needed to ensure all workers using same optimizer state\n        pass\n\n    def broadcast_object(self, v: Any, name: str | None = None) -> Any:\n        output = [v]\n        dist.broadcast_object_list(output)\n        return output[0]\n\n    def wait_optimizer_synced(self, optimizer: Optimizer):\n        pass\n\n    @contextlib.contextmanager\n    def prepare_model_update(self, model: nn.Module, should_step: bool):\n        if should_step:\n            yield\n        else:\n            # Prevents DDP from syncing gradients during accumulation step\n            with model.no_sync():\n                yield\n\n    @contextlib.contextmanager\n    def prepare_optimizer_update(self, optimizer: Optimizer):\n        yield\n\n    @classmethod\n    def is_available(cls) -> bool:\n        return dist.is_available() and dist.is_initialized()\n\n    @classmethod\n    def gather_all_tensors_fn(cls) -> Callable | None:\n        return gather_all_tensors\n\n    @classmethod\n    def get_ray_trainer_backend(cls, **kwargs) -> Any | None:\n        from ray.train.torch import TorchConfig\n\n        return TorchConfig()\n\n    @classmethod\n    def get_trainer_cls(cls, backend_config: BackendConfig) -> tuple[type[DataParallelTrainer], dict[str, Any]]:\n        return TorchTrainer, dict(torch_config=backend_config)\n\n    def shutdown(self):\n        # TODO(travis): currently Ray handles this for us, but is subject to hangs if one of the workers raises an\n        # exception and the other makes a collective op. We should figure out a way to make this safe to call\n        # multiple times. It looks like there is a fix we can make use of when we upgrade to Ray 2.1:\n        # https://discuss.ray.io/t/torchtrainer-hangs-when-only-1-worker-raises-error/7447/11\n        # dist.destroy_process_group()\n        pass\n\n    def create_checkpoint_handle(\n        self,\n        dist_model: nn.Module,\n        model: nn.Module,\n        optimizer: Optimizer | None = None,\n        scheduler: Optional[\"LRScheduler\"] = None,\n    ) -> \"Checkpoint\":\n        from ludwig.utils.checkpoint_utils import MultiNodeCheckpoint\n\n        return MultiNodeCheckpoint(self, model, optimizer, scheduler)\n\n    def to_device(self, model: Union[\"BaseModel\", DDP], device: torch.device | None = None) -> nn.Module:\n        try:\n            return model.to_device(device if device is not None else get_torch_device())\n        except AttributeError:\n            # Model is already wrapped in DistributedDataParallel, so it has already been moved to device\n            return model\n\n\ndef local_rank_and_size() -> tuple[int, int]:\n    # DeepSpeed CLI and other tools may set these environment variables for us.\n    local_rank, local_size = os.environ.get(\"LOCAL_RANK\"), os.environ.get(\"LOCAL_SIZE\")\n    if local_rank is not None and local_size is not None:\n        return int(local_rank), int(local_size)\n\n    # Gather the rank and hostnames from every worker so we can count up how many belong to the same host, which\n    # constitutes the local group.\n    rank = dist.get_rank()\n    host = socket.gethostname()\n    output = [None for _ in range(dist.get_world_size())]\n    dist.all_gather_object(output, (rank, host))\n\n    # Every time we find a worker with the same host, we increment the size counter.\n    # The local rank is determined by the world rank relative to the other workers on the same host, so every time\n    # we see a worker on our host with a lower rank, we increment the rank counter.\n    local_size = 0\n    local_rank = 0\n    for other_rank, other_host in output:\n        if other_host == host:\n            local_size += 1\n            if other_rank < rank:\n                local_rank += 1\n\n    return local_rank, local_size\n"
  },
  {
    "path": "ludwig/distributed/deepspeed.py",
    "content": "import logging\nimport os\nimport warnings\nfrom collections.abc import Mapping\nfrom typing import Any, Optional, TYPE_CHECKING\n\nimport deepspeed\nimport deepspeed.comm\nimport torch\nfrom deepspeed.utils.zero_to_fp32 import get_fp32_state_dict_from_zero_checkpoint\nfrom packaging import version\nfrom torch import nn\nfrom torch.optim.optimizer import Optimizer\n\nfrom ludwig.constants import MIN_POSSIBLE_BATCH_SIZE\nfrom ludwig.distributed.ddp import DDPStrategy\nfrom ludwig.modules.optimization_modules import get_optimizer_class_and_kwargs\nfrom ludwig.utils.checkpoint_utils import Checkpoint\nfrom ludwig.utils.model_utils import extract_tensors, replace_tensors\n\n_deepspeed_0101 = version.parse(deepspeed.__version__) >= version.parse(\"0.10.1\")\n\n\nif TYPE_CHECKING:\n    from ludwig.modules.lr_scheduler import LRScheduler\n    from ludwig.schema.trainer import ECDTrainerConfig\n\n\nDEFAULT_ZERO_OPTIMIZATION = {\n    \"stage\": \"auto\",\n    \"stage3_gather_16bit_weights_on_model_save\": \"auto\",\n    \"offload_optimizer\": {\"device\": \"auto\"},\n    \"offload_param\": {\"device\": \"auto\"},\n}\n\n# Filter out warnings about DeepSpeed use of deprecated methods. Can remove on upgrade to DeepSpeed 0.9.\nwarnings.filterwarnings(\n    action=\"ignore\",\n    category=UserWarning,\n    module=\"torch.distributed.distributed_c10d\",\n)\n\n\nclass DeepSpeedStrategy(DDPStrategy):\n    def __init__(\n        self,\n        zero_optimization: dict[str, Any] | None = None,\n        fp16: dict[str, Any] | None = None,\n        bf16: dict[str, Any] | None = None,\n        compression_training: dict[str, Any] | None = None,\n        **kwargs\n    ):\n        # If we're initializing from a `deepspeed` CLI command, deepspeed will have already been initialized, as\n        # indicated by the presence of the LOCAL_RANK var. Otherwise, we're initializing from Ray / torchrun, and will\n        # need to set this var ourselves, then init DeepSpeed here.\n        local_rank, local_size = os.environ.get(\"LOCAL_RANK\"), os.environ.get(\"LOCAL_SIZE\")\n        init_deepspeed = local_rank is None or local_size is None\n\n        super().__init__(**kwargs)\n        self.zero_optimization = zero_optimization or DEFAULT_ZERO_OPTIMIZATION\n        self.fp16 = fp16\n        self.bf16 = bf16\n        self.compression_training = compression_training\n\n        if init_deepspeed:\n            os.environ[\"LOCAL_RANK\"] = str(self.local_rank())\n            os.environ[\"LOCAL_SIZE\"] = str(self.local_size())\n            os.environ[\"RANK\"] = str(self.rank())\n            os.environ[\"WORLD_SIZE\"] = str(self.size())\n            deepspeed.init_distributed()\n\n    def _log_on_init(self):\n        logging.info(\"Using DeepSpeed strategy\")\n\n    def prepare(\n        self,\n        model: nn.Module,\n        trainer_config: \"ECDTrainerConfig\",\n        base_learning_rate: float,\n    ) -> tuple[nn.Module, Optimizer]:\n        # If `batch_size=auto`, we set to MIN_POSSIBLE_BATCH_SIZE temporarily until auto-tuning adjusts it`\n        # We can really set it to be whatever we want, as it will be overridden by the auto-tuning.\n        batch_size = (\n            trainer_config.batch_size if isinstance(trainer_config.batch_size, int) else MIN_POSSIBLE_BATCH_SIZE\n        )\n        # Paged and 8-bit optimizers are not supported by Deepspeed - just whatever is supported\n        # by torch.optim.Optimizer. https://www.deepspeed.ai/docs/config-json/#optimizer-parameters.\n        if trainer_config.optimizer.is_paged or trainer_config.optimizer.is_8bit:\n            raise ValueError(\"Cannot use a paged or 8-bit optimizer with DeepSpeed.\")\n        optimizer_cls, optimizer_kwargs = get_optimizer_class_and_kwargs(trainer_config.optimizer, base_learning_rate)\n        ds_config = {\n            \"amp\": {\n                \"enabled\": trainer_config.use_mixed_precision,\n            },\n            \"optimizer\": {\"type\": optimizer_cls.__name__, \"params\": optimizer_kwargs},\n            \"zero_optimization\": self.zero_optimization,\n            \"gradient_clipping\": trainer_config.gradient_clipping.clipglobalnorm,\n            \"train_micro_batch_size_per_gpu\": batch_size,\n            \"gradient_accumulation_steps\": trainer_config.gradient_accumulation_steps,\n            \"steps_per_print\": trainer_config.steps_per_checkpoint or 10000,\n        }\n\n        # DeepSpeed doesn't like passing these params as None values\n        if self.fp16 is not None:\n            ds_config[\"fp16\"] = self.fp16\n        if self.bf16 is not None:\n            ds_config[\"bf16\"] = self.bf16\n        if self.compression_training is not None:\n            ds_config[\"compression_training\"] = self.compression_training\n\n        model_engine, optimizer, _, _ = deepspeed.initialize(\n            model=model,\n            model_parameters=model.parameters(),\n            lr_scheduler=None,  # Don't let DeepSpeed manage the learning rate scheduler\n            config=ds_config,\n            dist_init_required=False,\n        )\n\n        if hasattr(optimizer, \"optimizer\"):\n            # Zero-3 wraps the optimizer\n            optimizer = optimizer.optimizer\n\n        return model_engine, optimizer\n\n    def prepare_for_inference(self, model: nn.Module) -> nn.Module:\n        ds_config = {}\n        model_engine = deepspeed.init_inference(model=model, config=ds_config)\n        return model_engine\n\n    def to_device(self, model: nn.Module, device: torch.device | None = None) -> nn.Module:\n        return model\n\n    def backward(self, loss: torch.Tensor, model: nn.Module):\n        # See: https://github.com/huggingface/accelerate/blob/main/src/accelerate/utils/deepspeed.py\n        # runs backpropagation and handles mixed precision\n        model.backward(loss)\n\n        # Deepspeed's `engine.step` performs the following operations:\n        # - gradient accumulation check\n        # - gradient clipping\n        # - optimizer step\n        # - zero grad\n        # - checking overflow\n        # - lr_scheduler step (only if engine.lr_scheduler is not None)\n        model.step()\n        # and this plugin overrides the above calls with no-ops when Accelerate runs under\n        # Deepspeed, but allows normal functionality for non-Deepspeed cases thus enabling a simple\n        # training loop that works transparently under many training regimes.\n\n    def step(self, optimizer: Optimizer, *args, **kwargs):\n        # Handled by `self.backward(loss)`\n        pass\n\n    def zero_grad(self, optimizer: Optimizer):\n        # Handled by `self.backward(loss)`\n        pass\n\n    def set_batch_size(self, model: nn.Module, batch_size: int):\n        # Adapted from:\n        # https://github.com/microsoft/DeepSpeed/blob/7ce371b139521b1ebbf052f0496b1a16397c1d19/deepspeed/runtime/engine.py#L422  # noqa: E501\n        model._config.micro_batch_size_per_gpu = batch_size\n        model._config.train_batch_size = batch_size * self.size() * model._config.gradient_accumulation_steps\n\n    def barrier(self):\n        deepspeed.comm.barrier()\n\n    def allow_gradient_accumulation(self) -> bool:\n        \"\"\"DeepSpeed handles gradient accumulation internally.\"\"\"\n        return False\n\n    def allow_mixed_precision(self) -> bool:\n        \"\"\"DeepSpeed handles mixed precision internally.\"\"\"\n        return False\n\n    def allow_clip_gradients(self) -> bool:\n        \"\"\"DeepSpeed handles gradient clipping internally.\"\"\"\n        return False\n\n    def prepare_before_load(self) -> bool:\n        \"\"\"DeepSpeed requires the engine to be re-initialized before loading.\n\n        https://deepspeed.readthedocs.io/en/latest/model-checkpointing.html#loading-training-checkpoints\n        \"\"\"\n        return True\n\n    @classmethod\n    def is_model_parallel(cls) -> bool:\n        return True\n\n    def create_checkpoint_handle(\n        self,\n        dist_model: nn.Module,\n        model: nn.Module,\n        optimizer: Optimizer | None = None,\n        scheduler: Optional[\"LRScheduler\"] = None,\n    ) -> Checkpoint:\n        return DeepSpeedCheckpoint(self, dist_model, optimizer, scheduler)\n\n    @classmethod\n    def extract_model_for_serialization(cls, model: nn.Module) -> nn.Module | tuple[nn.Module, list[dict]]:\n        return extract_tensors(model)\n\n    @classmethod\n    def replace_model_from_serialization(cls, state: nn.Module | tuple[nn.Module, list[dict]]) -> nn.Module:\n        assert isinstance(state, tuple)\n        model, model_weights = state\n        replace_tensors(model, model_weights, torch.device(\"cpu\"))\n        return model\n\n\nclass DeepSpeedCheckpoint(Checkpoint):\n    def prepare(self, directory: str):\n        if self.distributed.local_rank() == 0:\n            # Checkpoints need to be written on every rank, but the directory only needs to be created once per node.\n            super().prepare(directory)\n\n    def load(self, save_path: str, device: torch.device | None = None) -> bool:\n        \"\"\"Load a checkpoint.\n\n        For DeepSpeed, we need every worker to independently load back the model weights, as the checkpoints themselves\n        may be sharded (when using DeepSpeed Zero3).\n\n        https://deepspeed.readthedocs.io/en/latest/model-checkpointing.html#loading-training-checkpoints\n        \"\"\"\n        # NOTE(geoffrey): `load_module_strict=False` because this code path is frequently used to load models trained\n        # using adapter-based fine-tuning, where the checkpoints only contain the adapter weights, and not the full\n        # model weights. This may lead to silent, unexpected behavior for resuming full model fine-tuning,\n        # where all the model weights *must* be loaded in.\n        # TODO(geoffrey): Add a boolean arg to function to control load_module_strict behavior.\n        _, client_state = self.model.load_checkpoint(\n            save_path, load_lr_scheduler_states=False, load_module_strict=False\n        )\n        self.global_step = self._get_global_step(client_state, save_path)\n        if self.scheduler is not None and \"scheduler_state\" in client_state:\n            self.scheduler.load_state_dict(client_state[\"scheduler_state\"])\n        return True\n\n    def save(self, save_path: str, global_step: int):\n        client_state = {\n            \"global_step\": global_step,\n        }\n        if self.scheduler is not None:\n            client_state[\"scheduler_state\"] = self.scheduler.state_dict()\n\n        kwargs = {}\n        if _deepspeed_0101:\n            kwargs[\"exclude_frozen_parameters\"] = True\n\n        self.model.save_checkpoint(save_path, client_state=client_state, **kwargs)\n\n    def get_state_for_inference(self, save_path: str, device: torch.device | None = None) -> Mapping[str, Any]:\n        if self.model.zero_optimization_stage() == 3:\n            return get_fp32_state_dict_from_zero_checkpoint(save_path)\n\n        self.model.load_checkpoint(\n            save_path, load_optimizer_states=False, load_lr_scheduler_states=False, load_module_only=True\n        )\n        return self.model.module.cpu().state_dict()\n"
  },
  {
    "path": "ludwig/distributed/fsdp.py",
    "content": "import logging\nfrom typing import TYPE_CHECKING\n\nimport torch\nfrom torch import nn\nfrom torch.distributed.fsdp import FullyShardedDataParallel as FSDP\nfrom torch.optim import Optimizer\n\nfrom ludwig.distributed.ddp import DDPStrategy\nfrom ludwig.modules.optimization_modules import create_optimizer\n\nif TYPE_CHECKING:\n    from ludwig.schema.trainer import ECDTrainerConfig\n\n\nclass FSDPStrategy(DDPStrategy):\n    def _log_on_init(self):\n        logging.info(\"Using FSDP strategy\")\n\n    def prepare(\n        self,\n        model: nn.Module,\n        trainer_config: \"ECDTrainerConfig\",\n        base_learning_rate: float,\n    ) -> tuple[nn.Module, Optimizer]:\n        return FSDP(model), create_optimizer(model, trainer_config.optimizer, base_learning_rate)\n\n    def to_device(self, model: nn.Module, device: torch.device | None = None) -> nn.Module:\n        return model\n\n    @classmethod\n    def is_model_parallel(cls) -> bool:\n        return True\n"
  },
  {
    "path": "ludwig/encoders/__init__.py",
    "content": "# register all encoders\nimport ludwig.encoders.bag_encoders\nimport ludwig.encoders.category_encoders\nimport ludwig.encoders.date_encoders\nimport ludwig.encoders.generic_encoders\nimport ludwig.encoders.h3_encoders\nimport ludwig.encoders.image\nimport ludwig.encoders.sequence_encoders\nimport ludwig.encoders.set_encoders\nimport ludwig.encoders.text_encoders  # noqa\n"
  },
  {
    "path": "ludwig/encoders/bag_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BAG, ENCODER_OUTPUT\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.embedding_modules import EmbedWeighted\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.schema.encoders.bag_encoders import BagEmbedWeightedConfig\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_encoder(\"embed\", BAG)\nclass BagEmbedWeightedEncoder(Encoder):\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int = 50,\n        representation: str = \"dense\",\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        force_embedding_size: bool = False,\n        embeddings_on_cpu: bool = False,\n        fc_layers=None,\n        num_fc_layers: int = 0,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict[str, Any] | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0.0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  EmbedWeighted\")\n        self.embed_weighted = EmbedWeighted(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            self.embed_weighted.output_shape[-1],\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return BagEmbedWeightedConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([len(self.vocab)])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x vocab size], type torch.int32\n\n        :param return: embeddings of shape [batch x embed size], type torch.float32\n        \"\"\"\n        hidden = self.embed_weighted(inputs)\n        hidden = self.fc_stack(hidden)\n\n        return {ENCODER_OUTPUT: hidden}\n"
  },
  {
    "path": "ludwig/encoders/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom abc import ABC, abstractmethod\n\nfrom torch import nn\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.torch_utils import LudwigModule\n\n\n@DeveloperAPI\nclass Encoder(LudwigModule, ABC):\n    @abstractmethod\n    def forward(self, inputs, training=None, mask=None):\n        raise NotImplementedError\n\n    def get_embedding_layer(self) -> nn.Module:\n        \"\"\"Returns layer that embeds inputs, used for computing explanations.\n\n        Captum adds an evaluation hook to this module returned by this function. The hook copies the module's return\n        with .clone(). The module returned by this function must return a tensor, not a dictionary of tensors.\n        \"\"\"\n        return next(self.children())\n\n    @property\n    def name(self) -> str:\n        return self.__class__.__name__\n"
  },
  {
    "path": "ludwig/encoders/category_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nfrom torch import nn\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CATEGORY, ENCODER_OUTPUT\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.embedding_modules import Embed\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.category_encoders import (\n    CategoricalEmbedConfig,\n    CategoricalOneHotEncoderConfig,\n    CategoricalPassthroughEncoderConfig,\n    CategoricalSparseConfig,\n)\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_encoder(\"passthrough\", [CATEGORY])\nclass CategoricalPassthroughEncoder(Encoder):\n    def __init__(self, input_size=1, encoder_config=None, **kwargs):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.input_size = input_size\n        self.identity = nn.Identity()\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x 1]\n        \"\"\"\n        return {\"encoder_output\": self.identity(inputs.float())}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return CategoricalPassthroughEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.input_shape\n\n    def get_embedding_layer(self) -> nn.Module:\n        return self.identity\n\n\n@DeveloperAPI\n@register_encoder(\"dense\", CATEGORY)\nclass CategoricalEmbedEncoder(Encoder):\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int = 50,\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | dict | None = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Embed\")\n        self.embed = Embed(\n            vocab=vocab,\n            embedding_size=embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=embedding_initializer,\n        )\n        self.embedding_size = self.embed.embedding_size\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x 1], type torch.int32\n\n        :param return: embeddings of shape [batch x embed size], type torch.float32\n        \"\"\"\n        embedded = self.embed(inputs)\n        return {ENCODER_OUTPUT: embedded}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return CategoricalEmbedConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.embedding_size])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n\n@DeveloperAPI\n@register_encoder(\"sparse\", CATEGORY)\nclass CategoricalSparseEncoder(Encoder):\n    def __init__(\n        self,\n        vocab: list[str],\n        embeddings_trainable: bool = False,\n        pretrained_embeddings: str | None = None,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | dict | None = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  Embed\")\n        self.embed = Embed(\n            vocab=vocab,\n            embedding_size=len(vocab),\n            representation=\"sparse\",\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=embedding_initializer,\n        )\n        self.embedding_size = self.embed.embedding_size\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x 1], type torch.int32\n\n        :param return: embeddings of shape [batch x embed size], type torch.float32\n        \"\"\"\n        embedded = self.embed(inputs)\n        return {ENCODER_OUTPUT: embedded}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return CategoricalSparseConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.embedding_size])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n\n@DeveloperAPI\n@register_encoder(\"onehot\", [CATEGORY])\nclass CategoricalOneHotEncoder(Encoder):\n    def __init__(\n        self,\n        vocab: list[str],\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.vocab_size = len(vocab)\n        self.identity = nn.Identity()\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch, 1] or [batch]\n        \"\"\"\n        t = inputs.reshape(-1).long()\n        # the output of this must be a float so that it can be concatenated with other\n        # encoder outputs and passed to dense layers in the combiner, decoder, etc.\n        outputs = self.identity(torch.nn.functional.one_hot(t, num_classes=self.vocab_size).float())\n        return {\"encoder_output\": outputs}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return CategoricalOneHotEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.vocab_size])\n\n    def get_embedding_layer(self) -> nn.Module:\n        return self.identity\n"
  },
  {
    "path": "ludwig/encoders/date_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DATE, ENCODER_OUTPUT\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.embedding_modules import Embed\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.date_encoders import DateEmbedConfig, DateWaveConfig\nfrom ludwig.utils import torch_utils\n\nlogger = logging.getLogger(__name__)\n\n# Year, month, day, weekday, yearday, hour, minute, seconds, second_of_day.\n# TODO: Share this constant with date_feature.DATE_VECTOR_SIZE.\nDATE_INPUT_SIZE = 9\n\n\n@DeveloperAPI\n@register_encoder(\"embed\", DATE)\nclass DateEmbed(Encoder):\n    def __init__(\n        self,\n        embedding_size: int = 10,\n        embeddings_on_cpu: bool = False,\n        fc_layers: list[dict] | None = None,\n        num_fc_layers: int = 0,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param embedding_size: The maximum embedding size, the actual size\n            will be `min(vocabulary_size, embedding_size)` for `dense`\n            representations and exactly `vocabulary_size` for the `sparse`\n            encoding, where `vocabulary_size` is the number of different\n            strings appearing in the training set in the column the feature\n            is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n            on GPU memory if a GPU is used, as it allows for faster access,\n            but in some cases the embedding matrix may be really big and\n            this parameter forces the placement of the embedding matrix in\n            regular memory and the CPU is used to resolve them, slightly\n            slowing down the process as a result of data transfer between\n            CPU and GPU memory.\n        :param fc_layers: list of dictionaries containing the parameters of\n            all the fully connected layers.\n        :type fc_layers: List\n        :param num_fc_layers: Number of stacked fully connected layers.\n        :type num_fc_layers: Integer\n        :param output_size: Size of each layer.\n        :type output_size: Integer\n        :param use_bias: bool determines where to use a bias vector.\n        :type use_bias: bool\n        :param weights_initializer: Initializer for the weights (aka kernel)\n            matrix.\n        :type weights_initializer: string\n        :param bias_initializer: Initializer for the bias vector.\n        :type bias_initializer: string\n        :param norm: type of normalization to use 'batch' or 'layer'.\n        :type norm: string, default None\n        :param norm_params: parameters to pass to normalization function.\n        :type norm_params: dictionary\n        :param activation: Activation function to use.\n        :type activation: string\n        :param dropout: determines if there should be a dropout layer before\n            returning the encoder output.\n        :type dropout: float\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  year FCStack\")\n        self.year_fc = FCStack(\n            first_layer_input_size=1,\n            num_layers=1,\n            default_output_size=1,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=None,\n            default_norm_params=None,\n            default_activation=None,\n            default_dropout=dropout,\n        )\n\n        logger.debug(\"  month Embed\")\n        self.embed_month = Embed(\n            [str(i) for i in range(12)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  day Embed\")\n        self.embed_day = Embed(\n            [str(i) for i in range(31)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  weekday Embed\")\n        self.embed_weekday = Embed(\n            [str(i) for i in range(7)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  yearday Embed\")\n        self.embed_yearday = Embed(\n            [str(i) for i in range(366)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  hour Embed\")\n        self.embed_hour = Embed(\n            [str(i) for i in range(24)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  minute Embed\")\n        self.embed_minute = Embed(\n            [str(i) for i in range(60)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  second Embed\")\n        self.embed_second = Embed(\n            [str(i) for i in range(60)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        # Summed sizes of all of the embeddings.\n        fc_layer_input_size = (\n            self.year_fc.output_shape[0]\n            + self.embed_month.output_shape[0]\n            + self.embed_day.output_shape[0]\n            + self.embed_weekday.output_shape[0]\n            + self.embed_yearday.output_shape[0]\n            + self.embed_hour.output_shape[0]\n            + self.embed_minute.output_shape[0]\n            + self.embed_second.output_shape[0]\n            + 1  # for periodic_second_of_day.\n        )\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=fc_layer_input_size,\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input vector fed into the encoder.\n            Shape: [batch x DATE_INPUT_SIZE], type torch.int8\n        :type inputs: Tensor\n        \"\"\"\n        # ================ Embeddings ================\n        input_vector = inputs.type(torch.int)\n\n        scaled_year = self.year_fc(input_vector[:, 0:1].type(torch.float))\n        embedded_month = self.embed_month(input_vector[:, 1:2] - 1)\n        embedded_day = self.embed_day(input_vector[:, 2:3] - 1)\n        embedded_weekday = self.embed_weekday(input_vector[:, 3:4])\n        embedded_yearday = self.embed_yearday(input_vector[:, 4:5] - 1)\n        embedded_hour = self.embed_hour(input_vector[:, 5:6])\n        embedded_minute = self.embed_minute(input_vector[:, 6:7])\n        embedded_second = self.embed_second(input_vector[:, 7:8])\n        periodic_second_of_day = torch_utils.periodic(input_vector[:, 8:9].type(torch.float), 86400)\n\n        hidden = torch.cat(\n            [\n                scaled_year,\n                embedded_month,\n                embedded_day,\n                embedded_weekday,\n                embedded_yearday,\n                embedded_hour,\n                embedded_minute,\n                embedded_second,\n                periodic_second_of_day,\n            ],\n            dim=1,\n        )\n\n        # ================ FC Stack ================\n        # logger.debug('  flatten hidden: {0}'.format(hidden))\n\n        hidden = self.fc_stack(hidden)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return DateEmbedConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([DATE_INPUT_SIZE])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n\n@DeveloperAPI\n@register_encoder(\"wave\", DATE)\nclass DateWave(Encoder):\n    def __init__(\n        self,\n        fc_layers: list[FCStack] | None = None,\n        num_fc_layers: int = 1,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param fc_layers: list of dictionaries containing the parameters of\n            all the fully connected layers.\n        :type fc_layers: List\n        :param num_fc_layers: Number of stacked fully connected layers.\n        :type num_fc_layers: Integer\n        :param output_size: Size of each layer.\n        :type output_size: Integer\n        :param use_bias: bool determines where to use a bias vector.\n        :type use_bias: bool\n        :param weights_initializer: Initializer for the weights (aka kernel)\n            matrix.\n        :type weights_initializer: string\n        :param bias_initializer: Initializer for the bias vector.\n        :type bias_initializer: string\n        :param norm: type of normalization to use 'batch' or 'layer'.\n        :type norm: string, default None\n        :param norm_params: parameters to pass to normalization function.\n        :type norm_params: dictionary\n        :param activation: Activation function to use.\n        :type activation: string\n        :param dropout: determines if there should be a dropout layer before\n            returning the encoder output.\n        :type dropout: float\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        logger.debug(\"  year FCStack\")\n        self.year_fc = FCStack(\n            first_layer_input_size=1,\n            num_layers=1,\n            default_output_size=1,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=None,\n            default_norm_params=None,\n            default_activation=None,\n            default_dropout=dropout,\n        )\n\n        # Summed sizes of all of the embeddings.\n        # Additional 8 for periodic_[month, day, ..., second_of_day].\n        fc_layer_input_size = self.year_fc.output_shape[0] + 8\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=fc_layer_input_size,\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input vector fed into the encoder.\n            Shape: [batch x DATE_INPUT_SIZE], type torch.int8\n        :type inputs: Tensor\n        \"\"\"\n        # ================ Embeddings ================\n        input_vector = inputs.type(torch.float)\n        scaled_year = self.year_fc(input_vector[:, 0:1])\n        periodic_month = torch_utils.periodic(input_vector[:, 1:2], 12)\n        periodic_day = torch_utils.periodic(input_vector[:, 2:3], 31)\n        periodic_weekday = torch_utils.periodic(input_vector[:, 3:4], 7)\n        periodic_yearday = torch_utils.periodic(input_vector[:, 4:5], 366)\n        periodic_hour = torch_utils.periodic(input_vector[:, 5:6], 24)\n        periodic_minute = torch_utils.periodic(input_vector[:, 6:7], 60)\n        periodic_second = torch_utils.periodic(input_vector[:, 7:8], 60)\n        periodic_second_of_day = torch_utils.periodic(input_vector[:, 8:9], 86400)\n\n        hidden = torch.cat(\n            [\n                scaled_year,\n                periodic_month,\n                periodic_day,\n                periodic_weekday,\n                periodic_yearday,\n                periodic_hour,\n                periodic_minute,\n                periodic_second,\n                periodic_second_of_day,\n            ],\n            dim=1,\n        )\n\n        # ================ FC Stack ================\n        # logger.debug('  flatten hidden: {0}'.format(hidden))\n\n        hidden = self.fc_stack(hidden)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return DateWaveConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([DATE_INPUT_SIZE])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n"
  },
  {
    "path": "ludwig/encoders/generic_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, ENCODER_OUTPUT, NUMBER, TEXT, TIMESERIES, VECTOR\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.schema.encoders.base import BaseEncoderConfig, DenseEncoderConfig, PassthroughEncoderConfig\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_encoder(\"passthrough\", [BINARY, NUMBER, TEXT, VECTOR])\nclass PassthroughEncoder(Encoder):\n    def __init__(self, input_size=1, encoder_config=None, **kwargs):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.input_size = input_size\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x 1], type tf.float32\n        \"\"\"\n        return {ENCODER_OUTPUT: inputs}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return PassthroughEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.input_shape\n\n\n@DeveloperAPI\n@register_encoder(\"dense\", [BINARY, NUMBER, VECTOR, TIMESERIES])\nclass DenseEncoder(Encoder):\n    def __init__(\n        self,\n        input_size,\n        fc_layers=None,\n        num_layers=1,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        activation=\"relu\",\n        dropout=0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.input_size = input_size\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=input_size,\n            layers=fc_layers,\n            num_layers=num_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n               Shape: [batch x 1], type tf.float32\n        \"\"\"\n        return {ENCODER_OUTPUT: self.fc_stack(inputs)}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return DenseEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.fc_stack.layers[-1][\"output_size\"]])\n"
  },
  {
    "path": "ludwig/encoders/h3_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, H3\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.embedding_modules import Embed, EmbedSequence\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.modules.initializer_modules import get_initializer\nfrom ludwig.modules.recurrent_modules import RecurrentStack\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.h3_encoders import H3EmbedConfig, H3RNNConfig, H3WeightedSumConfig\nfrom ludwig.utils import torch_utils\n\nlogger = logging.getLogger(__name__)\n\n# TODO: Share this with h3_feature.H3_VECTOR_LENGTH\nH3_INPUT_SIZE = 19\n\n\n@DeveloperAPI\n@register_encoder(\"embed\", H3)\nclass H3Embed(Encoder):\n    def __init__(\n        self,\n        embedding_size: int = 10,\n        embeddings_on_cpu: bool = False,\n        fc_layers: list | None = None,\n        num_fc_layers: int = 0,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str = None,\n        norm_params: dict = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        reduce_output: str = \"sum\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for\n               `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memory and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.embedding_size = embedding_size\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n\n        logger.debug(\"  mode Embed\")\n        self.embed_mode = Embed(\n            [str(i) for i in range(3)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            force_embedding_size=True,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  edge Embed\")\n        self.embed_edge = Embed(\n            [str(i) for i in range(7)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            force_embedding_size=True,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  resolution Embed\")\n        self.embed_resolution = Embed(\n            [str(i) for i in range(16)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            force_embedding_size=True,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  base cell Embed\")\n        self.embed_base_cell = Embed(\n            [str(i) for i in range(122)],\n            embedding_size,\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            force_embedding_size=True,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  cells Embed\")\n        self.embed_cells = EmbedSequence(\n            [str(i) for i in range(8)],\n            embedding_size,\n            max_sequence_length=(H3_INPUT_SIZE - 4),\n            representation=\"dense\",\n            embeddings_trainable=True,\n            pretrained_embeddings=None,\n            force_embedding_size=True,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=embedding_size,\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input vector fed into the encoder.\n               Shape: [batch x H3_INPUT_SIZE], type torch.int8\n        :type inputs: Tensor\n        \"\"\"\n        input_vector = inputs.int()\n\n        # ================ Embeddings ================\n        embedded_mode = self.embed_mode(input_vector[:, 0:1]).unsqueeze(1)\n        embedded_edge = self.embed_edge(input_vector[:, 1:2]).unsqueeze(1)\n        embedded_resolution = self.embed_resolution(input_vector[:, 2:3]).unsqueeze(1)\n        embedded_base_cell = self.embed_base_cell(input_vector[:, 3:4]).unsqueeze(1)\n        embedded_cells = self.embed_cells(input_vector[:, 4:])\n\n        # ================ Masking ================\n        # Mask out cells beyond the resolution of interest.\n        resolution = input_vector[:, 2]\n        mask = torch.unsqueeze(torch_utils.sequence_mask(resolution, 15), dim=-1).float()\n        # Batch size X 15(max resolution) X embedding size\n        masked_embedded_cells = embedded_cells * mask\n\n        # ================ Reduce ================\n        # Batch size X H3_INPUT_SIZE X embedding size\n        concatenated = torch.cat(\n            [embedded_mode, embedded_edge, embedded_resolution, embedded_base_cell, masked_embedded_cells], dim=1\n        )\n\n        hidden = self.reduce_sequence(concatenated)\n\n        # ================ FC Stack ================\n        # logger.debug('  flatten hidden: {0}'.format(hidden))\n        hidden = self.fc_stack(hidden)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return H3EmbedConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([H3_INPUT_SIZE])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n\n@DeveloperAPI\n@register_encoder(\"weighted_sum\", H3)\nclass H3WeightedSum(Encoder):\n    def __init__(\n        self,\n        embedding_size: int = 10,\n        embeddings_on_cpu: bool = False,\n        should_softmax: bool = False,\n        fc_layers: list | None = None,\n        num_fc_layers: int = 0,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for\n               `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memory and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.should_softmax = should_softmax\n        self.sum_sequence_reducer = SequenceReducer(reduce_mode=\"sum\")\n\n        self.h3_embed = H3Embed(\n            embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            weights_initializer=weights_initializer,\n            bias_initializer=bias_initializer,\n            reduce_output=\"None\",\n        )\n\n        self.register_buffer(\n            \"aggregation_weights\", torch.Tensor(get_initializer(weights_initializer)([H3_INPUT_SIZE, 1]))\n        )\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=self.h3_embed.output_shape[0],\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input vector fed into the encoder.\n               Shape: [batch x H3_INPUT_SIZE], type torch.int8\n        :type inputs: Tensor\n        \"\"\"\n        # ================ Embeddings ================\n        input_vector = inputs\n        embedded_h3 = self.h3_embed(input_vector)\n\n        # ================ Weighted Sum ================\n        if self.should_softmax:\n            weights = torch.softmax(self.aggregation_weights, dim=None)\n        else:\n            weights = self.aggregation_weights\n\n        hidden = self.sum_sequence_reducer(embedded_h3[ENCODER_OUTPUT] * weights)\n\n        # ================ FC Stack ================\n        # logger.debug('  flatten hidden: {0}'.format(hidden))\n        hidden = self.fc_stack(hidden)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return H3WeightedSumConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([H3_INPUT_SIZE])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n\n@DeveloperAPI\n@register_encoder(\"rnn\", H3)\nclass H3RNN(Encoder):\n    def __init__(\n        self,\n        embedding_size: int = 10,\n        embeddings_on_cpu: bool = False,\n        num_layers: int = 1,\n        hidden_size: int = 10,\n        cell_type: str = \"rnn\",\n        bidirectional: bool = False,\n        activation: str = \"tanh\",\n        recurrent_activation: str = \"sigmoid\",\n        use_bias: bool = True,\n        unit_forget_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        recurrent_initializer: str = \"orthogonal\",\n        bias_initializer: str = \"zeros\",\n        dropout: float = 0.0,\n        recurrent_dropout: float = 0.0,\n        reduce_output: str = \"last\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for\n               `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memory and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param num_layers: the number of stacked recurrent layers.\n        :type num_layers: Integer\n        :param cell_type: the type of recurrent cell to use.\n               Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`,\n               `ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`.\n               For reference about the differences between the cells please\n               refer to PyTorch's documentation. We suggest to use the\n               `block` variants on CPU and the `cudnn` variants on GPU\n               because of their increased speed.\n        :type cell_type: str\n        :param hidden_size: the size of the state of the rnn.\n        :type hidden_size: Integer\n        :param bidirectional: if `True` two recurrent networks will perform\n               encoding in the forward and backward direction and\n               their outputs will be concatenated.\n        :type bidirectional: Boolean\n        :param activation: Activation function to use.\n        :type activation: string\n        :param recurrent_activation: Activation function to use for the\n                recurrent step.\n        :type recurrent_activation: string\n        :param use_bias: bool determines where to use a bias vector\n        :type use_bias: bool\n        :param unit_forget_bias: if True add 1 to the bias forget gate at\n               initialization.\n        :type unit_forget_bias: bool\n        :param weights_initializer: Initializer for the weights (aka kernel)\n               matrix\n        :type weights_initializer: string\n        :param recurrent_initializer: Initializer for the recurrent weights\n               matrix\n        :type recurrent_initializer: string\n        :param bias_initializer: Initializer for the bias vector\n        :type bias_initializer: string\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: float\n        :param recurrent_dropout: Dropout rate for the RNN encoder of the H3 embeddings.\n        :type recurrent_dropout: float\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.embedding_size = embedding_size\n\n        self.h3_embed = H3Embed(\n            embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            weights_initializer=weights_initializer,\n            bias_initializer=bias_initializer,\n            reduce_output=\"None\",\n        )\n\n        logger.debug(\"  RecurrentStack\")\n        self.recurrent_stack = RecurrentStack(\n            input_size=self.h3_embed.output_shape[0],\n            max_sequence_length=H3_INPUT_SIZE,\n            hidden_size=hidden_size,\n            cell_type=cell_type,\n            num_layers=num_layers,\n            bidirectional=bidirectional,\n            use_bias=use_bias,\n            dropout=recurrent_dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input vector fed into the encoder.\n               Shape: [batch x H3_INPUT_SIZE], type torch.int8\n        :type inputs: Tensor\n        \"\"\"\n\n        # ================ Embeddings ================\n        embedded_h3 = self.h3_embed(inputs)\n\n        # ================ RNN ================\n        hidden, final_state = self.recurrent_stack(embedded_h3[ENCODER_OUTPUT])\n\n        return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return H3RNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([H3_INPUT_SIZE])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.recurrent_stack.output_shape\n"
  },
  {
    "path": "ludwig/encoders/image/__init__.py",
    "content": "import ludwig.encoders.image.base\nimport ludwig.encoders.image.timm  # noqa\nimport ludwig.encoders.image.torchvision  # noqa\n"
  },
  {
    "path": "ludwig/encoders/image/base.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, IMAGE\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.convolutional_modules import Conv2DStack, ResNet, UNetDownStack\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.modules.mlp_mixer_modules import MLPMixer\nfrom ludwig.schema.encoders.image.base import (\n    ImageEncoderConfig,\n    MLPMixerConfig,\n    ResNetConfig,\n    Stacked2DCNNConfig,\n    UNetEncoderConfig,\n    ViTConfig,\n)\nfrom ludwig.utils.torch_utils import FreezeModule\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\nclass ImageEncoder(Encoder):\n    pass\n\n\n@DeveloperAPI\n@register_encoder(\"stacked_cnn\", IMAGE)\nclass Stacked2DCNN(ImageEncoder):\n    def __init__(\n        self,\n        height: int,\n        width: int,\n        conv_layers: list[dict] | None = None,\n        num_conv_layers: int | None = None,\n        num_channels: int = None,\n        out_channels: int = 32,\n        kernel_size: int | tuple[int] = 3,\n        stride: int | tuple[int] = 1,\n        padding: int | tuple[int] | str = \"valid\",\n        dilation: int | tuple[int] = 1,\n        conv_use_bias: bool = True,\n        padding_mode: str = \"zeros\",\n        conv_norm: str | None = None,\n        conv_norm_params: dict[str, Any] | None = None,\n        conv_activation: str = \"relu\",\n        conv_dropout: int = 0,\n        pool_function: str = \"max\",\n        pool_kernel_size: int | tuple[int] = 2,\n        pool_stride: int | tuple[int] = None,\n        pool_padding: int | tuple[int] = 0,\n        pool_dilation: int | tuple[int] = 1,\n        groups: int = 1,\n        fc_layers: list[dict] | None = None,\n        num_fc_layers: int | None = 1,\n        output_size: int = 128,\n        fc_use_bias: bool = True,\n        fc_weights_initializer: str = \"xavier_uniform\",\n        fc_bias_initializer: str = \"zeros\",\n        fc_norm: str | None = None,\n        fc_norm_params: dict[str, Any] | None = None,\n        fc_activation: str = \"relu\",\n        fc_dropout: float = 0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        # map parameter input feature config names to internal names\n        img_height = height\n        img_width = width\n        first_in_channels = num_channels\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        if first_in_channels is None:\n            raise ValueError(\"first_in_channels must not be None.\")\n\n        logger.debug(\"  Conv2DStack\")\n        self.conv_stack_2d = Conv2DStack(\n            img_height=img_height,\n            img_width=img_width,\n            layers=conv_layers,\n            num_layers=num_conv_layers,\n            first_in_channels=first_in_channels,\n            default_out_channels=out_channels,\n            default_kernel_size=kernel_size,\n            default_stride=stride,\n            default_padding=padding,\n            default_dilation=dilation,\n            default_groups=groups,\n            default_use_bias=conv_use_bias,\n            default_padding_mode=padding_mode,\n            default_norm=conv_norm,\n            default_norm_params=conv_norm_params,\n            default_activation=conv_activation,\n            default_dropout=conv_dropout,\n            default_pool_function=pool_function,\n            default_pool_kernel_size=pool_kernel_size,\n            default_pool_stride=pool_stride,\n            default_pool_padding=pool_padding,\n            default_pool_dilation=pool_dilation,\n        )\n        out_channels, img_height, img_width = self.conv_stack_2d.output_shape\n        first_fc_layer_input_size = out_channels * img_height * img_width\n\n        self.flatten = torch.nn.Flatten()\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=first_fc_layer_input_size,\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=fc_use_bias,\n            default_weights_initializer=fc_weights_initializer,\n            default_bias_initializer=fc_bias_initializer,\n            default_norm=fc_norm,\n            default_norm_params=fc_norm_params,\n            default_activation=fc_activation,\n            default_dropout=fc_dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The inputs fed into the encoder.\n                Shape: [batch x channels x height x width], type torch.uint8\n        \"\"\"\n\n        hidden = self.conv_stack_2d(inputs)\n        hidden = self.flatten(hidden)\n        outputs = self.fc_stack(hidden)\n\n        return {ENCODER_OUTPUT: outputs}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageEncoderConfig]:\n        return Stacked2DCNNConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\n@DeveloperAPI\n@register_encoder(\"_resnet_legacy\", IMAGE)\nclass ResNetEncoder(ImageEncoder):\n    def __init__(\n        self,\n        height: int,\n        width: int,\n        resnet_size: int = 50,\n        num_channels: int = 3,\n        out_channels: int = 16,\n        kernel_size: int | tuple[int] = 3,\n        conv_stride: int | tuple[int] = 1,\n        first_pool_kernel_size: int | tuple[int] = None,\n        first_pool_stride: int | tuple[int] = None,\n        batch_norm_momentum: float = 0.1,\n        batch_norm_epsilon: float = 0.001,\n        fc_layers: list[dict] | None = None,\n        num_fc_layers: int | None = 1,\n        output_size: int = 256,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict[str, Any] | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        # map parameter input feature config names to internal names\n        img_height = height\n        img_width = width\n        first_in_channels = num_channels\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        logger.debug(\"  ResNet\")\n        self.resnet = ResNet(\n            img_height=img_height,\n            img_width=img_width,\n            first_in_channels=first_in_channels,\n            out_channels=out_channels,\n            resnet_size=resnet_size,\n            kernel_size=kernel_size,\n            conv_stride=conv_stride,\n            first_pool_kernel_size=first_pool_kernel_size,\n            first_pool_stride=first_pool_stride,\n            batch_norm_momentum=batch_norm_momentum,\n            batch_norm_epsilon=batch_norm_epsilon,\n        )\n        first_fc_layer_input_size = self.resnet.output_shape[0]\n\n        logger.debug(\"  FCStack\")\n        self.fc_stack = FCStack(\n            first_layer_input_size=first_fc_layer_input_size,\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        hidden = self.resnet(inputs)\n        axes = [2, 3]\n        hidden = torch.mean(hidden, axes)\n        hidden = self.fc_stack(hidden)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageEncoderConfig]:\n        return ResNetConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\n@DeveloperAPI\n@register_encoder(\"mlp_mixer\", IMAGE)\nclass MLPMixerEncoder(ImageEncoder):\n    def __init__(\n        self,\n        height: int,\n        width: int,\n        num_channels: int = None,\n        patch_size: int = 16,\n        embed_size: int = 512,\n        token_size: int = 2048,\n        channel_dim: int = 256,\n        num_layers: int = 8,\n        dropout: float = 0.0,\n        avg_pool: bool = True,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        # map parameter input feature config names to internal names\n        img_height = height\n        img_width = width\n        in_channels = num_channels\n\n        if num_channels is None:\n            raise RuntimeError(\"num_channels must not be None\")\n\n        self._input_shape = (in_channels, img_height, img_width)\n\n        logger.debug(\"  MLPMixer\")\n        self.mlp_mixer = MLPMixer(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=in_channels,\n            patch_size=patch_size,\n            embed_size=embed_size,\n            token_size=token_size,\n            channel_dim=channel_dim,\n            num_layers=num_layers,\n            dropout=dropout,\n            avg_pool=avg_pool,\n        )\n\n        self._output_shape = self.mlp_mixer.output_shape\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        hidden = self.mlp_mixer(inputs)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageEncoderConfig]:\n        return MLPMixerConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self._output_shape\n\n\n@DeveloperAPI\n@register_encoder(\"_vit_legacy\", IMAGE)\nclass ViTEncoder(ImageEncoder):\n    def __init__(\n        self,\n        height: int,\n        width: int,\n        num_channels: int = 3,\n        use_pretrained: bool = True,\n        pretrained_model: str = \"google/vit-base-patch16-224\",\n        saved_weights_in_checkpoint: bool = False,\n        hidden_size: int = 768,\n        num_hidden_layers: int = 12,\n        num_attention_heads: int = 12,\n        intermediate_size: int = 3072,\n        hidden_act: str = \"gelu\",\n        hidden_dropout_prob: float = 0.1,\n        attention_probs_dropout_prob: float = 0.1,\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        gradient_checkpointing: bool = False,\n        patch_size: int = 16,\n        trainable: bool = True,\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"Creates a ViT encoder using transformers.ViTModel.\n\n        use_pretrained: If True, uses a pretrained transformer based on the\n            pretrained_model argument.\n        pretrained: If str, expects the path to a pretrained model or the id of\n            a model on huggingface.co, and ignores the configuration provided in\n            the arguments.\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        try:\n            from transformers import ViTConfig, ViTModel\n        except ModuleNotFoundError:\n            raise RuntimeError(\n                \" transformers is not installed. \"\n                \"In order to install all image feature dependencies run \"\n                \"pip install ludwig[image]\"\n            )\n\n        # map parameter input feature config names to internal names\n        img_height = height\n        img_width = width\n        in_channels = num_channels\n\n        img_width = img_width or img_height\n        if img_width != img_height:\n            raise ValueError(\"img_height and img_width should be identical.\")\n        self._input_shape = (in_channels, img_height, img_width)\n\n        config_dict: dict\n        if use_pretrained and not saved_weights_in_checkpoint:\n            config_dict = {\n                \"pretrained_model_name_or_path\": pretrained_model,\n            }\n            transformer = ViTModel.from_pretrained(**config_dict)\n        else:\n            config_dict = {\n                \"image_size\": img_height,\n                \"num_channels\": in_channels,\n                \"patch_size\": patch_size,\n                \"hidden_size\": hidden_size,\n                \"num_hidden_layers\": num_hidden_layers,\n                \"num_attention_heads\": num_attention_heads,\n                \"intermediate_size\": intermediate_size,\n                \"hidden_act\": hidden_act,\n                \"hidden_dropout_prob\": hidden_dropout_prob,\n                \"attention_probs_dropout_prob\": attention_probs_dropout_prob,\n                \"initializer_range\": initializer_range,\n                \"layer_norm_eps\": layer_norm_eps,\n                \"gradient_checkpointing\": gradient_checkpointing,\n            }\n            config = ViTConfig(**config_dict)\n            transformer = ViTModel(config)\n\n        self.transformer = FreezeModule(transformer, frozen=not trainable)\n\n        self._output_shape = (transformer.config.hidden_size,)\n\n    def forward(self, inputs: torch.Tensor, head_mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        output = self.transformer.module(inputs, head_mask=head_mask)\n        return {ENCODER_OUTPUT: output.pooler_output}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageEncoderConfig]:\n        return ViTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n\n@DeveloperAPI\n@register_encoder(\"unet\", IMAGE)\nclass UNetEncoder(ImageEncoder):\n    def __init__(\n        self,\n        height: int,\n        width: int,\n        num_channels: int = 3,\n        conv_norm: str | None = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        if height % 16 or width % 16:\n            raise ValueError(f\"Invalid `height` {height} or `width` {width} for unet encoder\")\n\n        self.unet = UNetDownStack(\n            img_height=height,\n            img_width=width,\n            in_channels=num_channels,\n            norm=conv_norm,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        hidden, skips = self.unet(inputs)\n        return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: skips}\n\n    @staticmethod\n    def get_schema_cls() -> type[ImageEncoderConfig]:\n        return UNetEncoderConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.unet.output_shape\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.unet.input_shape\n"
  },
  {
    "path": "ludwig/encoders/image/timm.py",
    "content": "import logging\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, IMAGE\nfrom ludwig.encoders.image.base import ImageEncoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.image.timm import (\n    TimmCAFormerEncoderConfig,\n    TimmConvFormerEncoderConfig,\n    TimmEncoderConfig,\n    TimmPoolFormerEncoderConfig,\n)\n\nlogger = logging.getLogger(__name__)\n\n\ndef _get_timm():\n    try:\n        import timm\n    except ImportError:\n        raise ImportError(\"timm is required for this encoder. Install it with: pip install timm\")\n    return timm\n\n\n@DeveloperAPI\n@register_encoder(\"timm\", IMAGE)\nclass TimmEncoder(ImageEncoder):\n    \"\"\"Wraps any model from the timm (pytorch-image-models) library as a Ludwig image encoder.\n\n    This provides access to hundreds of pretrained vision models including MetaFormer variants\n    (CAFormer, ConvFormer, PoolFormer), ConvNeXt V2, EfficientFormer, and many more.\n\n    Usage in Ludwig config:\n        encoder:\n            type: timm\n            model_name: caformer_s18.sail_in22k_ft_in1k\n            use_pretrained: true\n            trainable: true\n    \"\"\"\n\n    def __init__(\n        self,\n        model_name: str = \"caformer_s18\",\n        use_pretrained: bool = True,\n        trainable: bool = True,\n        saved_weights_in_checkpoint: bool = False,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        timm = _get_timm()\n\n        pretrained = use_pretrained and not saved_weights_in_checkpoint\n        if pretrained:\n            logger.info(f\"Instantiating timm image encoder '{model_name}' with pretrained weights.\")\n        else:\n            logger.info(f\"Instantiating timm image encoder '{model_name}' without pretrained weights.\")\n\n        # num_classes=0 removes the classification head, returning pooled features\n        self.model = timm.create_model(model_name, pretrained=pretrained, num_classes=0)\n\n        # Get the model's expected input config for input_shape\n        data_config = timm.data.resolve_model_data_config(self.model)\n        self._input_size = data_config[\"input_size\"]  # (C, H, W)\n\n        # Compute output dim by running a dummy forward\n        with torch.no_grad():\n            dummy = torch.zeros(1, *self._input_size)\n            out = self.model(dummy)\n            self._output_dim = out.shape[-1]\n\n        for p in self.model.parameters():\n            p.requires_grad_(trainable)\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        return {ENCODER_OUTPUT: self.model(inputs)}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TimmEncoderConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self._output_dim])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_size)\n\n\n@DeveloperAPI\n@register_encoder(\"caformer\", IMAGE)\nclass TimmCAFormerEncoder(TimmEncoder):\n    \"\"\"CAFormer encoder — hybrid Conv+Attention MetaFormer achieving SOTA accuracy on ImageNet.\n\n    Variants: s18 (26M, 83.6%), s36 (39M, 84.5%), m36 (56M, 85.2%), b36 (99M, 85.5%).\n    \"\"\"\n\n    def __init__(self, model_name: str = \"caformer_s18\", **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TimmCAFormerEncoderConfig\n\n\n@DeveloperAPI\n@register_encoder(\"convformer\", IMAGE)\nclass TimmConvFormerEncoder(TimmEncoder):\n    \"\"\"ConvFormer encoder — pure CNN MetaFormer that outperforms ConvNeXt.\"\"\"\n\n    def __init__(self, model_name: str = \"convformer_s18\", **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TimmConvFormerEncoderConfig\n\n\n@DeveloperAPI\n@register_encoder(\"poolformer\", IMAGE)\nclass TimmPoolFormerEncoder(TimmEncoder):\n    \"\"\"PoolFormer encoder — MetaFormer using simple average pooling as token mixer.\"\"\"\n\n    def __init__(self, model_name: str = \"poolformerv2_s12\", **kwargs):\n        super().__init__(model_name=model_name, **kwargs)\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TimmPoolFormerEncoderConfig\n"
  },
  {
    "path": "ludwig/encoders/image/torchvision.py",
    "content": "import logging\nimport os\nfrom abc import abstractmethod\n\nimport torch\nimport torchvision.models as tvm\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, IMAGE\nfrom ludwig.encoders.image.base import ImageEncoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.image.torchvision import (\n    TVAlexNetEncoderConfig,\n    TVConvNeXtEncoderConfig,\n    TVDenseNetEncoderConfig,\n    TVEfficientNetEncoderConfig,\n    TVGoogLeNetEncoderConfig,\n    TVInceptionV3EncoderConfig,\n    TVMaxVitEncoderConfig,\n    TVMNASNetEncoderConfig,\n    TVMobileNetV2EncoderConfig,\n    TVMobileNetV3EncoderConfig,\n    TVRegNetEncoderConfig,\n    TVResNetEncoderConfig,\n    TVResNeXtEncoderConfig,\n    TVShuffleNetV2EncoderConfig,\n    TVSqueezeNetEncoderConfig,\n    TVSwinTransformerEncoderConfig,\n    TVVGGEncoderConfig,\n    TVViTEncoderConfig,\n    TVWideResNetEncoderConfig,\n)\nfrom ludwig.utils.image_utils import register_torchvision_model_variants, torchvision_model_registry, TVModelVariant\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\nclass TVBaseEncoder(ImageEncoder):\n    def __init__(\n        self,\n        model_variant: str | int = None,\n        use_pretrained: bool = True,\n        saved_weights_in_checkpoint: bool = False,\n        model_cache_dir: str | None = None,\n        trainable: bool = True,\n        **kwargs,\n    ):\n        super().__init__()\n\n        logger.debug(f\" {self.name}\")\n        # map parameter input feature config names to internal names\n        self.model_variant = model_variant\n        self.use_pretrained = use_pretrained\n        self.model_cache_dir = model_cache_dir\n\n        # remove any Ludwig specific keyword parameters\n        kwargs.pop(\"encoder_config\", None)\n        kwargs.pop(\"type\", None)\n        kwargs.pop(\"skip\", None)\n\n        # cache pre-trained models if requested\n        # based on https://github.com/pytorch/vision/issues/616#issuecomment-428637564\n        if self.model_cache_dir is not None:\n            os.environ[\"TORCH_HOME\"] = self.model_cache_dir\n\n        # retrieve function to create requested model\n        self.create_model = torchvision_model_registry[self.torchvision_model_type][\n            self.model_variant\n        ].create_model_function\n\n        # get weight specification\n        if use_pretrained and not saved_weights_in_checkpoint:\n            weights_specification = torchvision_model_registry[self.torchvision_model_type][\n                self.model_variant\n            ].model_weights.DEFAULT\n            logger.info(\n                f\"Instantiating torchvision image encoder '{self.torchvision_model_type}' with pretrained weights: \"\n                f\"{weights_specification}.\"\n            )\n        else:\n            weights_specification = None\n            if saved_weights_in_checkpoint:\n                logger.info(\n                    f\"Instantiating torchvision image encoder: '{self.torchvision_model_type}' \"\n                    \"with weights saved in the checkpoint.\"\n                )\n            else:\n                logger.info(\n                    f\"Instantiating torchvision image encoder: '{self.torchvision_model_type}' \"\n                    \"with no pretrained weights.\"\n                )\n\n        # get torchvision transforms object\n        transforms_obj = torchvision_model_registry[self.torchvision_model_type][\n            self.model_variant\n        ].model_weights.DEFAULT.transforms()\n\n        # capture key attributes from torchvision transform for later use\n        self.num_channels = len(transforms_obj.mean)\n        self.normalize_mean = transforms_obj.mean\n        self.normalize_std = transforms_obj.std\n        self.crop_size = transforms_obj.crop_size\n\n        logger.debug(f\"  {self.torchvision_model_type}\")\n        # create pretrained model with pretrained weights or None for untrained model\n        self.model = self.create_model(weights=weights_specification, **kwargs)\n\n        # remove final classification layer\n        self._remove_softmax_layer()\n\n        # freeze parameters if requested\n        for p in self.model.parameters():\n            p.requires_grad_(trainable)\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        return {ENCODER_OUTPUT: self.model(inputs)}\n\n    @abstractmethod\n    def _remove_softmax_layer(self):\n        \"\"\"Model specific method that allows the final softmax layer to be implemented in the Ludwig Decoder\n        component.  The model specific implementation should change the final softmax layer in the torchvision\n        model architecture to torch.nn.Identity().  This allows the output tensor from the preceding layer to be\n        passed to the Ludwig Combiner and then to the Decoder.\n\n        Returns: None\n        \"\"\"\n        raise NotImplementedError()\n\n    @property\n    def output_shape(self) -> torch.Size:\n        # create synthetic image and run through forward method\n        inputs = torch.randn([1, *self.input_shape])\n        output = self.model(inputs)\n        return torch.Size(output.shape[1:])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        # expected shape after all pre-processing\n        # len(transforms_obj.mean) determines the number of channels\n        # transforms_obj.crop_size determines the height and width of image\n        # [num_channels, height, width]\n        return torch.Size([self.num_channels, *(2 * self.crop_size)])\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(variant_id=\"base\", create_model_function=tvm.alexnet, model_weights=tvm.AlexNet_Weights),\n    ]\n)\n@register_encoder(\"alexnet\", IMAGE)\nclass TVAlexNetEncoder(TVBaseEncoder):\n    # specify base model type\n    torchvision_model_type: str = \"alexnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    # TODO: discussion w/ justin\n    # @property\n    # def get_torchvision_model_type(self):\n    #     return \"alexnet\"\n\n    def _remove_softmax_layer(self):\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVAlexNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\n            variant_id=\"tiny\", create_model_function=tvm.convnext_tiny, model_weights=tvm.ConvNeXt_Tiny_Weights\n        ),\n        TVModelVariant(\n            variant_id=\"small\", create_model_function=tvm.convnext_small, model_weights=tvm.ConvNeXt_Small_Weights\n        ),\n        TVModelVariant(\n            variant_id=\"base\", create_model_function=tvm.convnext_base, model_weights=tvm.ConvNeXt_Base_Weights\n        ),\n        TVModelVariant(\n            variant_id=\"large\", create_model_function=tvm.convnext_large, model_weights=tvm.ConvNeXt_Large_Weights\n        ),\n    ]\n)\n@register_encoder(\"convnext\", IMAGE)\nclass TVConvNeXtEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"convnext\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVConvNeXtEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(121, tvm.densenet121, tvm.DenseNet121_Weights),\n        TVModelVariant(161, tvm.densenet161, tvm.DenseNet161_Weights),\n        TVModelVariant(169, tvm.densenet169, tvm.DenseNet169_Weights),\n        TVModelVariant(201, tvm.densenet201, tvm.DenseNet201_Weights),\n    ]\n)\n@register_encoder(\"densenet\", IMAGE)\nclass TVDenseNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"densenet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVDenseNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"b0\", tvm.efficientnet_b0, tvm.EfficientNet_B0_Weights),\n        TVModelVariant(\"b1\", tvm.efficientnet_b1, tvm.EfficientNet_B1_Weights),\n        TVModelVariant(\"b2\", tvm.efficientnet_b2, tvm.EfficientNet_B2_Weights),\n        TVModelVariant(\"b3\", tvm.efficientnet_b3, tvm.EfficientNet_B3_Weights),\n        TVModelVariant(\"b4\", tvm.efficientnet_b4, tvm.EfficientNet_B4_Weights),\n        TVModelVariant(\"b5\", tvm.efficientnet_b5, tvm.EfficientNet_B5_Weights),\n        TVModelVariant(\"b6\", tvm.efficientnet_b6, tvm.EfficientNet_B6_Weights),\n        TVModelVariant(\"b7\", tvm.efficientnet_b7, tvm.EfficientNet_B7_Weights),\n        TVModelVariant(\"v2_s\", tvm.efficientnet_v2_s, tvm.EfficientNet_V2_S_Weights),\n        TVModelVariant(\"v2_m\", tvm.efficientnet_v2_m, tvm.EfficientNet_V2_M_Weights),\n        TVModelVariant(\"v2_l\", tvm.efficientnet_v2_l, tvm.EfficientNet_V2_L_Weights),\n    ]\n)\n@register_encoder(\"efficientnet\", IMAGE)\nclass TVEfficientNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"efficientnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVEfficientNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"base\", tvm.googlenet, tvm.GoogLeNet_Weights),\n    ]\n)\n@register_encoder(\"googlenet\", IMAGE)\nclass TVGoogLeNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"googlenet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n        # if auxiliary network exists, eliminate auxiliary network\n        # to resolve issue when loading a saved model which does not\n        # contain the auxiliary network\n        if self.model.aux_logits:\n            self.model.aux_logits = False\n            self.model.aux1 = None\n            self.model.aux2 = None\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVGoogLeNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"base\", tvm.inception_v3, tvm.Inception_V3_Weights),\n    ]\n)\n@register_encoder(\"inceptionv3\", IMAGE)\nclass TVInceptionV3Encoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"inceptionv3\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n        # if auxiliary network exists, eliminate auxiliary network\n        # to resolve issue when loading a saved model which does not\n        # contain the auxiliary network\n        if self.model.aux_logits:\n            self.model.aux_logits = False\n            self.model.AuxLogits = None\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVInceptionV3EncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"t\", tvm.maxvit_t, tvm.MaxVit_T_Weights),\n    ]\n)\n@register_encoder(\"maxvit\", IMAGE)\nclass TVMaxVitEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"maxvit\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVMaxVitEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"0_5\", tvm.mnasnet0_5, tvm.mnasnet.MNASNet0_5_Weights),\n        TVModelVariant(\"0_75\", tvm.mnasnet0_75, tvm.mnasnet.MNASNet0_75_Weights),\n        TVModelVariant(\"1_0\", tvm.mnasnet1_0, tvm.mnasnet.MNASNet1_0_Weights),\n        TVModelVariant(\"1_3\", tvm.mnasnet1_3, tvm.mnasnet.MNASNet1_3_Weights),\n    ]\n)\n@register_encoder(\"mnasnet\", IMAGE)\nclass TVMNASNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"mnasnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVMNASNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"base\", tvm.mobilenet_v2, tvm.MobileNet_V2_Weights),\n    ]\n)\n@register_encoder(\"mobilenetv2\", IMAGE)\nclass TVMobileNetV2Encoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"mobilenetv2\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVMobileNetV2EncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"small\", tvm.mobilenet_v3_small, tvm.MobileNet_V3_Small_Weights),\n        TVModelVariant(\"large\", tvm.mobilenet_v3_large, tvm.MobileNet_V3_Large_Weights),\n    ]\n)\n@register_encoder(\"mobilenetv3\", IMAGE)\nclass TVMobileNetV3Encoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"mobilenetv3\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVMobileNetV3EncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"x_16gf\", tvm.regnet_x_16gf, tvm.RegNet_X_16GF_Weights),\n        TVModelVariant(\"x_1_6gf\", tvm.regnet_x_1_6gf, tvm.RegNet_X_1_6GF_Weights),\n        TVModelVariant(\"x_32gf\", tvm.regnet_x_32gf, tvm.RegNet_X_32GF_Weights),\n        TVModelVariant(\"x_3_2gf\", tvm.regnet_x_3_2gf, tvm.RegNet_X_3_2GF_Weights),\n        TVModelVariant(\"x_400mf\", tvm.regnet_x_400mf, tvm.RegNet_X_400MF_Weights),\n        TVModelVariant(\"x_800mf\", tvm.regnet_x_800mf, tvm.RegNet_X_800MF_Weights),\n        TVModelVariant(\"x_8gf\", tvm.regnet_x_8gf, tvm.RegNet_X_8GF_Weights),\n        TVModelVariant(\"y_128gf\", tvm.regnet_y_128gf, tvm.RegNet_Y_128GF_Weights),\n        TVModelVariant(\"y_16gf\", tvm.regnet_y_16gf, tvm.RegNet_Y_16GF_Weights),\n        TVModelVariant(\"y_1_6gf\", tvm.regnet_y_1_6gf, tvm.RegNet_Y_1_6GF_Weights),\n        TVModelVariant(\"y_32gf\", tvm.regnet_y_32gf, tvm.RegNet_Y_32GF_Weights),\n        TVModelVariant(\"y_3_2gf\", tvm.regnet_y_3_2gf, tvm.RegNet_Y_3_2GF_Weights),\n        TVModelVariant(\"y_400mf\", tvm.regnet_y_400mf, tvm.RegNet_Y_400MF_Weights),\n        TVModelVariant(\"y_800mf\", tvm.regnet_y_800mf, tvm.RegNet_Y_800MF_Weights),\n        TVModelVariant(\"y_8gf\", tvm.regnet_y_8gf, tvm.RegNet_Y_8GF_Weights),\n    ]\n)\n@register_encoder(\"regnet\", IMAGE)\nclass TVRegNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"regnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVRegNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(18, tvm.resnet18, tvm.ResNet18_Weights),\n        TVModelVariant(34, tvm.resnet34, tvm.ResNet34_Weights),\n        TVModelVariant(50, tvm.resnet50, tvm.ResNet50_Weights),\n        TVModelVariant(101, tvm.resnet101, tvm.ResNet101_Weights),\n        TVModelVariant(152, tvm.resnet152, tvm.ResNet152_Weights),\n    ]\n)\n@register_encoder(\"resnet\", IMAGE)\nclass TVResNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"resnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVResNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"50_32x4d\", tvm.resnext50_32x4d, tvm.ResNeXt50_32X4D_Weights),\n        TVModelVariant(\"101_328xd\", tvm.resnext101_32x8d, tvm.ResNeXt101_32X8D_Weights),\n        TVModelVariant(\"101_64x4d\", tvm.resnext101_64x4d, tvm.ResNeXt101_64X4D_Weights),\n    ]\n)\n@register_encoder(\"resnext\", IMAGE)\nclass TVResNeXtEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"resnext\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVResNeXtEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"x0_5\", tvm.shufflenet_v2_x0_5, tvm.ShuffleNet_V2_X0_5_Weights),\n        TVModelVariant(\"x1_0\", tvm.shufflenet_v2_x1_0, tvm.ShuffleNet_V2_X1_0_Weights),\n        TVModelVariant(\"x1_5\", tvm.shufflenet_v2_x1_5, tvm.ShuffleNet_V2_X1_5_Weights),\n        TVModelVariant(\"x2_0\", tvm.shufflenet_v2_x2_0, tvm.ShuffleNet_V2_X2_0_Weights),\n    ]\n)\n@register_encoder(\"shufflenet_v2\", IMAGE)\nclass TVShuffleNetV2Encoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"shufflenet_v2\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVShuffleNetV2EncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"1_0\", tvm.squeezenet1_0, tvm.SqueezeNet1_0_Weights),\n        TVModelVariant(\"1_1\", tvm.squeezenet1_1, tvm.SqueezeNet1_1_Weights),\n    ]\n)\n@register_encoder(\"squeezenet\", IMAGE)\nclass TVSqueezeNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"squeezenet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        # SqueezeNet does not have a final nn.Linear() layer\n        # Use flatten output from last AdaptiveAvgPool2d layer\n        # as encoder output.\n        pass\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVSqueezeNetEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"t\", tvm.swin_t, tvm.Swin_T_Weights),\n        TVModelVariant(\"s\", tvm.swin_s, tvm.Swin_S_Weights),\n        TVModelVariant(\"b\", tvm.swin_b, tvm.Swin_B_Weights),\n    ]\n)\n@register_encoder(\"swin_transformer\", IMAGE)\nclass TVSwinTransformerEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"swin_transformer\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.head = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVSwinTransformerEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(11, tvm.vgg11, tvm.VGG11_Weights),\n        TVModelVariant(\"11_bn\", tvm.vgg11_bn, tvm.VGG11_BN_Weights),\n        TVModelVariant(13, tvm.vgg13, tvm.VGG13_Weights),\n        TVModelVariant(\"13_bn\", tvm.vgg13_bn, tvm.VGG13_BN_Weights),\n        TVModelVariant(16, tvm.vgg16, tvm.VGG16_Weights),\n        TVModelVariant(\"16_bn\", tvm.vgg16_bn, tvm.VGG16_BN_Weights),\n        TVModelVariant(19, tvm.vgg19, tvm.VGG19_Weights),\n        TVModelVariant(\"19_bn\", tvm.vgg19_bn, tvm.VGG19_BN_Weights),\n    ]\n)\n@register_encoder(\"vgg\", IMAGE)\nclass TVVGGEncoder(TVBaseEncoder):\n    # specify base torchvison model\n    torchvision_model_type: str = \"vgg\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.classifier[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVVGGEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"b_16\", tvm.vit_b_16, tvm.ViT_B_16_Weights),\n        TVModelVariant(\"b_32\", tvm.vit_b_32, tvm.ViT_B_32_Weights),\n        TVModelVariant(\"l_16\", tvm.vit_l_16, tvm.ViT_L_16_Weights),\n        TVModelVariant(\"l_32\", tvm.vit_l_32, tvm.ViT_L_32_Weights),\n        TVModelVariant(\"h_14\", tvm.vit_h_14, tvm.ViT_H_14_Weights),\n    ]\n)\n@register_encoder(\"vit\", IMAGE)\nclass TVViTEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"vit\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n\n        # Depending on model variant and weight specification, the expected image size\n        # will vary.  This code determines at run time what the expected image size will be\n        # and adds to the kwargs dictionary the parameter that specifies the image size.\n        # this is needed only if not using pretrained weights.  If pre-trained weights are\n        # specified, then the correct image size is set.\n        if not kwargs[\"use_pretrained\"]:\n            weights_specification = torchvision_model_registry[self.torchvision_model_type][\n                kwargs[\"model_variant\"]\n            ].model_weights.DEFAULT\n            kwargs[\"image_size\"] = weights_specification.transforms.keywords[\"crop_size\"]\n\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.heads[-1] = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVViTEncoderConfig\n\n\n@DeveloperAPI\n@register_torchvision_model_variants(\n    [\n        TVModelVariant(\"50_2\", tvm.wide_resnet50_2, tvm.Wide_ResNet50_2_Weights),\n        TVModelVariant(\"101_2\", tvm.wide_resnet101_2, tvm.Wide_ResNet101_2_Weights),\n    ]\n)\n@register_encoder(\"wide_resnet\", IMAGE)\nclass TVWideResNetEncoder(TVBaseEncoder):\n    # specify base torchvision model\n    torchvision_model_type: str = \"wide_resnet\"\n\n    def __init__(\n        self,\n        **kwargs,\n    ):\n        logger.debug(f\" {self.name}\")\n        super().__init__(**kwargs)\n\n    def _remove_softmax_layer(self) -> None:\n        self.model.fc = torch.nn.Identity()\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TVWideResNetEncoderConfig\n"
  },
  {
    "path": "ludwig/encoders/registry.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.utils.registry import Registry\n\n_encoder_registry = Registry()\n_sequence_encoder_registry = Registry()\n\n\n@DeveloperAPI\ndef get_encoder_registry() -> Registry:\n    return _encoder_registry\n\n\n@DeveloperAPI\ndef get_sequence_encoder_registry() -> Registry:\n    return _sequence_encoder_registry\n\n\ndef register_sequence_encoder(name: str):\n    def wrap(cls):\n        get_sequence_encoder_registry()[name] = cls\n        return cls\n\n    return wrap\n\n\ndef register_encoder(name: str, features: str | list[str]):\n    if isinstance(features, str):\n        features = [features]\n\n    def update_registry(registry_getter_fn, cls, feature):\n        feature_registry = registry_getter_fn().get(feature, {})\n        feature_registry[name] = cls\n        registry_getter_fn()[feature] = feature_registry\n\n    def wrap(cls):\n        for feature in features:\n            update_registry(get_encoder_registry, cls, feature)\n        return cls\n\n    return wrap\n\n\ndef get_encoder_cls(feature: str, name: str) -> type[Encoder]:\n    return get_encoder_registry()[feature][name]\n\n\ndef get_encoder_classes(feature: str) -> dict[str, type[Encoder]]:\n    return get_encoder_registry()[feature]\n"
  },
  {
    "path": "ludwig/encoders/sequence_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nfrom torch import nn\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUDIO, ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, SEQUENCE, TEXT, TIMESERIES\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder, register_sequence_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.attention_modules import TransformerStack\nfrom ludwig.modules.convolutional_modules import Conv1DStack, ParallelConv1D, ParallelConv1DStack\nfrom ludwig.modules.embedding_modules import EmbedSequence, TokenAndPositionEmbedding\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.modules.recurrent_modules import RecurrentStack\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.encoders.sequence_encoders import (\n    ParallelCNNConfig,\n    SequenceEmbedConfig,\n    SequenceEncoderConfig,\n    SequencePassthroughConfig,\n    StackedCNNConfig,\n    StackedCNNRNNConfig,\n    StackedParallelCNNConfig,\n    StackedRNNConfig,\n    StackedTransformerConfig,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass SequenceEncoder(Encoder):\n    pass\n\n\n@DeveloperAPI\n@register_encoder(\"passthrough\", [SEQUENCE, TEXT, TIMESERIES])\nclass SequencePassthroughEncoder(SequenceEncoder):\n    def __init__(\n        self,\n        reduce_output: str = None,\n        max_sequence_length: int = 256,\n        encoding_size: int = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param reduce_output: defines how to reduce the output tensor along\n               the `s` sequence length dimension if the rank of the tensor\n               is greater than 2. Available values are: `sum`,\n               `mean` or `avg`, `max`, `concat` (concatenates along\n               the first dimension), `last` (returns the last vector of the\n               first dimension) and `None` or `null` (which does not reduce\n               and returns the full tensor).\n        :param max_sequence_length: The maximum sequence length.\n        :param encoding_size: The size of the encoding vector, or None if sequence elements are scalars.\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n        self.max_sequence_length = max_sequence_length\n\n        logger.debug(f\" {self.name}\")\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output, max_sequence_length=max_sequence_length, encoding_size=encoding_size\n        )\n        if self.reduce_output is None:\n            self.supports_masking = True\n\n    def forward(self, input_sequence: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param input_sequence: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32 or\n                      [batch x sequence length x encoding size], type torch.float32\n        :type input_sequence: Tensor\n        :param mask: Sequence mask (not yet implemented).\n               Shape: [batch x sequence length]\n        :type mask: Tensor\n        \"\"\"\n        input_sequence = input_sequence.type(torch.float32)\n        while len(input_sequence.shape) < 3:\n            input_sequence = input_sequence.unsqueeze(-1)\n        hidden = self.reduce_sequence(input_sequence)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return SequencePassthroughConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.input_shape\n\n\n@DeveloperAPI\n@register_encoder(\"embed\", [SEQUENCE, TEXT])\nclass SequenceEmbedEncoder(SequenceEncoder):\n    def __init__(\n        self,\n        vocab,\n        max_sequence_length,\n        representation=\"dense\",\n        embedding_size=256,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        weights_initializer=None,\n        dropout=0,\n        reduce_output=\"sum\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param max_sequence_length: The maximum sequence length.\n        :type max_sequence_length: int\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memory and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :type embeddings_on_cpu: Boolean\n        :param weights_initializer: the initializer to use. If `None`, the default\n               initialized of each variable is used (`xavier_uniform`\n               in most cases). Options are: `constant`, `identity`, `zeros`,\n                `ones`, `orthogonal`, `normal`, `uniform`,\n                `truncated_normal`, `variance_scaling`, `xavier_normal`,\n                `xavier_uniform`, `xavier_normal`,\n                `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n                Alternatively it is possible to specify a dictionary with\n                a key `type` that identifies the type of initializer and\n                other keys for its parameters, e.g.\n                `{type: normal, mean: 0, stddev: 0}`.\n                To know the parameters of each initializer, please refer to\n                PyTorch's documentation.\n        :type weights_initializer: str\n        :param dropout: Tensor (torch.float) The dropout probability.\n        :type dropout: Tensor\n        :param reduce_output: defines how to reduce the output tensor along\n               the `s` sequence length dimension if the rank of the tensor\n               is greater than 2. Available values are: `sum`,\n               `mean` or `avg`, `max`, `concat` (concatenates along\n               the first dimension), `last` (returns the last vector of the\n               first dimension) and `None` or `null` (which does not reduce\n               and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n        self.embedding_size = embedding_size\n        self.max_sequence_length = max_sequence_length\n\n        self.reduce_output = reduce_output\n        if self.reduce_output is None:\n            self.supports_masking = True\n\n        logger.debug(\"  EmbedSequence\")\n        self.embed_sequence = EmbedSequence(\n            vocab,\n            embedding_size,\n            max_sequence_length=max_sequence_length,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=max_sequence_length,\n            encoding_size=self.embed_sequence.output_shape[-1],\n        )\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented in EmbedSequence)\n        \"\"\"\n        embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        hidden = self.reduce_sequence(embedded_sequence)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return SequenceEmbedConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.reduce_sequence.output_shape\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"parallel_cnn\")\n@register_encoder(\"parallel_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\nclass ParallelCNN(SequenceEncoder):\n    def __init__(\n        self,\n        should_embed=True,\n        vocab=None,\n        representation=\"dense\",\n        embedding_size=256,\n        max_sequence_length=None,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        conv_layers=None,\n        num_conv_layers=None,\n        filter_size=3,\n        num_filters=256,\n        pool_function=\"max\",\n        pool_size=None,\n        fc_layers=None,\n        num_fc_layers=None,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        activation=\"relu\",\n        dropout=0,\n        reduce_output=\"max\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: revise docstring\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param conv_layers: it is a list of dictionaries containing\n               the parameters of all the convolutional layers. The length\n               of the list determines the number of parallel convolutional\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `filter_size`, `num_filters`, `pool`,\n               `norm`, and `activation`. If any of those values\n               is missing from the dictionary, the default one specified\n               as a parameter of the encoder will be used instead. If both\n               `conv_layers` and `num_conv_layers` are `None`, a default\n               list will be assigned to `conv_layers` with the value\n               `[{filter_size: 2}, {filter_size: 3}, {filter_size: 4},\n               {filter_size: 5}]`.\n        :type conv_layers: List\n        :param num_conv_layers: if `conv_layers` is `None`, this is\n               the number of parallel convolutional layers.\n        :type num_conv_layers: Integer\n        :param filter_size:  if a `filter_size` is not already specified in\n               `conv_layers` this is the default `filter_size` that\n               will be used for each layer. It indicates how wide is\n               the 1d convolutional filter.\n        :type filter_size: Integer\n        :param num_filters: if a `num_filters` is not already specified in\n               `conv_layers` this is the default `num_filters` that\n               will be used for each layer. It indicates the number\n               of filters, and by consequence the output channels of\n               the 1d convolution.\n        :type num_filters: Integer\n        :param pool_size: if a `pool_size` is not already specified\n              in `conv_layers` this is the default `pool_size` that\n              will be used for each layer. It indicates the size of\n              the max pooling that will be performed along the `s` sequence\n              dimension after the convolution operation.\n        :type pool_size: Integer\n        :param fc_layers: it is a list of dictionaries containing\n               the parameters of all the fully connected layers. The length\n               of the list determines the number of stacked fully connected\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `output_size`, `norm` and `activation`.\n               If any of those values is missing from\n               the dictionary, the default one specified as a parameter of\n               the encoder will be used instead. If both `fc_layers` and\n               `num_fc_layers` are `None`, a default list will be assigned\n               to `fc_layers` with the value\n               `[{output_size: 512}, {output_size: 256}]`\n               (only applies if `reduce_output` is not `None`).\n        :type fc_layers: List\n        :param num_fc_layers: if `fc_layers` is `None`, this is the number\n               of stacked fully connected layers (only applies if\n               `reduce_output` is not `None`).\n        :type num_fc_layers: Integer\n        :param output_size: if a `output_size` is not already specified in\n               `fc_layers` this is the default `output_size` that will be used\n               for each layer. It indicates the size of the output\n               of a fully connected layer.\n        :type output_size: Integer\n        :param norm: if a `norm` is not already specified in `conv_layers`\n               or `fc_layers` this is the default `norm` that will be used\n               for each layer. It indicates the norm of the output.\n        :type norm: str\n        :param activation: Default activation function to use\n        :type activation: Str\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.max_sequence_length = max_sequence_length\n\n        if conv_layers is not None and num_conv_layers is None:\n            # use custom-defined layers\n            self.conv_layers = conv_layers\n            self.num_conv_layers = len(conv_layers)\n        elif conv_layers is None and num_conv_layers is not None:\n            # generate num_conv_layers with default parameters\n            self.conv_layers = None\n            self.num_conv_layers = num_conv_layers\n        elif conv_layers is None and num_conv_layers is None:\n            # use default layers with varying filter sizes\n            self.conv_layers = [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}]\n            self.num_conv_layers = 4\n        else:\n            raise ValueError(\"Invalid layer parametrization, use either conv_layers or num_conv_layers\")\n\n        # The user is expected to provide fc_layers or num_fc_layers\n        # The following logic handles the case where the user either provides\n        # both or neither.\n        if fc_layers is None and num_fc_layers is None:\n            # use default layers with varying filter sizes\n            fc_layers = [{\"output_size\": 512}, {\"output_size\": 256}]\n            num_fc_layers = 2\n        elif fc_layers is not None and num_fc_layers is not None:\n            raise ValueError(\"Invalid layer parametrization, use either fc_layers or num_fc_layers only. Not both.\")\n\n        self.should_embed = should_embed\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = EmbedSequence(\n                vocab,\n                embedding_size,\n                max_sequence_length=max_sequence_length,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n\n        logger.debug(\"  ParallelConv1D\")\n        in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size\n        self.parallel_conv1d = ParallelConv1D(\n            in_channels=in_channels,\n            max_sequence_length=self.max_sequence_length,\n            layers=self.conv_layers,\n            default_num_filters=num_filters,\n            default_filter_size=filter_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n            default_pool_function=pool_function,\n            default_pool_size=pool_size,\n            default_pool_padding=\"same\",\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=max_sequence_length,\n            encoding_size=self.parallel_conv1d.output_shape[-1],\n        )\n        if self.reduce_output is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=activation,\n                default_dropout=dropout,\n            )\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n            embedded_sequence = embedded_sequence.to(dtype=torch.float)\n\n        # shape=(?, sequence_length, embedding_size)\n        hidden = embedded_sequence\n\n        # ================ Conv Layers ================\n        hidden = self.parallel_conv1d(hidden, mask=mask)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return ParallelCNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is not None:\n            return self.fc_stack.output_shape\n        return self.parallel_conv1d.output_shape\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"stacked_cnn\")\n@register_encoder(\"stacked_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\nclass StackedCNN(SequenceEncoder):\n    def __init__(\n        self,\n        should_embed=True,\n        vocab=None,\n        representation=\"dense\",\n        embedding_size=256,\n        max_sequence_length=None,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        conv_layers=None,\n        num_conv_layers=None,\n        num_filters=256,\n        filter_size=5,\n        strides=1,\n        # todo: assess how to specify padding for equivalent to 'same'\n        padding=\"same\",\n        dilation_rate=1,\n        pool_function=\"max\",\n        pool_size=None,\n        pool_strides=None,\n        # todo: determine how to pool_padding equivalent of 'same'\n        pool_padding=\"same\",\n        fc_layers=None,\n        num_fc_layers=None,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        activation=\"relu\",\n        dropout=0,\n        reduce_output=\"max\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: fixup docstring\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param conv_layers: it is a list of dictionaries containing\n               the parameters of all the convolutional layers. The length\n               of the list determines the number of parallel convolutional\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `filter_size`, `num_filters`, `pool`,\n               `norm` and `activation`. If any of those values\n               is missing from the dictionary, the default one specified\n               as a parameter of the encoder will be used instead. If both\n               `conv_layers` and `num_conv_layers` are `None`, a default\n               list will be assigned to `conv_layers` with the value\n               `[{filter_size: 2}, {filter_size: 3}, {filter_size: 4},\n               {filter_size: 5}]`.\n        :type conv_layers: List\n        :param num_conv_layers: if `conv_layers` is `None`, this is\n               the number of stacked convolutional layers.\n        :type num_conv_layers: Integer\n        :param filter_size:  if a `filter_size` is not already specified in\n               `conv_layers` this is the default `filter_size` that\n               will be used for each layer. It indicates how wide is\n               the 1d convolutional filter.\n        :type filter_size: Integer\n        :param num_filters: if a `num_filters` is not already specified in\n               `conv_layers` this is the default `num_filters` that\n               will be used for each layer. It indicates the number\n               of filters, and by consequence the output channels of\n               the 1d convolution.\n        :type num_filters: Integer\n        :param pool_size: if a `pool_size` is not already specified\n              in `conv_layers` this is the default `pool_size` that\n              will be used for each layer. It indicates the size of\n              the max pooling that will be performed along the `s` sequence\n              dimension after the convolution operation.\n        :type pool_size: Integer\n        :param fc_layers: it is a list of dictionaries containing\n               the parameters of all the fully connected layers. The length\n               of the list determines the number of stacked fully connected\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `output_size`, `norm` and `activation`.\n               If any of those values is missing from\n               the dictionary, the default one specified as a parameter of\n               the encoder will be used instead. If both `fc_layers` and\n               `num_fc_layers` are `None`, a default list will be assigned\n               to `fc_layers` with the value\n               `[{output_size: 512}, {output_size: 256}]`\n               (only applies if `reduce_output` is not `None`).\n        :type fc_layers: List\n        :param num_fc_layers: if `fc_layers` is `None`, this is the number\n               of stacked fully connected layers (only applies if\n               `reduce_output` is not `None`).\n        :type num_fc_layers: Integer\n        :param output_size: if a `output_size` is not already specified in\n               `fc_layers` this is the default `output_size` that will be used\n               for each layer. It indicates the size of the output\n               of a fully connected layer.\n        :type output_size: Integer\n        :param norm: if a `norm` is not already specified in `conv_layers`\n               or `fc_layers` this is the default `norm` that will be used\n               for each layer. It indicates the norm of the output.\n        :type norm: str\n        :param activation: Default activation function to use\n        :type activation: Str\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        if conv_layers is not None and num_conv_layers is None:\n            # use custom-defined layers\n            self.conv_layers = conv_layers\n            self.num_conv_layers = len(conv_layers)\n        elif conv_layers is None and num_conv_layers is not None:\n            # generate num_conv_layers with default parameters\n            self.conv_layers = None\n            self.num_conv_layers = num_conv_layers\n        elif conv_layers is None and num_conv_layers is None:\n            # use default layers with varying filter sizes\n            self.conv_layers = [\n                {\n                    \"filter_size\": 7,\n                    \"pool_size\": 3,\n                },\n                {\n                    \"filter_size\": 7,\n                    \"pool_size\": 3,\n                },\n                {\n                    \"filter_size\": 3,\n                    \"pool_size\": None,\n                },\n                {\n                    \"filter_size\": 3,\n                    \"pool_size\": None,\n                },\n                {\n                    \"filter_size\": 3,\n                    \"pool_size\": None,\n                },\n                {\n                    \"filter_size\": 3,\n                    \"pool_size\": 3,\n                },\n            ]\n            self.num_conv_layers = 6\n        else:\n            raise ValueError(\"Invalid layer parametrization, use either conv_layers or \" \"num_conv_layers\")\n\n        # The user is expected to provide fc_layers or num_fc_layers\n        # The following logic handles the case where the user either provides\n        # both or neither.\n        if fc_layers is None and num_fc_layers is None:\n            # use default layers with varying filter sizes\n            fc_layers = [{\"output_size\": 512}, {\"output_size\": 256}]\n            num_fc_layers = 2\n        elif fc_layers is not None and num_fc_layers is not None:\n            raise ValueError(\"Invalid layer parametrization, use either fc_layers or \" \"num_fc_layers only. Not both.\")\n\n        self.max_sequence_length = max_sequence_length\n        self.num_filters = num_filters\n        self.should_embed = should_embed\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = EmbedSequence(\n                vocab,\n                embedding_size,\n                max_sequence_length=self.max_sequence_length,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n\n        logger.debug(\"  Conv1DStack\")\n        in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size\n        self.conv1d_stack = Conv1DStack(\n            in_channels=in_channels,\n            max_sequence_length=max_sequence_length,\n            layers=self.conv_layers,\n            default_num_filters=num_filters,\n            default_filter_size=filter_size,\n            default_strides=strides,\n            default_padding=padding,\n            default_dilation_rate=dilation_rate,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n            default_pool_function=pool_function,\n            default_pool_size=pool_size,\n            default_pool_strides=pool_strides,\n            default_pool_padding=pool_padding,\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=self.conv1d_stack.output_shape[-2],\n            encoding_size=self.conv1d_stack.output_shape[-1],\n        )\n        if self.reduce_output is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=activation,\n                default_dropout=dropout,\n            )\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return StackedCNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return self.conv1d_stack.output_shape\n        return self.fc_stack.output_shape\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n\n        # shape=(?, sequence_length, embedding_size)\n        hidden = embedded_sequence\n\n        # ================ Conv Layers ================\n        hidden = self.conv1d_stack(hidden, mask=mask)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        # no reduction: hidden [batch_size, seq_size, num_filters]\n        # with reduction: hidden [batch_size, output_size]\n        return {ENCODER_OUTPUT: hidden}\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"stacked_parallel_cnn\")\n@register_encoder(\"stacked_parallel_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\nclass StackedParallelCNN(SequenceEncoder):\n    def __init__(\n        self,\n        should_embed=True,\n        vocab=None,\n        representation=\"dense\",\n        embedding_size=256,\n        max_sequence_length=None,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        stacked_layers=None,\n        num_stacked_layers=None,\n        filter_size=3,\n        num_filters=256,\n        pool_function=\"max\",\n        pool_size=None,\n        fc_layers=None,\n        num_fc_layers=None,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        activation=\"relu\",\n        dropout=0,\n        reduce_output=\"max\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: review docstring\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param stacked_layers: it is a of lists of list of dictionaries\n               containing the parameters of the stack of\n               parallel convolutional layers. The length of the list\n               determines the number of stacked parallel\n               convolutional layers, length of the sub-lists determines\n               the number of parallel conv layers and the content\n               of each dictionary determines the parameters for\n               a specific layer. The available parameters for each layer are:\n               `filter_size`, `num_filters`, `pool_size`, `norm` and\n               `activation`. If any of those values\n               is missing from the dictionary, the default one specified\n               as a parameter of the encoder will be used instead. If both\n               `stacked_layers` and `num_stacked_layers` are `None`,\n               a default list will be assigned to `stacked_layers` with\n               the value `[[{filter_size: 2}, {filter_size: 3},\n               {filter_size: 4}, {filter_size: 5}], [{filter_size: 2},\n               {filter_size: 3}, {filter_size: 4}, {filter_size: 5}],\n               [{filter_size: 2}, {filter_size: 3}, {filter_size: 4},\n               {filter_size: 5}]]`.\n        :type stacked_layers: List\n        :param num_stacked_layers: if `stacked_layers` is `None`, this is\n               the number of elements in the stack of\n               parallel convolutional layers.\n        :type num_stacked_layers: Integer\n        :param filter_size:  if a `filter_size` is not already specified in\n               `conv_layers` this is the default `filter_size` that\n               will be used for each layer. It indicates how wide is\n               the 1d convolutional filter.\n        :type filter_size: Integer\n        :param num_filters: if a `num_filters` is not already specified in\n               `conv_layers` this is the default `num_filters` that\n               will be used for each layer. It indicates the number\n               of filters, and by consequence the output channels of\n               the 1d convolution.\n        :type num_filters: Integer\n        :param pool_size: if a `pool_size` is not already specified\n              in `conv_layers` this is the default `pool_size` that\n              will be used for each layer. It indicates the size of\n              the max pooling that will be performed along the `s` sequence\n              dimension after the convolution operation.\n        :type pool_size: Integer\n        :param fc_layers: it is a list of dictionaries containing\n               the parameters of all the fully connected layers. The length\n               of the list determines the number of stacked fully connected\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `output_size`, `norm` and `activation`.\n               If any of those values is missing from\n               the dictionary, the default one specified as a parameter of\n               the encoder will be used instead. If both `fc_layers` and\n               `num_fc_layers` are `None`, a default list will be assigned\n               to `fc_layers` with the value\n               `[{output_size: 512}, {output_size: 256}]`\n               (only applies if `reduce_output` is not `None`).\n        :type fc_layers: List\n        :param num_fc_layers: if `fc_layers` is `None`, this is the number\n               of stacked fully connected layers (only applies if\n               `reduce_output` is not `None`).\n        :type num_fc_layers: Integer\n        :param output_size: if a `output_size` is not already specified in\n               `fc_layers` this is the default `output_size` that will be used\n               for each layer. It indicates the size of the output\n               of a fully connected layer.\n        :type output_size: Integer\n        :param norm: if a `norm` is not already specified in `conv_layers`\n               or `fc_layers` this is the default `norm` that will be used\n               for each layer. It indicates the norm of the output.\n        :type norm: str\n        :param activation: Default activation function to use\n        :type activation: Str\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.max_sequence_length = max_sequence_length\n        self.embedding_size = embedding_size\n\n        if stacked_layers is not None and num_stacked_layers is None:\n            # use custom-defined layers\n            self.stacked_layers = stacked_layers\n            self.num_stacked_layers = len(stacked_layers)\n        elif stacked_layers is None and num_stacked_layers is not None:\n            # generate num_conv_layers with default parameters\n            self.stacked_layers = None\n            self.num_stacked_layers = num_stacked_layers\n        elif stacked_layers is None and num_stacked_layers is None:\n            # use default layers with varying filter sizes\n            self.stacked_layers = [\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n            ]\n            self.num_stacked_layers = 6\n        else:\n            raise ValueError(\"Invalid layer parametrization, use either stacked_layers or\" \" num_stacked_layers\")\n\n        # The user is expected to provide fc_layers or num_fc_layers\n        # The following logic handles the case where the user either provides\n        # both or neither.\n        if fc_layers is None and num_fc_layers is None:\n            # use default layers with varying filter sizes\n            fc_layers = [{\"output_size\": 512}, {\"output_size\": 256}]\n            num_fc_layers = 2\n        elif fc_layers is not None and num_fc_layers is not None:\n            raise ValueError(\"Invalid layer parametrization, use either fc_layers or \" \"num_fc_layers only. Not both.\")\n\n        self.should_embed = should_embed\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = EmbedSequence(\n                vocab,\n                embedding_size,\n                max_sequence_length=self.max_sequence_length,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n\n        in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size\n        logger.debug(\"  ParallelConv1DStack\")\n        self.parallel_conv1d_stack = ParallelConv1DStack(\n            in_channels=in_channels,\n            stacked_layers=self.stacked_layers,\n            max_sequence_length=max_sequence_length,\n            default_num_filters=num_filters,\n            default_filter_size=filter_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n            default_pool_function=pool_function,\n            default_pool_size=pool_size,\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=self.parallel_conv1d_stack.output_shape[-2],\n            encoding_size=self.parallel_conv1d_stack.output_shape[-1],\n        )\n        if self.reduce_output is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=activation,\n                default_dropout=dropout,\n            )\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return StackedParallelCNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is not None:\n            return self.fc_stack.output_shape\n        return self.parallel_conv1d_stack.output_shape\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n\n        # shape=(?, sequence_length, embedding_size)\n        hidden = embedded_sequence\n\n        # ================ Conv Layers ================\n        hidden = self.parallel_conv1d_stack(hidden, mask=mask)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        # no reduction: hidden [batch_size, seq_size, num_filter]\n        # with reduction: hidden [batch_size, output_size]\n        return {ENCODER_OUTPUT: hidden}\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"rnn\")\n@register_encoder(\"rnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\nclass StackedRNN(SequenceEncoder):\n    def __init__(\n        self,\n        should_embed=True,\n        vocab=None,\n        representation=\"dense\",\n        embedding_size=256,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        num_layers=1,\n        max_sequence_length=None,\n        state_size=256,\n        cell_type=\"rnn\",\n        bidirectional=False,\n        activation=\"tanh\",\n        recurrent_activation=\"sigmoid\",\n        unit_forget_bias=True,\n        recurrent_initializer=\"orthogonal\",\n        dropout=0.0,\n        recurrent_dropout=0.0,\n        fc_layers=None,\n        num_fc_layers=0,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        fc_activation=\"relu\",\n        fc_dropout=0,\n        reduce_output=\"last\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: fix up docstring\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param conv_layers: it is a list of dictionaries containing\n               the parameters of all the convolutional layers. The length\n               of the list determines the number of parallel convolutional\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `filter_size`, `num_filters`, `pool`,\n               `norm`, `activation` and `regularize`. If any of those values\n               is missing from the dictionary, the default one specified\n               as a parameter of the encoder will be used instead. If both\n               `conv_layers` and `num_conv_layers` are `None`, a default\n               list will be assigned to `conv_layers` with the value\n               `[{filter_size: 2}, {filter_size: 3}, {filter_size: 4},\n               {filter_size: 5}]`.\n        :type conv_layers: List\n        :param num_conv_layers: if `conv_layers` is `None`, this is\n               the number of stacked convolutional layers.\n        :type num_conv_layers: Integer\n        :param filter_size:  if a `filter_size` is not already specified in\n               `conv_layers` this is the default `filter_size` that\n               will be used for each layer. It indicates how wide is\n               the 1d convolutional filter.\n        :type filter_size: Integer\n        :param num_filters: if a `num_filters` is not already specified in\n               `conv_layers` this is the default `num_filters` that\n               will be used for each layer. It indicates the number\n               of filters, and by consequence the output channels of\n               the 1d convolution.\n        :type num_filters: Integer\n        :param pool_size: if a `pool_size` is not already specified\n              in `conv_layers` this is the default `pool_size` that\n              will be used for each layer. It indicates the size of\n              the max pooling that will be performed along the `s` sequence\n              dimension after the convolution operation.\n        :type pool_size: Integer\n        :param num_rec_layers: the number of stacked recurrent layers.\n        :type num_rec_layers: Integer\n        :param cell_type: the type of recurrent cell to use.\n               Available values are: `rnn`, `lstm`, `gru`.\n               For reference about the differences between the cells please\n               refer to PyTorch's documentation.\n        :type cell_type: str\n        :param state_size: the size of the state of the rnn.\n        :type state_size: Integer\n        :param bidirectional: if `True` two recurrent networks will perform\n               encoding in the forward and backward direction and\n               their outputs will be concatenated.\n        :type bidirectional: Boolean\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param recurrent_dropout: Dropout rate for the recurrent stack.\n        :type recurrent_dropout: float\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.max_sequence_length = max_sequence_length\n        self.hidden_size = state_size\n        self.embedding_size = embedding_size\n\n        self.should_embed = should_embed\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = EmbedSequence(\n                vocab,\n                embedding_size,\n                max_sequence_length=self.max_sequence_length,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n\n        logger.debug(\"  RecurrentStack\")\n        input_size = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size\n        self.recurrent_stack = RecurrentStack(\n            input_size=input_size,\n            hidden_size=state_size,\n            cell_type=cell_type,\n            max_sequence_length=max_sequence_length,\n            num_layers=num_layers,\n            bidirectional=bidirectional,\n            activation=activation,\n            recurrent_activation=recurrent_activation,\n            use_bias=use_bias,\n            unit_forget_bias=unit_forget_bias,\n            weights_initializer=weights_initializer,\n            recurrent_initializer=recurrent_initializer,\n            bias_initializer=bias_initializer,\n            dropout=recurrent_dropout,\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=self.recurrent_stack.output_shape[-2],\n            encoding_size=self.recurrent_stack.output_shape[-1],  # state_size\n        )\n        if self.reduce_output is None:\n            self.supports_masking = True\n        else:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=fc_activation,\n                default_dropout=fc_dropout,\n            )\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return StackedRNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is not None:\n            return self.fc_stack.output_shape\n        return self.recurrent_stack.output_shape\n\n    def input_dtype(self):\n        return torch.int32\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n\n        # shape=(?, sequence_length, embedding_size)\n        hidden = embedded_sequence\n\n        # ================ Recurrent Layers ================\n        hidden, final_state = self.recurrent_stack(hidden, mask=mask)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state}\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"cnnrnn\")\n@register_encoder(\"cnnrnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\nclass StackedCNNRNN(SequenceEncoder):\n    def __init__(\n        self,\n        should_embed=True,\n        vocab=None,\n        max_sequence_length=None,\n        representation=\"dense\",\n        embedding_size=256,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        conv_layers=None,\n        num_conv_layers=None,\n        num_filters=256,\n        filter_size=5,\n        strides=1,\n        padding=\"same\",\n        dilation_rate=1,\n        conv_activation=\"relu\",\n        conv_dropout=0.0,\n        pool_function=\"max\",\n        pool_size=2,\n        pool_strides=None,\n        pool_padding=\"same\",\n        num_rec_layers=1,\n        state_size=256,\n        cell_type=\"rnn\",\n        bidirectional=False,\n        activation=\"tanh\",\n        recurrent_activation=\"sigmoid\",\n        unit_forget_bias=True,\n        recurrent_initializer=\"orthogonal\",\n        dropout=0.0,\n        recurrent_dropout=0.0,\n        fc_layers=None,\n        num_fc_layers=0,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        fc_activation=\"relu\",\n        fc_dropout=0,\n        reduce_output=\"last\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: fix up docstring\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param num_layers: the number of stacked recurrent layers.\n        :type num_layers: Integer\n        :param cell_type: the type of recurrent cell to use.\n               Available values are: `rnn`, `lstm`, `gru`.\n               For reference about the differences between the cells please\n               refer to PyTorch's documentation.\n        :type cell_type: str\n        :param state_size: the size of the state of the rnn.\n        :type state_size: Integer\n        :param bidirectional: if `True` two recurrent networks will perform\n               encoding in the forward and backward direction and\n               their outputs will be concatenated.\n        :type bidirectional: Boolean\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param recurrent_dropout: Dropout rate for the recurrent stack.\n        :type recurrent_dropout: float\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        if conv_layers is not None and num_conv_layers is None:\n            # use custom-defined layers\n            self.conv_layers = conv_layers\n            self.num_conv_layers = len(conv_layers)\n        elif conv_layers is None and num_conv_layers is not None:\n            # generate num_conv_layers with default parameters\n            self.conv_layers = None\n            self.num_conv_layers = num_conv_layers\n        elif conv_layers is None and num_conv_layers is None:\n            # use default layers with varying filter sizes\n            self.conv_layers = [{\"pool_size\": 3}, {\"pool_size\": None}]\n            self.num_conv_layers = 2\n        else:\n            raise ValueError(\"Invalid layer parametrization, use either conv_layers or \" \"num_conv_layers\")\n\n        self.max_sequence_length = max_sequence_length\n        self.should_embed = should_embed\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = EmbedSequence(\n                vocab,\n                embedding_size,\n                max_sequence_length=self.max_sequence_length,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n\n        logger.debug(\"  Conv1DStack\")\n        in_channels = self.embed_sequence.output_shape[-1] if self.should_embed else embedding_size\n        self.conv1d_stack = Conv1DStack(\n            in_channels=in_channels,\n            max_sequence_length=max_sequence_length,\n            layers=self.conv_layers,\n            default_num_filters=num_filters,\n            default_filter_size=filter_size,\n            default_strides=strides,\n            default_padding=padding,\n            default_dilation_rate=dilation_rate,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=conv_activation,\n            default_dropout=conv_dropout,\n            default_pool_function=pool_function,\n            default_pool_size=pool_size,\n            default_pool_strides=pool_strides,\n            default_pool_padding=pool_padding,\n        )\n\n        logger.debug(\"  RecurrentStack\")\n        self.recurrent_stack = RecurrentStack(\n            input_size=self.conv1d_stack.output_shape[1],\n            hidden_size=state_size,\n            max_sequence_length=self.conv1d_stack.output_shape[0],\n            cell_type=cell_type,\n            num_layers=num_rec_layers,\n            bidirectional=bidirectional,\n            activation=activation,\n            recurrent_activation=recurrent_activation,\n            use_bias=use_bias,\n            unit_forget_bias=unit_forget_bias,\n            weights_initializer=weights_initializer,\n            recurrent_initializer=recurrent_initializer,\n            bias_initializer=bias_initializer,\n            dropout=recurrent_dropout,\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=self.recurrent_stack.output_shape[-2],\n            encoding_size=self.recurrent_stack.output_shape[-1],  # State size\n        )\n        if self.reduce_output is not None:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=fc_activation,\n                default_dropout=fc_dropout,\n            )\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return StackedCNNRNNConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is not None:\n            return self.fc_stack.output_shape\n        return self.recurrent_stack.output_shape\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n\n        # shape=(?, sequence_length, embedding_size)\n        hidden = embedded_sequence\n\n        # ================ Conv Layers ================\n        hidden = self.conv1d_stack(hidden, mask=mask)\n\n        # ================ Recurrent Layers ================\n        hidden, final_state = self.recurrent_stack(hidden)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        # no reduction: hidden [batch_size, seq_size, state_size]\n        # with reduction: hidden [batch_size, seq_size, output_size]\n        # final_state: if rnn/gru [batch_size, state_size]\n        #              lstm ([batch_size, state_size], [batch_size, state_size])\n        return {ENCODER_OUTPUT: hidden, ENCODER_OUTPUT_STATE: final_state}\n\n\n@DeveloperAPI\n@register_sequence_encoder(\"transformer\")\n@register_encoder(\"transformer\", [SEQUENCE, TEXT, TIMESERIES])\nclass StackedTransformer(SequenceEncoder):\n    def __init__(\n        self,\n        max_sequence_length,\n        should_embed=True,\n        vocab=None,\n        representation=\"dense\",\n        embedding_size=256,\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        embeddings_on_cpu=False,\n        num_layers=1,\n        hidden_size=256,\n        num_heads=8,\n        transformer_output_size=256,\n        dropout=0.1,\n        fc_layers=None,\n        num_fc_layers=0,\n        output_size=256,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        fc_activation=\"relu\",\n        fc_dropout=0,\n        reduce_output=\"last\",\n        encoder_config=None,\n        **kwargs,\n    ):\n        # todo: update docstring as needed\n        \"\"\"\n        :param should_embed: If True the input sequence is expected\n               to be made of integers and will be mapped into embeddings\n        :type should_embed: Boolean\n        :param vocab: Vocabulary of the input feature to encode\n        :type vocab: List\n        :param representation: the possible values are `dense` and `sparse`.\n               `dense` means the embeddings are initialized randomly,\n               `sparse` means they are initialized to be one-hot encodings.\n        :type representation: Str (one of 'dense' or 'sparse')\n        :param embedding_size: it is the maximum embedding size, the actual\n               size will be `min(vocabulary_size, embedding_size)`\n               for `dense` representations and exactly `vocabulary_size`\n               for the `sparse` encoding, where `vocabulary_size` is\n               the number of different strings appearing in the training set\n               in the column the feature is named after (plus 1 for `<UNK>`).\n        :type embedding_size: Integer\n        :param embeddings_trainable: If `True` embeddings are trained during\n               the training process, if `False` embeddings are fixed.\n               It may be useful when loading pretrained embeddings\n               for avoiding finetuning them. This parameter has effect only\n               for `representation` is `dense` as `sparse` one-hot encodings\n                are not trainable.\n        :type embeddings_trainable: Boolean\n        :param pretrained_embeddings: by default `dense` embeddings\n               are initialized randomly, but this parameter allows to specify\n               a path to a file containing embeddings in the GloVe format.\n               When the file containing the embeddings is loaded, only the\n               embeddings with labels present in the vocabulary are kept,\n               the others are discarded. If the vocabulary contains strings\n               that have no match in the embeddings file, their embeddings\n               are initialized with the average of all other embedding plus\n               some random noise to make them different from each other.\n               This parameter has effect only if `representation` is `dense`.\n        :type pretrained_embeddings: str (filepath)\n        :param embeddings_on_cpu: by default embeddings matrices are stored\n               on GPU memory if a GPU is used, as it allows\n               for faster access, but in some cases the embedding matrix\n               may be really big and this parameter forces the placement\n               of the embedding matrix in regular memroy and the CPU is used\n               to resolve them, slightly slowing down the process\n               as a result of data transfer between CPU and GPU memory.\n        :param conv_layers: it is a list of dictionaries containing\n               the parameters of all the convolutional layers. The length\n               of the list determines the number of parallel convolutional\n               layers and the content of each dictionary determines\n               the parameters for a specific layer. The available parameters\n               for each layer are: `filter_size`, `num_filters`, `pool`,\n               `norm`, `activation` and `regularize`. If any of those values\n               is missing from the dictionary, the default one specified\n               as a parameter of the encoder will be used instead. If both\n               `conv_layers` and `num_conv_layers` are `None`, a default\n               list will be assigned to `conv_layers` with the value\n               `[{filter_size: 2}, {filter_size: 3}, {filter_size: 4},\n               {filter_size: 5}]`.\n        :type conv_layers: List\n        :param num_conv_layers: if `conv_layers` is `None`, this is\n               the number of stacked convolutional layers.\n        :type num_conv_layers: Integer\n        :param filter_size:  if a `filter_size` is not already specified in\n               `conv_layers` this is the default `filter_size` that\n               will be used for each layer. It indicates how wide is\n               the 1d convolutional filter.\n        :type filter_size: Integer\n        :param num_filters: if a `num_filters` is not already specified in\n               `conv_layers` this is the default `num_filters` that\n               will be used for each layer. It indicates the number\n               of filters, and by consequence the output channels of\n               the 1d convolution.\n        :type num_filters: Integer\n        :param pool_size: if a `pool_size` is not already specified\n              in `conv_layers` this is the default `pool_size` that\n              will be used for each layer. It indicates the size of\n              the max pooling that will be performed along the `s` sequence\n              dimension after the convolution operation.\n        :type pool_size: Integer\n        :param num_rec_layers: the number of stacked recurrent layers.\n        :type num_rec_layers: Integer\n        :param cell_type: the type of recurrent cell to use.\n               Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`,\n               `ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`.\n               For reference about the differences between the cells please\n               refer to PyTorch's documentation. We suggest to use the\n               `block` variants on CPU and the `cudnn` variants on GPU\n               because of their increased speed.\n        :type cell_type: str\n        :param state_size: the size of the state of the rnn.\n        :type state_size: Integer\n        :param bidirectional: if `True` two recurrent networks will perform\n               encoding in the forward and backward direction and\n               their outputs will be concatenated.\n        :type bidirectional: Boolean\n        :param dropout: determines if there should be a dropout layer before\n               returning the encoder output.\n        :type dropout: Boolean\n        :param initializer: the initializer to use. If `None` it uses\n               `xavier_uniform`. Options are: `constant`, `identity`,\n               `zeros`, `ones`, `orthogonal`, `normal`, `uniform`,\n               `truncated_normal`, `variance_scaling`, `xavier_normal`,\n               `xavier_uniform`, `xavier_normal`,\n               `he_normal`, `he_uniform`, `lecun_normal`, `lecun_uniform`.\n               Alternatively it is possible to specify a dictionary with\n               a key `type` that identifies the type of initializer and\n               other keys for its parameters,\n               e.g. `{type: normal, mean: 0, stddev: 0}`.\n               To know the parameters of each initializer, please refer\n               to PyTorch's documentation.\n        :type initializer: str\n        :param reduce_output: defines how to reduce the output tensor of\n               the convolutional layers along the `s` sequence length\n               dimension if the rank of the tensor is greater than 2.\n               Available values are: `sum`, `mean` or `avg`, `max`, `concat`\n               (concatenates along the first dimension), `last` (returns\n               the last vector of the first dimension) and `None` or `null`\n               (which does not reduce and returns the full tensor).\n        :type reduce_output: str\n        \"\"\"\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.max_sequence_length = max_sequence_length\n\n        self.should_embed = should_embed\n        self.should_project = False\n        self.embed_sequence = None\n\n        if self.should_embed:\n            logger.debug(\"  EmbedSequence\")\n            self.embed_sequence = TokenAndPositionEmbedding(\n                max_sequence_length=max_sequence_length,\n                vocab=vocab,\n                embedding_size=embedding_size,\n                representation=representation,\n                embeddings_trainable=embeddings_trainable,\n                pretrained_embeddings=pretrained_embeddings,\n                embeddings_on_cpu=embeddings_on_cpu,\n                dropout=dropout,\n                embedding_initializer=weights_initializer,\n            )\n            # If vocab size is smaller than embedding size, embedding layer will use len(vocab) as embedding_size.\n            used_embedding_size = self.embed_sequence.output_shape[-1]\n            if used_embedding_size != hidden_size:\n                logger.debug(\"  project_to_embed_size\")\n                self.project_to_hidden_size = nn.Linear(self.embed_sequence.output_shape[-1], hidden_size)\n                self.should_project = True\n        else:\n            logger.debug(\"  project_to_embed_size\")\n            self.project_to_hidden_size = nn.Linear(embedding_size, hidden_size)\n            self.should_project = True\n\n        logger.debug(\"  TransformerStack\")\n        self.transformer_stack = TransformerStack(\n            input_size=hidden_size,\n            max_sequence_length=max_sequence_length,\n            hidden_size=hidden_size,\n            num_heads=num_heads,\n            output_size=transformer_output_size,\n            num_layers=num_layers,\n            dropout=dropout,\n        )\n\n        self.reduce_output = reduce_output\n        self.reduce_sequence = SequenceReducer(\n            reduce_mode=reduce_output,\n            max_sequence_length=self.transformer_stack.output_shape[-2],\n            encoding_size=self.transformer_stack.output_shape[-1],  # hidden_size\n        )\n        if self.reduce_output is None:\n            self.supports_masking = True\n        else:\n            logger.debug(\"  FCStack\")\n            self.fc_stack = FCStack(\n                self.reduce_sequence.output_shape[-1],\n                layers=fc_layers,\n                num_layers=num_fc_layers,\n                default_output_size=output_size,\n                default_use_bias=use_bias,\n                default_weights_initializer=weights_initializer,\n                default_bias_initializer=bias_initializer,\n                default_norm=norm,\n                default_norm_params=norm_params,\n                default_activation=fc_activation,\n                default_dropout=fc_dropout,\n            )\n\n    @staticmethod\n    def get_schema_cls() -> type[SequenceEncoderConfig]:\n        return StackedTransformerConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is not None:\n            return self.fc_stack.output_shape\n        return self.transformer_stack.output_shape\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        \"\"\"\n        :param inputs: The input sequence fed into the encoder.\n               Shape: [batch x sequence length], type torch.int32\n        :param mask: Input mask (unused, not yet implemented)\n        \"\"\"\n        # ================ Embeddings ================\n        if self.should_embed:\n            embedded_sequence = self.embed_sequence(inputs, mask=mask)\n        else:\n            embedded_sequence = inputs\n            while len(embedded_sequence.shape) < 3:\n                embedded_sequence = embedded_sequence.unsqueeze(-1)\n\n        # shape=(?, sequence_length, embedding_size)\n        if self.should_project:\n            hidden = self.project_to_hidden_size(embedded_sequence)\n        else:\n            hidden = embedded_sequence\n        # shape=(?, sequence_length, hidden)\n\n        # ================ Transformer Layers ================\n        hidden = self.transformer_stack(hidden, mask=mask)\n\n        # ================ Sequence Reduction ================\n        if self.reduce_output is not None:\n            hidden = self.reduce_sequence(hidden)\n\n            # ================ FC Layers ================\n            hidden = self.fc_stack(hidden, mask=mask)\n\n        return {ENCODER_OUTPUT: hidden}\n"
  },
  {
    "path": "ludwig/encoders/set_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, SET\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.embedding_modules import EmbedSet\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.set_encoders import SetSparseEncoderConfig\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\n@register_encoder(\"embed\", SET)\nclass SetSparseEncoder(Encoder):\n    def __init__(\n        self,\n        vocab: list[str],\n        representation: str = \"dense\",\n        embedding_size: int = 50,\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        embeddings_on_cpu: bool = False,\n        fc_layers=None,\n        num_fc_layers: int = 0,\n        output_size: int = 10,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict[str, Any] | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0.0,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n\n        logger.debug(f\" {self.name}\")\n\n        self.vocab_size = len(vocab)\n\n        logger.debug(\"  Embed\")\n        self.embed = EmbedSet(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=weights_initializer,\n        )\n\n        logger.debug(\"  FCStack\")\n        # TODO(shreya): Make sure this is updated when FCStack is updated\n        self.fc_stack = FCStack(\n            first_layer_input_size=self.embed.output_shape[-1],\n            layers=fc_layers,\n            num_layers=num_fc_layers,\n            default_output_size=output_size,\n            default_use_bias=use_bias,\n            default_weights_initializer=weights_initializer,\n            default_bias_initializer=bias_initializer,\n            default_norm=norm,\n            default_norm_params=norm_params,\n            default_activation=activation,\n            default_dropout=dropout,\n        )\n\n    def forward(self, inputs: torch.Tensor) -> EncoderOutputDict:\n        \"\"\"\n        Params:\n            inputs: The inputs fed into the encoder.\n                    Shape: [batch x vocab_size], type tf.int32.\n\n        Returns:\n            Embeddings of shape [batch x vocab_size x embed size], type float32.\n        \"\"\"\n        hidden = self.embed(inputs)\n        hidden = self.fc_stack(hidden)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return SetSparseEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.vocab_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.fc_stack.output_shape\n"
  },
  {
    "path": "ludwig/encoders/text_encoders.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport inspect\nimport logging\nfrom collections.abc import Callable\nfrom typing import Any, TYPE_CHECKING, TypeVar\n\nimport numpy as np\nimport torch\nfrom torch import nn\nfrom transformers import AutoConfig\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, TEXT\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.encoders.types import EncoderOutputDict\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig\nfrom ludwig.schema.encoders.text_encoders import (\n    ALBERTConfig,\n    AutoTransformerConfig,\n    BERTConfig,\n    CamemBERTConfig,\n    CTRLConfig,\n    DebertaV2Config,\n    DistilBERTConfig,\n    ELECTRAConfig,\n    FlauBERTConfig,\n    GPT2Config,\n    GPTConfig,\n    LLMEncoderConfig,\n    LongformerConfig,\n    MT5Config,\n    RoBERTaConfig,\n    T5Config,\n    TfIdfEncoderConfig,\n    TransformerXLConfig,\n    XLMConfig,\n    XLMRoBERTaConfig,\n    XLNetConfig,\n)\nfrom ludwig.schema.llms.peft import adapter_registry, BaseAdapterConfig\nfrom ludwig.utils.data_utils import clear_data_cache\nfrom ludwig.utils.hf_utils import load_pretrained_hf_model_with_hub_fallback\nfrom ludwig.utils.llm_utils import get_context_len, initialize_adapter, load_pretrained_from_config\nfrom ludwig.utils.tokenizers import HFTokenizer\nfrom ludwig.utils.torch_utils import FreezeModule\n\nif TYPE_CHECKING:\n    from transformers import PretrainedConfig, PreTrainedModel\n\n    from ludwig.schema.encoders.text_encoders import HFEncoderConfig\n\nlogger = logging.getLogger(__name__)\n\n\ndef _cls_pooled_error_message(encoder: str):\n    # TODO(Arnav): Remove this once we have reduce_output options set for\n    # each encoder type in the schema\n    raise ValueError(f\"reduce_output cannot be cls_pooled for {encoder}\")\n\n\nclass HFTextEncoder(Encoder):\n    def _init_config(self, transformer, schema_keys: list[str], encoder_config: SequenceEncoderConfig):\n        \"\"\"Creates a config object for the encoder using the transformer model and the passed-in encoder config.\n\n        The transformer's config is only known after it is instantiated, so we must update the\n        encoder config with the values from the transformer config.\n\n        Args:\n            transformer: The transformer model.\n            schema_keys: The keys in the encoder config schema. We only want to update the encoder config\n                with the values from the transformer config that are in the schema.\n            encoder_config: The existing encoder config containing defaults and user-specified values.\n                If the values in this config differ from the transformer's config, the transformer's config\n                values will override this config's values.\n        Returns:\n            A new encoder config object with the updated values from the transformer config.\n        \"\"\"\n        transformer_config = transformer.config.to_dict()\n        final_hf_config_params = {k: v for k, v in transformer_config.items() if k in schema_keys}\n        encoder_config_dict = encoder_config.to_dict()\n        encoder_config_dict.update(final_hf_config_params)\n        return self.get_schema_cls().from_dict(encoder_config_dict)\n\n    def _init_transformer_from_scratch(\n        self, hf_model_cls: type, hf_config_cls: type, hf_config_params: dict[str, Any], vocab_size: int\n    ):\n        \"\"\"Initializes the transformer model from scratch. This is in contrast to loading a pre-trained model.\n\n        Args:\n            hf_model_cls: The HuggingFace model class.\n            hf_config_cls: The HuggingFace config class.\n            hf_config_params: The HuggingFace config parameters exposed through the Ludwig schema.\n            vocab_size: The vocab size of the dataset. Because we are training from scratch, we can resize the\n                token embeddings table freely.\n        Returns:\n            The transformer model.\n        \"\"\"\n        config = hf_config_cls(**hf_config_params)\n        transformer = hf_model_cls(config)\n        self._maybe_resize_token_embeddings(transformer, vocab_size)\n        return transformer\n\n    def _maybe_resize_token_embeddings(self, transformer, vocab_size: int) -> None:\n        \"\"\"Resizes the token embeddings if the vocab size is different from the transformer's vocab size.\n\n        This should only happen if we are instantiating a model from scratch (i.e. not loading from a pretrained model\n        or checkpoint). Pretrained models update the vocab size stored in the config. This means if we are loading a\n        pretrained model from a checkpoint, the config vocab size should match the model's vocab size.\n\n        It is important that pretrained models update the vocab size stored in the config because sometimes the\n        pretrained models will have an embeddings table that is a different size than the vocab size. Examples:\n\n        CamemBERT:  https://github.com/huggingface/tokenizers/issues/900#issue-1122256698\n        T5:         https://github.com/huggingface/transformers/issues/4875#issue-635471552\n\n        Args:\n            transformer: The transformer model.\n            vocab_size: The vocab size of the dataset.\n        \"\"\"\n        if vocab_size != transformer.config.vocab_size:\n            transformer.resize_token_embeddings(vocab_size)\n\n    def _wrap_transformer(\n        self, transformer: nn.Module, adapter: BaseAdapterConfig | dict | None, trainable: bool\n    ) -> nn.Module:\n        if adapter is not None:\n            from peft import get_peft_model\n\n            if isinstance(adapter, dict):\n                adapter_cls = adapter_registry[adapter[\"type\"]]\n                adapter = adapter_cls.model_validate(adapter)\n            peft_config = adapter.to_config()\n            transformer = get_peft_model(transformer, peft_config)\n\n            logger.info(\"==================================================\")\n            logger.info(\"Trainable Parameter Summary For Fine-Tuning:\")\n            transformer.print_trainable_parameters()\n            logger.info(\"==================================================\")\n        return FreezeModule(transformer, frozen=not trainable)\n\n    def get_embedding_layer(self) -> nn.Module:\n        return next(self.transformer.module.children())\n\n\nHFModelT = TypeVar(\"HFModelT\", bound=\"PreTrainedModel\")\nHFConfigT = TypeVar(\"HFConfigT\", bound=\"PretrainedConfig\")\nConfigT = TypeVar(\"ConfigT\", bound=\"HFEncoderConfig\")\n\n\nclass HFTextEncoderImpl(HFTextEncoder):\n    def __init__(\n        self,\n        model_cls: type[HFModelT],\n        config_cls: type[HFConfigT],\n        schema_cls: type[ConfigT],\n        max_sequence_length: int,\n        use_pretrained: bool,\n        pretrained_model_name_or_path: str,\n        saved_weights_in_checkpoint: bool,\n        reduce_output: str,\n        trainable: bool,\n        adapter: BaseAdapterConfig | None,\n        pretrained_kwargs: dict,\n        encoder_config: ConfigT | None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        # TODO(travis): get_hf_config_param_names should be implemented as abstract in HFEncoderConfig\n        vocab_size = kwargs[\"vocab_size\"]\n        hf_config_params = {k: v for k, v in kwargs.items() if k in schema_cls.get_hf_config_param_names()}\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                model_cls, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(model_cls, config_cls, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[\"pooler_output\"]\n        else:\n            hidden = transformer_outputs[\"last_hidden_state\"][:, 1:-1, :]  # bos + [sent] + sep\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.hidden_size])\n        if self.reduce_output == \"concat\":\n            return torch.Size(\n                [\n                    (self.max_sequence_length - 2) * self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"albert\", TEXT)\nclass ALBERTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"albert-base-v2\"\n\n    def __init__(\n        self,\n        max_sequence_length,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        reduce_output: str = \"cls_pooled\",\n        vocab_size: int = 30000,\n        embedding_size: int = 128,\n        hidden_size: int = 4096,\n        num_hidden_layers: int = 12,\n        num_hidden_groups: int = 1,\n        num_attention_heads: int = 64,\n        intermediate_size: int = 16384,\n        inner_group_num: int = 1,\n        hidden_act: str = \"gelu_new\",\n        hidden_dropout_prob: float = 0,\n        attention_probs_dropout_prob: float = 0,\n        max_position_embeddings: int = 512,\n        type_vocab_size: int = 2,\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        classifier_dropout_prob: float = 0.1,\n        position_embedding_type: str = \"absolute\",\n        pad_token_id: int = 0,\n        bos_token_id: int = 2,\n        eos_token_id: int = 3,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import AlbertConfig, AlbertModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            embedding_size=embedding_size,\n            hidden_size=hidden_size,\n            num_hidden_layers=num_hidden_layers,\n            num_hidden_groups=num_hidden_groups,\n            num_attention_heads=num_attention_heads,\n            intermediate_size=intermediate_size,\n            inner_group_num=inner_group_num,\n            hidden_act=hidden_act,\n            hidden_dropout_prob=hidden_dropout_prob,\n            attention_probs_dropout_prob=attention_probs_dropout_prob,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n            initializer_range=initializer_range,\n            layer_norm_eps=layer_norm_eps,\n            classifier_dropout_prob=classifier_dropout_prob,\n            position_embedding_type=position_embedding_type,\n            pad_token_id=pad_token_id,\n            bos_token_id=bos_token_id,\n            eos_token_id=eos_token_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                AlbertModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(AlbertModel, AlbertConfig, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return ALBERTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"mt5\", TEXT)\nclass MT5Encoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"google/mt5-base\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        reduce_output: str = \"sum\",\n        vocab_size: int = 250112,\n        d_model: int = 512,\n        d_kv: int = 64,\n        d_ff: int = 1024,\n        num_layers: int = 8,\n        num_decoder_layers: int = None,\n        num_heads: int = 6,\n        relative_attention_num_buckets: int = 32,\n        dropout_rate: float = 0.1,\n        layer_norm_epsilon: float = 1e-06,\n        initializer_factor: float = 1.0,\n        feed_forward_proj: str = \"gated-gelu\",\n        is_encoder_decoder: bool = True,\n        use_cache: bool = True,\n        tokenizer_class: str = \"T5Tokenizer\",\n        tie_word_embeddings: bool = False,\n        pad_token_id: int = 0,\n        eos_token_id: int = 1,\n        decoder_start_token_id: int = 0,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import MT5Config, MT5EncoderModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            d_model=d_model,\n            d_kv=d_kv,\n            d_ff=d_ff,\n            num_layers=num_layers,\n            num_decoder_layers=num_decoder_layers,\n            num_heads=num_heads,\n            relative_attention_num_buckets=relative_attention_num_buckets,\n            dropout_rate=dropout_rate,\n            layer_norm_epsilon=layer_norm_epsilon,\n            initializer_factor=initializer_factor,\n            feed_forward_proj=feed_forward_proj,\n            is_encoder_decoder=is_encoder_decoder,\n            use_cache=use_cache,\n            tokenizer_class=tokenizer_class,\n            tie_word_embeddings=tie_word_embeddings,\n            pad_token_id=pad_token_id,\n            eos_token_id=eos_token_id,\n            decoder_start_token_id=decoder_start_token_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                MT5EncoderModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(MT5EncoderModel, MT5Config, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n        )\n        hidden = transformer_outputs[0][:, 1:-1, :]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return MT5Config\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by MT5 tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"xlmroberta\", TEXT)\nclass XLMRoBERTaEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"xlm-roberta-base\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"cls_pooled\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = None,\n        pad_token_id: int = 1,\n        bos_token_id: int = 0,\n        eos_token_id: int = 2,\n        max_position_embeddings: int = 514,\n        type_vocab_size: int = 1,\n        add_pooling_layer: bool = True,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import XLMRobertaConfig, XLMRobertaModel\n\n        hf_config_params = dict(\n            pad_token_id=pad_token_id,\n            bos_token_id=bos_token_id,\n            eos_token_id=eos_token_id,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                XLMRobertaModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                XLMRobertaModel, XLMRobertaConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return XLMRoBERTaConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by XLMRoberta tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"bert\", TEXT)\nclass BERTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"bert-base-uncased\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        reduce_output: str = \"cls_pooled\",\n        vocab_size: int = 30522,\n        hidden_size: int = 768,\n        num_hidden_layers: int = 12,\n        num_attention_heads: int = 12,\n        intermediate_size: int = 3072,\n        hidden_act: str | Callable = \"gelu\",\n        hidden_dropout_prob: float = 0.1,\n        attention_probs_dropout_prob: float = 0.1,\n        max_position_embeddings: int = 512,\n        type_vocab_size: int = 2,\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        pad_token_id: int = 0,\n        gradient_checkpointing: bool = False,\n        position_embedding_type: str = \"absolute\",\n        classifier_dropout: float = None,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import BertConfig, BertModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            hidden_size=hidden_size,\n            num_hidden_layers=num_hidden_layers,\n            num_attention_heads=num_attention_heads,\n            intermediate_size=intermediate_size,\n            hidden_act=hidden_act,\n            hidden_dropout_prob=hidden_dropout_prob,\n            attention_probs_dropout_prob=attention_probs_dropout_prob,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n            initializer_range=initializer_range,\n            layer_norm_eps=layer_norm_eps,\n            pad_token_id=pad_token_id,\n            gradient_checkpointing=gradient_checkpointing,\n            position_embedding_type=position_embedding_type,\n            classifier_dropout=classifier_dropout,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                BertModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(BertModel, BertConfig, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return BERTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    # TODO(shreya): Confirm that this is it\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"xlm\", TEXT)\nclass XLMEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"xlm-mlm-en-2048\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        reduce_output: str = \"sum\",\n        vocab_size: int = 30145,\n        emb_dim: int = 2048,\n        n_layers: int = 12,\n        n_heads: int = 16,\n        dropout: float = 0.1,\n        attention_dropout: float = 0.1,\n        gelu_activation: bool = True,\n        sinusoidal_embeddings: bool = False,\n        causal: bool = False,\n        asm: bool = False,\n        n_langs: int = 1,\n        use_lang_emb: bool = True,\n        max_position_embeddings: int = 512,\n        embed_init_std: float = 2048**-0.5,\n        layer_norm_eps: float = 1e-12,\n        init_std: float = 0.02,\n        bos_index: int = 0,\n        eos_index: int = 1,\n        pad_index: int = 2,\n        unk_index: int = 3,\n        mask_index: int = 5,\n        is_encoder: bool = True,\n        start_n_top: int = 5,\n        end_n_top: int = 5,\n        mask_token_id: int = 0,\n        lang_id: int = 0,\n        pad_token_id: int = 2,\n        bos_token_id: int = 0,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import XLMConfig, XLMModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            emb_dim=emb_dim,\n            n_layers=n_layers,\n            n_heads=n_heads,\n            dropout=dropout,\n            attention_dropout=attention_dropout,\n            gelu_activation=gelu_activation,\n            sinusoidal_embeddings=sinusoidal_embeddings,\n            causal=causal,\n            asm=asm,\n            n_langs=n_langs,\n            use_lang_emb=use_lang_emb,\n            max_position_embeddings=max_position_embeddings,\n            embed_init_std=embed_init_std,\n            layer_norm_eps=layer_norm_eps,\n            init_std=init_std,\n            bos_index=bos_index,\n            eos_index=eos_index,\n            pad_index=pad_index,\n            unk_index=unk_index,\n            mask_index=mask_index,\n            is_encoder=is_encoder,\n            start_n_top=start_n_top,\n            end_n_top=end_n_top,\n            mask_token_id=mask_token_id,\n            lang_id=lang_id,\n            pad_token_id=pad_token_id,\n            bos_token_id=bos_token_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                XLMModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(XLMModel, XLMConfig, hf_config_params, vocab_size)\n\n        self.config = self._init_config(transformer, hf_config_params, encoder_config)\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return XLMConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    # TODO(shreya): Confirm that this is it\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"gpt\", TEXT)\nclass GPTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"openai-gpt\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        reduce_output: str = \"sum\",\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 30522,\n        n_positions: int = 40478,\n        n_ctx: int = 512,\n        n_embd: int = 768,\n        n_layer: int = 12,\n        n_head: int = 12,\n        afn: str = \"gelu\",\n        resid_pdrop: float = 0.1,\n        embd_pdrop: float = 0.1,\n        attn_pdrop: float = 0.1,\n        layer_norm_epsilon: float = 1e-5,\n        initializer_range: float = 0.02,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import OpenAIGPTConfig, OpenAIGPTModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            n_positions=n_positions,\n            n_ctx=n_ctx,\n            n_embd=n_embd,\n            n_layer=n_layer,\n            n_head=n_head,\n            afn=afn,\n            resid_pdrop=resid_pdrop,\n            embd_pdrop=embd_pdrop,\n            attn_pdrop=attn_pdrop,\n            layer_norm_epsilon=layer_norm_epsilon,\n            initializer_range=initializer_range,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                OpenAIGPTModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                OpenAIGPTModel, OpenAIGPTConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return GPTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size])\n        elif self.reduce_output == \"concat\":\n            return torch.Size([self.transformer.module.config.hidden_size * self.max_sequence_length])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"gpt2\", TEXT)\nclass GPT2Encoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"gpt2\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 50257,\n        n_positions: int = 1024,\n        n_ctx: int = 1024,\n        n_embd: int = 768,\n        n_layer: int = 12,\n        n_head: int = 12,\n        n_inner: int | None = None,\n        activation_function: str = \"gelu\",\n        resid_pdrop: float = 0.1,\n        embd_pdrop: float = 0.1,\n        attn_pdrop: float = 0.1,\n        layer_norm_epsilon: float = 1e-5,\n        initializer_range: float = 0.02,\n        scale_attn_weights: bool = True,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import GPT2Config, GPT2Model\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            n_positions=n_positions,\n            n_ctx=n_ctx,\n            n_embd=n_embd,\n            n_layer=n_layer,\n            n_head=n_head,\n            n_inner=n_inner,\n            activation_function=activation_function,\n            resid_pdrop=resid_pdrop,\n            embd_pdrop=embd_pdrop,\n            attn_pdrop=attn_pdrop,\n            layer_norm_epsilon=layer_norm_epsilon,\n            initializer_range=initializer_range,\n            scale_attn_weights=scale_attn_weights,\n        )\n\n        if use_pretrained:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                GPT2Model, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(GPT2Model, GPT2Config, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return GPT2Config\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size])\n        elif self.reduce_output == \"concat\":\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"deberta\", TEXT)\nclass DeBERTaEncoder(HFTextEncoderImpl):\n    def __init__(self, *args, **kwargs):\n        from transformers import DebertaV2Config as _DebertaV2Config\n        from transformers import DebertaV2Model\n\n        super().__init__(DebertaV2Model, _DebertaV2Config, DebertaV2Config, *args, **kwargs)\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return DebertaV2Config\n\n\n@DeveloperAPI\n@register_encoder(\"roberta\", TEXT)\nclass RoBERTaEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"roberta-base\"\n\n    def __init__(\n        self,\n        max_sequence_length,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"cls_pooled\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = None,\n        pad_token_id: int = 1,\n        bos_token_id: int = 0,\n        eos_token_id: int = 2,\n        max_position_embeddings: int = 514,\n        type_vocab_size: int = 1,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import RobertaConfig, RobertaModel\n\n        hf_config_params = dict(\n            pad_token_id=pad_token_id,\n            bos_token_id=bos_token_id,\n            eos_token_id=eos_token_id,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                RobertaModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(RobertaModel, RobertaConfig, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]  # bos + [sent] + sep\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return RoBERTaConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.hidden_size])\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"transformer_xl\", TEXT)\nclass TransformerXLEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"transfo-xl-wt103\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 267735,\n        cutoffs: list[int] = [20000, 40000, 200000],\n        d_model: int = 1024,\n        d_embed: int = 1024,\n        n_head: int = 16,\n        d_head: int = 64,\n        d_inner: int = 4096,\n        div_val: int = 4,\n        pre_lnorm: bool = False,\n        n_layer: int = 18,\n        mem_len: int = 1600,\n        clamp_len: int = 1000,\n        same_length: bool = True,\n        proj_share_all_but_first: bool = True,\n        attn_type: int = 0,\n        sample_softmax: int = -1,\n        adaptive: bool = True,\n        dropout: float = 0.1,\n        dropatt: float = 0.0,\n        untie_r: bool = True,\n        init: str = \"normal\",\n        init_range: float = 0.01,\n        proj_init_std: float = 0.01,\n        init_std: float = 0.02,\n        layer_norm_epsilon: float = 1e-5,\n        eos_token_id: int = 0,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import TransfoXLConfig, TransfoXLModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            cutoffs=cutoffs,\n            d_model=d_model,\n            d_embed=d_embed,\n            n_head=n_head,\n            d_head=d_head,\n            d_inner=d_inner,\n            div_val=div_val,\n            pre_lnorm=pre_lnorm,\n            n_layer=n_layer,\n            mem_len=mem_len,\n            clamp_len=clamp_len,\n            same_length=same_length,\n            proj_share_all_but_first=proj_share_all_but_first,\n            attn_type=attn_type,\n            sample_softmax=sample_softmax,\n            adaptive=adaptive,\n            dropout=dropout,\n            dropatt=dropatt,\n            untie_r=untie_r,\n            init=init,\n            init_range=init_range,\n            proj_init_std=proj_init_std,\n            init_std=init_std,\n            layer_norm_epsilon=layer_norm_epsilon,\n            eos_token_id=eos_token_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                TransfoXLModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            config = TransfoXLConfig(**hf_config_params)\n            transformer = TransfoXLModel(config)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        transformer_outputs = self.transformer.module(inputs)\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TransformerXLConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.d_model])\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.d_model * self.max_sequence_length])\n        return torch.Size([self.transformer.module.config.d_model])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"xlnet\", TEXT)\nclass XLNetEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"xlnet-base-cased\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 32000,\n        d_model: int = 1024,\n        n_layer: int = 24,\n        n_head: int = 16,\n        d_inner: int = 4096,\n        ff_activation: str = \"gelu\",\n        untie_r: bool = True,\n        attn_type: str = \"bi\",\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        dropout: float = 0.1,\n        mem_len: int | None = 512,\n        reuse_len: int | None = None,\n        use_mems_eval: bool = True,\n        use_mems_train: bool = False,\n        bi_data: bool = False,\n        clamp_len: int = -1,\n        same_length: bool = False,\n        summary_type: str = \"last\",\n        summary_use_proj: bool = True,\n        summary_activation: str = \"tanh\",\n        summary_last_dropout: float = 0.1,\n        start_n_top: int = 5,\n        end_n_top: int = 5,\n        pad_token_id: int = 5,\n        bos_token_id: int = 1,\n        eos_token_id: int = 2,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import XLNetConfig, XLNetModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            d_model=d_model,\n            n_layer=n_layer,\n            n_head=n_head,\n            d_inner=d_inner,\n            ff_activation=ff_activation,\n            untie_r=untie_r,\n            attn_type=attn_type,\n            initializer_range=initializer_range,\n            layer_norm_eps=layer_norm_eps,\n            dropout=dropout,\n            mem_len=mem_len,\n            reuse_len=reuse_len,\n            use_mems_eval=use_mems_eval,\n            use_mems_train=use_mems_train,\n            bi_data=bi_data,\n            clamp_len=clamp_len,\n            same_length=same_length,\n            summary_type=summary_type,\n            summary_use_proj=summary_use_proj,\n            summary_activation=summary_activation,\n            summary_last_dropout=summary_last_dropout,\n            start_n_top=start_n_top,\n            end_n_top=end_n_top,\n            pad_token_id=pad_token_id,\n            bos_token_id=bos_token_id,\n            eos_token_id=eos_token_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                XLNetModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(XLNetModel, XLNetConfig, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return XLNetConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.d_model])\n        elif self.reduce_output == \"concat\":\n            return torch.Size([self.transformer.module.config.d_model * self.max_sequence_length])\n        return torch.Size([self.transformer.module.config.d_model])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"distilbert\", TEXT)\nclass DistilBERTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"distilbert-base-uncased\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        use_pretrained: bool = True,\n        vocab_size: int = 30522,\n        max_position_embeddings: int = 512,\n        sinusoidal_pos_embds: bool = False,\n        n_layers: int = 6,\n        n_heads: int = 12,\n        dim: int = 768,\n        hidden_dim: int = 3072,\n        dropout: float = 0.1,\n        attention_dropout: float = 0.1,\n        activation: str | Callable = \"gelu\",\n        initializer_range: float = 0.02,\n        qa_dropout: float = 0.1,\n        seq_classif_dropout: float = 0.2,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import DistilBertConfig, DistilBertModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            max_position_embeddings=max_position_embeddings,\n            sinusoidal_pos_embds=sinusoidal_pos_embds,\n            n_layers=n_layers,\n            n_heads=n_heads,\n            dim=dim,\n            hidden_dim=hidden_dim,\n            dropout=dropout,\n            attention_dropout=attention_dropout,\n            activation=activation,\n            initializer_range=initializer_range,\n            qa_dropout=qa_dropout,\n            seq_classif_dropout=seq_classif_dropout,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                DistilBertModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                DistilBertModel, DistilBertConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.max_sequence_length = max_sequence_length\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.last_inputs = None\n        self.last_hidden = None\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n        )\n        hidden = transformer_outputs[0][:, 1:-1, :]\n        self.last_inputs = inputs\n        self.last_hidden = hidden\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return DistilBERTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer.\n            return torch.Size([self.max_sequence_length - 2, self.transformer.module.config.dim])\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.dim * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.dim])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"ctrl\", TEXT)\nclass CTRLEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"ctrl\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 246534,\n        n_positions: int = 256,\n        n_ctx: int = 256,\n        n_embd: int = 1280,\n        dff: int = 8192,\n        n_layer: int = 48,\n        n_head: int = 16,\n        resid_pdrop: float = 0.1,\n        embd_pdrop: float = 0.1,\n        attn_pdrop: float = 0.1,\n        layer_norm_epsilon: float = 1e-6,\n        initializer_range: float = 0.02,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import CTRLConfig, CTRLModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            n_positions=n_positions,\n            n_ctx=n_ctx,\n            n_embd=n_embd,\n            dff=dff,\n            n_layer=n_layer,\n            n_head=n_head,\n            resid_pdrop=resid_pdrop,\n            embd_pdrop=embd_pdrop,\n            attn_pdrop=attn_pdrop,\n            layer_norm_epsilon=layer_norm_epsilon,\n            initializer_range=initializer_range,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                CTRLModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n            self.vocab_size = transformer.config.vocab_size\n        else:\n            transformer = self._init_transformer_from_scratch(CTRLModel, CTRLConfig, hf_config_params, vocab_size)\n            self.vocab_size = vocab_size\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.max_sequence_length = max_sequence_length\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls():\n        return CTRLConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.n_embd])\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.n_embd * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.n_embd])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"camembert\", TEXT)\nclass CamemBERTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"camembert-base\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"cls-pooled\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 30522,\n        hidden_size: int = 768,\n        num_hidden_layers: int = 12,\n        num_attention_heads: int = 12,\n        intermediate_size: int = 3072,\n        hidden_act: str | Callable = \"gelu\",\n        hidden_dropout_prob: float = 0.1,\n        attention_probs_dropout_prob: float = 0.1,\n        max_position_embeddings: int = 512,\n        type_vocab_size: int = 2,\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        pad_token_id: int = 0,\n        gradient_checkpointing: bool = False,\n        position_embedding_type: str = \"absolute\",\n        classifier_dropout: float = None,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import CamembertConfig, CamembertModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            hidden_size=hidden_size,\n            num_hidden_layers=num_hidden_layers,\n            num_attention_heads=num_attention_heads,\n            intermediate_size=intermediate_size,\n            hidden_act=hidden_act,\n            hidden_dropout_prob=hidden_dropout_prob,\n            attention_probs_dropout_prob=attention_probs_dropout_prob,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n            initializer_range=initializer_range,\n            layer_norm_eps=layer_norm_eps,\n            pad_token_id=pad_token_id,\n            gradient_checkpointing=gradient_checkpointing,\n            position_embedding_type=position_embedding_type,\n            classifier_dropout=classifier_dropout,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                CamembertModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                CamembertModel, CamembertConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return CamemBERTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by BERT tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"t5\", TEXT)\nclass T5Encoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"t5-small\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 32128,\n        d_model: int = 512,\n        d_kv: int = 64,\n        d_ff: int = 2048,\n        num_layers: int = 6,\n        num_decoder_layers: int | None = None,\n        num_heads: int = 8,\n        relative_attention_num_buckets: int = 32,\n        dropout_rate: float = 0.1,\n        layer_norm_eps: float = 1e-6,\n        initializer_factor: float = 1,\n        feed_forward_proj: str = \"relu\",\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import T5Config, T5Model\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            d_model=d_model,\n            d_kv=d_kv,\n            d_ff=d_ff,\n            num_layers=num_layers,\n            num_decoder_layers=num_decoder_layers,\n            num_heads=num_heads,\n            relative_attention_num_buckets=relative_attention_num_buckets,\n            dropout_rate=dropout_rate,\n            layer_norm_eps=layer_norm_eps,\n            initializer_factor=initializer_factor,\n            feed_forward_proj=feed_forward_proj,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                T5Model, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(T5Model, T5Config, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            inputs,\n            decoder_input_ids=inputs,\n            attention_mask=mask,\n        )\n        hidden = transformer_outputs[0][:, 0:-1, :]  # [eos token]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return T5Config\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 1 to remove EOS token added by T5 tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 1,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -1 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 1)])\n        return torch.Size([self.transformer.module.config.d_model])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"flaubert\", TEXT)\nclass FlauBERTEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"flaubert/flaubert_small_cased\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 30145,\n        pre_norm: bool = False,\n        layerdrop: float = 0.0,\n        emb_dim: int = 2048,\n        n_layers: int = 12,\n        n_heads: int = 16,\n        dropout: float = 0.1,\n        attention_dropout: float = 0.1,\n        gelu_activation: bool = True,\n        sinusoidal_embeddings: bool = False,\n        causal: bool = False,\n        asm: bool = False,\n        n_langs: int = 1,\n        use_lang_emb: bool = True,\n        max_position_embeddings: int = 512,\n        embed_init_std: float = 2048**-0.5,\n        init_std: int = 0.02,\n        layer_norm_eps: float = 1e-12,\n        bos_index: int = 0,\n        eos_index: int = 1,\n        pad_index: int = 2,\n        unk_index: int = 3,\n        mask_index: int = 5,\n        is_encoder: bool = True,\n        mask_token_id: int = 0,\n        lang_id: int = 1,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import FlaubertConfig, FlaubertModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            pre_norm=pre_norm,\n            layerdrop=layerdrop,\n            emb_dim=emb_dim,\n            n_layers=n_layers,\n            n_heads=n_heads,\n            dropout=dropout,\n            attention_dropout=dropout,\n            gelu_activation=gelu_activation,\n            sinusoidal_embeddings=sinusoidal_embeddings,\n            causal=causal,\n            asm=asm,\n            n_langs=n_langs,\n            use_lang_emb=use_lang_emb,\n            max_position_embeddings=max_position_embeddings,\n            embed_init_std=embed_init_std,\n            init_std=init_std,\n            layer_norm_eps=layer_norm_eps,\n            bos_index=bos_index,\n            eos_index=eos_index,\n            pad_index=pad_index,\n            unk_index=unk_index,\n            mask_index=mask_index,\n            is_encoder=is_encoder,\n            mask_token_id=mask_token_id,\n            lang_id=lang_id,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                FlaubertModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                FlaubertModel, FlaubertConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0][:, 1:-1, :]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return FlauBERTConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.emb_dim])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"electra\", TEXT)\nclass ELECTRAEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"google/electra-small-discriminator\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 30522,\n        embedding_size: int = 128,\n        hidden_size: int = 256,\n        num_hidden_layers: int = 12,\n        num_attention_heads: int = 4,\n        intermediate_size: int = 1024,\n        hidden_act: str | Callable = \"gelu\",\n        hidden_dropout_prob: float = 0.1,\n        attention_probs_dropout_prob: float = 0.1,\n        max_position_embeddings: int = 512,\n        type_vocab_size: int = 2,\n        initializer_range: float = 0.02,\n        layer_norm_eps: float = 1e-12,\n        position_embedding_type: str = \"absolute\",\n        classifier_dropout: float | None = None,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import ElectraConfig, ElectraModel\n\n        hf_config_params = dict(\n            vocab_size=vocab_size,\n            embedding_size=embedding_size,\n            hidden_size=hidden_size,\n            num_hidden_layers=num_hidden_layers,\n            num_attention_heads=num_attention_heads,\n            intermediate_size=intermediate_size,\n            hidden_act=hidden_act,\n            hidden_dropout_prob=hidden_dropout_prob,\n            attention_probs_dropout_prob=attention_probs_dropout_prob,\n            max_position_embeddings=max_position_embeddings,\n            type_vocab_size=type_vocab_size,\n            initializer_range=initializer_range,\n            layer_norm_eps=layer_norm_eps,\n            position_embedding_type=position_embedding_type,\n            classifier_dropout=classifier_dropout,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                ElectraModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(ElectraModel, ElectraConfig, hf_config_params, vocab_size)\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.max_sequence_length = max_sequence_length\n        self.reduce_output = reduce_output\n        if self.reduce_output == \"cls_pooled\":\n            _cls_pooled_error_message(self.__class__.__name__)\n        self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        hidden = transformer_outputs[0][:, 1:-1, :]\n        hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return ELECTRAConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"longformer\", TEXT)\nclass LongformerEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = \"allenai/longformer-base-4096\"\n\n    def __init__(\n        self,\n        max_sequence_length: int,\n        use_pretrained: bool = True,\n        attention_window: list[int] | int = 512,\n        sep_token_id: int = 2,\n        pretrained_model_name_or_path: str = DEFAULT_MODEL_NAME,\n        saved_weights_in_checkpoint: bool = False,\n        reduce_output: str | None = \"cls_pooled\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int = 50265,\n        num_tokens: int | None = None,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import LongformerConfig, LongformerModel\n\n        hf_config_params = dict(\n            attention_window=attention_window,\n            sep_token_id=sep_token_id,\n            vocab_size=vocab_size,\n            **kwargs,\n        )\n\n        if use_pretrained and not saved_weights_in_checkpoint:\n            pretrained_kwargs = pretrained_kwargs or {}\n            transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n                LongformerModel, pretrained_model_name_or_path, **pretrained_kwargs\n            )\n        else:\n            transformer = self._init_transformer_from_scratch(\n                LongformerModel, LongformerConfig, hf_config_params, vocab_size\n            )\n\n        if encoder_config is not None:\n            self.config = self._init_config(transformer, hf_config_params.keys(), encoder_config)\n        else:\n            self.config = None\n\n        self.reduce_output = reduce_output\n        if not self.reduce_output == \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(reduce_mode=reduce_output)\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.max_sequence_length = max_sequence_length\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n        transformer_outputs = self.transformer.module(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        if self.reduce_output == \"cls_pooled\":\n            hidden = transformer_outputs[1]\n        else:\n            hidden = transformer_outputs[0][:, 1:-1, :]  # bos + [sent] + sep\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return LongformerConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # Subtract 2 to remove CLS and PAD tokens added by Longformer (== Roberta) tokenizer.\n            return torch.Size(\n                [\n                    self.max_sequence_length - 2,\n                    self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"auto_transformer\", TEXT)\nclass AutoTransformerEncoder(HFTextEncoder):\n    DEFAULT_MODEL_NAME = None\n\n    def __init__(\n        self,\n        pretrained_model_name_or_path: str,\n        max_sequence_length: int,\n        reduce_output: str = \"sum\",\n        trainable: bool = False,\n        adapter: BaseAdapterConfig | None = None,\n        vocab_size: int | None = None,\n        pretrained_kwargs: dict = None,\n        encoder_config=None,\n        **kwargs,\n    ):\n        super().__init__()\n\n        from transformers import AutoModel\n\n        pretrained_kwargs = pretrained_kwargs or {}\n        transformer, _ = load_pretrained_hf_model_with_hub_fallback(\n            AutoModel, pretrained_model_name_or_path, **pretrained_kwargs\n        )\n        self._maybe_resize_token_embeddings(transformer, vocab_size)\n\n        self.config = self._init_config(transformer, [], encoder_config)\n\n        # Precompute the set of params that are included in the forward signature of the AutoModel implementation so\n        # we can filter out unused params during the `forward` call.\n        self.forward_kwargs = set(inspect.signature(transformer.forward).parameters.keys())\n\n        self.transformer = self._wrap_transformer(transformer, adapter, trainable)\n        self.reduce_output = reduce_output\n        if self.reduce_output != \"cls_pooled\":\n            self.reduce_sequence = SequenceReducer(\n                reduce_mode=reduce_output, encoding_size=self.transformer.module.config.hidden_size\n            )\n        self.max_sequence_length = max_sequence_length\n\n    def _maybe_resize_token_embeddings(self, transformer, vocab_size: int | None = None):\n        \"\"\"Overridden because AutoModel should use its own vocab size unless vocab size is explicitly specified.\"\"\"\n        if vocab_size is not None:\n            transformer.resize_token_embeddings(vocab_size)\n            self.vocab_size = vocab_size\n        else:\n            self.vocab_size = transformer.config.vocab_size\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        if mask is not None:\n            mask = mask.to(torch.int32)\n\n        # The forward signature of AutoModel is not consistent across implementations, so we need to make sure we're\n        # only passing in params included in the forward signature.\n        kwargs = dict(\n            input_ids=inputs,\n            attention_mask=mask,\n            token_type_ids=torch.zeros_like(inputs),\n        )\n        kwargs = {k: v for k, v in kwargs.items() if k in self.forward_kwargs}\n\n        transformer_outputs = self.transformer.module(**kwargs)\n        if self.reduce_output == \"cls_pooled\":\n            # this works only if the user know that the specific model\n            # they want to use has the same outputs of\n            # the BERT base class call() function\n            hidden = transformer_outputs[\"pooler_output\"]\n        else:\n            hidden = transformer_outputs[\"last_hidden_state\"]\n            hidden = self.reduce_sequence(hidden, self.reduce_output)\n        return {ENCODER_OUTPUT: hidden}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return AutoTransformerConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if self.reduce_output is None:\n            # TODO(justin): This may need to be conditioned on which AutoModel gets chosen.\n            return torch.Size([self.max_sequence_length, self.transformer.module.config.hidden_size])\n        if self.reduce_output == \"concat\":\n            return torch.Size(\n                [\n                    self.max_sequence_length * self.transformer.module.config.hidden_size,\n                ]\n            )\n        elif self.reduce_output == \"concat\":\n            # add the -2 to account of start and end tokens.\n            return torch.Size([self.transformer.module.config.hidden_size * (self.max_sequence_length - 2)])\n        return torch.Size([self.transformer.module.config.hidden_size])\n\n    @property\n    def input_dtype(self) -> torch.dtype:\n        return torch.int32\n\n\n@DeveloperAPI\n@register_encoder(\"tf_idf\", [TEXT])\nclass TfIdfEncoder(Encoder):\n    def __init__(\n        self,\n        max_sequence_length: int,\n        encoder_config=None,\n        str2idf=None,\n        vocab=None,\n        vocab_size: int = None,\n        **kwargs,\n    ):\n        super().__init__()\n        self.config = encoder_config\n        self.max_sequence_length = max_sequence_length\n        self.vocab_size = vocab_size\n\n        logger.debug(f\" {self.name}\")\n\n        # Convert mapping of token -> frequency to a dense array\n        idf = np.zeros(vocab_size)\n        for i, s in enumerate(vocab):\n            idf[i] = str2idf[s]\n        self.register_buffer(\"idf\", torch.from_numpy(idf).float().unsqueeze(0))\n\n    def forward(self, t: torch.Tensor, mask: torch.Tensor | None = None) -> EncoderOutputDict:\n        # Compute the term frequency within each row\n        tf = torch.stack([t_i.bincount(minlength=self.vocab_size) for t_i in torch.unbind(t.long())])\n\n        # Normalize the term frequency by the number of tokens in each row\n        tf = tf / tf.sum(dim=1).unsqueeze(-1)\n\n        # Multiply the term frequency by the inverse document frequency\n        tfidf = tf * self.idf\n\n        return {ENCODER_OUTPUT: tfidf}\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return TfIdfEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.vocab_size])\n\n    def get_embedding_layer(self) -> nn.Module:\n        return self\n\n\n@DeveloperAPI\n@register_encoder(\"llm\", [TEXT])\nclass LLMEncoder(Encoder):\n    # Per-adapter type prefixes for parameter names in the state dict, taken from\n    # https://github.com/huggingface/peft/blob/0f1e9091cc975eb5458cc163bf1843a34fb42b76/src/peft/utils/save_and_load.py#L173C9-L180\n    ADAPTER_PARAM_NAME_PREFIX = {\n        \"adalora\": \"lora_\",\n        \"ia3\": \"ia3_\",\n        \"lora\": \"lora_\",\n    }\n\n    def __init__(self, encoder_config: LLMEncoderConfig = None, **kwargs):\n        super().__init__()\n        self.register_load_state_dict_post_hook(self.remove_missing_non_adapter_keys)\n\n        self.config = encoder_config\n\n        self.adapter_is_initialized = False\n\n        self.model_name = self.config.base_model\n        self.model_config = AutoConfig.from_pretrained(self.config.base_model)\n\n        self.model = load_pretrained_from_config(self.config, model_config=self.model_config)\n        self.curr_device = next(self.model.parameters()).device\n        logger.info(\"Done.\")\n\n        self.context_len = get_context_len(self.model_config)\n\n        # TODO(Arnav): This needs be more flexible to account for RoPE Scaling\n        # When merging input IDs and target IDs for LLM fine-tuning, we want to make sure that the merged tensor is\n        # not longer than the global maximum sequence length. This is provided in the preprocessing config. We never\n        # want to exceed the maximum possible context length so we also check for that.\n        if self.config.max_sequence_length:\n            max_sequence_length = self.config.max_sequence_length\n            self.max_sequence_length = (\n                max_sequence_length if max_sequence_length <= self.context_len else self.context_len\n            )\n        else:\n            self.max_sequence_length = self.context_len\n\n        # Initialize tokenizer\n        self.tokenizer = HFTokenizer(self.config.base_model).tokenizer\n\n        self.attention_masks = None\n\n        clear_data_cache()\n\n        # Because we use the last hidden state as encoder output rather than the logits, the final module of the model\n        # has input pass through but no gradient update in the backward pass. This can lead to a DDP error. Freezing\n        # the module prevents this from happening. This is done at initialization to prevent \"unused parameters\" errors\n        # from happening when the encoder is used before `prepare_for_training` is called, for example during batch\n        # size tuning.\n        out_module = list(self.model.modules())[-1]\n        out_module.requires_grad_(requires_grad=False)\n\n    @staticmethod\n    def get_schema_cls() -> type[BaseEncoderConfig]:\n        return LLMEncoderConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.model_config.hidden_size])\n\n    def get_embedding_layer(self) -> nn.Module:\n        return self\n\n    def initialize_adapter(self):\n        \"\"\"If an adapter config is provided, we want to wrap the model with a PEFT model for fine-tuning.\"\"\"\n        if self.config.adapter:\n            self.model = initialize_adapter(self.model, self.config)\n\n            logger.info(\"==================================================\")\n            logger.info(\"Trainable Parameter Summary For LLM Encoder Fine-Tuning\")\n            logger.info(f\"Fine-tuning with adapter: {self.config.adapter.type}\")\n            self.model.print_trainable_parameters()\n            logger.info(\"==================================================\")\n\n            self.adapter_is_initialized = True\n\n    def prepare_for_training(self):\n        # TODO: this implementation will not work if resuming from a previous checkpoint. Need to fix this.\n        if self.config.quantization:\n            self.prepare_for_quantized_training()\n        self.initialize_adapter()\n\n    def prepare_for_quantized_training(self):\n        from peft import prepare_model_for_kbit_training\n\n        self.model = prepare_model_for_kbit_training(self.model, use_gradient_checkpointing=False)\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None):\n        # Get the hidden state of the last layer and return it as the text encoding\n        model_outputs = self.model(input_ids=inputs, output_hidden_states=True).hidden_states[-1]\n\n        return {ENCODER_OUTPUT: model_outputs.type(torch.float32)}\n\n    def _save_to_state_dict(self, destination: dict, prefix: str, keep_vars: bool):\n        # This is called by `torch.nn.Module.state_dict()` under the hood. `state_dict()` does additional work to\n        # prep the dictionary, get submodule state, and run hooks. Overriding this method only impacts the\n        # contents of the state_dict.\n        # The three args to this method are supplied by Module.state_dict\n        # https://github.com/pytorch/pytorch/blob/8739d1e3f9b08f4282fe79fc8dacd781d16913ff/torch/nn/modules/module.py#L1824\n        if self.config.adapter and self.adapter_is_initialized:\n            # get_peft_model_state_dict geneates a state dict that only contains the adapter weights\n            from peft.utils.save_and_load import get_peft_model_state_dict\n\n            sd = get_peft_model_state_dict(self.model)\n            destination.update(sd)\n\n        else:\n            super()._save_to_state_dict(destination, prefix=prefix, keep_vars=keep_vars)\n\n    def state_dict(self, *args, destination=None, prefix=\"\", keep_vars=False):\n        destination = super().state_dict(destination, prefix=prefix, keep_vars=keep_vars)\n\n        if self.config.adapter and self.adapter_is_initialized:\n            adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type]\n            exclude_model_keys = [k for k in destination.keys() if adapter_type_prefix not in k]\n\n            for k in exclude_model_keys:\n                del destination[k]\n\n        return destination\n\n    def _load_from_state_dict(\n        self, state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs\n    ):\n        # Call this first to make sure torch can do its usual load. In the adapter case, this should essentially be a\n        # no-op, but the adapter weights will be collected in `unexpected_keys` because PEFT changes the parameter\n        # names under the hood.\n\n        super()._load_from_state_dict(\n            state_dict, prefix, local_metadata, strict, missing_keys, unexpected_keys, error_msgs\n        )\n\n        if self.config.adapter and self.adapter_is_initialized:\n            # When using an adapter, only the adapter weights are saved, and so we only want to load those weights.\n            # Under the hood, PEFT alters the names of the parameters, which leads to an \"unexpected keys\" error when\n            # using strict mode. This block uses PEFT's version of `load_state_dict` to handle loading in weights.\n            from peft.utils.save_and_load import set_peft_model_state_dict\n\n            adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type]\n            peft_model_state_dict = {k: v for k, v in state_dict.items() if adapter_type_prefix in k}\n            set_peft_model_state_dict(self.model, peft_model_state_dict)\n\n    def remove_missing_non_adapter_keys(self, module, incompatible_keys):\n        \"\"\"Update the missing and unexpected keys lists to reflect custom adapter state load logic.\n\n        This method should never return anything unless the underlying torch hook logic is updated. Any changes to the\n        lists in `incompatible_keys` must be made in-place.\n\n        Args:\n            module: The torch module with newly loaded state\n            incompatible_keys: A tuple with the lists of missing and unexpected keys that were recorded while loading\n        \"\"\"\n        # If no adapter was used, `LLMEncoder.load_state_dict` should use the default `torch.Module.load_state_dict`\n        # code path to load weights and no modification should be necessary.\n        if self.config.adapter and self.adapter_is_initialized:\n            adapter_type_prefix = self.ADAPTER_PARAM_NAME_PREFIX[self.config.adapter.type]\n            missing_keys, unexpected_keys = incompatible_keys\n\n            # The state dict uses fully qualified parameter names, but this function does not have access to the\n            # fully qualified names or a prefix to recreate them. Iterate over the missing keys and greedily select the\n            # first non-adapter key that shares a suffix with a model parameter name.\n            sample_missing_key = \"\"\n            sample_model_key = \"\"\n            for k in missing_keys:\n                # Exclude any adapter weight--those should not be missing. Let torch handle that downstream.\n                if adapter_type_prefix not in k:\n                    sample_model_keys = [p for p, _ in self.named_parameters() if p in k]\n                    if sample_model_keys:\n                        sample_model_key = sample_model_keys[0]\n                        sample_missing_key = k\n                        break\n            sd_prefix = sample_missing_key.replace(sample_model_key, \"\")\n\n            # When loading the adapter weights in strict mode, torch will register the base model weights as missing\n            # from the state dict and raise an exception. The base model weights are intended to be excluded, so the\n            # missing_keys list is updated post-load to avoid the error.\n            for k, _ in self.named_parameters():\n                full_name = f\"{sd_prefix}{k}\"\n                if full_name in missing_keys and adapter_type_prefix not in full_name:\n                    missing_keys.remove(full_name)\n\n            # peft changes the adapter parameter names under the hood to include the adapter name. When retreiving the\n            # adapter state dict, however, the name is not included. This causes the adpater weights to be recorded as\n            # unexpected parameters. `LLMEncoder._load_from_state_dict` loads the adapter parameters using a peft\n            # utility that accounts for the updated names, so here we remove any adapter parameters from the unexpected\n            # keys list to avoid errors.\n            from peft.utils.save_and_load import get_peft_model_state_dict\n\n            sd = get_peft_model_state_dict(self.model)\n            for k in sd.keys():\n                if k in unexpected_keys:\n                    unexpected_keys.remove(k)\n"
  },
  {
    "path": "ludwig/encoders/types.py",
    "content": "from typing import TypedDict\n\nimport torch\n\n\nclass EncoderOutputDict(TypedDict, total=False):\n    encoder_output: torch.Tensor\n    encoder_output_state: torch.Tensor  # only used by sequence and h3 encoders\n    attentions: torch.Tensor  # only used by the vit legacy encoder\n"
  },
  {
    "path": "ludwig/error.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom ludwig.api_annotations import PublicAPI\n\n\n@PublicAPI\nclass LudwigError(Exception):\n    \"\"\"Base class for all custom exceptions raised by the Ludwig framework.\"\"\"\n\n    def __reduce__(self):\n        \"\"\"Docs: https://docs.python.org/3/library/pickle.html#object.__reduce__.\"\"\"\n        raise NotImplementedError(\n            \"Implement __reduce__ for all subclasses of LudwigError as it's necessary for \"\n            \"serialization by Ray. See https://github.com/ludwig-ai/ludwig/pull/2695.\"\n        )\n\n\n@PublicAPI\nclass InputDataError(LudwigError, ValueError):\n    \"\"\"Exception raised for errors in the input data.\n\n    Appropriate for data which is not convertible to the input feature type, columns with all missing values,\n    categorical columns with only one category, etc...\n\n    Attributes:\n        column - The name of the input column which caused the error\n        feature_type - The Ludwig feature type which caused the error (number, binary, category...).\n        message - An error message describing the situation.\n    \"\"\"\n\n    def __init__(self, column_name: str, feature_type: str, message: str):\n        self.column_name = column_name\n        self.feature_type = feature_type\n        self.message = message\n        super().__init__(message)\n\n    def __str__(self):\n        return f'Column \"{self.column_name}\" as {self.feature_type} feature: {self.message}'\n\n    def __reduce__(self):\n        return type(self), (self.column_name, self.feature_type, self.message)\n\n\n@PublicAPI\nclass ConfigValidationError(LudwigError, ValueError):\n    \"\"\"Exception raised for errors in the Ludwig configuration.\n\n    Appropriate for bad configuration values, missing required configuration values, etc...\n\n    Attributes:\n        message - An error message describing the situation.\n    \"\"\"\n\n    def __init__(self, message: str):\n        self.message = message\n        super().__init__(message)\n\n    def __reduce__(self):\n        return type(self), (self.message,)\n"
  },
  {
    "path": "ludwig/evaluate.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport sys\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import FULL, TEST, TRAINING, VALIDATION\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n\ndef evaluate_cli(\n    model_path: str,\n    dataset: str | dict | pd.DataFrame = None,\n    data_format: str = None,\n    split: str = FULL,\n    batch_size: int = 128,\n    skip_save_unprocessed_output: bool = False,\n    skip_save_predictions: bool = False,\n    skip_save_eval_stats: bool = False,\n    skip_collect_predictions: bool = False,\n    skip_collect_overall_stats: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    logging_level: int = logging.INFO,\n    **kwargs,\n) -> None:\n    \"\"\"Loads pre-trained model and evaluates its performance by comparing the predictions against ground truth.\n\n     # Inputs\n\n     :param model_path: (str) filepath to pre-trained model.\n     :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n         source containing the entire dataset to be used in the evaluation.\n     :param data_format: (str, default: `None`) format to interpret data\n         sources. Will be inferred automatically if not specified.  Valid\n         formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n         `'fwf'`, `'hdf5'` (cache file produced during previous training),\n         `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n         `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n         `'stata'`, `'tsv'`.\n     :param split: (str, default: `full`) split on which\n         to perform predictions. Valid values are `'training'`, `'validation'`,\n         `'test'` and `'full'`.\n     :param batch_size: (int, default `128`) size of batches for processing.\n     :param skip_save_unprocessed_output: (bool, default: `False`) by default\n         predictions and their probabilities are saved in both raw\n         unprocessed numpy files containing tensors and as postprocessed\n         CSV files (one for each output feature). If this parameter is True,\n         only the CSV ones are saved and the numpy ones are skipped.\n     :param skip_save_predictions: (bool, default: `False`) skips saving test\n         predictions CSV files\n     :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n         statistics JSON file\n    :param skip_collect_predictions: (bool, default: `False`) skips\n         collecting post-processed predictions during eval.\n     :param skip_collect_overall_stats: (bool, default: `False`) skips\n         collecting overall stats during eval.\n     :param output_directory: (str, default: `'results'`) the directory that\n         will contain the training statistics, TensorBoard logs, the saved\n         model and the training progress files.\n     :param gpus: (list, default: `None`) list of GPUs that are available\n         for training.\n     :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n            [0, 1] allowed to allocate per GPU device.\n     :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n         to use multithreading parallelism to improve performance at\n         the cost of determinism.\n     :param callbacks: (list, default: `None`) a list of\n         `ludwig.callbacks.Callback` objects that provide hooks into the\n         Ludwig pipeline.\n     :param backend: (Union[Backend, str]) `Backend` or string name\n         of backend to use to execute preprocessing / training steps.\n     :param logging_level: (int) Log level that will be sent to stderr.\n\n     # Returns\n\n     :return: (`None`)\n    \"\"\"\n    model = LudwigModel.load(\n        model_path,\n        logging_level=logging_level,\n        backend=backend,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n    )\n    model.evaluate(\n        dataset=dataset,\n        data_format=data_format,\n        batch_size=batch_size,\n        split=split,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        collect_predictions=not skip_collect_predictions,\n        collect_overall_stats=not skip_collect_overall_stats,\n        output_directory=output_directory,\n        return_type=\"dict\",\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \"\n        \"and evaluates its performance by comparing\"\n        \"its predictions with ground truth.\",\n        prog=\"ludwig evaluate\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\"--dataset\", help=\"input data file path\", required=True)\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n    parser.add_argument(\n        \"-s\", \"--split\", default=FULL, choices=[TRAINING, VALIDATION, TEST, FULL], help=\"the split to test the model on\"\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n\n    # -------------------------\n    # Output results parameters\n    # -------------------------\n    parser.add_argument(\n        \"-od\", \"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\"\n    )\n    parser.add_argument(\n        \"-ssuo\",\n        \"--skip_save_unprocessed_output\",\n        help=\"skips saving intermediate NPY output files\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-sses\",\n        \"--skip_save_eval_stats\",\n        help=\"skips saving intermediate JSON eval statistics\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-scp\", \"--skip_collect_predictions\", help=\"skips collecting predictions\", action=\"store_true\", default=False\n    )\n    parser.add_argument(\n        \"-scos\",\n        \"--skip_collect_overall_stats\",\n        help=\"skips collecting overall stats\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # ------------------\n    # Generic parameters\n    # ------------------\n    parser.add_argument(\"-bs\", \"--batch_size\", type=int, default=128, help=\"size of batches\")\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\"-g\", \"--gpus\", type=int, default=0, help=\"list of gpu to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-dpt\",\n        \"--disable_parallel_threads\",\n        action=\"store_false\",\n        dest=\"allow_parallel_threads\",\n        help=\"disable PyTorch from using multithreading for reproducibility\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n    args.evaluate_performance = True\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"evaluate\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.test_performance\")\n\n    backend = initialize_backend(args.backend)\n    if backend.is_coordinator():\n        print_ludwig(\"Evaluate\", LUDWIG_VERSION)\n        logger.info(f\"Dataset path: {args.dataset}\")\n        logger.info(f\"Model path: {args.model_path}\")\n        logger.info(\"\")\n\n    evaluate_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/experiment.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport os\nimport sys\n\nimport pandas as pd\n\nfrom ludwig.api import kfold_cross_validate, LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import CONTINUE_PROMPT, FULL, HYPEROPT, HYPEROPT_WARNING, TEST, TRAINING, VALIDATION\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.data_utils import load_config_from_str, load_yaml, save_json\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig, query_yes_no\n\nlogger = logging.getLogger(__name__)\n\n\ndef experiment_cli(\n    config: str | dict,\n    dataset: str | dict | pd.DataFrame = None,\n    training_set: str | dict | pd.DataFrame = None,\n    validation_set: str | dict | pd.DataFrame = None,\n    test_set: str | dict | pd.DataFrame = None,\n    training_set_metadata: str | dict = None,\n    data_format: str = None,\n    experiment_name: str = \"experiment\",\n    model_name: str = \"run\",\n    model_load_path: str = None,\n    model_resume_path: str = None,\n    eval_split: str = TEST,\n    skip_save_training_description: bool = False,\n    skip_save_training_statistics: bool = False,\n    skip_save_model: bool = False,\n    skip_save_progress: bool = False,\n    skip_save_log: bool = False,\n    skip_save_processed_input: bool = False,\n    skip_save_unprocessed_output: bool = False,\n    skip_save_predictions: bool = False,\n    skip_save_eval_stats: bool = False,\n    skip_collect_predictions: bool = False,\n    skip_collect_overall_stats: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    random_seed: int = default_random_seed,\n    logging_level: int = logging.INFO,\n    **kwargs,\n):\n    \"\"\"Trains a model on a dataset's training and validation splits and uses it to predict on the test split. It\n    saves the trained model and the statistics of training and testing.\n\n     # Inputs\n\n     :param config: (Union[str, dict]) in-memory representation of\n             config or string path to a YAML config file.\n     :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n         source containing the entire dataset to be used in the experiment.\n         If it has a split column, it will be used for splitting (0 for train,\n         1 for validation, 2 for test), otherwise the dataset will be\n         randomly split.\n     :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n         source containing training data.\n     :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n         source containing validation data.\n     :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n         source containing test data.\n     :param training_set_metadata: (Union[str, dict], default: `None`)\n         metadata JSON file or loaded metadata.  Intermediate preprocessed\n         structure containing the mappings of the input\n         dataset created the first time an input file is used in the same\n         directory with the same name and a '.meta.json' extension.\n     :param data_format: (str, default: `None`) format to interpret data\n         sources. Will be inferred automatically if not specified.  Valid\n         formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n         `'fwf'`, `'hdf5'` (cache file produced during previous training),\n         `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n         `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n         `'stata'`, `'tsv'`.\n     :param experiment_name: (str, default: `'experiment'`) name for\n         the experiment.\n     :param model_name: (str, default: `'run'`) name of the model that is\n         being used.\n     :param model_load_path: (str, default: `None`) if this is specified the\n         loaded model will be used as initialization\n         (useful for transfer learning).\n     :param model_resume_path: (str, default: `None`) resumes training of\n         the model from the path specified. The config is restored.\n         In addition to config, training statistics and loss for\n         epoch and the state of the optimizer are restored such that\n         training can be effectively continued from a previously interrupted\n         training process.\n     :param eval_split: (str, default: `test`) split on which\n         to perform evaluation. Valid values are `training`, `validation`\n         and `test`.\n     :param skip_save_training_description: (bool, default: `False`) disables\n         saving the description JSON file.\n     :param skip_save_training_statistics: (bool, default: `False`) disables\n         saving training statistics JSON file.\n     :param skip_save_model: (bool, default: `False`) disables\n         saving model weights and hyperparameters each time the model\n         improves. By default Ludwig saves model weights after each epoch\n         the validation metric improves, but if the model is really big\n         that can be time consuming. If you do not want to keep\n         the weights and just find out what performance a model can get\n         with a set of hyperparameters, use this parameter to skip it,\n         but the model will not be loadable later on and the returned model\n         will have the weights obtained at the end of training, instead of\n         the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n         progress each epoch. By default Ludwig saves weights and stats\n         after each epoch for enabling resuming of training, but if\n         the model is really big that can be time consuming and will uses\n         twice as much space, use this parameter to skip it, but training\n         cannot be resumed later on.\n     :param skip_save_log: (bool, default: `False`) disables saving\n         TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n         but if it is not needed turning it off can slightly increase the\n         overall speed.\n     :param skip_save_processed_input: (bool, default: `False`) if input\n         dataset is provided it is preprocessed and cached by saving an HDF5\n         and JSON files to avoid running the preprocessing again. If this\n         parameter is `False`, the HDF5 and JSON file are not saved.\n     :param skip_save_unprocessed_output: (bool, default: `False`) by default\n         predictions and their probabilities are saved in both raw\n         unprocessed numpy files containing tensors and as postprocessed\n         CSV files (one for each output feature). If this parameter is True,\n         only the CSV ones are saved and the numpy ones are skipped.\n     :param skip_save_predictions: (bool, default: `False`) skips saving test\n         predictions CSV files\n     :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n         statistics JSON file\n    :param skip_collect_predictions: (bool, default: `False`) skips\n         collecting post-processed predictions during eval.\n     :param skip_collect_overall_stats: (bool, default: `False`) skips\n         collecting overall stats during eval.\n     :param output_directory: (str, default: `'results'`) the directory that\n         will contain the training statistics, TensorBoard logs, the saved\n         model and the training progress files.\n     :param gpus: (list, default: `None`) list of GPUs that are available\n         for training.\n     :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n            [0, 1] allowed to allocate per GPU device.\n     :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n         to use multithreading parallelism to improve performance at\n         the cost of determinism.\n     :param callbacks: (list, default: `None`) a list of\n         `ludwig.callbacks.Callback` objects that provide hooks into the\n         Ludwig pipeline.\n     :param backend: (Union[Backend, str]) `Backend` or string name\n         of backend to use to execute preprocessing / training steps.\n     :param random_seed: (int: default: 42) random seed used for weights\n         initialization, splits and any other random function.\n     :param logging_level: (int) Log level that will be sent to stderr.\n\n     # Return\n     :return: (Tuple[LudwigModel, dict, dict, tuple, str)):\n        `(model, evaluation_statistics, training_statistics, preprocessed_data, output_directory)`\n         `model` LudwigModel instance\n         `evaluation_statistics` dictionary with evaluation performance\n             statistics on the test_set,\n         `training_statistics` is a nested dictionary of dataset -> feature_name -> metric_name -> List of metrics.\n                Each metric corresponds to each training checkpoint.\n         `preprocessed_data` tuple containing preprocessed\n         `(training_set, validation_set, test_set)`, `output_directory`\n         filepath string to where results are stored.\n    \"\"\"\n    if HYPEROPT in config:\n        if not query_yes_no(HYPEROPT_WARNING + CONTINUE_PROMPT):\n            exit(1)\n\n    if isinstance(config, str):\n        config = load_yaml(config)\n    backend = initialize_backend(backend or config.get(\"backend\"))\n\n    if model_load_path:\n        model = LudwigModel.load(\n            model_load_path,\n            logging_level=logging_level,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n    else:\n        model = LudwigModel(\n            config=config,\n            logging_level=logging_level,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n    eval_stats, train_stats, preprocessed_data, output_directory = model.experiment(\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        experiment_name=experiment_name,\n        model_name=model_name,\n        model_resume_path=model_resume_path,\n        eval_split=eval_split,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        skip_collect_predictions=skip_collect_predictions,\n        skip_collect_overall_stats=skip_collect_overall_stats,\n        output_directory=output_directory,\n        random_seed=random_seed,\n    )\n\n    return model, eval_stats, train_stats, preprocessed_data, output_directory\n\n\ndef kfold_cross_validate_cli(\n    k_fold,\n    config=None,\n    dataset=None,\n    data_format=None,\n    output_directory=\"results\",\n    random_seed=default_random_seed,\n    skip_save_k_fold_split_indices=False,\n    **kwargs,\n):\n    \"\"\"Wrapper function to performs k-fold cross validation.\n\n    # Inputs\n    :param k_fold: (int) number of folds to create for the cross-validation\n    :param config: (Union[str, dict], default: None) a dictionary or file path containing model configuration. Refer to\n        the [User Guide] (http://ludwig.ai/user_guide/#model-config) for details.\n    :param dataset: (string, default: None)\n    :param output_directory: (string, default: 'results')\n    :param random_seed: (int) Random seed used k-fold splits.\n    :param skip_save_k_fold_split_indices: (boolean, default: False) Disables saving k-fold split indices\n    :return: None\n    \"\"\"\n\n    kfold_cv_stats, kfold_split_indices = kfold_cross_validate(\n        k_fold,\n        config=config,\n        dataset=dataset,\n        data_format=data_format,\n        output_directory=output_directory,\n        random_seed=random_seed,\n    )\n\n    # save k-fold cv statistics\n    save_json(os.path.join(output_directory, \"kfold_training_statistics.json\"), kfold_cv_stats)\n\n    # save k-fold split indices\n    if not skip_save_k_fold_split_indices:\n        save_json(os.path.join(output_directory, \"kfold_split_indices.json\"), kfold_split_indices)\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script trains and evaluates a model\", prog=\"ludwig experiment\", usage=\"%(prog)s [options]\"\n    )\n\n    # ----------------------------\n    # Experiment naming parameters\n    # ----------------------------\n    parser.add_argument(\"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\")\n    parser.add_argument(\"--experiment_name\", type=str, default=\"experiment\", help=\"experiment name\")\n    parser.add_argument(\"--model_name\", type=str, default=\"run\", help=\"name for the model\")\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\n        \"--dataset\",\n        help=\"input data file path. \"\n        \"If it has a split column, it will be used for splitting \"\n        \"(0: train, 1: validation, 2: test), \"\n        \"otherwise the dataset will be randomly split\",\n    )\n    parser.add_argument(\"--training_set\", help=\"input train data file path\")\n    parser.add_argument(\"--validation_set\", help=\"input validation data file path\")\n    parser.add_argument(\"--test_set\", help=\"input test data file path\")\n\n    parser.add_argument(\n        \"--training_set_metadata\",\n        help=\"input metadata JSON file path. An intermediate preprocessed file \"\n        \"containing the mappings of the input file created \"\n        \"the first time a file is used, in the same directory \"\n        \"with the same name and a .json extension\",\n    )\n\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n\n    parser.add_argument(\n        \"-es\",\n        \"--eval_split\",\n        default=TEST,\n        choices=[TRAINING, VALIDATION, TEST, FULL],\n        help=\"the split to evaluate the model on\",\n    )\n\n    parser.add_argument(\n        \"-sspi\",\n        \"--skip_save_processed_input\",\n        help=\"skips saving intermediate HDF5 and JSON files\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-ssuo\",\n        \"--skip_save_unprocessed_output\",\n        help=\"skips saving intermediate NPY output files\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # -----------------\n    # K-fold parameters\n    # -----------------\n    parser.add_argument(\n        \"-kf\", \"--k_fold\", type=int, default=None, help=\"number of folds for a k-fold cross validation run \"\n    )\n    parser.add_argument(\n        \"-skfsi\",\n        \"--skip_save_k_fold_split_indices\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving indices generated to split training data set \"\n        \"for the k-fold cross validation run, but if it is not needed \"\n        \"turning it off can slightly increase the overall speed\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    config = parser.add_mutually_exclusive_group(required=True)\n    config.add_argument(\n        \"-c\",\n        \"--config\",\n        type=load_yaml,\n        help=\"Path to the YAML file containing the model configuration\",\n    )\n    config.add_argument(\n        \"-cs\",\n        \"--config_str\",\n        dest=\"config\",\n        type=load_config_from_str,\n        help=\"JSON or YAML serialized string of the model configuration\",\n    )\n\n    parser.add_argument(\"-mlp\", \"--model_load_path\", help=\"path of a pretrained model to load as initialization\")\n    parser.add_argument(\"-mrp\", \"--model_resume_path\", help=\"path of the model directory to resume training of\")\n    parser.add_argument(\n        \"-sstd\",\n        \"--skip_save_training_description\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving the description JSON file\",\n    )\n    parser.add_argument(\n        \"-ssts\",\n        \"--skip_save_training_statistics\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving training statistics JSON file\",\n    )\n    parser.add_argument(\n        \"-sstp\",\n        \"--skip_save_predictions\",\n        help=\"skips saving test predictions CSV files\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-sstes\",\n        \"--skip_save_eval_stats\",\n        help=\"skips saving eval statistics JSON file\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-ssm\",\n        \"--skip_save_model\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving model weights and hyperparameters each time \"\n        \"the model improves. \"\n        \"By default Ludwig saves model weights after each epoch \"\n        \"the validation metric improves, but if the model is really big \"\n        \"that can be time consuming. If you do not want to keep \"\n        \"the weights and just find out what performance a model can get \"\n        \"with a set of hyperparameters, use this parameter to skip it,\"\n        \"but the model will not be loadable later on\",\n    )\n    parser.add_argument(\n        \"-ssp\",\n        \"--skip_save_progress\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving progress each epoch. By default Ludwig saves \"\n        \"weights and stats after each epoch for enabling resuming \"\n        \"of training, but if the model is really big that can be \"\n        \"time consuming and will uses twice as much space, use \"\n        \"this parameter to skip it, but training cannot be resumed \"\n        \"later on\",\n    )\n    parser.add_argument(\n        \"-ssl\",\n        \"--skip_save_log\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving TensorBoard logs. By default Ludwig saves \"\n        \"logs for the TensorBoard, but if it is not needed turning it off \"\n        \"can slightly increase the overall speed\",\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-rs\",\n        \"--random_seed\",\n        type=int,\n        default=42,\n        help=\"a random seed that is going to be used anywhere there is a call \"\n        \"to a random number generator: data splitting, parameter \"\n        \"initialization and training set shuffling\",\n    )\n    parser.add_argument(\"-g\", \"--gpus\", nargs=\"+\", type=int, default=None, help=\"list of GPUs to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-dpt\",\n        \"--disable_parallel_threads\",\n        action=\"store_false\",\n        dest=\"allow_parallel_threads\",\n        help=\"disable PyTorch from using multithreading for reproducibility\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"experiment\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.experiment\")\n\n    args.backend = initialize_backend(args.backend or args.config.get(\"backend\"))\n    if args.backend.is_coordinator():\n        print_ludwig(\"Experiment\", LUDWIG_VERSION)\n\n    if args.k_fold is None:\n        experiment_cli(**vars(args))\n    else:\n        kfold_cross_validate_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/explain/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/explain/captum.py",
    "content": "import copy\nimport gc\nimport logging\nfrom collections import defaultdict\nfrom dataclasses import dataclass\n\nimport numpy as np\nimport numpy.typing as npt\nimport pandas as pd\nimport torch\nfrom captum.attr import LayerIntegratedGradients, TokenReferenceBase\nfrom captum.attr._utils.input_layer_wrapper import InputIdentity\nfrom torch.autograd import Variable\nfrom tqdm import tqdm\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.constants import (\n    BINARY,\n    CATEGORY,\n    DATE,\n    IMAGE,\n    INPUT_FEATURES,\n    MINIMUM_BATCH_SIZE,\n    NAME,\n    NUMBER,\n    PREPROCESSING,\n    SEQUENCE,\n    SET,\n    TEXT,\n    UNKNOWN_SYMBOL,\n)\nfrom ludwig.data.preprocessing import preprocess_for_prediction\nfrom ludwig.explain.explainer import Explainer\nfrom ludwig.explain.explanation import ExplanationsResult\nfrom ludwig.explain.util import get_pred_col, replace_layer_with_copy\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.models.ecd import ECD\nfrom ludwig.utils.torch_utils import DEVICE\n\nlogger = logging.getLogger(__name__)\n\n# These types as provided as integer values and passed through an embedding layer that breaks integrated gradients.\n# As such, we need to take care to encode them before handing them to the explainer.\nEMBEDDED_TYPES = {SEQUENCE, TEXT, CATEGORY, SET, DATE}\n\n\n@dataclass\nclass ExplanationRunConfig:\n    \"\"\"Mutable state containing runtime configuration for explanation process.\n\n    This is useful for updating the batch size used during explanation so it can be propagated across calls to\n    `get_total_attribution`.\n    \"\"\"\n\n    batch_size: int\n\n\ndef retry_with_halved_batch_size(run_config: ExplanationRunConfig):\n    \"\"\"Function wrapper that retries an fn with a halved batch size.\n\n    We want to maintain as large of a batch size as possible to maximize throughput. However, calculating explanations\n    requires significantly more memory, and the original batch sized used during training may be too large and cause a\n    CUDA OOM error, for example, if using GPUs.\n\n    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.\n    \"\"\"\n\n    def retry_with_halved_batch_size_fn(fn):\n        def retry_with_halved_batch_size_wrapper(*args, **kwargs):\n            latest_error = None\n            while run_config.batch_size >= MINIMUM_BATCH_SIZE:\n                try:\n                    return fn(*args, **kwargs)\n                except RuntimeError as e:\n                    latest_error = e\n                    # PyTorch only generates Runtime errors for CUDA OOM.\n                    gc.collect()\n                    if \"CUDA out of memory\" in str(e) or isinstance(e, torch.cuda.OutOfMemoryError):\n                        logger.exception(f\"OOM at batch_size={run_config.batch_size}, halving and trying again\")\n                        run_config.batch_size //= 2\n                    else:\n                        # Not a CUDA error\n                        raise\n\n            raise RuntimeError(\n                f\"Ran into latest error {latest_error} during explanation. \"\n                \"If a CUDA out of memory error, then the batch size could not be reduced any further.\"\n            )\n\n        return retry_with_halved_batch_size_wrapper\n\n    return retry_with_halved_batch_size_fn\n\n\nclass WrapperModule(torch.nn.Module):\n    \"\"\"Model used by the explainer to generate predictions.\n\n    Unlike Ludwig's ECD class, this wrapper takes individual args as inputs to the forward function. We derive the order\n    of these args from the order of the input_feature keys in ECD, which is guaranteed to be consistent (Python\n    dictionaries are ordered consistently), so we can map back to the input feature dictionary as a second step within\n    this wrapper.\n    \"\"\"\n\n    def __init__(self, model: ECD, target: str):\n        super().__init__()\n        self.model = model\n        self.target = target\n        self.input_maps = LudwigFeatureDict()\n        self.input_maps.update(\n            {\n                arg_name: InputIdentity(arg_name)\n                for arg_name in self.model.input_features.keys()\n                if self.model.input_features.get(arg_name).type() not in EMBEDDED_TYPES\n            }\n        )\n\n    def forward(self, *args):\n        # Add back the dictionary structure so it conforms to ECD format.\n        input_features: LudwigFeatureDict = self.model.input_features\n        inputs = {\n            # Send the input through the identity layer so that we can use the output of the layer for attribution.\n            # Except for text/category features where we use the embedding layer for attribution.\n            feat_name: (\n                feat_input\n                if input_features.get(feat_name).type() in EMBEDDED_TYPES\n                else self.input_maps.get(feat_name)(feat_input)\n            )\n            for feat_name, feat_input in zip(input_features.keys(), args)\n        }\n\n        outputs = self.model(inputs)\n\n        # At this point we only have the raw logits, but to make explainability work we need the probabilities\n        # and predictions as well, so derive them.\n        predictions = {}\n        for of_name in self.model.output_features:\n            predictions[of_name] = self.model.output_features.get(of_name).predictions(outputs, of_name)\n\n        pred_t = get_pred_col(predictions, self.target)\n\n        # If the target feature is a non-scalar type (vector, set, etc.), sum it to get a scalar value.\n        # https://github.com/pytorch/captum/issues/377\n        if len(pred_t.shape) > 1 and self.model.output_features.get(self.target).type() not in {\n            CATEGORY,\n            NUMBER,\n            BINARY,\n        }:\n            pred_t = torch.sum(pred_t.reshape(pred_t.shape[0], -1), dim=1)\n\n        return pred_t\n\n\n@PublicAPI(stability=\"experimental\")\nclass IntegratedGradientsExplainer(Explainer):\n    def explain(self) -> ExplanationsResult:\n        \"\"\"Explain the model's predictions using Integrated Gradients.\n\n        # Return\n\n        :return: ExplanationsResult containing the explanations.\n            `global_explanations`: (Explanation) Aggregate explanation for the entire input data.\n\n            `row_explanations`: (List[Explanation]) A list of explanations, one for each row in the input data. Each\n            explanation contains the integrated gradients for each label in the target feature's vocab with respect to\n            each input feature.\n\n            `expected_values`: (List[float]) of length [output feature cardinality] Average convergence delta for each\n            label in the target feature's vocab.\n        \"\"\"\n\n        # TODO(travis): add back skip encoders at the end in finally. Shouldn't be an issue in most cases as we\n        # typically perform explanations on a loaded model and don't use it to predict afterwards.\n        self.model.model.unskip()\n        self.model.model.to(DEVICE)\n\n        input_features: LudwigFeatureDict = self.model.model.input_features\n        run_config = ExplanationRunConfig(batch_size=self.model.config_obj.trainer.batch_size)\n\n        get_input_tensors_with_retry = retry_with_halved_batch_size(run_config)(get_input_tensors)\n        get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_total_attribution)\n\n        # Convert input data into embedding tensors from the output of the model encoders.\n        inputs_encoded = get_input_tensors_with_retry(self.model, self.inputs_df, run_config)\n        sample_encoded = get_input_tensors_with_retry(self.model, self.sample_df, run_config)\n        baseline = get_baseline(self.model, sample_encoded)\n\n        # Compute attribution for each possible output feature label separately.\n        expected_values = []\n        for target_idx in tqdm(range(self.vocab_size), desc=\"Explain\"):\n            total_attribution, feat_to_token_attributions, total_attribution_global = get_total_attribution_with_retry(\n                self.model,\n                self.target_feature_name,\n                target_idx if self.is_category_target else None,\n                inputs_encoded,\n                baseline,\n                len(self.inputs_df),\n                run_config,\n            )\n\n            # Aggregate token attributions\n            feat_to_token_attributions_global = {}\n            for feat_name, token_attributions in feat_to_token_attributions.items():\n                token_attributions_global = defaultdict(float)\n                # sum attributions for each token\n                for token, token_attribution in (ta for tas in token_attributions for ta in tas):\n                    token_attributions_global[token] += abs(token_attribution)\n                # divide by number of samples to get average attribution per token\n                token_attributions_global = {\n                    token: token_attribution / max(0, len(token_attributions))\n                    for token, token_attribution in token_attributions_global.items()\n                }\n                # convert to list of tuples and sort by attribution\n                token_attributions_global = sorted(token_attributions_global.items(), key=lambda x: x[1], reverse=True)\n                # keep only top 100 tokens\n                token_attributions_global = token_attributions_global[:100]\n                feat_to_token_attributions_global[feat_name] = token_attributions_global\n\n            self.global_explanation.add(\n                input_features.keys(), total_attribution_global, feat_to_token_attributions_global\n            )\n\n            for i, (feature_attributions, explanation) in enumerate(zip(total_attribution, self.row_explanations)):\n                # Add the feature attributions to the explanation object for this row.\n                explanation.add(\n                    input_features.keys(),\n                    feature_attributions,\n                    {k: v[i] for k, v in feat_to_token_attributions.items()},\n                )\n\n            # TODO(travis): for force plots, need something similar to SHAP E[X]\n            expected_values.append(0.0)\n\n            if self.is_binary_target:\n                # For binary targets, we only need to compute attribution for the positive class (see below).\n                break\n\n        # For binary targets, add an extra attribution for the negative class (false).\n        if self.is_binary_target:\n            le_true = self.global_explanation.label_explanations[0]\n            negated_attributions = le_true.to_array() * -1\n            negated_token_attributions = {\n                fa.feature_name: [(t, -a) for t, a in fa.token_attributions]\n                for fa in le_true.feature_attributions\n                if fa.token_attributions is not None\n            }\n            # Prepend the negative class to the list of label explanations.\n            self.global_explanation.add(\n                input_features.keys(), negated_attributions, negated_token_attributions, prepend=True\n            )\n\n            for explanation in self.row_explanations:\n                le_true = explanation.label_explanations[0]\n                negated_attributions = le_true.to_array() * -1\n                negated_token_attributions = {\n                    fa.feature_name: [(t, -a) for t, a in fa.token_attributions]\n                    for fa in le_true.feature_attributions\n                    if fa.token_attributions is not None\n                }\n                # Prepend the negative class to the list of label explanations.\n                explanation.add(input_features.keys(), negated_attributions, negated_token_attributions, prepend=True)\n\n            # TODO(travis): for force plots, need something similar to SHAP E[X]\n            expected_values.append(0.0)\n\n        return ExplanationsResult(self.global_explanation, self.row_explanations, expected_values)\n\n\ndef get_input_tensors(\n    model: LudwigModel, input_set: pd.DataFrame, run_config: ExplanationRunConfig\n) -> list[torch.Tensor]:\n    \"\"\"Convert the input data into a list of variables, one for each input feature.\n\n    # Inputs\n\n    :param model: The LudwigModel to use for encoding.\n    :param input_set: The input data to encode of shape [batch size, num input features].  # Return\n    :return: A list of variables, one for each input feature. Shape of each variable is [batch size, embedding size].\n    \"\"\"\n    # Ignore sample_ratio and sample_size from the model config, since we want to explain all the data.\n    sample_ratio_bak = model.config_obj.preprocessing.sample_ratio\n    sample_size_bak = model.config_obj.preprocessing.sample_size\n    model.config_obj.preprocessing.sample_ratio = 1.0\n    model.config_obj.preprocessing.sample_size = None\n\n    config = model.config_obj.to_dict()\n    training_set_metadata = copy.deepcopy(model.training_set_metadata)\n    for feature in config[INPUT_FEATURES]:\n        preprocessing = training_set_metadata[feature[NAME]][PREPROCESSING]\n        if preprocessing.get(\"cache_encoder_embeddings\"):\n            preprocessing[\"cache_encoder_embeddings\"] = False\n\n    # Convert raw input data into preprocessed tensor data\n    dataset, _ = preprocess_for_prediction(\n        config,\n        dataset=input_set,\n        training_set_metadata=training_set_metadata,\n        data_format=\"auto\",\n        split=\"full\",\n        include_outputs=False,\n        backend=model.backend,\n        callbacks=model.callbacks,\n    )\n\n    # Restore sample_ratio and sample_size\n    model.config_obj.preprocessing.sample_ratio = sample_ratio_bak\n    model.config_obj.preprocessing.sample_size = sample_size_bak\n\n    # Make sure the number of rows in the preprocessed dataset matches the number of rows in the input data\n    assert (\n        dataset.to_df().shape[0] == input_set.shape[0]\n    ), f\"Expected {input_set.shape[0]} rows in preprocessed dataset, but got {dataset.to_df().shape[0]}\"\n\n    # Convert dataset into a dict of tensors, and split each tensor into batches to control GPU memory usage\n    inputs = {\n        name: torch.from_numpy(dataset.dataset[feature.proc_column]).split(run_config.batch_size)\n        for name, feature in model.model.input_features.items()\n    }\n\n    # Dict of lists to list of dicts\n    input_batches = [dict(zip(inputs, t)) for t in zip(*inputs.values())]\n\n    # List of dicts to dict of lists\n    preproc_inputs = {k: torch.cat([d[k] for d in input_batches]) for k in input_batches[0]}\n\n    data_to_predict = [v for _, v in preproc_inputs.items()]\n    tensors = []\n    for t in data_to_predict:\n        # TODO(travis): Consider changing to `if not torch.is_floating_point(t.dtype)` to simplify, then handle bool\n        # case in this block.\n        if t.dtype == torch.int8 or t.dtype == torch.int16 or t.dtype == torch.int32 or t.dtype == torch.int64:\n            # Don't wrap input into a variable if it's an integer type, since it will be used as an index into the\n            # embedding table. We explain the output of the embedding table, not the input to the embedding table using\n            # LayerIntegratedGradients.\n            tensors.append(t)\n        else:\n            # Wrap input into a variable so torch will track the gradient and LayerIntegratedGradients can explain it.\n            if t.dtype == torch.bool:\n                t = t.to(torch.float32)\n            tensors.append(Variable(t, requires_grad=True))\n\n    return tensors\n\n\ndef get_baseline(model: LudwigModel, sample_encoded: list[Variable]) -> list[torch.Tensor]:\n    # TODO(travis): pre-compute this during training from the full training dataset.\n    input_features: LudwigFeatureDict = model.model.input_features\n\n    baselines = []\n    for sample_input, (name, feature) in zip(sample_encoded, input_features.items()):\n        metadata = model.training_set_metadata[name]\n        if feature.type() == TEXT:\n            PAD_IND = metadata.get(\"pad_idx\", metadata.get(\"word_pad_idx\"))\n            token_reference = TokenReferenceBase(reference_token_idx=PAD_IND)\n            baseline = token_reference.generate_reference(sequence_length=sample_input.shape[1], device=DEVICE)\n        elif feature.type() == CATEGORY:\n            most_popular_token = max(metadata[\"str2freq\"], key=metadata[\"str2freq\"].get)\n            most_popular_tok_idx = metadata[\"str2idx\"].get(most_popular_token)\n\n            # If an unknown is defined, use that as the baseline index, else use the most popular token\n            baseline_tok_idx = metadata[\"str2idx\"].get(UNKNOWN_SYMBOL, most_popular_tok_idx)\n            baseline = torch.tensor(baseline_tok_idx, device=DEVICE)\n        elif feature.type() == IMAGE:\n            baseline = torch.zeros_like(sample_input[0], device=DEVICE)\n        else:\n            # For a robust baseline, we take the mean of all samples from the training data.\n            baseline = torch.mean(sample_input.float(), dim=0)\n        baselines.append(baseline.unsqueeze(0))\n\n    return baselines\n\n\ndef get_total_attribution(\n    model: LudwigModel,\n    target_feature_name: str,\n    target_idx: int | None,\n    feature_inputs: list[Variable],\n    baseline: list[torch.Tensor],\n    nsamples: int,\n    run_config: ExplanationRunConfig,\n) -> tuple[npt.NDArray[np.float64], dict[str, list[list[tuple[str, float]]]]]:\n    \"\"\"Compute the total attribution for each input feature for each row in the input data.\n\n    Args:\n        model: The Ludwig model to explain.\n        target_feature_name: The name of the target feature to explain.\n        target_idx: The index of the target feature label to explain if the target feature is a category.\n        feature_inputs: The preprocessed input data as a list of tensors of length [num_features].\n        baseline: The baseline input data as a list of tensors of length [num_features].\n        nsamples: The total number of samples in the input data.\n\n    Returns:\n        The token-attribution pair for each token in the input feature for each row in the input data. The members of\n        the output tuple are structured as follows:\n\n        `total_attribution_rows`: (npt.NDArray[np.float64]) of shape [num_rows, num_features]\n        The total attribution for each input feature for each row in the input data.\n\n        `feat_to_token_attributions`: (Dict[str, List[List[Tuple[str, float]]]]) with values of shape\n        [num_rows, seq_len, 2]\n\n        `total_attribution_global`: (npt.NDArray[np.float64]) of shape [num_features]\n        The attribution for each input feature aggregated across all input data.\n    \"\"\"\n    input_features: LudwigFeatureDict = model.model.input_features\n\n    # Configure the explainer, which includes wrapping the model so its interface conforms to\n    # the format expected by Captum.\n    model.model.zero_grad()\n    explanation_model = WrapperModule(model.model, target_feature_name)\n\n    layers = []\n    for feat_name, feat in input_features.items():\n        if feat.type() in EMBEDDED_TYPES:\n            # Get embedding layer from encoder, which is the first child of the encoder.\n            target_layer = feat.encoder_obj.get_embedding_layer()\n\n            # If the current layer matches any layer in the list, make a deep copy of the layer.\n            if len(layers) > 0 and any(target_layer == layer for layer in layers):\n                # Replace the layer with a deep copy of the layer to ensure that the attributions unique for each input\n                # feature that uses a shared layer.\n                # Recommended here: https://github.com/pytorch/captum/issues/794#issuecomment-1093021638\n                replace_layer_with_copy(feat, target_layer)\n                target_layer = feat.encoder_obj.get_embedding_layer()  # get the new copy\n        else:\n            # Get the wrapped input layer.\n            target_layer = explanation_model.input_maps.get(feat_name)\n\n        layers.append(target_layer)\n\n    explainer = LayerIntegratedGradients(explanation_model, layers)\n\n    feature_inputs_splits = [ipt.split(run_config.batch_size) for ipt in feature_inputs]\n    baseline = [t.to(DEVICE) for t in baseline]\n\n    total_attribution_rows = None\n    total_attribution_global = None\n    feat_to_token_attributions = defaultdict(list)\n    for input_batch in zip(*feature_inputs_splits):\n        input_batch = [ipt.to(DEVICE) for ipt in input_batch]\n        attribution = explainer.attribute(\n            tuple(input_batch),\n            baselines=tuple(baseline),\n            target=target_idx,\n            # https://captum.ai/docs/faq#i-am-facing-out-of-memory-oom-errors-when-using-captum-how-do-i-resolve-this\n            internal_batch_size=run_config.batch_size,\n        )\n\n        attributions_reduced = []\n        for a in attribution:\n            a_reduced = a.detach().cpu()\n            if a_reduced.ndim == 2 or a_reduced.ndim == 3:\n                # Reduces category-level attributions of shape [batch_size, embedding_dim] by summing over the\n                # embedding dimension to get attributions of shape [batch_size].\n                # Reduces token-level attributions of shape [batch_size, sequence_length, embedding_dim] by summing\n                # over the embedding dimension to get attributions of shape [batch_size, sequence_length]. We keep\n                # the sequence dimension so we can map the attributions to the tokens.\n                a_reduced = a_reduced.sum(dim=-1)\n            elif a_reduced.ndim == 4:\n                # Reduce pixel-level attributions of shape [batch_size, num_channels, height, width] by summing\n                # over the channel and spatial dimensions to get attributions of shape [batch_size].\n                a_reduced = a_reduced.sum(dim=(1, 2, 3))\n            attributions_reduced.append(a_reduced)\n\n        for inputs, attrs, (name, feat) in zip(input_batch, attributions_reduced, input_features.items()):\n            if feat.type() == TEXT:\n                tok_attrs = get_token_attributions(model, name, inputs.detach().cpu(), attrs)\n                feat_to_token_attributions[name].append(tok_attrs)\n\n        # Reduce attribution to [num_input_features, batch_size] by summing over the sequence dimension (if present).\n        attribution = [a.sum(dim=-1) if a.ndim == 2 else a for a in attributions_reduced]\n        attribution = np.stack(attribution)\n\n        # Transpose to [batch_size, num_input_features]\n        attribution = attribution.T\n\n        if total_attribution_rows is not None:\n            total_attribution_rows = np.concatenate([total_attribution_rows, attribution], axis=0)\n        else:\n            total_attribution_rows = attribution\n\n        if total_attribution_global is not None:\n            total_attribution_global += attribution.sum(axis=0)\n        else:\n            total_attribution_global = attribution.sum(axis=0)\n\n    total_attribution_global /= nsamples\n\n    feat_to_token_attributions = {k: [e for lst in v for e in lst] for k, v in feat_to_token_attributions.items()}\n\n    return total_attribution_rows, feat_to_token_attributions, total_attribution_global\n\n\ndef get_token_attributions(\n    model: LudwigModel,\n    feature_name: str,\n    input_ids: torch.Tensor,\n    token_attributions: torch.Tensor,\n) -> list[list[tuple[str, float]]]:\n    \"\"\"Convert token-level attributions to an array of token-attribution pairs of shape.\n\n    [batch_size, sequence_length, 2].\n\n    Args:\n        model: The LudwigModel used to generate the attributions.\n        feature_name: The name of the feature for which the attributions were generated.\n        input_ids: The input ids of shape [batch_size, sequence_length].\n        token_attributions: The token-level attributions of shape [batch_size, sequence_length].\n\n    Returns:\n        An array of token-attribution pairs of shape [batch_size, sequence_length, 2].\n    \"\"\"\n    assert (\n        input_ids.dtype == torch.int8\n        or input_ids.dtype == torch.int16\n        or input_ids.dtype == torch.int32\n        or input_ids.dtype == torch.int64\n    )\n\n    # Normalize token-level attributions to visualize the relative importance of each token.\n    norm = torch.linalg.norm(token_attributions, dim=1)\n    # Safe divide by zero by setting the norm to 1 if the norm is 0.\n    norm = torch.where(norm == 0, torch.ones_like(norm), norm)\n    token_attributions = token_attributions / norm.unsqueeze(-1)\n\n    # map input ids to input tokens via the vocabulary\n    feature = model.training_set_metadata[feature_name]\n    vocab = feature.get(\"idx2str\", feature.get(\"word_idx2str\"))\n    idx2str = np.vectorize(lambda idx: vocab[idx])\n    input_tokens = idx2str(input_ids)\n\n    # add attribution to the input tokens\n    tok_attrs = [\n        list(zip(t, a)) for t, a in zip(input_tokens, token_attributions.tolist())\n    ]  # [batch_size, sequence_length, 2]\n\n    return tok_attrs\n"
  },
  {
    "path": "ludwig/explain/captum_ray.py",
    "content": "from collections import defaultdict\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport ray\nfrom torch.autograd import Variable\nfrom tqdm import tqdm\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.explain.captum import (\n    ExplanationRunConfig,\n    get_baseline,\n    get_input_tensors,\n    get_total_attribution,\n    IntegratedGradientsExplainer,\n    retry_with_halved_batch_size,\n)\nfrom ludwig.explain.explanation import ExplanationsResult\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.utils.torch_utils import get_torch_device\n\n\n@PublicAPI(stability=\"experimental\")\nclass RayIntegratedGradientsExplainer(IntegratedGradientsExplainer):\n    def __init__(self, *args, resources_per_task: dict[str, Any] = None, num_workers: int = 1, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.resources_per_task = resources_per_task or {}\n        self.num_workers = num_workers\n\n    def explain(self) -> ExplanationsResult:\n        \"\"\"Explain the model's predictions using Integrated Gradients.\n\n        # Return\n\n        :return: ExplanationsResult containing the explanations.\n            `global_explanations`: (Explanation) Aggregate explanation for the entire input data.\n\n            `row_explanations`: (List[Explanation]) A list of explanations, one for each row in the input data. Each\n            explanation contains the integrated gradients for each label in the target feature's vocab with respect to\n            each input feature.\n\n            `expected_values`: (List[float]) of length [output feature cardinality] Average convergence delta for each\n            label in the target feature's vocab.\n        \"\"\"\n        self.model.model.cpu()\n        input_features: LudwigFeatureDict = self.model.model.input_features\n        model_ref = ray.put(self.model)\n        run_config = ExplanationRunConfig(batch_size=self.model.config_obj.trainer.batch_size)\n\n        # Convert input data into embedding tensors from the output of the model encoders.\n        inputs_encoded_ref = get_input_tensors_task.options(**self.resources_per_task).remote(\n            model_ref, ray.put(self.inputs_df), run_config\n        )\n        sample_encoded_ref = get_input_tensors_task.options(**self.resources_per_task).remote(\n            model_ref, ray.put(self.sample_df), run_config\n        )\n\n        inputs_encoded, run_config = ray.get(inputs_encoded_ref)\n        sample_encoded, run_config = ray.get(sample_encoded_ref)\n        baseline = get_baseline(self.model, sample_encoded)\n\n        inputs_encoded_ref = ray.put(inputs_encoded)\n        baseline_ref = ray.put(baseline)\n\n        if self.is_category_target:\n            # Evenly divide the list of labels among the desired number of workers (Ray tasks).\n            # For example, 4 GPUs -> 4 workers. We do this instead of creating nlabels tasks because\n            # there is significant overhead to spawning a Ray task.\n            target_splits = split_list(list(range(self.vocab_size)), self.num_workers)\n        else:\n            # No target index to compare against exists for number features.\n            # For binary targets, we only need to compute attribution for the positive class (see below).\n            # May need to revisit in the future for additional feature types.\n            target_splits = [[None]]\n\n        # Compute attribution for each possible output feature label separately.\n        attrs_refs = []\n        for target_indices in target_splits:\n            attrs_ref = get_total_attribution_task.options(**self.resources_per_task).remote(\n                model_ref,\n                self.target_feature_name,\n                target_indices,\n                inputs_encoded_ref,\n                baseline_ref,\n                len(self.inputs_df),\n                run_config,\n            )\n            attrs_refs.append(attrs_ref)\n\n        # Await the completion of our Ray tasks, then merge the results.\n        expected_values = []\n        for attrs_ref in tqdm(attrs_refs, desc=\"Explain\"):\n            attrs = ray.get(attrs_ref)\n            for total_attribution, feat_to_token_attributions, total_attribution_global in attrs:\n                # Aggregate token attributions\n                feat_to_token_attributions_global = {}\n                for feat_name, token_attributions in feat_to_token_attributions.items():\n                    token_attributions_global = defaultdict(float)\n                    # sum attributions for each token\n                    for token, token_attribution in (ta for tas in token_attributions for ta in tas):\n                        token_attributions_global[token] += token_attribution\n                    # divide by number of samples to get average attribution per token\n                    token_attributions_global = {\n                        token: token_attribution / max(0, len(token_attributions))\n                        for token, token_attribution in token_attributions_global.items()\n                    }\n                    # convert to list of tuples and sort by attribution\n                    token_attributions_global = sorted(\n                        token_attributions_global.items(), key=lambda x: x[1], reverse=True\n                    )\n                    # keep only top 100 tokens\n                    token_attributions_global = token_attributions_global[:100]\n                    feat_to_token_attributions_global[feat_name] = token_attributions_global\n\n                self.global_explanation.add(\n                    input_features.keys(), total_attribution_global, feat_to_token_attributions_global\n                )\n\n                for i, (feature_attributions, explanation) in enumerate(zip(total_attribution, self.row_explanations)):\n                    # Add the feature attributions to the explanation object for this row.\n                    explanation.add(\n                        input_features.keys(),\n                        feature_attributions,\n                        {k: v[i] for k, v in feat_to_token_attributions.items()},\n                    )\n\n                # TODO(travis): for force plots, need something similar to SHAP E[X]\n                expected_values.append(0.0)\n\n        # For binary targets, add an extra attribution for the negative class (false).\n        if self.is_binary_target:\n            le_true = self.global_explanation.label_explanations[0]\n            negated_attributions = le_true.to_array() * -1\n            negated_token_attributions = {\n                fa.feature_name: [(t, -a) for t, a in fa.token_attributions]\n                for fa in le_true.feature_attributions\n                if fa.token_attributions is not None\n            }\n            # Prepend the negative class to the list of label explanations.\n            self.global_explanation.add(\n                input_features.keys(), negated_attributions, negated_token_attributions, prepend=True\n            )\n\n            for explanation in self.row_explanations:\n                le_true = explanation.label_explanations[0]\n                negated_attributions = le_true.to_array() * -1\n                negated_token_attributions = {\n                    fa.feature_name: [(t, -a) for t, a in fa.token_attributions]\n                    for fa in le_true.feature_attributions\n                    if fa.token_attributions is not None\n                }\n                # Prepend the negative class to the list of label explanations.\n                explanation.add(input_features.keys(), negated_attributions, negated_token_attributions, prepend=True)\n\n            # TODO(travis): for force plots, need something similar to SHAP E[X]\n            expected_values.append(0.0)\n\n        return ExplanationsResult(self.global_explanation, self.row_explanations, expected_values)\n\n\n@ray.remote(max_calls=1)\ndef get_input_tensors_task(\n    model: LudwigModel, df: pd.DataFrame, run_config: ExplanationRunConfig\n) -> tuple[list[Variable], ExplanationRunConfig]:\n    model.model.unskip()\n    model.model.to(get_torch_device())\n    try:\n        get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_input_tensors)\n        return get_total_attribution_with_retry(model, df, run_config), run_config\n    finally:\n        model.model.cpu()\n\n\n@ray.remote(max_calls=1)\ndef get_total_attribution_task(\n    model: LudwigModel,\n    target_feature_name: str,\n    target_indices: list[int | None],\n    inputs_encoded: list[Variable],\n    baseline: list[Variable],\n    nsamples: int,\n    run_config: ExplanationRunConfig,\n) -> list[np.array]:\n    model.model.unskip()\n    model.model.to(get_torch_device())\n    try:\n        get_total_attribution_with_retry = retry_with_halved_batch_size(run_config)(get_total_attribution)\n        return [\n            get_total_attribution_with_retry(\n                model=model,\n                target_feature_name=target_feature_name,\n                target_idx=target_idx,\n                feature_inputs=inputs_encoded,\n                baseline=baseline,\n                nsamples=nsamples,\n                run_config=run_config,\n            )\n            for target_idx in tqdm(target_indices, desc=\"Explain\")\n        ]\n    finally:\n        model.model.cpu()\n\n\ndef split_list(v, n):\n    \"\"\"Splits a list into n roughly equal sub-lists.\n\n    Source: https://stackoverflow.com/a/2135920\n    \"\"\"\n    k, m = divmod(len(v), n)\n    return (v[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n))\n"
  },
  {
    "path": "ludwig/explain/explainer.py",
    "content": "from abc import ABCMeta, abstractmethod\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, CATEGORY, TYPE\nfrom ludwig.explain.explanation import Explanation, ExplanationsResult\nfrom ludwig.explain.util import prepare_data\n\n\n@DeveloperAPI\nclass Explainer(metaclass=ABCMeta):\n    def __init__(\n        self,\n        model: LudwigModel,\n        inputs_df: pd.DataFrame,\n        sample_df: pd.DataFrame,\n        target: str,\n    ):\n        \"\"\"Constructor for the explainer.\n\n        # Inputs\n\n        :param model: (LudwigModel) The LudwigModel to explain.\n        :param inputs_df: (pd.DataFrame) The input data to explain.\n        :param sample_df: (pd.DataFrame) A sample of the ground truth data.\n        :param target: (str) The name of the target to explain.\n        \"\"\"\n        self.model = model\n        self.inputs_df = inputs_df\n        self.sample_df = sample_df\n        self.target = target\n        self.inputs_df, self.sample_df, self.feature_cols, self.target_feature_name = prepare_data(\n            model, inputs_df, sample_df, target\n        )\n\n        self.global_explanation = Explanation(self.target_feature_name)\n        self.row_explanations = [Explanation(self.target_feature_name) for _ in self.inputs_df.index]\n\n        # Lookup from column name to output feature\n        config = self.model.config\n        self.output_feature_map = {feature[\"column\"]: feature for feature in config[\"output_features\"]}\n\n    @property\n    def is_binary_target(self) -> bool:\n        \"\"\"Whether the target is binary.\"\"\"\n        return self.output_feature_map[self.target_feature_name][TYPE] == BINARY\n\n    @property\n    def is_category_target(self) -> bool:\n        \"\"\"Whether the target is categorical.\"\"\"\n        return self.output_feature_map[self.target_feature_name][TYPE] == CATEGORY\n\n    @property\n    def vocab_size(self) -> int:\n        \"\"\"The vocab size of the target feature.\n\n        For regression (number) this is 1, for binary it is 2, and for category it is the vocab size.\n        \"\"\"\n        if self.is_category_target:\n            return self.model.training_set_metadata[self.target_feature_name][\"vocab_size\"]\n        elif self.is_binary_target:\n            return 2\n        return 1\n\n    @abstractmethod\n    def explain(self) -> ExplanationsResult:\n        \"\"\"Explain the model's predictions.\n\n        # Return\n\n        :return: ExplanationsResult containing the explanations.\n        \"\"\"\n"
  },
  {
    "path": "ludwig/explain/explanation.py",
    "content": "from dataclasses import dataclass, field\n\nimport numpy as np\nimport numpy.typing as npt\n\nfrom ludwig.api_annotations import DeveloperAPI, PublicAPI\n\n\n@DeveloperAPI\n@dataclass\nclass FeatureAttribution:\n    \"\"\"Stores the attribution for a single input feature.\"\"\"\n\n    # The name of the input feature.\n    feature_name: str\n\n    # The scalar attribution for the input feature.\n    attribution: float\n\n    # (Optional) The attribution for each token in the input feature as an array of shape (seq_len, 2).\n    token_attributions: list[tuple[str, float]] = None\n\n\n@DeveloperAPI\n@dataclass\nclass LabelExplanation:\n    \"\"\"Stores the feature attributions for a single label in the target feature's vocab.\"\"\"\n\n    # The attribution for each input feature.\n    feature_attributions: list[FeatureAttribution] = field(default_factory=list)\n\n    def add(self, feature_name: str, attribution: float, token_attributions: list[tuple[str, float]] = None):\n        \"\"\"Add the attribution for a single input feature.\"\"\"\n        self.feature_attributions.append(FeatureAttribution(feature_name, attribution, token_attributions))\n\n    def to_array(self) -> npt.NDArray[np.float64]:\n        \"\"\"Convert the explanation to a 1D array of shape (num_features,).\"\"\"\n        return np.array([fa.attribution for fa in self.feature_attributions])\n\n\n@DeveloperAPI\n@dataclass\nclass Explanation:\n    \"\"\"Stores the explanations for a single row of input data.\n\n    Contains the feature attributions for each label in the target feature's vocab.\n    \"\"\"\n\n    target: str\n\n    # The explanations for each label in the vocab of the target feature.\n    label_explanations: list[LabelExplanation] = field(default_factory=list)\n\n    def add(\n        self,\n        feat_names: list[str],\n        feat_attributions: npt.NDArray[np.float64],\n        feat_to_token_attributions: dict[str, list[tuple[str, float]]] = None,\n        prepend: bool = False,\n    ):\n        \"\"\"Add the feature attributions for a single label.\"\"\"\n        assert len(feat_names) == len(\n            feat_attributions\n        ), f\"Expected {len(feat_names)} feature attributions, got {len(feat_attributions)}\"\n        if len(self.label_explanations) > 0:\n            # Check that the feature attributions are the same shape as existing explanations.\n            assert self.label_explanations[0].to_array().shape == feat_attributions.shape, (\n                f\"Expected feature attributions of shape {self.label_explanations[0].to_array().shape}, \"\n                f\"got {feat_attributions.shape}\"\n            )\n\n        le = LabelExplanation()\n        for i, feat_name in enumerate(feat_names):\n            le.add(\n                feat_name,\n                feat_attributions[i],\n                feat_to_token_attributions.get(feat_name) if feat_to_token_attributions else None,\n            )\n        self.label_explanations.insert(0, le) if prepend else self.label_explanations.append(le)\n\n    def to_array(self) -> npt.NDArray[np.float64]:\n        \"\"\"Convert the explanation to a 2D array of shape (num_labels, num_features).\"\"\"\n        return np.array([le.to_array() for le in self.label_explanations])\n\n\n@PublicAPI(stability=\"experimental\")\n@dataclass\nclass ExplanationsResult:\n    # Aggregate explanation for the entire input data.\n    global_explanation: Explanation  # GlobalExplanation\n\n    # A list of explanations, one for each row in the input data.\n    # Each explanation contains the feature attributions for each label in the target feature's vocab.\n    row_explanations: list[Explanation]\n\n    # Expected value for each label in the target feature's vocab.\n    expected_values: list[float]\n"
  },
  {
    "path": "ludwig/explain/util.py",
    "content": "from copy import deepcopy\n\nimport pandas as pd\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import COLUMN, INPUT_FEATURES, PREPROCESSING, SPLIT\nfrom ludwig.data.split import get_splitter\nfrom ludwig.features.base_feature import BaseFeature\n\n\ndef filter_cols(df, cols):\n    cols = {c.lower() for c in cols}\n    retain_cols = [c for c in df.columns if c.lower() in cols]\n    return df[retain_cols]\n\n\ndef prepare_data(model: LudwigModel, inputs_df: pd.DataFrame, sample_df: pd.DataFrame, target: str):\n    config = model.config\n    feature_cols = [feature[COLUMN] for feature in config[INPUT_FEATURES]]\n    if SPLIT in config.get(PREPROCESSING, {}):\n        # Keep columns required for Ludwig preprocessing\n        splitter = get_splitter(**config[PREPROCESSING][SPLIT])\n        feature_cols += splitter.required_columns\n    target_feature_name = get_feature_name(model, target)\n\n    inputs_df = filter_cols(inputs_df, feature_cols)\n    if sample_df is not None:\n        sample_df = filter_cols(sample_df, feature_cols)\n\n    return inputs_df, sample_df, feature_cols, target_feature_name\n\n\ndef get_pred_col(preds, target):\n    t = target.lower()\n    for c in preds.keys():\n        if c.lower() == t:\n            if \"probabilities\" in preds[c]:\n                return preds[c][\"probabilities\"]\n            else:\n                return preds[c][\"predictions\"]\n    raise ValueError(f\"Unable to find target column {t} in {preds.keys()}\")\n\n\ndef get_feature_name(model: LudwigModel, target: str) -> str:\n    t = target.lower()\n    for c in model.training_set_metadata.keys():\n        if c.lower() == t:\n            return c\n    raise ValueError(f\"Unable to find target column {t} in {model.training_set_metadata.keys()}\")\n\n\ndef get_absolute_module_key_from_submodule(module: torch.nn.Module, submodule: torch.nn.Module):\n    \"\"\"Get the absolute module key for each param in the target layer.\n\n    Assumes that the keys in the submodule are relative to the module.\n\n    We find the params from the submodule in the module by comparing the data\n    pointers, since the data returned by named_parameters is by reference.\n    More information on checking if tensors point to the same place in storage can be found here:\n    https://discuss.pytorch.org/t/any-way-to-check-if-two-tensors-have-the-same-base/44310/2\n    \"\"\"\n    absolute_keys = []\n    for module_key, module_param in module.named_parameters():\n        for _, submodule_param in submodule.named_parameters():\n            if submodule_param.data_ptr() == module_param.data_ptr():\n                absolute_keys.append(module_key)\n                break\n    return absolute_keys\n\n\ndef replace_layer_with_copy(feat: BaseFeature, target_layer: torch.nn.Module):\n    \"\"\"Replaces a layer in a feature with a copy of the layer in-place.\n\n    This is useful in a tied weights scenario, where a single encoder may be used by multiple features. If we leave\n    as-is, Captum complains about the resulting computation graph. The solution is to create an identical\n    (deep) copy of the layer fed into Captum: https://github.com/pytorch/captum/issues/794#issuecomment-1093021638\n\n    This is safe to do during the explain step because we are essentially running inference, and no model artifacts are\n    being saved during the explain step.\n\n    TODO(geoffrey): if a user ever wants to train immediately after explain (i.e. w/o loading weights from the disk),\n    we might want to implement this as a context so that we can restore the original encoder object at the end.\n    Will defer this implementation for now because that scenario seems unlikely.\n\n    At a high-level the approach is the following:\n    1. Create a deep-copy of the entire encoder object and set it as the feature's encoder object\n    2. Replace the tensors in the copied encoder object with the tensors from the original encoder object, except for\n         the tensors in the target layer. We want to explain these tensors, so we want to keep them as deep copies.\n\n    This approach ensures that at most 2 copies of the encoder object are in memory at any given time.\n    \"\"\"\n    with torch.no_grad():\n        # Get the original encoder object and a mapping from param names to the params themselves.\n        orig_encoder_obj = feat.encoder_obj\n        orig_encoder_obj_state_dict = orig_encoder_obj.state_dict()\n\n        # Deep copy the original encoder object and set the copy as this feature's encoder object.\n        copy_encoder_obj = deepcopy(orig_encoder_obj)\n        feat.encoder_obj = copy_encoder_obj\n\n        # We have to get the absolute module key in order to do string matching because the target_layer keys are\n        # relative to itself. If we were to leave it as-is and attempt to suffix match, we may get duplicates for\n        # common layers i.e. \"LayerNorm.weight\" and \"LayerNorm.bias\". Getting the absolute module key ensures we\n        # use values like \"transformer.module.embedding.LayerNorm.weight\" instead.\n        keys_to_keep_copy = get_absolute_module_key_from_submodule(orig_encoder_obj, target_layer)\n\n        # Get the tensors to keep from the copied encoder object. These are the tensors in the target layer.\n        for key, param in copy_encoder_obj.named_parameters():\n            if key not in keys_to_keep_copy:\n                param.data = orig_encoder_obj_state_dict[key].data\n"
  },
  {
    "path": "ludwig/export.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport os\nimport sys\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\nfrom ludwig.utils.triton_utils import export_triton as utils_export_triton\n\nlogger = logging.getLogger(__name__)\n\n\ndef export_torchscript(\n    model_path: str, model_only: bool = False, output_path: str | None = None, device: str | None = None, **kwargs\n) -> None:\n    \"\"\"Exports a model to torchscript.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param model_only: (bool, default: `False`) If true, scripts and exports the model only.\n    :param output_path: directory to store torchscript. If `None`, defaults to model_path\n\n    # Return\n    :returns: (`None`)\n    \"\"\"\n    logger.info(f\"Model path: {model_path}\")\n    logger.info(f\"Saving model only: {model_only}\")\n\n    if output_path is None:\n        logger.info(\"output_path is None, defaulting to model_path\")\n        output_path = model_path\n    logger.info(f\"Output path: {output_path}\")\n    logger.info(\"\\n\")\n\n    model = LudwigModel.load(model_path)\n    os.makedirs(output_path, exist_ok=True)\n    model.save_torchscript(output_path, model_only=model_only, device=device)\n\n    logger.info(f\"Saved to: {output_path}\")\n\n\ndef export_triton(model_path, output_path=\"model_repository\", model_name=\"ludwig_model\", model_version=1, **kwargs):\n    \"\"\"Exports a model in torchscript format with config for Triton serving.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param output_path: (str, default: `'model_repository'`)  directory to store the\n        triton models.\n    :param model_name: (str, default: `'ludwig_model'`) save triton under this name.\n    :param model_name: (int, default: `1`) save model under this verison.\n\n    # Return\n\n    :returns: (`None`)\n    \"\"\"\n    logger.info(f\"Model path: {model_path}\")\n    logger.info(f\"Output path: {output_path}\")\n    logger.info(f\"Model name: {model_name}\")\n    logger.info(f\"Model version: {model_version}\")\n    logger.info(\"\\n\")\n\n    model = LudwigModel.load(model_path)\n    os.makedirs(output_path, exist_ok=True)\n\n    utils_export_triton(model=model, output_path=output_path, model_name=model_name, model_version=model_version)\n\n    logger.info(f\"Saved to: {output_path}\")\n\n\ndef export_mlflow(model_path, output_path=\"mlflow\", registered_model_name=None, **kwargs):\n    \"\"\"Exports a model to MLflow.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param output_path: (str, default: `'mlflow'`)  directory to store the\n        mlflow model.\n    :param registered_model_name: (str, default: `None`) save mlflow under this\n        name in the model registry. Saved locally if `None`.\n\n    # Return\n\n    :returns: (`None`)\n    \"\"\"\n    logger.info(f\"Model path: {model_path}\")\n    logger.info(f\"Output path: {output_path}\")\n    logger.info(\"\\n\")\n\n    from ludwig.contribs.mlflow.model import export_model\n\n    export_model(model_path, output_path, registered_model_name)\n\n    logger.info(f\"Saved to: {output_path}\")\n\n\ndef cli_export_torchscript(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \" \"and saves it as torchscript.\",\n        prog=\"ludwig export_torchscript\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\n        \"-mo\",\n        \"--model_only\",\n        help=\"Script and export the model only.\",\n        action=\"store_true\",\n    )\n    parser.add_argument(\n        \"-d\",\n        \"--device\",\n        type=str,\n        help=(\n            'Device to use for torchscript tracing (e.g. \"cuda\" or \"cpu\"). Ideally, this is the same as the device '\n            \"used when the model is loaded.\"\n        ),\n        default=None,\n    )\n\n    # -----------------\n    # Output parameters\n    # -----------------\n    parser.add_argument(\n        \"-op\",\n        \"--output_path\",\n        type=str,\n        help=\"path where to save the export model. If not specified, defaults to model_path.\",\n        default=None,\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"export_torchscript\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.export\")\n\n    print_ludwig(\"Export Torchscript\", LUDWIG_VERSION)\n\n    export_torchscript(**vars(args))\n\n\ndef cli_export_triton(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \" \"and saves it as torchscript for Triton.\",\n        prog=\"ludwig export_triton\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\"-mn\", \"--model_name\", help=\"model name\", default=\"ludwig_model\")\n    parser.add_argument(\"-mv\", \"--model_version\", type=int, help=\"model version\", default=1)\n\n    # -----------------\n    # Output parameters\n    # -----------------\n    parser.add_argument(\"-op\", \"--output_path\", type=str, help=\"path where to save the export model\", required=True)\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"export_triton\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.export\")\n\n    print_ludwig(\"Export Triton\", LUDWIG_VERSION)\n\n    export_triton(**vars(args))\n\n\ndef cli_export_mlflow(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \" \"and saves it as an MLFlow model.\",\n        prog=\"ludwig export_mlflow\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\n        \"-mn\",\n        \"--registered_model_name\",\n        help=\"model name to upload to in MLflow model registry\",\n    )\n\n    # -----------------\n    # Output parameters\n    # -----------------\n    parser.add_argument(\n        \"-op\", \"--output_path\", type=str, help=\"path where to save the exported model\", default=\"mlflow\"\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"export_mlflow\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.export\")\n\n    print_ludwig(\"Export MLFlow\", LUDWIG_VERSION)\n\n    export_mlflow(**vars(args))\n\n\nif __name__ == \"__main__\":\n    if len(sys.argv) > 1:\n        if sys.argv[1] == \"savedmodel\":\n            cli_export_torchscript(sys.argv[2:])\n        elif sys.argv[1] == \"mlflow\":\n            cli_export_mlflow(sys.argv[2:])\n        elif sys.argv[1] == \"triton\":\n            cli_export_triton(sys.argv[2:])\n        else:\n            print(\"Unrecognized command\")\n    else:\n        print(\"Unrecognized command\")\n"
  },
  {
    "path": "ludwig/features/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/features/audio_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\n\nimport numpy as np\nimport torch\nimport torchaudio\nfrom packaging import version\n\nfrom ludwig.constants import AUDIO, AUDIO_FEATURE_KEYS, COLUMN, NAME, PREPROCESSING, PROC_COLUMN, SRC, TYPE\nfrom ludwig.features.base_feature import BaseFeatureMixin\nfrom ludwig.features.sequence_feature import SequenceInputFeature\nfrom ludwig.schema.features.audio_feature import AudioInputFeatureConfig\nfrom ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.audio_utils import (\n    calculate_mean,\n    calculate_var,\n    get_default_audio,\n    get_fbank,\n    get_group_delay,\n    get_length_in_samp,\n    get_max_length_stft_based,\n    get_non_symmetric_length,\n    get_phase_stft_magnitude,\n    get_stft_magnitude,\n    is_torch_audio_tuple,\n    read_audio_from_bytes_obj,\n    read_audio_from_path,\n)\nfrom ludwig.utils.data_utils import get_abs_path\nfrom ludwig.utils.fs_utils import has_remote_protocol\nfrom ludwig.utils.misc_utils import set_default_value\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n_TORCH_200 = version.parse(torch.__version__) >= version.parse(\"2.0.0\")\n\n\nclass _AudioPreprocessing(torch.nn.Module):\n    audio_feature_dict: dict[str, float | int | str]\n\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.audio_feature_dict = {\n            key: value\n            for key, value in metadata[\"preprocessing\"].items()\n            if key in AUDIO_FEATURE_KEYS and value is not None\n        }\n        self.feature_dim = metadata[\"feature_dim\"]\n        self.max_length = metadata[\"max_length\"]\n        self.padding_value = metadata[\"preprocessing\"][\"padding_value\"]\n        self.normalization_type = metadata[\"preprocessing\"][\"norm\"]\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if not torch.jit.isinstance(v, list[tuple[torch.Tensor, int]]):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        processed_audio_matrix = []\n        for audio, sampling_rate_in_hz in v:\n            processed_audio = AudioFeatureMixin._transform_to_feature(\n                audio,\n                sampling_rate_in_hz,\n                self.audio_feature_dict,\n                self.feature_dim,\n                self.max_length,\n                self.padding_value,\n                self.normalization_type,\n            )\n            processed_audio_matrix.append(processed_audio)\n        return torch.stack(processed_audio_matrix)\n\n\nclass AudioFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return AUDIO\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        first_audio_file_path = column.head(1).iloc[0]\n        _, sampling_rate_in_hz = torchaudio.load(first_audio_file_path)\n\n        feature_dim = AudioFeatureMixin._get_feature_dim(preprocessing_parameters, sampling_rate_in_hz)\n        audio_file_length_limit_in_s = preprocessing_parameters[\"audio_file_length_limit_in_s\"]\n        max_length = AudioFeatureMixin._get_max_length_feature(\n            preprocessing_parameters, sampling_rate_in_hz, audio_file_length_limit_in_s\n        )\n        return {\n            \"feature_dim\": feature_dim,\n            \"sampling_rate_in_hz\": sampling_rate_in_hz,\n            \"max_length\": max_length,\n            \"reshape\": (max_length, feature_dim),\n        }\n\n    @staticmethod\n    def _get_feature_dim(preprocessing_parameters: PreprocessingConfigDict, sampling_rate_in_hz):\n        feature_type = preprocessing_parameters[TYPE]\n\n        if feature_type == \"raw\":\n            feature_dim = 1\n        elif feature_type == \"stft_phase\":\n            feature_dim_symmetric = get_length_in_samp(\n                preprocessing_parameters[\"window_length_in_s\"], sampling_rate_in_hz\n            )\n            feature_dim = 2 * get_non_symmetric_length(feature_dim_symmetric)\n        elif feature_type in [\"stft\", \"group_delay\"]:\n            feature_dim_symmetric = get_length_in_samp(\n                preprocessing_parameters[\"window_length_in_s\"], sampling_rate_in_hz\n            )\n            feature_dim = get_non_symmetric_length(feature_dim_symmetric)\n        elif feature_type == \"fbank\":\n            feature_dim = preprocessing_parameters[\"num_filter_bands\"]\n        else:\n            raise ValueError(f\"{feature_type} is not recognized.\")\n\n        return feature_dim\n\n    @staticmethod\n    def _process_in_memory(\n        column,\n        audio_feature_dict,\n        feature_dim,\n        max_length,\n        padding_value,\n        normalization_type,\n        audio_file_length_limit_in_s,\n        backend,\n    ):\n        df_engine = backend.df_engine\n        if _TORCH_200:\n            # Read audio from path if the version of torch is >= 2.0.0.\n            raw_audio = backend.read_binary_files(column, map_fn=read_audio_from_path)\n        else:\n            raw_audio = backend.read_binary_files(column, map_fn=read_audio_from_bytes_obj)\n\n        try:\n            default_audio = get_default_audio([audio for audio in raw_audio if is_torch_audio_tuple(audio)])\n        except RuntimeError as e:\n            raise RuntimeError(f\"Unable to process audio files provided: {e}\") from e\n\n        raw_audio = df_engine.map_objects(raw_audio, lambda row: row if is_torch_audio_tuple(row) else default_audio)\n        processed_audio = df_engine.map_objects(\n            raw_audio,\n            lambda row: AudioFeatureMixin._transform_to_feature(\n                audio=row[0],\n                sampling_rate_in_hz=row[1],\n                audio_feature_dict=audio_feature_dict,\n                feature_dim=feature_dim,\n                max_length=max_length,\n                padding_value=padding_value,\n                normalization_type=normalization_type,\n            ).numpy(),  # non-torchscript preprocessing requires np.ndarray\n        )\n\n        audio_stats = df_engine.map_objects(\n            raw_audio,\n            lambda row: AudioFeatureMixin._get_stats(\n                audio=row[0],\n                sampling_rate_in_hz=row[1],\n                max_length_in_s=audio_file_length_limit_in_s,\n            ),\n        )\n\n        def reduce(series):\n            merged_stats = None\n            for audio_stats in series:\n                if merged_stats is None:\n                    merged_stats = audio_stats.copy()\n                else:\n                    AudioFeatureMixin._merge_stats(merged_stats, audio_stats)\n            return merged_stats\n\n        merged_stats = df_engine.reduce_objects(audio_stats, reduce)\n        merged_stats[\"mean\"] = calculate_mean(merged_stats[\"sum\"], merged_stats[\"count\"])\n        merged_stats[\"var\"] = calculate_var(merged_stats[\"sum\"], merged_stats[\"sum2\"], merged_stats[\"count\"])\n        merged_stats[\"std\"] = np.sqrt(merged_stats[\"var\"] / float(merged_stats[\"count\"]))\n        print_statistics = (\n            \"{} audio files loaded.\\n\"\n            \"Statistics of audio file lengths:\\n\"\n            \"- mean: {:.4f}\\n\"\n            \"- std: {:.4f}\\n\"\n            \"- max: {:.4f}\\n\"\n            \"- min: {:.4f}\\n\"\n            \"- cropped audio_files: {}\\n\"\n            \"Max length was given as {}s\"\n        ).format(\n            merged_stats[\"count\"],\n            merged_stats[\"mean\"],\n            merged_stats[\"std\"],\n            merged_stats[\"max\"],\n            merged_stats[\"min\"],\n            merged_stats[\"cropped\"],\n            audio_file_length_limit_in_s,\n        )\n        logger.debug(print_statistics)\n        return processed_audio\n\n    @staticmethod\n    def _transform_to_feature(\n        audio: torch.Tensor,\n        sampling_rate_in_hz: int,\n        audio_feature_dict: dict[str, float | int | str],\n        feature_dim: int,\n        max_length: int,\n        padding_value: float,\n        normalization_type: str | None = None,\n        type_key: str = TYPE,\n    ):\n        feature_type: str = str(audio_feature_dict[type_key])\n        if feature_type == \"raw\":\n            audio_feature = torch.unsqueeze(audio[0], dim=-1)\n        elif feature_type in [\"stft\", \"stft_phase\", \"group_delay\", \"fbank\"]:\n            audio_feature = AudioFeatureMixin._get_2D_feature(\n                audio, feature_type, audio_feature_dict, sampling_rate_in_hz\n            )\n            audio_feature = torch.transpose(audio_feature, 0, 1)\n        else:\n            raise ValueError(f\"{feature_type} is not recognized.\")\n\n        # Outer conditional is type refinement from Union[str, None] to str\n        if normalization_type is not None:\n            if normalization_type == \"per_file\":\n                mean = torch.mean(audio_feature, dim=0)\n                std = torch.std(audio_feature, dim=0)\n                audio_feature = torch.divide((audio_feature - mean), std + 1.0e-10)\n            elif normalization_type == \"global\":\n                raise ValueError(\"not implemented yet\")\n\n        feature_length = audio_feature.shape[0]\n        broadcast_feature_length = min(feature_length, max_length)\n        audio_feature_padded = torch.full(\n            (max_length, feature_dim), padding_value, dtype=torch.float32, device=audio_feature.device\n        )\n        audio_feature_padded[:broadcast_feature_length, :] = audio_feature[:max_length, :]\n\n        return audio_feature_padded\n\n    @staticmethod\n    def _get_stats(audio, sampling_rate_in_hz, max_length_in_s):\n        audio_length_in_s = audio.shape[-1] / float(sampling_rate_in_hz)\n        return {\n            \"count\": 1,\n            \"sum\": audio_length_in_s,\n            \"sum2\": audio_length_in_s * audio_length_in_s,\n            \"min\": audio_length_in_s,\n            \"max\": audio_length_in_s,\n            \"cropped\": 1 if audio_length_in_s > max_length_in_s else 0,\n        }\n\n    @staticmethod\n    def _merge_stats(merged_stats, audio_stats):\n        merged_stats[\"count\"] += audio_stats[\"count\"]\n        merged_stats[\"sum\"] += audio_stats[\"sum\"]\n        merged_stats[\"sum2\"] += audio_stats[\"sum2\"]\n        merged_stats[\"min\"] = min(merged_stats[\"min\"], audio_stats[\"min\"])\n        merged_stats[\"max\"] = max(merged_stats[\"max\"], audio_stats[\"max\"])\n        merged_stats[\"cropped\"] += audio_stats[\"cropped\"]\n\n    @staticmethod\n    def _get_2D_feature(\n        audio: torch.Tensor,\n        feature_type: str,\n        audio_feature_dict: dict[str, float | int | str],\n        sampling_rate_in_hz: int,\n    ) -> torch.Tensor:\n        window_length_in_s = audio_feature_dict[\"window_length_in_s\"]\n        window_shift_in_s = audio_feature_dict[\"window_shift_in_s\"]\n        assert torch.jit.isinstance(window_length_in_s, float)\n        assert torch.jit.isinstance(window_shift_in_s, float)\n\n        window_length_in_samp = get_length_in_samp(window_length_in_s, sampling_rate_in_hz)\n\n        if \"num_fft_points\" in audio_feature_dict:\n            num_fft_points = audio_feature_dict[\"num_fft_points\"]\n            assert torch.jit.isinstance(num_fft_points, int)\n\n            if num_fft_points < window_length_in_samp:\n                raise ValueError(\n                    \"num_fft_points: {} < window length in \"\n                    \"samples: {} (corresponds to window length\"\n                    \" in s: {}\".format(num_fft_points, window_length_in_s, window_length_in_samp)\n                )\n        else:\n            num_fft_points = window_length_in_samp\n\n        if \"window_type\" in audio_feature_dict:\n            window_type = audio_feature_dict[\"window_type\"]\n            assert torch.jit.isinstance(window_type, str)\n        else:\n            window_type = \"hamming\"\n\n        if feature_type == \"stft_phase\":\n            return get_phase_stft_magnitude(\n                audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type\n            )\n        elif feature_type == \"stft\":\n            return get_stft_magnitude(\n                audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type\n            )\n        elif feature_type == \"group_delay\":\n            return get_group_delay(\n                audio, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type\n            )\n        elif feature_type == \"fbank\":\n            num_filter_bands = audio_feature_dict[\"num_filter_bands\"]\n            assert torch.jit.isinstance(num_filter_bands, int)\n\n            return get_fbank(\n                audio,\n                sampling_rate_in_hz,\n                window_length_in_s,\n                window_shift_in_s,\n                num_fft_points,\n                window_type,\n                num_filter_bands,\n            )\n        else:\n            raise ValueError(f'feature_type \"{feature_type}\" is not recognized.')\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        set_default_value(feature_config[\"preprocessing\"], \"in_memory\", preprocessing_parameters[\"in_memory\"])\n\n        name = feature_config[NAME]\n        column = input_df[feature_config[COLUMN]]\n\n        num_audio_files = len(column)\n        if num_audio_files == 0:\n            raise ValueError(\"There are no audio files in the dataset provided.\")\n\n        first_audio_entry = next(iter(column))\n        logger.debug(f\"Detected audio feature type is {type(first_audio_entry)}\")\n\n        if not isinstance(first_audio_entry, str) and not isinstance(first_audio_entry, torch.Tensor):\n            raise ValueError(\n                \"Invalid audio feature data type.  Detected type is {}, \"\n                \"expected either string for local/remote file path or Torch Tensor.\".format(type(first_audio_entry))\n            )\n\n        src_path = None\n        if SRC in metadata:\n            if isinstance(first_audio_entry, str) and not has_remote_protocol(first_audio_entry):\n                src_path = os.path.dirname(os.path.abspath(metadata.get(SRC)))\n        abs_path_column = backend.df_engine.map_objects(  # This gets the CSV file path\n            column, lambda row: get_abs_path(src_path, row) if isinstance(row, str) else row\n        )\n\n        num_audio_utterances = len(input_df[feature_config[COLUMN]])\n        padding_value = preprocessing_parameters[\"padding_value\"]\n        normalization_type = preprocessing_parameters[\"norm\"]\n\n        feature_dim = metadata[name][\"feature_dim\"]\n        max_length = metadata[name][\"max_length\"]\n        audio_feature_dict = {\n            key: value\n            for key, value in preprocessing_parameters.items()\n            if key in AUDIO_FEATURE_KEYS and value is not None\n        }\n        audio_file_length_limit_in_s = preprocessing_parameters[\"audio_file_length_limit_in_s\"]\n\n        if num_audio_utterances == 0:\n            raise ValueError(\"There are no audio files in the dataset provided.\")\n\n        if feature_config[PREPROCESSING][\"in_memory\"]:\n            audio_features = AudioFeatureMixin._process_in_memory(\n                abs_path_column,\n                audio_feature_dict,\n                feature_dim,\n                max_length,\n                padding_value,\n                normalization_type,\n                audio_file_length_limit_in_s,\n                backend,\n            )\n            proc_df[feature_config[PROC_COLUMN]] = audio_features\n\n        return proc_df\n\n    @staticmethod\n    def _get_max_length_feature(\n        preprocessing_parameters: PreprocessingConfigDict, sampling_rate_in_hz, audio_length_limit_in_s\n    ):\n        feature_type = preprocessing_parameters[TYPE]\n        audio_length_limit_in_samp = audio_length_limit_in_s * sampling_rate_in_hz\n\n        if not audio_length_limit_in_samp.is_integer():\n            raise ValueError(\n                \"Audio_file_length_limit has to be chosen \"\n                \"so that {} (in s) * {} (sampling rate in Hz) \"\n                \"is an integer.\".format(audio_length_limit_in_s, sampling_rate_in_hz)\n            )\n        audio_length_limit_in_samp = int(audio_length_limit_in_samp)\n\n        if feature_type == \"raw\":\n            return audio_length_limit_in_samp\n        elif feature_type in [\"stft\", \"stft_phase\", \"group_delay\", \"fbank\"]:\n            window_length_in_s = preprocessing_parameters[\"window_length_in_s\"]\n            window_shift_in_s = preprocessing_parameters[\"window_shift_in_s\"]\n            return get_max_length_stft_based(\n                audio_length_limit_in_samp, window_length_in_s, window_shift_in_s, sampling_rate_in_hz\n            )\n        else:\n            raise ValueError(f\"{feature_type} is not recognized.\")\n\n\nclass AudioInputFeature(AudioFeatureMixin, SequenceInputFeature):\n    def __init__(self, input_feature_config: AudioInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs)\n\n        if not getattr(self.encoder_obj.config, \"embedding_size\", None):\n            raise ValueError(\"embedding_size has to be defined - \" 'check \"update_config_with_metadata()\"')\n        if not getattr(self.encoder_obj.config, \"max_sequence_length\", None):\n            raise ValueError(\"max_sequence_length has to be defined - \" 'check \"update_config_with_metadata()\"')\n\n    def forward(self, inputs, mask=None):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype == torch.float32\n        assert len(inputs.shape) == 3, f\"expected 3D shape, found: {inputs.shape}\"\n\n        encoder_output = self.encoder_obj(inputs, mask=mask)\n\n        return encoder_output\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.encoder_obj.config.max_sequence_length, self.encoder_obj.config.embedding_size])\n\n    @property\n    def input_dtype(self):\n        return torch.float32\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.max_sequence_length = feature_metadata[\"max_length\"]\n        feature_config.encoder.embedding_size = feature_metadata[\"feature_dim\"]\n        feature_config.encoder.should_embed = False\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _AudioPreprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return AudioInputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/bag_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom collections import Counter\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import BAG, COLUMN, NAME, PROC_COLUMN\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature\nfrom ludwig.features.feature_utils import set_str_to_idx\nfrom ludwig.features.set_feature import _SetPreprocessing\nfrom ludwig.schema.features.bag_feature import BagInputFeatureConfig\nfrom ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.strings_utils import create_vocabulary\n\nlogger = logging.getLogger(__name__)\n\n\nclass BagFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return BAG\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column.astype(str)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        vocabulary = create_vocabulary(\n            column,\n            preprocessing_parameters[\"tokenizer\"],\n            num_most_frequent=preprocessing_parameters[\"most_common\"],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            processor=backend.df_engine,\n        )\n        return {\n            \"idx2str\": vocabulary.vocab,\n            \"str2idx\": vocabulary.str2idx,\n            \"str2freq\": vocabulary.str2freq,\n            \"vocab_size\": len(vocabulary.str2idx),\n            \"max_set_size\": vocabulary.max_sequence_length,\n        }\n\n    @staticmethod\n    def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend):\n        def to_vector(set_str):\n            bag_vector = np.zeros((len(metadata[\"str2idx\"]),), dtype=np.float32)\n            col_counter = Counter(set_str_to_idx(set_str, metadata[\"str2idx\"], preprocessing_parameters[\"tokenizer\"]))\n\n            bag_vector[list(col_counter.keys())] = list(col_counter.values())\n            return bag_vector\n\n        return backend.df_engine.map_objects(column, to_vector)\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        proc_df[feature_config[PROC_COLUMN]] = BagFeatureMixin.feature_data(\n            input_df[feature_config[COLUMN]],\n            metadata[feature_config[NAME]],\n            preprocessing_parameters,\n            backend,\n        )\n        return proc_df\n\n\nclass BagInputFeature(BagFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: BagInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        # assert inputs.dtype == tf.bool # this fails\n\n        encoder_output = self.encoder_obj(inputs)\n\n        return encoder_output\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([len(self.encoder_obj.config.vocab)])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.vocab = feature_metadata[\"idx2str\"]\n\n    @staticmethod\n    def get_schema_cls():\n        return BagInputFeatureConfig\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SetPreprocessing(metadata, is_bag=True)\n"
  },
  {
    "path": "ludwig/features/base_feature.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom abc import ABC, abstractmethod, abstractstaticmethod\nfrom dataclasses import dataclass\nfrom typing import Any\n\nimport torch\nfrom torch import Tensor\n\nfrom ludwig.constants import (\n    ENCODER_OUTPUT,\n    ENCODER_OUTPUT_STATE,\n    HIDDEN,\n    LENGTHS,\n    LOGITS,\n    LOSS,\n    PREDICTIONS,\n    PROBABILITIES,\n)\nfrom ludwig.decoders.registry import get_decoder_cls\nfrom ludwig.encoders.registry import get_encoder_cls\nfrom ludwig.features.feature_utils import get_input_size_with_dependencies\nfrom ludwig.modules.fully_connected_modules import FCStack\nfrom ludwig.modules.loss_modules import create_loss\nfrom ludwig.modules.metric_modules import LossMetric, LudwigMetric, MeanMetric\nfrom ludwig.modules.metric_registry import get_metric_classes, get_metric_cls, get_metric_tensor_input\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.features.base import BaseFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureConfigDict,\n    FeatureMetadataDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.calibration import CalibrationModule\nfrom ludwig.utils.torch_utils import LudwigModule\nfrom ludwig.utils.types import DataFrame, TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseFeatureMixin(ABC):\n    \"\"\"Parent class for feature mixins.\n\n    Feature mixins support preprocessing functionality shared across input and output features.\n    \"\"\"\n\n    @abstractstaticmethod\n    def type() -> str:\n        \"\"\"Returns the type of feature this mixin supports.\"\"\"\n        raise NotImplementedError\n\n    @abstractstaticmethod\n    def cast_column(column: DataFrame, backend) -> DataFrame:\n        \"\"\"Returns a copy of the dataset column for the given feature, potentially after a type cast.\n\n        Args:\n            column: Pandas column of values.\n            backend: (Union[Backend, str]) Backend to use for feature data processing.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractstaticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column: DataFrame,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        \"\"\"Returns a dictionary of feature metadata.\n\n        Args:\n            config: Ludwig model config dict.\n            column: Pandas column of values.\n            preprocessing_parameters: Preprocessing configuration for this feature.\n            backend: (Union[Backend, str]) Backend to use for feature data processing.\n        \"\"\"\n        raise NotImplementedError\n\n    @abstractstaticmethod\n    def add_feature_data(\n        feature_config: FeatureConfigDict,\n        input_df: DataFrame,\n        proc_df: dict[str, DataFrame],\n        metadata: TrainingSetMetadataDict,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,  # Union[Backend, str]\n        skip_save_processed_input: bool,\n    ) -> None:\n        \"\"\"Runs preprocessing on the input_df and stores results in the proc_df and metadata dictionaries.\n\n        Args:\n            feature_config: Feature configuration.\n            input_df: Pandas column of values.\n            proc_df: Dict of processed columns of data. Feature data is added to this.\n            metadata: Metadata returned by get_feature_meta(). Additional information may be added to this.\n            preprocessing_parameters: Preprocessing configuration for this feature.\n            backend: (Union[Backend, str]) Backend to use for feature data processing.\n            skip_save_processed_input: Whether to skip saving the processed input.\n        \"\"\"\n        raise NotImplementedError\n\n\n@dataclass\nclass ModuleWrapper:\n    \"\"\"Used to prevent the PredictModule from showing up an attribute on the feature module.\n\n    This is necessary to avoid inflight errors from DeepSpeed. These errors occur when DeepSpeed believes that a param\n    is still in the process of being processed asynchronously (allgathered, etc.).\n    \"\"\"\n\n    module: torch.nn.Module\n\n\nclass PredictModule(torch.nn.Module):\n    \"\"\"Base class for all modules that convert model outputs to predictions.\n\n    Explicit member variables needed here for scripting, as Torchscript will not be able to recognize global variables\n    during scripting.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.predictions_key = PREDICTIONS\n        self.probabilities_key = PROBABILITIES\n        self.logits_key = LOGITS\n\n\nclass BaseFeature:\n    \"\"\"Base class for all features.\n\n    Note that this class is not-cooperative (does not forward kwargs), so when constructing feature class hierarchies,\n    there should be only one parent class that derives from base feature.  Other functionality should be put into mixin\n    classes to avoid the diamond pattern.\n    \"\"\"\n\n    def __init__(self, feature: BaseFeatureConfig):\n        super().__init__()\n\n        if not feature.name:\n            raise ValueError(\"Missing feature name\")\n        self.feature_name = feature.name\n\n        if not feature.column:\n            feature.column = self.feature_name\n        self.column = feature.column\n\n        self.proc_column = feature.proc_column\n\n\nclass InputFeature(BaseFeature, LudwigModule, ABC):\n    \"\"\"Parent class for all input features.\"\"\"\n\n    def create_sample_input(self, batch_size: int = 2):\n        # Used by get_model_inputs(), which is used for tracing-based torchscript generation.\n        return torch.rand([batch_size, *self.input_shape]).to(self.input_dtype)\n\n    def unskip(self) -> \"InputFeature\":\n        \"\"\"Convert feature using passthrough wrapper back to full encoder.\"\"\"\n        return self\n\n    @staticmethod\n    @abstractmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    def update_config_after_module_init(self, feature_config):\n        \"\"\"Updates the config after the torch.nn.Module objects have been initialized.\"\"\"\n\n    def initialize_encoder(self, encoder_config):\n        encoder_cls = get_encoder_cls(self.type(), encoder_config.type)\n        encoder_schema = encoder_cls.get_schema_cls().Schema()\n        encoder_params_dict = encoder_schema.dump(encoder_config)\n        return encoder_cls(encoder_config=encoder_config, **encoder_params_dict)\n\n    @classmethod\n    def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"string\"\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        raise NotImplementedError(\"Torchscript tracing not supported for feature\")\n\n\nclass OutputFeature(BaseFeature, LudwigModule, ABC):\n    \"\"\"Parent class for all output features.\"\"\"\n\n    def __init__(\n        self,\n        feature: BaseOutputFeatureConfig,\n        other_output_features: dict[str, \"OutputFeature\"],\n        *args,\n        **kwargs,\n    ):\n        \"\"\"Defines defaults, overwrites them based on the feature dictionary, and sets up dependencies.\n\n        Any output feature can depend on one or more other output features. The `other_output_features` input dictionary\n        should contain entries for any dependent output features, which is accomplished by constructing output features\n        in topographically sorted order. Attributes of any dependent output features are used to properly initialize\n        this feature's sizes.\n        \"\"\"\n        super().__init__(feature)\n\n        # List of names of metrics that this OutputFeature computes.\n        self.metric_names = []\n        self.loss = feature.loss\n        self.reduce_input = feature.reduce_input\n        self.reduce_dependencies = feature.reduce_dependencies\n\n        # List of feature names that this output feature is dependent on.\n        self.dependencies = feature.dependencies\n\n        logger.debug(\" output feature fully connected layers\")\n        logger.debug(\"  FCStack\")\n\n        self.input_size = get_input_size_with_dependencies(feature.input_size, self.dependencies, other_output_features)\n        feature.input_size = self.input_size\n\n        self.fc_stack = FCStack(\n            first_layer_input_size=self.input_size,\n            layers=feature.decoder.fc_layers,\n            num_layers=feature.decoder.num_fc_layers,\n            default_output_size=feature.decoder.fc_output_size,\n            default_use_bias=feature.decoder.fc_use_bias,\n            default_weights_initializer=feature.decoder.fc_weights_initializer,\n            default_bias_initializer=feature.decoder.fc_bias_initializer,\n            default_norm=feature.decoder.fc_norm,\n            default_norm_params=feature.decoder.fc_norm_params,\n            default_activation=feature.decoder.fc_activation,\n            default_dropout=feature.decoder.fc_dropout,\n        )\n        self._calibration_module = self.create_calibration_module(feature)\n        self._prediction_module = ModuleWrapper(self.create_predict_module())\n\n        # set up two sequence reducers, one for inputs and other for dependencies\n        self.reduce_sequence_input = SequenceReducer(reduce_mode=self.reduce_input)\n        if self.dependencies:\n            self.dependency_reducers = torch.nn.ModuleDict()\n            # todo: re-evaluate need for separate handling of `attention` reducer\n            #       currently this code does not support `attention`\n            for dependency in self.dependencies:\n                self.dependency_reducers[dependency] = SequenceReducer(reduce_mode=self.reduce_dependencies)\n\n    def create_sample_output(self, batch_size: int = 2):\n        output_shape = self.output_shape\n        shape = [batch_size, *self.output_shape] if output_shape != torch.Size([1]) else [batch_size]\n        return torch.rand(shape).to(self.get_output_dtype())\n\n    @abstractmethod\n    def get_prediction_set(self):\n        \"\"\"Returns the set of tensor keys returned by this feature's PredictModule.\n\n        TODO(Justin): Move this to the PredictModule.\n        \"\"\"\n        raise NotImplementedError(\"OutputFeature is missing implementation for get_prediction_set.\")\n\n    @classmethod\n    @abstractmethod\n    def get_output_dtype(cls):\n        \"\"\"Returns the Tensor data type feature outputs.\"\"\"\n\n    def initialize_decoder(self, decoder_config):\n        # Input to the decoder is the output feature's FC hidden layer.\n        decoder_config.input_size = self.fc_stack.output_shape[-1]\n        decoder_cls = get_decoder_cls(self.type(), decoder_config.type)\n        decoder_schema = decoder_cls.get_schema_cls().Schema()\n        decoder_params_dict = decoder_schema.dump(decoder_config)\n        return decoder_cls(decoder_config=decoder_config, **decoder_params_dict)\n\n    def train_loss(self, targets: Tensor, predictions: dict[str, Tensor], feature_name):\n        loss_class = type(self.train_loss_function)\n        prediction_key = output_feature_utils.get_feature_concat_name(feature_name, loss_class.get_loss_inputs())\n        return self.train_loss_function(predictions[prediction_key], targets)\n\n    def eval_loss(self, targets: Tensor, predictions: dict[str, Tensor]):\n        loss_class = type(self.train_loss_function)\n        prediction_key = loss_class.get_loss_inputs()\n        if isinstance(self.eval_loss_metric, MeanMetric):\n            # MeanMetric's forward() implicitly updates the running average.\n            # For MeanMetrics, we use get_current_value() to compute the loss without changing the state. All metrics\n            # are updated at the BaseModel level as part of update_metrics().\n            return self.eval_loss_metric.get_current_value(predictions[prediction_key].detach(), targets)\n        return self.eval_loss_metric(predictions[prediction_key].detach(), targets)\n\n    def _setup_loss(self):\n        self.train_loss_function = create_loss(self.loss)\n        self._eval_loss_metric = ModuleWrapper(get_metric_cls(self.type(), self.loss.type)(config=self.loss))\n\n    def _setup_metrics(self):\n        kwargs = {}\n        for name, cls in get_metric_classes(self.type()).items():\n            if cls.can_report(self) and isinstance(cls, LossMetric):\n                kwargs[name] = cls(config=self.loss, **self.metric_kwargs())\n            elif cls.can_report(self):\n                kwargs[name] = cls(**self.metric_kwargs())\n        self._metric_functions = {\n            LOSS: self.eval_loss_metric,\n            **kwargs,\n        }\n        self.metric_names = sorted(list(self._metric_functions.keys()))\n\n    def create_calibration_module(self, feature: BaseOutputFeatureConfig) -> CalibrationModule:\n        \"\"\"Creates and returns a CalibrationModule that converts logits to a probability distribution.\"\"\"\n        return None\n\n    @property\n    def eval_loss_metric(self) -> LudwigMetric:\n        return self._eval_loss_metric.module\n\n    @property\n    def calibration_module(self) -> torch.nn.Module:\n        \"\"\"Returns the CalibrationModule used to convert logits to a probability distribution.\"\"\"\n        return self._calibration_module\n\n    @abstractmethod\n    def create_predict_module(self) -> PredictModule:\n        \"\"\"Creates and returns a `nn.Module` that converts raw model outputs (logits) to predictions.\n\n        This module is needed when generating the Torchscript model using scripting.\n        \"\"\"\n        raise NotImplementedError()\n\n    @property\n    def prediction_module(self) -> PredictModule:\n        \"\"\"Returns the PredictModule used to convert model outputs to predictions.\"\"\"\n        return self._prediction_module.module\n\n    def predictions(self, all_decoder_outputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        \"\"\"Computes actual predictions from the outputs of feature decoders.\n\n        TODO(Justin): Consider refactoring this to accept feature-specific decoder outputs.\n\n        Args:\n            all_decoder_outputs: A dictionary of {feature name}::{tensor_name} -> output tensor.\n        Returns:\n            Dictionary of tensors with predictions as well as any additional tensors that may be\n            necessary for computing evaluation metrics.\n        \"\"\"\n        return self.prediction_module(all_decoder_outputs, feature_name)\n\n    @abstractmethod\n    def logits(self, combiner_outputs: dict[str, torch.Tensor], target=None, **kwargs) -> dict[str, torch.Tensor]:\n        \"\"\"Unpacks and feeds combiner_outputs to the decoder. Invoked as part of the output feature's forward pass.\n\n        If target is not None, then we are in training.\n\n        Args:\n            combiner_outputs: Dictionary of tensors from the combiner's forward pass.\n        Returns:\n            Dictionary of decoder's output tensors (non-normalized), as well as any additional\n            tensors that may be necessary for computing predictions or evaluation metrics.\n        \"\"\"\n        raise NotImplementedError(\"OutputFeature is missing logits() implementation.\")\n\n    def metric_kwargs(self) -> dict[str, Any]:\n        \"\"\"Returns arguments that are used to instantiate an instance of each metric class.\"\"\"\n        return {}\n\n    def update_metrics(self, targets: Tensor, predictions: dict[str, Tensor]) -> None:\n        \"\"\"Updates metrics with the given targets and predictions.\n\n        Args:\n            targets: Tensor with target values for this output feature.\n            predictions: Dict of tensors returned by predictions().\n        \"\"\"\n        for metric_name, metric_fn in self._metric_functions.items():\n            prediction_key = get_metric_tensor_input(metric_name)\n            metric_fn = metric_fn.to(predictions[prediction_key].device)\n            metric_fn.update(predictions[prediction_key].detach(), targets)\n\n    def get_metrics(self):\n        metric_vals = {}\n        for metric_name, metric_fn in self._metric_functions.items():\n            try:\n                computed_metric = metric_fn.compute()\n            except Exception as e:\n                logger.exception(f\"Caught exception computing metric: {metric_name} with error: {e}.\")\n                continue\n\n            # Metrics from torchmetrics can be a straightforward tensor.\n            if isinstance(computed_metric, Tensor):\n                metric_vals[metric_name] = computed_metric.detach().cpu().numpy().item()\n            else:\n                # Metrics from torchmetrics can be a dict of tensors.\n                # For example, ROUGE is returned as a dictionary of tensors.\n                # Unpack.\n                for sub_metric_name, metric in computed_metric.items():\n                    metric_vals[sub_metric_name] = metric.detach().cpu().numpy().item()\n        return metric_vals\n\n    def reset_metrics(self):\n        for _, metric_fn in self._metric_functions.items():\n            if metric_fn is not None:\n                metric_fn.reset()\n\n    def forward(\n        self,\n        combiner_outputs: dict[str, torch.Tensor],\n        other_output_feature_outputs: dict[str, torch.Tensor],\n        mask: torch.Tensor | None = None,\n        target: torch.Tensor | None = None,\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Forward pass that takes in output from the combiner, and passes it through to the decoder.\n\n        Args:\n            combiner_outputs: Dict of outputs from the combiner.\n            other_output_feature_outputs: Dict of tensors from other output features. Used for resolving dependencies.\n            mask: (Unused). Tensor for masking.\n            target: Tensor with targets. During training, targets != None. During prediction, targets = None.\n\n        Returns:\n            Dict of output tensors, with at least 'last_hidden' and 'logits' as keys, as well as any additional tensor\n            results from the decoder.\n        \"\"\"\n        # extract the combined hidden layer\n        combiner_hidden = combiner_outputs[\"combiner_output\"]\n        hidden = self.prepare_decoder_inputs(combiner_hidden, other_output_feature_outputs, mask=mask)\n\n        # ================ Predictions ================\n        logits_input = {HIDDEN: hidden}\n        # pass supplemental data from encoders to decoder\n        if ENCODER_OUTPUT_STATE in combiner_outputs:\n            logits_input[ENCODER_OUTPUT_STATE] = combiner_outputs[ENCODER_OUTPUT_STATE]\n        if LENGTHS in combiner_outputs:\n            logits_input[LENGTHS] = combiner_outputs[LENGTHS]\n\n        logits = self.logits(logits_input, target=target)\n\n        # For binary and number features, self.logits() is a tensor.\n        # There are two special cases where self.logits() is a dict:\n        #   categorical\n        #       keys: logits, projection_input\n        #   sequence\n        #       keys: logits\n        # TODO(Justin): Clean this up.\n        if isinstance(logits, Tensor):\n            logits = {\"logits\": logits}\n\n        # For multi-class features, we must choose a consistent tuple subset.\n        return {\n            # last_hidden used for dependencies processing\n            \"last_hidden\": hidden,\n            **logits,\n        }\n\n    @abstractmethod\n    def postprocess_predictions(\n        self,\n        result: dict[str, Tensor],\n        metadata: TrainingSetMetadataDict,\n    ):\n        raise NotImplementedError\n\n    @classmethod\n    def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"string\"\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        raise NotImplementedError(\"Torchscript tracing not supported for feature\")\n\n    @staticmethod\n    @abstractmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    @abstractmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        pass\n\n    def output_specific_fully_connected(self, inputs, mask=None):\n        feature_hidden = inputs\n        original_feature_hidden = inputs\n\n        # flatten inputs\n        if len(original_feature_hidden.shape) > 2:\n            feature_hidden = torch.reshape(feature_hidden, (-1, list(feature_hidden.shape)[-1]))\n\n        # pass it through fc_stack\n        feature_hidden = self.fc_stack(feature_hidden, mask=mask)\n        feature_hidden_size = feature_hidden.shape[-1]\n\n        # reshape back to original first and second dimension\n        if len(original_feature_hidden.shape) > 2:\n            sequence_length = original_feature_hidden.shape[1]\n            feature_hidden = torch.reshape(feature_hidden, (-1, sequence_length, feature_hidden_size))\n\n        return feature_hidden\n\n    def prepare_decoder_inputs(\n        self, combiner_hidden: Tensor, other_output_features: dict[str, Tensor], mask=None\n    ) -> Tensor:\n        \"\"\"Takes the combiner output and the outputs of other outputs features computed so far and performs:\n\n        - reduction of combiner outputs (if needed)\n        - concatenating the outputs of dependent features (if needed)\n        - output_specific fully connected layers (if needed)\n\n        Args:\n            combiner_hidden: hidden state of the combiner\n            other_output_features: output tensors from other output features\n        \"\"\"\n        # ================ Reduce Inputs ================\n        feature_hidden = combiner_hidden\n        if self.reduce_input is not None and len(combiner_hidden.shape) > 2:\n            feature_hidden = self.reduce_sequence_input(combiner_hidden)\n\n        # ================ Concat Dependencies ================\n        if self.dependencies:\n            feature_hidden = output_feature_utils.concat_dependencies(\n                self.column, self.dependencies, self.dependency_reducers, feature_hidden, other_output_features\n            )\n\n        # ================ Output-wise Fully Connected ================\n        feature_hidden = self.output_specific_fully_connected(feature_hidden, mask=mask)\n\n        return feature_hidden\n\n\nclass PassthroughPreprocModule(torch.nn.Module):\n    \"\"\"Combines preprocessing and encoding into a single module for TorchScript inference.\n\n    For encoder outputs that were cached during preprocessing, the encoder is simply the identity function in the ECD\n    module. As such, we need this module to apply the encoding that would normally be done during preprocessing for\n    realtime inference.\n    \"\"\"\n\n    def __init__(self, preproc: torch.nn.Module, encoder: torch.nn.Module):\n        self.preproc = preproc\n        self.encoder = encoder\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        preproc_v = self.preproc(v)\n        return self.encoder(preproc_v)\n\n\ndef create_passthrough_input_feature(feature: InputFeature, config: BaseFeatureConfig) -> InputFeature:\n    \"\"\"Creates a shim input feature that acts as a transparent identifiy function on the input data.\n\n    Used when the feature's encoder embeddings were cached in preprocessing. This way, we don't need to make any changes\n    to the underlying interface in such cases other than to swap the feature that would normally do the encoding with\n    this one.\n    \"\"\"\n\n    class _InputPassthroughFeature(InputFeature):\n        def __init__(self, config: BaseFeatureConfig):\n            super().__init__(config)\n\n        def forward(self, inputs, mask=None):\n            assert isinstance(inputs, torch.Tensor)\n            return {ENCODER_OUTPUT: inputs}\n\n        @property\n        def input_dtype(self):\n            # Doesn't matter as combiner will need to cast them to float32 anyway\n            return torch.float32\n\n        @property\n        def input_shape(self):\n            return feature.encoder_obj.output_shape\n\n        @property\n        def output_shape(self) -> torch.Size:\n            return feature.encoder_obj.output_shape\n\n        @staticmethod\n        def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n            return feature.update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs)\n\n        @staticmethod\n        def get_schema_cls():\n            return feature.get_schema_cls()\n\n        @staticmethod\n        def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n            return PassthroughPreprocModule(feature.create_preproc_module(metadata), feature)\n\n        @staticmethod\n        def type():\n            return feature.type()\n\n        def unskip(self) -> InputFeature:\n            return feature\n\n        @property\n        def encoder_obj(self) -> torch.nn.Module:\n            return feature.encoder_obj\n\n    return _InputPassthroughFeature(config)\n"
  },
  {
    "path": "ludwig/features/binary_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import BINARY, COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROBABILITIES, PROBABILITY, PROC_COLUMN\nfrom ludwig.error import InputDataError\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.schema.features.binary_feature import BinaryInputFeatureConfig, BinaryOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureConfigDict,\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import calibration, output_feature_utils, strings_utils\nfrom ludwig.utils.eval_utils import (\n    average_precision_score,\n    ConfusionMatrix,\n    precision_recall_curve,\n    roc_auc_score,\n    roc_curve,\n)\nfrom ludwig.utils.types import DataFrame, TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass _BinaryPreprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        str2bool = metadata.get(\"str2bool\")\n        self.str2bool = str2bool or {v: True for v in strings_utils.BOOL_TRUE_STRS}\n        self.should_lower = str2bool is None\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if torch.jit.isinstance(v, list[tuple[torch.Tensor, int]]):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        if torch.jit.isinstance(v, list[torch.Tensor]):\n            v = torch.stack(v)\n\n        if torch.jit.isinstance(v, torch.Tensor):\n            return v.to(dtype=torch.float32)\n\n        v = [s.strip() for s in v]\n        if self.should_lower:\n            v = [s.lower() for s in v]\n        indices = [self.str2bool.get(s, False) for s in v]\n        return torch.tensor(indices, dtype=torch.float32)\n\n\nclass _BinaryPostprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        bool2str = metadata.get(\"bool2str\")\n        self.bool2str = {i: v for i, v in enumerate(bool2str)} if bool2str is not None else None\n        self.predictions_key = PREDICTIONS\n        self.probabilities_key = PROBABILITIES\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key)\n\n        if self.bool2str is not None:\n            predictions = predictions.to(dtype=torch.int32)\n            predictions = [self.bool2str.get(pred, self.bool2str[0]) for pred in predictions]\n\n        probabilities = torch.stack([1 - probabilities, probabilities], dim=-1)\n\n        return {\n            self.predictions_key: predictions,\n            self.probabilities_key: probabilities,\n        }\n\n\nclass _BinaryPredict(PredictModule):\n    def __init__(self, threshold, calibration_module=None):\n        super().__init__()\n        self.threshold = threshold\n        self.calibration_module = calibration_module\n\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n\n        if self.calibration_module is not None:\n            probabilities = self.calibration_module(logits)\n        else:\n            probabilities = torch.sigmoid(logits)\n\n        predictions = probabilities >= self.threshold\n        return {\n            self.probabilities_key: probabilities,\n            self.predictions_key: predictions,\n            self.logits_key: logits,\n        }\n\n\nclass BinaryFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return BINARY\n\n    @staticmethod\n    def cast_column(column, backend):\n        \"\"\"Cast column of dtype object to bool.\n\n        Unchecked casting to boolean when given a column of dtype object converts all non-empty cells to True. We check\n        the values of the column directly and manually determine the best dtype to use.\n        \"\"\"\n        values = backend.df_engine.compute(column.drop_duplicates())\n\n        if strings_utils.values_are_pandas_numbers(values):\n            # If numbers, convert to float so it can be converted to bool\n            column = column.astype(float).astype(bool)\n        elif strings_utils.values_are_pandas_bools(values):\n            # If booleans, manually assign boolean values\n            column = backend.df_engine.map_objects(\n                column, lambda x: x.lower() in strings_utils.PANDAS_TRUE_STRS\n            ).astype(bool)\n        else:\n            # If neither numbers or booleans, they are strings (objects)\n            column = column.astype(object)\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column: DataFrame,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        if column.dtype != object:\n            return {}\n\n        distinct_values = backend.df_engine.compute(column.drop_duplicates())\n        if len(distinct_values) > 2:\n            raise InputDataError(\n                column.name, BINARY, f\"expects 2 distinct values, found {distinct_values.values.tolist()}\"\n            )\n        if preprocessing_parameters[\"fallback_true_label\"]:\n            fallback_true_label = preprocessing_parameters[\"fallback_true_label\"]\n        else:\n            fallback_true_label = sorted(distinct_values)[0]\n            preprocessing_parameters[\"fallback_true_label\"] = fallback_true_label\n\n        try:\n            str2bool = {v: strings_utils.str2bool(v) for v in distinct_values}\n        except Exception as e:\n            logger.warning(\n                f\"Binary feature {column.name} has at least 1 unconventional boolean value: {e}. \"\n                f\"We will now interpret {fallback_true_label} as 1 and the other values as 0. \"\n                f\"If this is incorrect, please use the category feature type or \"\n                f\"manually specify the true value with `preprocessing.fallback_true_label`.\"\n            )\n            str2bool = {v: strings_utils.str2bool(v, fallback_true_label) for v in distinct_values}\n\n        bool2str = [k for k, v in sorted(str2bool.items(), key=lambda item: item[1])]\n        return {\"str2bool\": str2bool, \"bool2str\": bool2str, \"fallback_true_label\": fallback_true_label}\n\n    @staticmethod\n    def add_feature_data(\n        feature_config: FeatureConfigDict,\n        input_df: DataFrame,\n        proc_df: dict[str, DataFrame],\n        metadata: TrainingSetMetadataDict,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input: bool,\n    ) -> None:\n        column = input_df[feature_config[COLUMN]]\n\n        if column.dtype == object:\n            metadata = metadata[feature_config[NAME]]\n            if \"str2bool\" in metadata:\n                column = backend.df_engine.map_objects(column, lambda x: metadata[\"str2bool\"][str(x)])\n            else:\n                # No predefined mapping from string to bool, so compute it directly\n                column = backend.df_engine.map_objects(column, strings_utils.str2bool)\n\n        proc_df[feature_config[PROC_COLUMN]] = column.astype(np.bool_)\n\n        return proc_df\n\n\nclass BinaryInputFeature(BinaryFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: BinaryInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n        input_feature_config.encoder.input_size = self.input_shape[-1]\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.bool, torch.int64, torch.float32]\n        assert len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)\n\n        if len(inputs.shape) == 1:\n            inputs = inputs[:, None]\n\n        # Inputs to the binary encoder could be of dtype torch.bool. Linear layer\n        # weights are of dtype torch.float32. The inputs and the weights need to\n        # be of the same dtype.\n        if inputs.dtype == torch.bool:\n            inputs = inputs.type(torch.float32)\n\n        encoder_outputs = self.encoder_obj(inputs)\n        return encoder_outputs\n\n    @property\n    def input_dtype(self):\n        return torch.bool\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    def get_schema_cls():\n        return BinaryInputFeatureConfig\n\n    def create_sample_input(self, batch_size: int = 2):\n        return torch.rand([batch_size]) > 0.5\n\n    @classmethod\n    def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"string\" if metadata.get(\"str2bool\") else \"int32\"\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _BinaryPreprocessing(metadata)\n\n\nclass BinaryOutputFeature(BinaryFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: BinaryOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.threshold = output_feature_config.threshold\n        super().__init__(output_feature_config, output_features, **kwargs)\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):\n        hidden = inputs[HIDDEN]\n        return self.decoder_obj(hidden)\n\n    def create_calibration_module(self, feature: BinaryOutputFeatureConfig) -> torch.nn.Module:\n        \"\"\"Creates the appropriate calibration module based on the feature config.\n\n        Today, only one type of calibration (\"temperature_scaling\") is available, but more options may be supported in\n        the future.\n        \"\"\"\n        if feature.calibration:\n            calibration_cls = calibration.get_calibration_cls(BINARY, \"temperature_scaling\")\n            return calibration_cls(binary=True)\n        return None\n\n    def create_predict_module(self) -> PredictModule:\n        # A lot of code assumes output features have a prediction module, but if we are using a passthrough\n        # decoder then there is no threshold.\n        threshold = getattr(self, \"threshold\", 0.5)\n        return _BinaryPredict(threshold, calibration_module=self.calibration_module)\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, PROBABILITIES, LOGITS}\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.bool\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        overall_stats = {}\n        confusion_matrix = ConfusionMatrix(targets, predictions[PREDICTIONS], labels=[\"False\", \"True\"])\n        overall_stats[\"confusion_matrix\"] = confusion_matrix.cm.tolist()\n        overall_stats[\"overall_stats\"] = confusion_matrix.stats()\n        overall_stats[\"per_class_stats\"] = confusion_matrix.per_class_stats()\n        fpr, tpr, thresholds = roc_curve(targets, predictions[PROBABILITIES])\n        overall_stats[\"roc_curve\"] = {\n            \"false_positive_rate\": fpr.tolist(),\n            \"true_positive_rate\": tpr.tolist(),\n        }\n        overall_stats[\"roc_auc_macro\"] = roc_auc_score(targets, predictions[PROBABILITIES], average=\"macro\")\n        overall_stats[\"roc_auc_micro\"] = roc_auc_score(targets, predictions[PROBABILITIES], average=\"micro\")\n        ps, rs, thresholds = precision_recall_curve(targets, predictions[PROBABILITIES])\n        overall_stats[\"precision_recall_curve\"] = {\n            \"precisions\": ps.tolist(),\n            \"recalls\": rs.tolist(),\n        }\n        overall_stats[\"average_precision_macro\"] = average_precision_score(\n            targets, predictions[PROBABILITIES], average=\"macro\"\n        )\n        overall_stats[\"average_precision_micro\"] = average_precision_score(\n            targets, predictions[PROBABILITIES], average=\"micro\"\n        )\n        overall_stats[\"average_precision_samples\"] = average_precision_score(\n            targets, predictions[PROBABILITIES], average=\"samples\"\n        )\n\n        return overall_stats\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        class_names = [\"False\", \"True\"]\n        if \"bool2str\" in metadata:\n            class_names = metadata[\"bool2str\"]\n\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in result:\n            if \"bool2str\" in metadata:\n                result[predictions_col] = result[predictions_col].map(\n                    lambda pred: metadata[\"bool2str\"][pred],\n                )\n\n        probabilities_col = f\"{self.feature_name}_{PROBABILITIES}\"\n        if probabilities_col in result:\n            false_col = f\"{probabilities_col}_{class_names[0]}\"\n            true_col = f\"{probabilities_col}_{class_names[1]}\"\n            prob_col = f\"{self.feature_name}_{PROBABILITY}\"\n\n            result = result.assign(\n                **{\n                    false_col: lambda x: 1 - x[probabilities_col],\n                    true_col: lambda x: x[probabilities_col],\n                    prob_col: np.where(\n                        result[probabilities_col] > 0.5, result[probabilities_col], 1 - result[probabilities_col]\n                    ),\n                    probabilities_col: result[probabilities_col].map(lambda x: [1 - x, x]),\n                },\n            )\n\n        return result\n\n    @staticmethod\n    def get_schema_cls():\n        return BinaryOutputFeatureConfig\n\n    @classmethod\n    def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"string\" if metadata.get(\"bool2str\") else \"int32\"\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _BinaryPostprocessing(metadata)\n\n    def metric_kwargs(self) -> dict:\n        \"\"\"Returns arguments that are used to instantiate an instance of each metric class.\"\"\"\n        return {\"task\": \"binary\"}\n"
  },
  {
    "path": "ludwig/features/category_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import Any\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import (\n    CATEGORY,\n    CATEGORY_DISTRIBUTION,\n    COLUMN,\n    HIDDEN,\n    LOGITS,\n    NAME,\n    PREDICTIONS,\n    PREPROCESSING,\n    PROBABILITIES,\n    PROBABILITY,\n    PROC_COLUMN,\n    PROJECTION_INPUT,\n)\nfrom ludwig.error import InputDataError\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.features.vector_feature import VectorFeatureMixin\nfrom ludwig.schema.features.category_feature import (\n    CategoryDistributionOutputFeatureConfig,\n    CategoryInputFeatureConfig,\n    CategoryOutputFeatureConfig,\n)\nfrom ludwig.schema.features.loss.loss import CORNLossConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import calibration, output_feature_utils\nfrom ludwig.utils.eval_utils import ConfusionMatrix\nfrom ludwig.utils.math_utils import int_type, softmax\nfrom ludwig.utils.strings_utils import create_vocabulary_single_token, UNKNOWN_SYMBOL\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass _CategoryPreprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.str2idx = metadata[\"str2idx\"]\n        if UNKNOWN_SYMBOL in self.str2idx:\n            self.unk = self.str2idx[UNKNOWN_SYMBOL]\n        else:\n            # self.unk is set to 0 to comply with Torchscript type tracing and will\n            # likely not be used during training, but potentially during inference\n            self.unk = 0\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if not torch.jit.isinstance(v, list[str]):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        indices = [self.str2idx.get(s.strip(), self.unk) for s in v]\n        return torch.tensor(indices, dtype=torch.int32)\n\n\nclass _CategoryPostprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.idx2str = {i: v for i, v in enumerate(metadata[\"idx2str\"])}\n        self.unk = UNKNOWN_SYMBOL\n        self.predictions_key = PREDICTIONS\n        self.probabilities_key = PROBABILITIES\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key)\n\n        inv_preds = [self.idx2str.get(pred, self.unk) for pred in predictions]\n\n        return {\n            self.predictions_key: inv_preds,\n            self.probabilities_key: probabilities,\n        }\n\n\nclass _CategoryPredict(PredictModule):\n    def __init__(self, calibration_module=None, use_cumulative_probs=False):\n        super().__init__()\n        self.calibration_module = calibration_module\n\n        # Derive the label from the cumulative probability distribution of the ordered category logits.\n        # Taken from CORN loss implementation:\n        # https://github.com/Raschka-research-group/coral-pytorch/blob/main/coral_pytorch/dataset.py#L123\n        self.use_cumulative_probs = use_cumulative_probs\n\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n\n        if self.use_cumulative_probs:\n            if self.calibration_module is not None:\n                probabilities = self.calibration_module(logits)\n            else:\n                probabilities = torch.sigmoid(logits)\n            probabilities = torch.cumprod(probabilities, dim=1)\n\n            predict_levels = probabilities > 0.5\n            predictions = torch.sum(predict_levels, dim=1)\n        else:\n            if self.calibration_module is not None:\n                probabilities = self.calibration_module(logits)\n            else:\n                probabilities = torch.softmax(logits, -1)\n            predictions = torch.argmax(probabilities, -1)\n\n        predictions = predictions.long()\n\n        # EXPECTED SHAPE OF RETURNED TENSORS\n        # predictions: [batch_size]\n        # probabilities: [batch_size, num_classes]\n        # logits: [batch_size, num_classes]\n        return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits}\n\n\nclass CategoryFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return CATEGORY\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column.astype(str)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        idx2str, str2idx, str2freq = create_vocabulary_single_token(\n            column,\n            num_most_frequent=preprocessing_parameters[\"most_common\"],\n            processor=backend.df_engine,\n        )\n\n        if \"vocab\" in preprocessing_parameters and preprocessing_parameters[\"vocab\"]:  # Check that vocab is non-empty\n            # If vocab was explciitly provided, override the inferred vocab\n            idx2str = preprocessing_parameters[\"vocab\"]\n            str2idx = {s: i for i, s in enumerate(idx2str)}\n            str2freq = {k: str2freq.get(k, 0) for k in idx2str}\n\n        if \"fallback_label\" in preprocessing_parameters:\n            # This is a category output feature for LLMs\n            # Check if the fallback label is in the vocab, if not add it.\n            if preprocessing_parameters[\"fallback_label\"] not in str2idx:\n                str2idx[preprocessing_parameters[\"fallback_label\"]] = len(str2idx)\n                idx2str.append(preprocessing_parameters[\"fallback_label\"])\n                str2freq[preprocessing_parameters[\"fallback_label\"]] = 0\n\n        vocab_size = len(str2idx)\n        if not is_input_feature and vocab_size <= 1:\n            # Category output feature with vocab size 1\n            raise InputDataError(\n                column.name,\n                CATEGORY,\n                f\"\"\"\n                At least 2 distinct values are required for category output features, but column\n                only contains {str(idx2str)}.\n                \"\"\",\n            )\n        if vocab_size <= 1:\n            # Category input feature with vocab size 1\n            logger.info(\n                f\"Input feature '{column.name}' contains only 1 distinct value {str(idx2str)}. This is not useful\"\n                \" for machine learning models because this feature has zero variance. Consider removing this feature\"\n                \" from your input features.\"\n            )\n        return {\"idx2str\": idx2str, \"str2idx\": str2idx, \"str2freq\": str2freq, \"vocab_size\": vocab_size}\n\n    @staticmethod\n    def feature_data(backend, column, metadata):\n        def __replace_token_with_idx(value: Any, metadata: TrainingSetMetadataDict, fallback_symbol_idx: int) -> int:\n            stripped_value = value.strip()\n            if stripped_value in metadata[\"str2idx\"]:\n                return metadata[\"str2idx\"][stripped_value]\n            logger.warning(f\"\"\"\n                Encountered unknown symbol '{stripped_value}' for '{column.name}' during category\n                feature preprocessing. This should never happen during training. If this happens during\n                inference, this may be an indication that not all possible symbols were present in your\n                training set. Consider re-splitting your data to ensure full representation, or setting\n                preprocessing.most_common parameter to be smaller than this feature's total vocabulary\n                size, {len(metadata[\"str2idx\"])}, which will ensure that the model is architected and\n                trained with an UNKNOWN symbol. Returning the index for the most frequent symbol,\n                {metadata[\"idx2str\"][fallback_symbol_idx]}, instead.\n                \"\"\")\n            return fallback_symbol_idx\n\n        # No unknown symbol in Metadata from preprocessing means that all values\n        # should be mappable to vocabulary\n        if UNKNOWN_SYMBOL not in metadata[\"str2idx\"]:\n            # If no unknown is defined, just use the most popular token's index as the fallback index\n            most_popular_token = max(metadata[\"str2freq\"], key=metadata[\"str2freq\"].get)\n            most_popular_token_idx = metadata[\"str2idx\"].get(most_popular_token)\n            return backend.df_engine.map_objects(\n                column,\n                lambda x: __replace_token_with_idx(x, metadata, most_popular_token_idx),\n                meta=(column.name, int),\n            ).astype(int_type(metadata[\"vocab_size\"]))\n        else:\n            return backend.df_engine.map_objects(\n                column,\n                lambda x: (\n                    metadata[\"str2idx\"][x.strip()]\n                    if x.strip() in metadata[\"str2idx\"]\n                    else metadata[\"str2idx\"][UNKNOWN_SYMBOL]\n                ),\n                meta=(column.name, int),\n            ).astype(int_type(metadata[\"vocab_size\"]))\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        proc_df[feature_config[PROC_COLUMN]] = CategoryFeatureMixin.feature_data(\n            backend,\n            input_df[feature_config[COLUMN]],\n            metadata[feature_config[NAME]],\n        )\n\n        return proc_df\n\n\nclass CategoryDistributionFeatureMixin(VectorFeatureMixin):\n    @staticmethod\n    def type():\n        return CATEGORY_DISTRIBUTION\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        idx2str = preprocessing_parameters[\"vocab\"]\n        str2idx = {s: i for i, s in enumerate(idx2str)}\n        return {\n            \"preprocessing\": preprocessing_parameters,\n            \"idx2str\": idx2str,\n            \"str2idx\": str2idx,\n            \"vocab_size\": len(idx2str),\n        }\n\n\nclass CategoryInputFeature(CategoryFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: CategoryInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in (torch.int8, torch.int16, torch.int32, torch.int64)\n        assert len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)\n\n        inputs = inputs.reshape(-1, 1)\n        if inputs.dtype == torch.int8 or inputs.dtype == torch.int16:\n            inputs = inputs.type(torch.int)\n        encoder_output = self.encoder_obj(inputs)\n\n        return encoder_output\n\n    @property\n    def input_dtype(self):\n        return torch.int32\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self.encoder_obj.output_shape)\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.vocab = feature_metadata[\"idx2str\"]\n        feature_config.encoder.skip = feature_metadata[PREPROCESSING].get(\"cache_encoder_embeddings\", False)\n\n    @staticmethod\n    def get_schema_cls():\n        return CategoryInputFeatureConfig\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _CategoryPreprocessing(metadata)\n\n\nclass CategoryOutputFeature(CategoryFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: CategoryOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.num_classes = output_feature_config.num_classes\n        self.top_k = output_feature_config.top_k\n\n        # TODO(travis): make this more general to other cumulative loss functions\n        self.use_cumulative_probs = isinstance(output_feature_config.loss, CORNLossConfig)\n\n        super().__init__(output_feature_config, output_features, **kwargs)\n        if hasattr(output_feature_config.decoder, \"num_classes\"):\n            output_feature_config.decoder.num_classes = output_feature_config.num_classes\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):  # hidden\n        hidden = inputs[HIDDEN]\n\n        # EXPECTED SHAPES FOR RETURNED TENSORS\n        # logits: shape [batch_size, num_classes]\n        # hidden: shape [batch_size, size of final fully connected layer]\n        return {LOGITS: self.decoder_obj(hidden), PROJECTION_INPUT: hidden}\n\n    def create_calibration_module(self, feature: CategoryOutputFeatureConfig) -> torch.nn.Module:\n        \"\"\"Creates the appropriate calibration module based on the feature config.\n\n        Today, only one type of calibration (\"temperature_scaling\") is available, but more options may be supported in\n        the future.\n        \"\"\"\n        if feature.calibration:\n            calibration_cls = calibration.get_calibration_cls(CATEGORY, \"temperature_scaling\")\n            return calibration_cls(num_classes=self.num_classes)\n        return None\n\n    def create_predict_module(self) -> PredictModule:\n        return _CategoryPredict(\n            calibration_module=self.calibration_module, use_cumulative_probs=self.use_cumulative_probs\n        )\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, PROBABILITIES, LOGITS}\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.int64\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    def metric_kwargs(self):\n        return {\"top_k\": self.top_k, \"num_classes\": self.num_classes}\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.num_classes = feature_metadata[\"vocab_size\"]\n        feature_config.top_k = min(feature_config.num_classes, feature_config.top_k)\n\n        # If labels are provided, then this is a classification task for LLMs\n        if hasattr(feature_config.preprocessing, \"vocab\"):\n            # Enrich the feature config's decoder with str2idx\n            feature_config.decoder.str2idx = feature_metadata[\"str2idx\"]\n\n        if isinstance(feature_config.loss.class_weights, (list, tuple)):\n            if len(feature_config.loss.class_weights) != feature_config.num_classes:\n                raise ValueError(\n                    f\"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with \"\n                    f\"the number of classes ({feature_config.num_classes}) for feature {feature_config.column}. \"\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and their order and consider there needs to be a weight \"\n                    \"for the <UNK> class too.\"\n                )\n\n        if isinstance(feature_config.loss.class_weights, dict):\n            if feature_metadata[\"str2idx\"].keys() != feature_config.loss.class_weights.keys():\n                raise ValueError(\n                    f\"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with \"\n                    f'the classes ({feature_metadata[\"str2idx\"].keys()}) of feature {feature_config.column}. '\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and consider there needs to be a weight \"\n                    \"for the <UNK> class too.\"\n                )\n            else:\n                class_weights = feature_config.loss.class_weights\n                idx2str = feature_metadata[\"idx2str\"]\n                class_weights_list = [class_weights[s] for s in idx2str]\n                feature_config.loss.class_weights = class_weights_list\n\n        if feature_config.loss.class_similarities_temperature > 0:\n            if feature_config.loss.class_similarities is not None:\n                similarities = feature_config.loss.class_similarities\n                temperature = feature_config.loss.class_similarities_temperature\n\n                curr_row = 0\n                first_row_length = 0\n                is_first_row = True\n                for row in similarities:\n                    if is_first_row:\n                        first_row_length = len(row)\n                        is_first_row = False\n                        curr_row += 1\n                    else:\n                        curr_row_length = len(row)\n                        if curr_row_length != first_row_length:\n                            raise ValueError(\n                                \"The length of row {} of the class_similarities \"\n                                \"of {} is {}, different from the length of \"\n                                \"the first row {}. All rows must have \"\n                                \"the same length.\".format(\n                                    curr_row, feature_config.column, curr_row_length, first_row_length\n                                )\n                            )\n                        else:\n                            curr_row += 1\n                all_rows_length = first_row_length\n\n                if all_rows_length != len(similarities):\n                    raise ValueError(\n                        \"The class_similarities matrix of {} has \"\n                        \"{} rows and {} columns, \"\n                        \"their number must be identical.\".format(\n                            feature_config.column, len(similarities), all_rows_length\n                        )\n                    )\n\n                if all_rows_length != feature_config.num_classes:\n                    raise ValueError(\n                        f\"The size of the class_similarities matrix of {feature_config.column} is \"\n                        f\"{all_rows_length}, different from the number of classes ({feature_config.num_classes}). \"\n                        \"Check the metadata JSON file to see the classes \"\n                        \"and their order and \"\n                        \"consider <UNK> class too.\"\n                    )\n\n                similarities = np.array(similarities, dtype=np.float32)\n                for i in range(len(similarities)):\n                    similarities[i, :] = softmax(similarities[i, :], temperature=temperature)\n\n                feature_config.loss.class_similarities = similarities\n            else:\n                raise ValueError(\n                    \"class_similarities_temperature > 0, \"\n                    \"but no class_similarities are provided \"\n                    \"for feature {}\".format(feature_config.column)\n                )\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        overall_stats = {}\n        confusion_matrix = ConfusionMatrix(targets, predictions[PREDICTIONS], labels=train_set_metadata[\"idx2str\"])\n        overall_stats[\"confusion_matrix\"] = confusion_matrix.cm.tolist()\n        overall_stats[\"overall_stats\"] = confusion_matrix.stats()\n        overall_stats[\"per_class_stats\"] = confusion_matrix.per_class_stats()\n\n        return overall_stats\n\n    def postprocess_predictions(\n        self,\n        predictions,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in predictions:\n            if \"idx2str\" in metadata:\n                predictions[predictions_col] = predictions[predictions_col].map(lambda pred: metadata[\"idx2str\"][pred])\n\n        probabilities_col = f\"{self.feature_name}_{PROBABILITIES}\"\n        if probabilities_col in predictions:\n            prob_col = f\"{self.feature_name}_{PROBABILITY}\"\n            predictions[prob_col] = predictions[probabilities_col].map(max)\n            predictions[probabilities_col] = predictions[probabilities_col].map(lambda pred: pred.tolist())\n            if \"idx2str\" in metadata:\n                for i, label in enumerate(metadata[\"idx2str\"]):\n                    key = f\"{probabilities_col}_{label}\"\n\n                    # Use default param to force a capture before the loop completes, see:\n                    # https://stackoverflow.com/questions/2295290/what-do-lambda-function-closures-capture\n                    predictions[key] = predictions[probabilities_col].map(\n                        lambda prob, i=i: prob[i],\n                    )\n\n        top_k_col = f\"{self.feature_name}_predictions_top_k\"\n        if top_k_col in predictions:\n            if \"idx2str\" in metadata:\n                predictions[top_k_col] = predictions[top_k_col].map(\n                    lambda pred_top_k: [metadata[\"idx2str\"][pred] for pred in pred_top_k]\n                )\n\n        return predictions\n\n    @staticmethod\n    def get_schema_cls():\n        return CategoryOutputFeatureConfig\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _CategoryPostprocessing(metadata)\n\n\nclass CategoryDistributionOutputFeature(CategoryDistributionFeatureMixin, CategoryOutputFeature):\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.float32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.num_classes])\n\n    @staticmethod\n    def get_schema_cls():\n        return CategoryDistributionOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/date_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom datetime import date, datetime\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import COLUMN, DATE, PROC_COLUMN\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature\nfrom ludwig.schema.features.date_feature import DateInputFeatureConfig\nfrom ludwig.types import (\n    FeatureConfigDict,\n    FeatureMetadataDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils.date_utils import create_vector_from_datetime_obj, parse_datetime\nfrom ludwig.utils.types import DataFrame, TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\nDATE_VECTOR_LENGTH = 9\n\n\nclass _DatePreprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if torch.jit.isinstance(v, list[torch.Tensor]):\n            v = torch.stack(v)\n\n        if torch.jit.isinstance(v, torch.Tensor):\n            return v.to(dtype=torch.int)\n        else:\n            raise ValueError(f\"Unsupported input: {v}\")\n\n\nclass DateFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return DATE\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        return {\"preprocessing\": preprocessing_parameters}\n\n    @staticmethod\n    def date_to_list(date_value, datetime_format, preprocessing_parameters):\n        try:\n            if isinstance(date_value, datetime):\n                datetime_obj = date_value\n            elif isinstance(date_value, date):\n                datetime_obj = datetime.combine(date=date_value, time=datetime.min.time())\n            elif isinstance(date_value, str) and datetime_format is not None:\n                try:\n                    datetime_obj = datetime.strptime(date_value, datetime_format)\n                except ValueError:\n                    datetime_obj = parse_datetime(date_value)\n            else:\n                datetime_obj = parse_datetime(date_value)\n        except Exception as e:\n            logger.error(\n                f\"Error parsing date: '{date_value}' with error '{e}' \"\n                \"Please provide a datetime format that parses it \"\n                \"in the preprocessing section of the date feature \"\n                \"in the config. \"\n                \"The preprocessing fill in value will be used.\"\n                \"For more details: \"\n                \"https://ludwig-ai.github.io/ludwig-docs/latest/configuration/features/date_features/#date-features-preprocessing\"  # noqa\n            )\n            fill_value = preprocessing_parameters[\"fill_value\"]\n            if fill_value != \"\":\n                datetime_obj = parse_datetime(fill_value)\n            else:\n                datetime_obj = datetime.now()\n\n        return create_vector_from_datetime_obj(datetime_obj)\n\n    @staticmethod\n    def add_feature_data(\n        feature_config: FeatureConfigDict,\n        input_df: DataFrame,\n        proc_df: dict[str, DataFrame],\n        metadata: TrainingSetMetadataDict,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,  # Union[Backend, str]\n        skip_save_processed_input: bool,\n    ) -> None:\n        datetime_format = preprocessing_parameters[\"datetime_format\"]\n        proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects(\n            input_df[feature_config[COLUMN]],\n            lambda x: np.array(\n                DateFeatureMixin.date_to_list(x, datetime_format, preprocessing_parameters), dtype=np.int32\n            ),\n        )\n        return proc_df\n\n\nclass DateInputFeature(DateFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: DateInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor), type(inputs)\n        assert inputs.dtype in [torch.int16, torch.int32, torch.int64, torch.float32], inputs.dtype\n        inputs_encoded = self.encoder_obj(inputs)\n        return inputs_encoded\n\n    @property\n    def input_dtype(self):\n        return torch.int32\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([DATE_VECTOR_LENGTH])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    def create_sample_input(self, batch_size: int = 2):\n        date = [2013, 2, 26, 1, 57, 0, 0, 0, 0]\n        return torch.Tensor([date for _ in range(batch_size)]).type(torch.int32)\n\n    @staticmethod\n    def get_schema_cls():\n        return DateInputFeatureConfig\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _DatePreprocessing(metadata)\n"
  },
  {
    "path": "ludwig/features/feature_registries.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom typing import Any, TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    CATEGORY_DISTRIBUTION,\n    DATE,\n    H3,\n    IMAGE,\n    NUMBER,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    VECTOR,\n)\nfrom ludwig.features.audio_feature import AudioFeatureMixin, AudioInputFeature\nfrom ludwig.features.bag_feature import BagFeatureMixin, BagInputFeature\nfrom ludwig.features.binary_feature import BinaryFeatureMixin, BinaryInputFeature, BinaryOutputFeature\nfrom ludwig.features.category_feature import (\n    CategoryDistributionFeatureMixin,\n    CategoryDistributionOutputFeature,\n    CategoryFeatureMixin,\n    CategoryInputFeature,\n    CategoryOutputFeature,\n)\nfrom ludwig.features.date_feature import DateFeatureMixin, DateInputFeature\nfrom ludwig.features.h3_feature import H3FeatureMixin, H3InputFeature\nfrom ludwig.features.image_feature import ImageFeatureMixin, ImageInputFeature, ImageOutputFeature\nfrom ludwig.features.number_feature import NumberFeatureMixin, NumberInputFeature, NumberOutputFeature\nfrom ludwig.features.sequence_feature import SequenceFeatureMixin, SequenceInputFeature, SequenceOutputFeature\nfrom ludwig.features.set_feature import SetFeatureMixin, SetInputFeature, SetOutputFeature\nfrom ludwig.features.text_feature import TextFeatureMixin, TextInputFeature, TextOutputFeature\nfrom ludwig.features.timeseries_feature import TimeseriesFeatureMixin, TimeseriesInputFeature, TimeseriesOutputFeature\nfrom ludwig.features.vector_feature import VectorFeatureMixin, VectorInputFeature, VectorOutputFeature\nfrom ludwig.utils.misc_utils import get_from_registry\n\nif TYPE_CHECKING:\n    from ludwig.models.base import BaseModel\n    from ludwig.schema.model_types.base import ModelConfig\n\n\n@DeveloperAPI\ndef get_base_type_registry() -> dict:\n    return {\n        TEXT: TextFeatureMixin,\n        CATEGORY: CategoryFeatureMixin,\n        SET: SetFeatureMixin,\n        BAG: BagFeatureMixin,\n        BINARY: BinaryFeatureMixin,\n        NUMBER: NumberFeatureMixin,\n        SEQUENCE: SequenceFeatureMixin,\n        TIMESERIES: TimeseriesFeatureMixin,\n        IMAGE: ImageFeatureMixin,\n        AUDIO: AudioFeatureMixin,\n        H3: H3FeatureMixin,\n        DATE: DateFeatureMixin,\n        VECTOR: VectorFeatureMixin,\n        CATEGORY_DISTRIBUTION: CategoryDistributionFeatureMixin,\n    }\n\n\n@DeveloperAPI\ndef get_input_type_registry() -> dict:\n    return {\n        TEXT: TextInputFeature,\n        NUMBER: NumberInputFeature,\n        BINARY: BinaryInputFeature,\n        CATEGORY: CategoryInputFeature,\n        SET: SetInputFeature,\n        SEQUENCE: SequenceInputFeature,\n        IMAGE: ImageInputFeature,\n        AUDIO: AudioInputFeature,\n        TIMESERIES: TimeseriesInputFeature,\n        BAG: BagInputFeature,\n        H3: H3InputFeature,\n        DATE: DateInputFeature,\n        VECTOR: VectorInputFeature,\n    }\n\n\n@DeveloperAPI\ndef get_output_type_registry() -> dict:\n    return {\n        CATEGORY: CategoryOutputFeature,\n        BINARY: BinaryOutputFeature,\n        NUMBER: NumberOutputFeature,\n        SEQUENCE: SequenceOutputFeature,\n        SET: SetOutputFeature,\n        TEXT: TextOutputFeature,\n        TIMESERIES: TimeseriesOutputFeature,\n        VECTOR: VectorOutputFeature,\n        CATEGORY_DISTRIBUTION: CategoryDistributionOutputFeature,\n        IMAGE: ImageOutputFeature,\n    }\n\n\ndef update_config_with_metadata(config_obj: \"ModelConfig\", training_set_metadata: dict[str, Any]):\n    # populate input features fields depending on data\n    for input_feature in config_obj.input_features:\n        feature = get_from_registry(input_feature.type, get_input_type_registry())\n        feature.update_config_with_metadata(input_feature, training_set_metadata[input_feature.name])\n\n    # populate output features fields depending on data\n    for output_feature in config_obj.output_features:\n        feature = get_from_registry(output_feature.type, get_output_type_registry())\n        feature.update_config_with_metadata(output_feature, training_set_metadata[output_feature.name])\n\n\ndef update_config_with_model(config_obj: \"ModelConfig\", model: \"BaseModel\"):\n    \"\"\"Updates the config with the final input feature params given a model.\n\n    This function should only be called to update the config after the model is initialized. Currently only implemented\n    for input features because it is only relevant for HuggingFace text encoders. HuggingFace text encoders only know\n    their final config after class initialization.\n    \"\"\"\n    for input_feature in config_obj.input_features:\n        model_input_feature = model.input_features.get(input_feature.name)\n        model_input_feature.update_config_after_module_init(input_feature)\n"
  },
  {
    "path": "ludwig/features/feature_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport re\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import NAME, PREPROCESSING, SEQUENCE, TEXT, TIMESERIES, TYPE\nfrom ludwig.utils.data_utils import hash_dict\nfrom ludwig.utils.strings_utils import get_tokenizer_from_registry, UNKNOWN_SYMBOL\n\nSEQUENCE_TYPES = {SEQUENCE, TEXT, TIMESERIES}\nFEATURE_NAME_SUFFIX = \"__ludwig\"\nFEATURE_NAME_SUFFIX_LENGTH = len(FEATURE_NAME_SUFFIX)\n\n\ndef should_regularize(regularize_layers):\n    regularize = False\n    if isinstance(regularize_layers, bool) and regularize_layers:\n        regularize = True\n    elif isinstance(regularize_layers, (list, tuple)) and regularize_layers and regularize_layers[-1]:\n        regularize = True\n    return regularize\n\n\ndef set_str_to_idx(set_string, feature_dict, tokenizer_name):\n    try:\n        tokenizer = get_tokenizer_from_registry(tokenizer_name)()\n    except ValueError:\n        raise Exception(f\"Tokenizer {tokenizer_name} not supported\")\n\n    out = [feature_dict.get(item, feature_dict[UNKNOWN_SYMBOL]) for item in tokenizer(set_string)]\n\n    return np.array(out, dtype=np.int32)\n\n\ndef compute_token_probabilities(\n    probabilities: list | tuple | np.ndarray,\n) -> np.ndarray:\n    \"\"\"Gets the maximum probability per timestep.\n\n    Args:\n        probabilities: An iterable of iterables or np.ndarray with shape (sequence_length, num_classes)\n            where each inner iterable or np.ndarray is the probability distribution for a single timestep.\n    Returns:\n        An np.ndarray with shape (sequence_length,) containing the maximum probability for each timestep.\n    \"\"\"\n    if isinstance(probabilities, (list, tuple)):\n        if not hasattr(probabilities[0], \"__len__\"):\n            raise ValueError(\n                \"Received token probabilities as a flat 1D list. Expected list of list of probabilities \"\n                \"(sequence_length, vocab_size).\"\n            )\n        max_probs = []\n        for timestep_probs in probabilities:\n            max_probs.append(np.max(timestep_probs))\n        max_probs = np.array(max_probs)\n    elif isinstance(probabilities, np.ndarray):\n        if len(probabilities.shape) != 2:\n            raise ValueError(\n                f\"Received token probabilities with non 2D shape: {probabilities.shape}. Expected shape: \"\n                \"(sequence_length, vocab_size).\"\n            )\n        max_probs = np.max(probabilities, axis=-1)\n    else:\n        raise ValueError(f\"probabilities type must be in [list, tuple, np.ndarray]. Got {type(probabilities)}\")\n    return max_probs\n\n\ndef compute_sequence_probability(\n    sequence_probabilities: np.ndarray,\n    max_sequence_length: int | None = None,\n    return_log_prob: bool = True,\n) -> float:\n    \"\"\"Computes the sequence level probability.\n\n    Args:\n        sequence_probabilities: An iterable of iterables or np.ndarray with shape (sequence_length,)\n        max_sequence_length: The maximum sequence length to use. If None, uses the first dim of `sequence_probabilities`\n        return_log_prob: Whether to return the log probability. Defaults to True.\n    \"\"\"\n    if max_sequence_length is None:\n        max_sequence_length = sequence_probabilities.shape[0]\n\n    sequence_probabilities = sequence_probabilities[:max_sequence_length]\n\n    if return_log_prob:\n        return np.sum(np.log(np.clip(sequence_probabilities, 1e-10, 1.0)))\n    else:\n        return np.prod(sequence_probabilities)\n\n\ndef sanitize(name):\n    \"\"\"Replaces invalid id characters.\"\"\"\n    return re.sub(\"\\\\W|^(?=\\\\d)\", \"_\", name)\n\n\ndef compute_feature_hash(feature: dict) -> str:\n    \"\"\"This function computes a hash for each feature based on the preprocessing dictionary associated with each\n    feature, as well as the feature's type.\n\n    Args:\n        feature: Feature dictionary\n\n    Returns: Feature hash name\n    \"\"\"\n    feature_data = dict(\n        preprocessing=feature.get(PREPROCESSING, {}),\n        type=feature[TYPE],\n    )\n    return sanitize(feature[NAME]) + \"_\" + hash_dict(feature_data).decode(\"ascii\")\n\n\ndef get_input_size_with_dependencies(\n    combiner_output_size: int, dependencies: list[str], other_output_features  # Dict[str, \"OutputFeature\"]\n):\n    \"\"\"Returns the input size for the first layer of this output feature's FC stack, accounting for dependencies on\n    other output features.\n\n    In the forward pass, the hidden states of any dependent output features get concatenated with the combiner's output.\n    If this output feature depends on other output features, then the input size for this feature's FCStack is the sum\n    of the output sizes of other output features + the combiner's output size.\n    \"\"\"\n    input_size_with_dependencies = combiner_output_size\n    for feature_name in dependencies:\n        if other_output_features[feature_name].fc_stack.num_layers:\n            input_size_with_dependencies += other_output_features[feature_name].fc_stack.output_shape[-1]\n        else:\n            # 0-layer FCStack. Use the output feature's input size.\n            input_size_with_dependencies += other_output_features[feature_name].input_size\n    return input_size_with_dependencies\n\n\ndef get_module_dict_key_from_name(name: str, feature_name_suffix: str = FEATURE_NAME_SUFFIX) -> str:\n    \"\"\"Returns a key that's guaranteed to be compatible with torch.\"\"\"\n    key = name.replace(\".\", \"__ludwig_punct_period__\")\n    return key + feature_name_suffix\n\n\ndef get_name_from_module_dict_key(key: str, feature_name_suffix_length: int = FEATURE_NAME_SUFFIX_LENGTH) -> str:\n    \"\"\"Reverse of get_module_dict_key_from_name.\"\"\"\n    name = key.replace(\"__ludwig_punct_period__\", \".\")\n    return name[:-feature_name_suffix_length]\n\n\nclass LudwigFeatureDict(torch.nn.Module):\n    \"\"\"Torch ModuleDict wrapper that permits keys with any name.\n\n    Torch's ModuleDict implementation doesn't allow certain keys to be used if they conflict with existing class\n    attributes, e.g.\n\n    > torch.nn.ModuleDict({'type': torch.nn.Module()})  # Raises KeyError.\n\n    This class is a simple wrapper around torch's ModuleDict that mitigates possible conflicts by using a key-suffixing\n    protocol.\n\n    This is also tracked in Pytorch: https://github.com/pytorch/pytorch/issues/71203.\n    \"\"\"\n\n    def __init__(self):\n        super().__init__()\n        self.module_dict = torch.nn.ModuleDict()\n        self.internal_key_to_original_name_map = {}\n\n    def get(self, key) -> torch.nn.Module:\n        return self.module_dict[get_module_dict_key_from_name(key)]\n\n    def set(self, key: str, module: torch.nn.Module) -> None:\n        module_dict_key_name = get_module_dict_key_from_name(key)\n        self.internal_key_to_original_name_map[module_dict_key_name] = key\n        self.module_dict[module_dict_key_name] = module\n\n    def __len__(self) -> int:\n        return len(self.module_dict)\n\n    def __next__(self) -> None:\n        return next(iter(self))\n\n    def __iter__(self) -> None:\n        return iter(self.keys())\n\n    def keys(self) -> list[str]:\n        return [\n            get_name_from_module_dict_key(feature_name)\n            for feature_name in self.internal_key_to_original_name_map.keys()\n        ]\n\n    def values(self) -> list[torch.nn.Module]:\n        return [module for _, module in self.module_dict.items()]\n\n    def items(self) -> list[tuple[str, torch.nn.Module]]:\n        return [\n            (get_name_from_module_dict_key(feature_name), module) for feature_name, module in self.module_dict.items()\n        ]\n\n    def update(self, modules: dict[str, torch.nn.Module]) -> None:\n        for feature_name, module in modules.items():\n            self.set(feature_name, module)\n"
  },
  {
    "path": "ludwig/features/h3_feature.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import COLUMN, H3, PROC_COLUMN\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature\nfrom ludwig.schema.features.h3_feature import H3InputFeatureConfig\nfrom ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.h3_util import h3_to_components\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\nMAX_H3_RESOLUTION = 15\nH3_VECTOR_LENGTH = MAX_H3_RESOLUTION + 4\nH3_PADDING_VALUE = 7\n\n\nclass _H3Preprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.max_h3_resolution = MAX_H3_RESOLUTION\n        self.h3_padding_value = H3_PADDING_VALUE\n        self.computed_fill_value = float(metadata[\"preprocessing\"][\"computed_fill_value\"])\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if torch.jit.isinstance(v, list[torch.Tensor]):\n            v = torch.stack(v)\n\n        if not torch.jit.isinstance(v, torch.Tensor):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        v = torch.nan_to_num(v, nan=self.computed_fill_value)\n        v = v.long()\n\n        outputs: list[torch.Tensor] = []\n        for v_i in v:\n            components = h3_to_components(v_i)\n            header: list[int] = [\n                components.mode,\n                components.edge,\n                components.resolution,\n                components.base_cell,\n            ]\n            cells_padding: list[int] = [self.h3_padding_value] * (self.max_h3_resolution - len(components.cells))\n            output = torch.tensor(header + components.cells + cells_padding, dtype=torch.uint8, device=v.device)\n            outputs.append(output)\n\n        return torch.stack(outputs)\n\n\nclass H3FeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return H3\n\n    @staticmethod\n    def cast_column(column, backend):\n        try:\n            return column.astype(int)\n        except ValueError:\n            logger.warning(\"H3Feature could not be read as int directly. Reading as float and converting to int.\")\n            return column.astype(float).astype(int)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        return {}\n\n    @staticmethod\n    def h3_to_list(h3_int):\n        components = h3_to_components(h3_int)\n        header = [components.mode, components.edge, components.resolution, components.base_cell]\n        cells_padding = [H3_PADDING_VALUE] * (MAX_H3_RESOLUTION - len(components.cells))\n        return header + components.cells + cells_padding\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        column = input_df[feature_config[COLUMN]]\n        if column.dtype == object:\n            column = backend.df_engine.map_objects(column, int)\n        column = backend.df_engine.map_objects(column, H3FeatureMixin.h3_to_list)\n\n        proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects(\n            column, lambda x: np.array(x, dtype=np.uint8)\n        )\n        return proc_df\n\n\nclass H3InputFeature(H3FeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: H3InputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.uint8, torch.int64]\n        assert len(inputs.shape) == 2\n\n        inputs_encoded = self.encoder_obj(inputs)\n\n        return inputs_encoded\n\n    @property\n    def input_dtype(self):\n        return torch.uint8\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([H3_VECTOR_LENGTH])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _H3Preprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return H3InputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/image_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\nimport warnings\nfrom collections import Counter\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom functools import partial\nfrom typing import Any\n\nimport numpy as np\nimport torch\nfrom torchvision import transforms\nfrom torchvision.transforms import functional as F\nfrom torchvision.transforms.functional import normalize\n\nfrom ludwig.constants import (\n    CHECKSUM,\n    COLUMN,\n    ENCODER,\n    HEIGHT,\n    IMAGE,\n    IMAGENET1K,\n    INFER_IMAGE_DIMENSIONS,\n    INFER_IMAGE_MAX_HEIGHT,\n    INFER_IMAGE_MAX_WIDTH,\n    INFER_IMAGE_NUM_CLASSES,\n    INFER_IMAGE_SAMPLE_SIZE,\n    LOGITS,\n    NAME,\n    NUM_CHANNELS,\n    PREDICTIONS,\n    PREPROCESSING,\n    PROC_COLUMN,\n    REQUIRES_EQUAL_DIMENSIONS,\n    SRC,\n    TRAINING,\n    TYPE,\n    WIDTH,\n)\nfrom ludwig.data.cache.types import wrap\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.image.torchvision import TVModelVariant\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.schema.features.augmentation.base import BaseAugmentationConfig\nfrom ludwig.schema.features.augmentation.image import (\n    AutoAugmentationConfig,\n    RandomBlurConfig,\n    RandomBrightnessConfig,\n    RandomContrastConfig,\n    RandomHorizontalFlipConfig,\n    RandomRotateConfig,\n    RandomVerticalFlipConfig,\n)\nfrom ludwig.schema.features.image_feature import ImageInputFeatureConfig, ImageOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.augmentation_utils import get_augmentation_op, register_augmentation_op\nfrom ludwig.utils.data_utils import get_abs_path\nfrom ludwig.utils.dataframe_utils import is_dask_series_or_df\nfrom ludwig.utils.fs_utils import has_remote_protocol, upload_h5\nfrom ludwig.utils.image_utils import (\n    get_class_mask_from_image,\n    get_gray_default_image,\n    get_image_from_class_mask,\n    get_unique_channels,\n    grayscale,\n    num_channels_in_image,\n    read_image_from_bytes_obj,\n    read_image_from_path,\n    resize_image,\n    ResizeChannels,\n    torchvision_model_registry,\n)\nfrom ludwig.utils.misc_utils import set_default_value\nfrom ludwig.utils.types import Series, TorchscriptPreprocessingInput\n\n# constants used for Ludwig image preprocessing\nIMAGENET1K_MEAN = [0.485, 0.456, 0.406]\nIMAGENET1K_STD = [0.229, 0.224, 0.225]\n\n\nlogger = logging.getLogger(__name__)\n\n\n###\n# Image specific augmentation operations\n###\n@register_augmentation_op(name=\"auto_augmentation\", features=IMAGE)\nclass AutoAugment(torch.nn.Module):\n    def __init__(self, config: AutoAugmentationConfig):\n        super().__init__()\n        self.auto_augmentation_method = config.method\n        self.augmentation_method = self.get_augmentation_method()\n\n    def get_augmentation_method(self):\n        if self.auto_augmentation_method == \"trivial_augment\":\n            return transforms.TrivialAugmentWide()\n        if self.auto_augmentation_method == \"auto_augment\":\n            return transforms.AutoAugment()\n        if self.auto_augmentation_method == \"rand_augment\":\n            return transforms.RandAugment()\n        raise ValueError(f\"Unsupported auto-augmentation method: {self.auto_augmentation_method}\")\n\n    def forward(self, imgs: torch.Tensor) -> torch.Tensor:\n        method = self.augmentation_method\n        uint8imgs = imgs.to(torch.uint8)\n        augmented_imgs = method(uint8imgs)\n\n        return augmented_imgs.to(torch.float32)\n\n\n@register_augmentation_op(name=\"random_vertical_flip\", features=IMAGE)\nclass RandomVFlip(torch.nn.Module):\n    def __init__(\n        self,\n        config: RandomVerticalFlipConfig,\n    ):\n        super().__init__()\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            imgs = F.vflip(imgs)\n\n        return imgs\n\n\n@register_augmentation_op(name=\"random_horizontal_flip\", features=IMAGE)\nclass RandomHFlip(torch.nn.Module):\n    def __init__(\n        self,\n        config: RandomHorizontalFlipConfig,\n    ):\n        super().__init__()\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            imgs = F.hflip(imgs)\n\n        return imgs\n\n\n@register_augmentation_op(name=\"random_rotate\", features=IMAGE)\nclass RandomRotate(torch.nn.Module):\n    def __init__(self, config: RandomRotateConfig):\n        super().__init__()\n        self.degree = config.degree\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            # map angle to interval (-degree, +degree)\n            angle = (torch.rand(1) * 2 * self.degree - self.degree).item()\n            return F.rotate(imgs, angle)\n        else:\n            return imgs\n\n\n@register_augmentation_op(name=\"random_contrast\", features=IMAGE)\nclass RandomContrast(torch.nn.Module):\n    def __init__(self, config: RandomContrastConfig):\n        super().__init__()\n        self.min_contrast = config.min\n        self.contrast_adjustment_range = config.max - config.min\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            # random contrast adjustment\n            adjust_factor = (torch.rand(1) * self.contrast_adjustment_range + self.min_contrast).item()\n            return F.adjust_contrast(imgs, adjust_factor)\n        else:\n            return imgs\n\n\n@register_augmentation_op(name=\"random_brightness\", features=IMAGE)\nclass RandomBrightness(torch.nn.Module):\n    def __init__(self, config: RandomBrightnessConfig):\n        super().__init__()\n        self.min_brightness = config.min\n        self.brightness_adjustment_range = config.max - config.min\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            # random contrast adjustment\n            adjust_factor = (torch.rand(1) * self.brightness_adjustment_range + self.min_brightness).item()\n            return F.adjust_brightness(imgs, adjust_factor)\n        else:\n            return imgs\n\n\n@register_augmentation_op(name=\"random_blur\", features=IMAGE)\nclass RandomBlur(torch.nn.Module):\n    def __init__(self, config: RandomBlurConfig):\n        super().__init__()\n        self.kernel_size = [config.kernel_size, config.kernel_size]\n\n    def forward(self, imgs):\n        if torch.rand(1) < 0.5:\n            imgs = F.gaussian_blur(imgs, self.kernel_size)\n\n        return imgs\n\n\nclass ImageAugmentation(torch.nn.Module):\n    def __init__(\n        self,\n        augmentation_list: list[BaseAugmentationConfig],\n        normalize_mean: list[float] | None = None,\n        normalize_std: list[float] | None = None,\n    ):\n        super().__init__()\n\n        logger.debug(f\"Creating augmentation pipeline: {augmentation_list}\")\n\n        self.normalize_mean = normalize_mean\n        self.normalize_std = normalize_std\n\n        if self.training:\n            self.augmentation_steps = torch.nn.Sequential()\n            for aug_config in augmentation_list:\n                try:\n                    aug_op = get_augmentation_op(IMAGE, aug_config.type)\n                    self.augmentation_steps.append(aug_op(aug_config))\n                except KeyError:\n                    raise ValueError(f\"Invalid augmentation operation specification: {aug_config}\")\n        else:\n            # TODO: should this raise an exception if not in training mode?\n            self.augmentation_steps = None\n\n    def forward(self, imgs):\n        if self.augmentation_steps:\n            # convert from float to uint8 values - this is required for the augmentation\n            imgs = self._convert_back_to_uint8(imgs)\n\n            logger.debug(\"Executing augmentation pipeline steps: %s\", self.augmentation_steps)\n            imgs = self.augmentation_steps(imgs)\n\n            # convert back to float32 values and renormalize if needed\n            imgs = self._renormalize_image(imgs)\n\n        return imgs\n\n    # function to partially undo the TorchVision ImageClassification transformation.\n    #  back out the normalization step and convert from float32 to uint8 dtype\n    #  to make the tensor displayable as an image\n    #  crop size remains the same\n    def _convert_back_to_uint8(self, images):\n        if self.normalize_mean:\n            mean = torch.as_tensor(self.normalize_mean, dtype=torch.float32).view(-1, 1, 1)\n            std = torch.as_tensor(self.normalize_std, dtype=torch.float32).view(-1, 1, 1)\n            return images.mul(std).add(mean).mul(255.0).type(torch.uint8)\n        else:\n            return images.mul(255.0).type(torch.uint8)\n\n    # function to redo part of the TorchVision ImageClassification transformation.\n    #  convert uint8 to float32\n    #  apply the imagenet1k normalization\n    def _renormalize_image(self, images):\n        if self.normalize_mean:\n            mean = torch.as_tensor(self.normalize_mean, dtype=torch.float32).view(-1, 1, 1)\n            std = torch.as_tensor(self.normalize_std, dtype=torch.float32).view(-1, 1, 1)\n            return images.type(torch.float32).div(255.0).sub(mean).div(std)\n        else:\n            return images.type(torch.float32).div(255.0)\n\n\n@dataclass\nclass ImageTransformMetadata:\n    height: int\n    width: int\n    num_channels: int\n\n\ndef _get_torchvision_transform(\n    torchvision_parameters: TVModelVariant,\n) -> tuple[torch.nn.Module, ImageTransformMetadata]:\n    \"\"\"Returns a torchvision transform that is compatible with the model variant.\n\n    Note that the raw torchvision transform is not returned. Instead, a Sequential module that includes\n    image resizing is returned. This is because the raw torchvision transform assumes that the input image has\n    three channels, which is not always the case with images input into Ludwig.\n\n    Args:\n        torchvision_parameters: The parameters for the torchvision model variant.\n    Returns:\n        (torchvision_transform, transform_metadata): A torchvision transform and the metadata for the transform.\n    \"\"\"\n    torchvision_transform_raw = torchvision_parameters.model_weights.DEFAULT.transforms()\n    torchvision_transform = torch.nn.Sequential(\n        ResizeChannels(num_channels=3),\n        torchvision_transform_raw,\n    )\n    transform_metadata = ImageTransformMetadata(\n        height=torchvision_transform_raw.crop_size[0],\n        width=torchvision_transform_raw.crop_size[0],\n        num_channels=len(torchvision_transform_raw.mean),\n    )\n    return (torchvision_transform, transform_metadata)\n\n\ndef _get_torchvision_parameters(model_type: str, model_variant: str) -> TVModelVariant:\n    return torchvision_model_registry.get(model_type).get(model_variant)\n\n\ndef is_torchvision_encoder(encoder_obj: Encoder) -> bool:\n    # TODO(travis): do this through an interface rather than conditional logic\n    from ludwig.encoders.image.torchvision import TVBaseEncoder\n\n    return isinstance(encoder_obj, TVBaseEncoder)\n\n\nclass _ImagePreprocessing(torch.nn.Module):\n    \"\"\"Torchscript-enabled version of preprocessing done by ImageFeatureMixin.add_feature_data.\"\"\"\n\n    def __init__(\n        self,\n        metadata: TrainingSetMetadataDict,\n        torchvision_transform: torch.nn.Module | None = None,\n        transform_metadata: ImageTransformMetadata | None = None,\n    ):\n        super().__init__()\n\n        self.resize_method = metadata[\"preprocessing\"][\"resize_method\"]\n        self.torchvision_transform = torchvision_transform\n        if transform_metadata is not None:\n            self.height = transform_metadata.height\n            self.width = transform_metadata.width\n            self.num_channels = transform_metadata.num_channels\n            self.channel_class_map = torch.Tensor([])\n        else:\n            self.height = metadata[\"preprocessing\"][\"height\"]\n            self.width = metadata[\"preprocessing\"][\"width\"]\n            self.num_channels = metadata[\"preprocessing\"][\"num_channels\"]\n            self.channel_class_map = torch.ByteTensor(metadata[\"preprocessing\"][\"channel_class_map\"])\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        \"\"\"Takes a list of images and adjusts the size and number of channels as specified in the metadata.\n\n        If `v` is already a torch.Tensor, we assume that the images are already preprocessed to be the same size.\n        \"\"\"\n        # Nested conditional is a workaround to short-circuit boolean evaluation.\n        if not torch.jit.isinstance(v, list[torch.Tensor]):\n            if not torch.jit.isinstance(v, torch.Tensor):\n                raise ValueError(f\"Unsupported input: {v}\")\n\n        if self.torchvision_transform is not None:\n            # perform pre-processing for torchvision pretrained model encoders\n            if torch.jit.isinstance(v, list[torch.Tensor]):\n                imgs = [self.torchvision_transform(img) for img in v]\n            else:\n                # convert batch of image tensors to a list and then run torchvision pretrained\n                # model transforms on each image\n                imgs = [self.torchvision_transform(img) for img in torch.unbind(v)]\n\n            # collect the list of images into a batch\n            imgs_stacked = torch.stack(imgs)\n        else:\n            # perform pre-processing for Ludwig defined image encoders\n            if torch.jit.isinstance(v, list[torch.Tensor]):\n                imgs = [resize_image(img, (self.height, self.width), self.resize_method) for img in v]\n                imgs_stacked = torch.stack(imgs)\n            else:\n                imgs_stacked = v\n\n            _, num_channels, height, width = imgs_stacked.shape\n\n            # Ensure images are the size expected by the model\n            if height != self.height or width != self.width:\n                imgs_stacked = resize_image(imgs_stacked, (self.height, self.width), self.resize_method)\n\n            # Ensures images have the number of channels expected by the model\n            if num_channels != self.num_channels:\n                if self.num_channels == 1:\n                    imgs_stacked = grayscale(imgs_stacked)\n                elif num_channels < self.num_channels:\n                    extra_channels = self.num_channels - num_channels\n                    imgs_stacked = torch.nn.functional.pad(imgs_stacked, [0, 0, 0, 0, 0, extra_channels])\n                else:\n                    raise ValueError(\n                        f\"Number of channels cannot be reconciled. metadata.num_channels = \"\n                        f\"{self.num_channels}, but imgs.shape[1] = {num_channels}\"\n                    )\n\n            # Create class-masked images if required\n            if self.channel_class_map.shape[0]:\n                masks = []\n                for img in imgs_stacked:\n                    mask = get_class_mask_from_image(self.channel_class_map, img)\n                    masks.append(mask)\n                imgs_stacked = torch.stack(masks)\n            else:\n                imgs_stacked = imgs_stacked.type(torch.float32) / 255\n\n        return imgs_stacked\n\n\nclass _ImagePostprocessing(torch.nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.logits_key = LOGITS\n        self.predictions_key = PREDICTIONS\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        logits = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.logits_key)\n\n        return {self.predictions_key: predictions, self.logits_key: logits}\n\n\nclass _ImagePredict(PredictModule):\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        predictions = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.predictions_key)\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n\n        return {self.predictions_key: predictions, self.logits_key: logits}\n\n\nclass ImageFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return IMAGE\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        return {PREPROCESSING: preprocessing_parameters}\n\n    @staticmethod\n    def _read_image_if_bytes_obj_and_resize(\n        img_entry: bytes | torch.Tensor | np.ndarray | str,\n        img_width: int,\n        img_height: int,\n        should_resize: bool,\n        num_channels: int,\n        resize_method: str,\n        user_specified_num_channels: bool,\n        standardize_image: str,\n        channel_class_map: torch.Tensor,\n    ) -> np.ndarray | None:\n        \"\"\":param img_entry Union[bytes, torch.Tensor, np.ndarray, str]: if str file path to the image else\n        torch.Tensor of the image itself :param img_width: expected width of the image :param img_height: expected\n        height of the image :param should_resize: Should the image be resized? :param resize_method: type of\n        resizing method :param num_channels: expected number of channels in the first image :param\n        user_specified_num_channels: did the user specify num channels? :param standardize_image: specifies whether\n        to standarize image with imagenet1k specifications :param channel_class_map: A tensor mapping channel\n        values to classes, where dim=0 is the class :return: image object as a numpy array.\n\n        Helper method to read and resize an image according to model definition. If the user doesn't specify a number of\n        channels, we use the first image in the dataset as the source of truth. If any image in the dataset doesn't have\n        the same number of channels as the first image, raise an exception.\n\n        If the user specifies a number of channels, we try to convert all the images to the specifications by dropping\n        channels/padding 0 channels\n        \"\"\"\n\n        if isinstance(img_entry, bytes):\n            img = read_image_from_bytes_obj(img_entry, num_channels)\n        elif isinstance(img_entry, str):\n            img = read_image_from_path(img_entry, num_channels)\n        elif isinstance(img_entry, np.ndarray):\n            img = torch.from_numpy(np.array(img_entry, copy=True)).permute(2, 0, 1)\n        else:\n            img = img_entry\n\n        if not isinstance(img, torch.Tensor):\n            warnings.warn(f\"Image with value {img} cannot be read\")\n            return None\n\n        img_num_channels = num_channels_in_image(img)\n        # Convert to grayscale if needed.\n        if num_channels == 1 and img_num_channels != 1:\n            img = grayscale(img)\n            img_num_channels = 1\n\n        if should_resize:\n            img = resize_image(img, (img_height, img_width), resize_method)\n\n        if user_specified_num_channels:\n            # Number of channels is specified by the user\n            # img_padded = np.zeros((img_height, img_width, num_channels),\n            #                       dtype=np.uint8)\n            # min_num_channels = min(num_channels, img_num_channels)\n            # img_padded[:, :, :min_num_channels] = img[:, :, :min_num_channels]\n            # img = img_padded\n            if num_channels > img_num_channels:\n                extra_channels = num_channels - img_num_channels\n                img = torch.nn.functional.pad(img, [0, 0, 0, 0, 0, extra_channels])\n\n            if img_num_channels != num_channels:\n                logger.warning(\n                    \"Image has {} channels, where as {} \"\n                    \"channels are expected. Dropping/adding channels \"\n                    \"with 0s as appropriate\".format(img_num_channels, num_channels)\n                )\n        else:\n            # If the image isn't like the first image, raise exception\n            if img_num_channels != num_channels:\n                raise ValueError(\n                    \"Image has {} channels, unlike the first image, which \"\n                    \"has {} channels. Make sure all the images have the same \"\n                    \"number of channels or use the num_channels property in \"\n                    \"image preprocessing\".format(img_num_channels, num_channels)\n                )\n\n        if img.shape[1] != img_height or img.shape[2] != img_width:\n            raise ValueError(\n                \"Images are not of the same size. \"\n                \"Expected size is {}, \"\n                \"current image size is {}.\"\n                \"Images are expected to be all of the same size \"\n                \"or explicit image width and height are expected \"\n                \"to be provided. \"\n                \"Additional information: \"\n                \"https://ludwig-ai.github.io/ludwig-docs/latest/configuration/features/image_features\"\n                \"#image-features-preprocessing\".format([img_height, img_width, num_channels], img.shape)\n            )\n\n        # Create class-masked image if required\n        if channel_class_map.shape[0]:\n            img = get_class_mask_from_image(channel_class_map, img)\n        else:\n            # casting and rescaling\n            img = img.type(torch.float32) / 255\n\n            if standardize_image == IMAGENET1K:\n                img = normalize(img, mean=IMAGENET1K_MEAN, std=IMAGENET1K_STD)\n\n        return img.numpy()\n\n    @staticmethod\n    def _read_image_with_pretrained_transform(\n        img_entry: bytes | torch.Tensor | np.ndarray,\n        transform_fn: Callable,\n    ) -> np.ndarray | None:\n        if isinstance(img_entry, bytes):\n            img = read_image_from_bytes_obj(img_entry)\n        elif isinstance(img_entry, str):\n            img = read_image_from_path(img_entry)\n        elif isinstance(img_entry, np.ndarray):\n            img = torch.from_numpy(img_entry).permute(2, 0, 1)\n        else:\n            img = img_entry\n\n        if not isinstance(img, torch.Tensor):\n            warnings.warn(f\"Image with value {img} cannot be read\")\n            return None\n\n        img = transform_fn(img)\n\n        return img.numpy()\n\n    @staticmethod\n    def _set_image_and_height_equal_for_encoder(\n        width: int, height: int, preprocessing_parameters: dict, encoder_type: str\n    ) -> tuple[int, int]:\n        \"\"\"Some pretrained image encoders require images with the same dimension, or images with a specific width\n        and heigh values. The returned width and height are set based on compatibility with the downstream encoder\n        using the encoder parameters for the feature.\n\n        Args:\n            width: Represents the width of the image. This is either specified in the user config, or inferred using\n                a sample of images.\n            height: Represents the height of the image. This is either specified in the user config, or inferred using\n                a sample of images.\n            preprocessing_parameters: Parameters defining how the image feature should be preprocessed\n            encoder_type: The name of the encoder\n\n        Return:\n            (width, height) Updated width and height so that they are equal\n        \"\"\"\n\n        if preprocessing_parameters[REQUIRES_EQUAL_DIMENSIONS] and height != width:\n            width = height = min(width, height)\n            # Update preprocessing parameters dictionary to reflect new height and width values\n            preprocessing_parameters[\"width\"] = width\n            preprocessing_parameters[\"height\"] = height\n            logger.info(\n                f\"Set image feature height and width to {width} to be compatible with\" f\" {encoder_type} encoder.\"\n            )\n        return width, height\n\n    @staticmethod\n    def _infer_image_size(\n        image_sample: list[torch.Tensor],\n        max_height: int,\n        max_width: int,\n        preprocessing_parameters: dict,\n        encoder_type: str,\n    ) -> tuple[int, int]:\n        \"\"\"Infers the size to use from a group of images. The returned height will be the average height of images\n        in image_sample rounded to the nearest integer, or max_height. Likewise for width.\n\n        Args:\n            image_sample: Sample of images to use to infer image size. Must be formatted as [channels, height, width].\n            max_height: Maximum height.\n            max_width: Maximum width.\n            preprocessing_parameters: Parameters defining how the image feature should be preprocessed\n            encoder_type: The name of the encoder\n\n        Return:\n            (height, width) The inferred height and width.\n        \"\"\"\n\n        height_avg = sum(x.shape[1] for x in image_sample) / len(image_sample)\n        width_avg = sum(x.shape[2] for x in image_sample) / len(image_sample)\n        height = min(int(round(height_avg)), max_height)\n        width = min(int(round(width_avg)), max_width)\n\n        # Update height and width if the downstream encoder requires images\n        # with  the same dimension or specific width and height values\n        width, height = ImageFeatureMixin._set_image_and_height_equal_for_encoder(\n            width, height, preprocessing_parameters, encoder_type\n        )\n\n        logger.debug(f\"Inferring height: {height} and width: {width}\")\n        return height, width\n\n    @staticmethod\n    def _infer_number_of_channels(image_sample: list[torch.Tensor]):\n        \"\"\"Infers the channel depth to use from a group of images.\n\n        We make the assumption that the majority of datasets scraped from the web will be RGB, so if we get a mixed bag\n        of images we should default to that. However, if the majority of the sample images have a specific channel depth\n        (other than 3) this is probably intentional so we keep it, but log an info message.\n        \"\"\"\n        n_images = len(image_sample)\n        channel_frequency = Counter([num_channels_in_image(x) for x in image_sample])\n        if channel_frequency[1] > n_images / 2:\n            # If the majority of images in sample are 1 channel, use 1.\n            num_channels = 1\n        elif channel_frequency[2] > n_images / 2:\n            # If the majority of images in sample are 2 channel, use 2.\n            num_channels = 2\n        elif channel_frequency[4] > n_images / 2:\n            # If the majority of images in sample are 4 channel, use 4.\n            num_channels = 4\n        else:\n            # Default case: use 3 channels.\n            num_channels = 3\n        logger.info(f\"Inferring num_channels from the first {n_images} images.\")\n        logger.info(\"\\n\".join([f\"  images with {k} channels: {v}\" for k, v in sorted(channel_frequency.items())]))\n        if num_channels == max(channel_frequency, key=channel_frequency.get):\n            logger.info(\n                f\"Using {num_channels} channels because it is the majority in sample. If an image with\"\n                f\" a different depth is read, will attempt to convert to {num_channels} channels.\"\n            )\n        else:\n            logger.info(f\"Defaulting to {num_channels} channels.\")\n        logger.info(\n            \"To explicitly set the number of channels, define num_channels in the preprocessing dictionary of \"\n            \"the image input feature config.\"\n        )\n        return num_channels\n\n    @staticmethod\n    def _infer_image_num_classes(\n        image_sample: list[torch.Tensor],\n        num_channels: int,\n        num_classes: int,\n    ) -> torch.Tensor:\n        \"\"\"Infers the number of channel classes from a group of images (for image segmentation). The returned\n        tensor contains the channel value for each class, where dim=0 is the class.\n\n        Args:\n            image_sample: Sample of images to use to infer image size. Must be formatted as [channels, height, width].\n            num_channels: Expected number of channels\n            num_classes: Expected number of channel classes or None\n\n        Return:\n            channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class.\n        \"\"\"\n        n_images = len(image_sample)\n        logger.info(f\"Inferring num_classes from the first {n_images} images.\")\n        channel_class_map = get_unique_channels(image_sample, num_channels, num_classes)\n\n        inferred_num_classes = channel_class_map.shape[0]\n        if num_classes:\n            if num_classes < inferred_num_classes:\n                raise ValueError(\n                    f\"Images inferred num classes {inferred_num_classes} exceeds `num_classes` {num_classes}.\"\n                )\n            elif num_classes > inferred_num_classes:\n                logger.warning(\n                    \"Images inferred num classes {} does not match `num_classes` {}. \"\n                    \"Using inferred num classes {}.\".format(inferred_num_classes, num_classes, inferred_num_classes)\n                )\n\n        return channel_class_map\n\n    @staticmethod\n    def _finalize_preprocessing_parameters(\n        preprocessing_parameters: dict,\n        encoder_type: str,\n        column: Series,\n    ) -> tuple:\n        \"\"\"Helper method to determine the height, width and number of channels for preprocessing the image data.\n\n        This is achieved by looking at the parameters provided by the user. When there are some missing parameters, we\n        fall back on to the first image in the dataset. The assumption being that all the images in the data are\n        expected be of the same size with the same number of channels.\n\n        Args:\n            preprocessing_parameters: Parameters defining how the image feature should be preprocessed\n            encoder_type: The name of the encoder\n            column: The data itself. Can be a Pandas, Modin or Dask series.\n        \"\"\"\n\n        explicit_height_width = preprocessing_parameters[HEIGHT] or preprocessing_parameters[WIDTH]\n        explicit_num_channels = NUM_CHANNELS in preprocessing_parameters and preprocessing_parameters[NUM_CHANNELS]\n\n        if preprocessing_parameters[INFER_IMAGE_DIMENSIONS] and not (explicit_height_width and explicit_num_channels):\n            sample_size = min(len(column), preprocessing_parameters[INFER_IMAGE_SAMPLE_SIZE])\n        else:\n            sample_size = 1  # Take first image\n\n        sample = []\n        sample_num_bytes = []\n        failed_entries = []\n        for image_entry in column.head(sample_size):\n            if isinstance(image_entry, bytes):\n                image = read_image_from_bytes_obj(image_entry)\n            elif isinstance(image_entry, str):\n                # Tries to read image as PNG or numpy file from the path.\n                image, num_bytes = read_image_from_path(image_entry, return_num_bytes=True)\n                if num_bytes is not None:\n                    sample_num_bytes.append(num_bytes)\n            else:\n                image = image_entry\n\n            if isinstance(image, torch.Tensor):\n                sample.append(image)\n            elif isinstance(image, np.ndarray):\n                sample.append(torch.from_numpy(image).permute(2, 0, 1))\n            else:\n                failed_entries.append(image_entry)\n        if len(sample) == 0:\n            failed_entries_repr = \"\\n\\t- \".join(failed_entries)\n            raise ValueError(\n                f\"Images dimensions cannot be inferred. Failed to read {sample_size} images as samples:\"\n                f\"\\n\\t- {failed_entries_repr}.\"\n            )\n\n        should_resize = False\n        if explicit_height_width:\n            should_resize = True\n            try:\n                height = int(preprocessing_parameters[HEIGHT])\n                width = int(preprocessing_parameters[WIDTH])\n                # Update height and width if the downstream encoder requires images\n                # with the same dimension or specific width and height values\n                width, height = ImageFeatureMixin._set_image_and_height_equal_for_encoder(\n                    width, height, preprocessing_parameters, encoder_type\n                )\n            except ValueError as e:\n                raise ValueError(\"Image height and width must be set and have \" \"positive integer values: \" + str(e))\n            if height <= 0 or width <= 0:\n                raise ValueError(\"Image height and width must be positive integers\")\n        else:\n            # User hasn't specified height and width.\n            # Default to inferring from sample or first image.\n            if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]:\n                should_resize = True\n                height, width = ImageFeatureMixin._infer_image_size(\n                    sample,\n                    max_height=preprocessing_parameters[INFER_IMAGE_MAX_HEIGHT],\n                    max_width=preprocessing_parameters[INFER_IMAGE_MAX_WIDTH],\n                    preprocessing_parameters=preprocessing_parameters,\n                    encoder_type=encoder_type,\n                )\n            else:\n                raise ValueError(\n                    \"Explicit image width/height are not set, infer_image_dimensions is false, \"\n                    \"and first image cannot be read, so image dimensions are unknown\"\n                )\n\n        if explicit_num_channels:\n            # User specified num_channels in the model/feature config\n            user_specified_num_channels = True\n            num_channels = preprocessing_parameters[NUM_CHANNELS]\n        else:\n            user_specified_num_channels = False\n            if preprocessing_parameters[INFER_IMAGE_DIMENSIONS]:\n                user_specified_num_channels = True\n                num_channels = ImageFeatureMixin._infer_number_of_channels(sample)\n            elif len(sample) > 0:\n                num_channels = num_channels_in_image(sample[0])\n            else:\n                raise ValueError(\n                    \"Explicit image num channels is not set, infer_image_dimensions is false, \"\n                    \"and first image cannot be read, so image num channels is unknown\"\n                )\n\n        assert isinstance(num_channels, int), ValueError(\"Number of image channels needs to be an integer\")\n\n        average_file_size = np.mean(sample_num_bytes) if sample_num_bytes else None\n\n        standardize_image = preprocessing_parameters[\"standardize_image\"]\n        if standardize_image == \"imagenet1k\" and num_channels != 3:\n            warnings.warn(\n                f\"'standardize_image=imagenet1k' is defined only for 'num_channels=3' but \"\n                f\"detected 'num_channels={num_channels}'.  For this situation setting 'standardize_image=None'.\",\n                RuntimeWarning,\n            )\n            standardize_image = None\n\n        if preprocessing_parameters[INFER_IMAGE_NUM_CLASSES] or preprocessing_parameters[\"num_classes\"]:\n            channel_class_map = ImageFeatureMixin._infer_image_num_classes(\n                sample, num_channels, preprocessing_parameters[\"num_classes\"]\n            )\n        else:\n            channel_class_map = torch.Tensor([])\n\n        return (\n            should_resize,\n            width,\n            height,\n            num_channels,\n            user_specified_num_channels,\n            average_file_size,\n            standardize_image,\n            channel_class_map,\n        )\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        set_default_value(feature_config[PREPROCESSING], \"in_memory\", preprocessing_parameters[\"in_memory\"])\n\n        name = feature_config[NAME]\n        column = input_df[feature_config[COLUMN]]\n        encoder_type = feature_config[ENCODER][TYPE] if ENCODER in feature_config.keys() else None\n\n        src_path = None\n        if SRC in metadata:\n            src_path = os.path.dirname(os.path.abspath(metadata.get(SRC)))\n        abs_path_column = backend.df_engine.map_objects(\n            column,\n            lambda row: get_abs_path(src_path, row) if isinstance(row, str) and not has_remote_protocol(row) else row,\n        )\n\n        # determine if specified encoder is a torchvision model\n        model_type = feature_config[ENCODER].get(\"type\", None) if ENCODER in feature_config.keys() else None\n        model_variant = feature_config[ENCODER].get(\"model_variant\") if ENCODER in feature_config.keys() else None\n        if model_variant:\n            torchvision_parameters = _get_torchvision_parameters(model_type, model_variant)\n        else:\n            torchvision_parameters = None\n\n        if torchvision_parameters:\n            logger.warning(\n                f\"Using the transforms specified for the torchvision model {model_type} {model_variant} \"\n                f\"This includes setting the number of channels is 3 and resizing the image to the needs of the model.\"\n            )\n\n            torchvision_transform, transform_metadata = _get_torchvision_transform(torchvision_parameters)\n\n            # torchvision_parameters is not None\n            # perform torchvision model transformations\n            read_image_if_bytes_obj_and_resize = partial(\n                ImageFeatureMixin._read_image_with_pretrained_transform,\n                transform_fn=torchvision_transform,\n            )\n            average_file_size = None\n\n            # save weight specification in preprocessing section\n            preprocessing_parameters[\"torchvision_model_default_weights\"] = (\n                f\"{torchvision_parameters.model_weights.DEFAULT}\"\n            )\n\n            # add torchvision model id to preprocessing section for torchscript\n            preprocessing_parameters[\"torchvision_model_type\"] = model_type\n            preprocessing_parameters[\"torchvision_model_variant\"] = model_variant\n\n            # get required setup parameters for in_memory = False processing\n            height = transform_metadata.height\n            width = transform_metadata.width\n            num_channels = transform_metadata.num_channels\n            channel_class_map = torch.Tensor([])\n        else:\n            # torchvision_parameters is None\n            # perform Ludwig specified transformations\n            (\n                should_resize,\n                width,\n                height,\n                num_channels,\n                user_specified_num_channels,\n                average_file_size,\n                standardize_image,\n                channel_class_map,\n            ) = ImageFeatureMixin._finalize_preprocessing_parameters(\n                preprocessing_parameters, encoder_type, abs_path_column\n            )\n\n            metadata[name][PREPROCESSING][\"height\"] = height\n            metadata[name][PREPROCESSING][\"width\"] = width\n            metadata[name][PREPROCESSING][\"num_channels\"] = num_channels\n            metadata[name][PREPROCESSING][\"num_classes\"] = channel_class_map.shape[0]\n            metadata[name][PREPROCESSING][\"channel_class_map\"] = channel_class_map.tolist()\n\n            read_image_if_bytes_obj_and_resize = partial(\n                ImageFeatureMixin._read_image_if_bytes_obj_and_resize,\n                img_width=width,\n                img_height=height,\n                should_resize=should_resize,\n                num_channels=num_channels,\n                resize_method=preprocessing_parameters[\"resize_method\"],\n                user_specified_num_channels=user_specified_num_channels,\n                standardize_image=standardize_image,\n                channel_class_map=channel_class_map,\n            )\n\n        # TODO: alternatively use get_average_image() for unreachable images\n        if channel_class_map.shape[0]:\n            default_image = get_gray_default_image(1, height, width).squeeze(0)\n            metadata[name][\"reshape\"] = (height, width)\n        else:\n            default_image = get_gray_default_image(num_channels, height, width)\n            metadata[name][\"reshape\"] = (num_channels, height, width)\n\n        in_memory = feature_config[PREPROCESSING][\"in_memory\"]\n        if in_memory or skip_save_processed_input:\n            proc_col = backend.read_binary_files(\n                abs_path_column, map_fn=read_image_if_bytes_obj_and_resize, file_size=average_file_size\n            )\n\n            num_failed_image_reads = (\n                proc_col.isna().sum().compute() if is_dask_series_or_df(proc_col, backend) else proc_col.isna().sum()\n            )\n\n            proc_col = backend.df_engine.map_objects(\n                proc_col, lambda row: default_image if not isinstance(row, np.ndarray) else row\n            )\n\n            proc_df[feature_config[PROC_COLUMN]] = proc_col\n        else:\n            num_images = len(abs_path_column)\n            num_failed_image_reads = 0\n\n            data_fp = backend.cache.get_cache_path(wrap(metadata.get(SRC)), metadata.get(CHECKSUM), TRAINING)\n            with upload_h5(data_fp) as h5_file:\n                # todo future add multiprocessing/multithreading\n                image_dataset = h5_file.create_dataset(\n                    feature_config[PROC_COLUMN] + \"_data\", (num_images, num_channels, height, width), dtype=np.float32\n                )\n                for i, img_entry in enumerate(abs_path_column):\n                    res = read_image_if_bytes_obj_and_resize(img_entry)\n                    if isinstance(res, np.ndarray):\n                        image_dataset[i, :height, :width, :] = res\n                    else:\n                        logger.warning(f\"Failed to read image {img_entry} while preprocessing feature `{name}`. \")\n                        image_dataset[i, :height, :width, :] = default_image\n                        num_failed_image_reads += 1\n                h5_file.flush()\n\n            proc_df[feature_config[PROC_COLUMN]] = np.arange(num_images)\n\n        if num_failed_image_reads > 0:\n            logger.warning(\n                f\"Failed to read {num_failed_image_reads} images while preprocessing feature `{name}`. \"\n                \"Using default image for these rows in the dataset.\"\n            )\n\n        return proc_df\n\n\nclass ImageInputFeature(ImageFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: ImageInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n        # set up for augmentation if it is enabled\n        if input_feature_config.augmentation:\n            # assume no image normalize is required\n            normalize_mean = normalize_std = None\n\n            # determine if specified encoder is a torchvision model\n            if is_torchvision_encoder(self.encoder_obj):\n                # encoder is a torchvision model\n                normalize_mean = self.encoder_obj.normalize_mean\n                normalize_std = self.encoder_obj.normalize_std\n            else:\n                # encoder is a Ludwig encoder, determine if standardize_image is set to IMAGENET1K\n                if input_feature_config.preprocessing.standardize_image == IMAGENET1K:\n                    normalize_mean = IMAGENET1K_MEAN\n                    normalize_std = IMAGENET1K_STD\n\n            # create augmentation pipeline object\n            self.augmentation_pipeline = ImageAugmentation(\n                input_feature_config.augmentation,\n                normalize_mean,\n                normalize_std,\n            )\n\n    def forward(self, inputs: torch.Tensor) -> torch.Tensor:\n        assert isinstance(inputs, torch.Tensor), f\"inputs to image feature must be a torch tensor, got {type(inputs)}\"\n        assert inputs.dtype in [torch.float32], f\"inputs to image feature must be a float32 tensor, got {inputs.dtype}\"\n\n        inputs_encoded = self.encoder_obj(inputs)\n\n        return inputs_encoded\n\n    @property\n    def input_dtype(self):\n        return torch.float32\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self.encoder_obj.input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    def update_config_after_module_init(self, feature_config):\n        if is_torchvision_encoder(self.encoder_obj):\n            # update feature preprocessing parameters to reflect used in torchvision pretrained model\n            # Note: image height and width is determined by the encoder crop_size attribute.  Source of this\n            # attribute is from the torchvision.transforms._presets.ImageClassification class.  This class stores\n            # crop_size as a single element list.  the single element in this list is used to set both the height\n            # and width of an image.\n            feature_config.preprocessing.height = self.encoder_obj.crop_size[0]\n            feature_config.preprocessing.width = self.encoder_obj.crop_size[0]\n            feature_config.preprocessing.num_channels = self.encoder_obj.num_channels\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        for key in [\"height\", \"width\", \"num_channels\", \"standardize_image\"]:\n            if hasattr(feature_config.encoder, key):\n                setattr(feature_config.encoder, key, feature_metadata[PREPROCESSING][key])\n\n    @staticmethod\n    def get_schema_cls():\n        return ImageInputFeatureConfig\n\n    @staticmethod\n    def create_preproc_module(metadata: dict[str, Any]) -> torch.nn.Module:\n        model_type = metadata[\"preprocessing\"].get(\"torchvision_model_type\")\n        model_variant = metadata[\"preprocessing\"].get(\"torchvision_model_variant\")\n        if model_variant:\n            torchvision_parameters = _get_torchvision_parameters(model_type, model_variant)\n        else:\n            torchvision_parameters = None\n\n        if torchvision_parameters:\n            torchvision_transform, transform_metadata = _get_torchvision_transform(torchvision_parameters)\n        else:\n            torchvision_transform = None\n            transform_metadata = None\n\n        return _ImagePreprocessing(\n            metadata, torchvision_transform=torchvision_transform, transform_metadata=transform_metadata\n        )\n\n    def get_augmentation_pipeline(self):\n        return self.augmentation_pipeline\n\n\nclass ImageOutputFeature(ImageFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: ImageOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        super().__init__(output_feature_config, output_features, **kwargs)\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs: dict[str, torch.Tensor], target=None, **kwargs):\n        return self.decoder_obj(inputs, target=target)\n\n    def metric_kwargs(self):\n        return dict(num_outputs=self.output_shape[0])\n\n    def create_predict_module(self) -> PredictModule:\n        return _ImagePredict()\n\n    def get_prediction_set(self):\n        return self.decoder_obj.get_prediction_set()\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.float32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.decoder_obj.output_shape\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.decoder_obj.input_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        for key in [\"height\", \"width\", \"num_channels\", \"num_classes\", \"standardize_image\"]:\n            if hasattr(feature_config.decoder, key):\n                setattr(feature_config.decoder, key, feature_metadata[PREPROCESSING][key])\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, metadata):\n        # no overall stats, just return empty dictionary\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n\n        if predictions_col in result:\n            channel_class_map = torch.ByteTensor(metadata[PREPROCESSING][\"channel_class_map\"])\n\n            if channel_class_map.shape[0]:\n\n                def class_mask2img(row):\n                    pred = row[predictions_col]\n                    return get_image_from_class_mask(channel_class_map, pred)\n\n                result[predictions_col] = result.apply(class_mask2img, axis=1)\n\n            return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _ImagePostprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return ImageOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/number_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport logging\nfrom abc import ABC, abstractmethod\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch import nn\n\nfrom ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, NUMBER, PREDICTIONS, PROC_COLUMN\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.schema.features.number_feature import NumberInputFeatureConfig, NumberOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass NumberTransformer(nn.Module, ABC):\n    @abstractmethod\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        pass\n\n    @abstractmethod\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        pass\n\n    @abstractmethod\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        pass\n\n    @abstractmethod\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        pass\n\n    @staticmethod\n    @abstractmethod\n    def fit_transform_params(column: np.ndarray, backend: Any) -> dict[str, Any]:\n        pass\n\n\nclass ZScoreTransformer(NumberTransformer):\n    def __init__(self, mean: float = None, std: float = None, **kwargs: dict):\n        super().__init__()\n        self.mu = float(mean) if mean is not None else mean\n        self.sigma = float(std) if std is not None else std\n        self.feature_name = kwargs.get(NAME, \"\")\n        if self.sigma == 0:\n            raise RuntimeError(\n                f\"Cannot apply zscore normalization to `{self.feature_name}` since it has a standard deviation of 0. \"\n                f\"This is most likely because `{self.feature_name}` has a constant value of {self.mu} for all rows in \"\n                \"the dataset. Consider removing this feature from your Ludwig config since it is not useful for \"\n                \"your machine learning model.\"\n            )\n\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        return (x - self.mu) / self.sigma\n\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        return x * self.sigma + self.mu\n\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return (x - self.mu) / self.sigma\n\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return x * self.sigma + self.mu\n\n    @staticmethod\n    def fit_transform_params(column: np.ndarray, backend: \"Backend\") -> dict[str, Any]:  # noqa\n        compute = backend.df_engine.compute\n        return {\n            \"mean\": compute(column.astype(np.float32).mean()),\n            \"std\": compute(column.astype(np.float32).std()),\n        }\n\n\nclass MinMaxTransformer(NumberTransformer):\n    def __init__(self, min: float = None, max: float = None, **kwargs: dict):\n        super().__init__()\n        self.min_value = float(min) if min is not None else min\n        self.max_value = float(max) if max is not None else max\n        if self.min_value is None or self.max_value is None:\n            self.range = None\n        else:\n            self.range = self.max_value - self.min_value\n\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        return (x - self.min_value) / self.range\n\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        if self.range is None:\n            raise ValueError(\"Numeric transformer needs to be instantiated with \" \"min and max values.\")\n        return x * self.range + self.min_value\n\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return (x - self.min_value) / self.range\n\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        if self.range is None:\n            raise ValueError(\"Numeric transformer needs to be instantiated with \" \"min and max values.\")\n        return x * self.range + self.min_value\n\n    @staticmethod\n    def fit_transform_params(column: np.ndarray, backend: \"Backend\") -> dict[str, Any]:  # noqa\n        compute = backend.df_engine.compute\n        return {\n            \"min\": compute(column.astype(np.float32).min()),\n            \"max\": compute(column.astype(np.float32).max()),\n        }\n\n\nclass InterQuartileTransformer(NumberTransformer):\n    def __init__(self, q1: float = None, q2: float = None, q3: float = None, **kwargs: dict):\n        super().__init__()\n        self.q1 = float(q1) if q1 is not None else q1\n        self.q2 = float(q2) if q2 is not None else q2\n        self.q3 = float(q3) if q3 is not None else q3\n        if self.q1 is None or self.q3 is None:\n            self.interquartile_range = None\n        else:\n            self.interquartile_range = self.q3 - self.q1\n        self.feature_name = kwargs.get(NAME, \"\")\n        if self.interquartile_range == 0:\n            raise RuntimeError(\n                f\"Cannot apply InterQuartileNormalization to `{self.feature_name}` since\"\n                \"the interquartile range is 0, which will result in a ZeroDivisionError.\"\n            )\n\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        return (x - self.q2) / self.interquartile_range\n\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        return x * self.interquartile_range + self.q2\n\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return (x - self.q2) / self.interquartile_range\n\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return x * self.interquartile_range + self.q2\n\n    @staticmethod\n    def fit_transform_params(column: np.ndarray, backend: \"Backend\") -> dict[str, Any]:  # noqa\n        # backend.df_engine.compute is not used here because `percentile` is not parallelized in dask.\n        # We compute the percentile directly.\n        return {\n            \"q1\": np.percentile(column.astype(np.float32), 25),\n            \"q2\": np.percentile(column.astype(np.float32), 50),\n            \"q3\": np.percentile(column.astype(np.float32), 75),\n        }\n\n\nclass Log1pTransformer(NumberTransformer):\n    def __init__(self, **kwargs: dict):\n        super().__init__()\n        self.feature_name = kwargs.get(NAME, \"\")\n\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        if np.any(x <= 0):\n            raise ValueError(\n                f\"One or more values in the `{self.feature_name}` feature are non-positive.  \"\n                \"log1p normalization is defined only for positive values.\"\n            )\n        return np.log1p(x)\n\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        return np.expm1(x)\n\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return torch.log1p(x)\n\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return torch.expm1(x)\n\n    @staticmethod\n    def fit_transform_params(column: np.ndarray, backend: \"Backend\") -> dict[str, Any]:  # noqa\n        return {}\n\n\nclass IdentityTransformer(NumberTransformer):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def transform(self, x: np.ndarray) -> np.ndarray:\n        return x\n\n    def inverse_transform(self, x: np.ndarray) -> np.ndarray:\n        return x\n\n    def transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return x\n\n    def inverse_transform_inference(self, x: torch.Tensor) -> torch.Tensor:\n        return x\n\n    @staticmethod\n    def fit_transform_params(column: np.ndarray, backend: \"Backend\") -> dict[str, Any]:  # noqa\n        return {}\n\n\nnumeric_transformation_registry = {\n    \"minmax\": MinMaxTransformer,\n    \"zscore\": ZScoreTransformer,\n    \"log1p\": Log1pTransformer,\n    \"iq\": InterQuartileTransformer,\n    None: IdentityTransformer,\n}\n\n\ndef get_transformer(metadata, preprocessing_parameters) -> NumberTransformer:\n    return get_from_registry(\n        preprocessing_parameters.get(\"normalization\", None),\n        numeric_transformation_registry,\n    )(**metadata)\n\n\nclass _OutlierReplacer(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.zscore_transformer = ZScoreTransformer(**metadata)\n        self.outlier_threshold = metadata[\"preprocessing\"].get(\"outlier_threshold\")\n        self.computed_outlier_fill_value = float(metadata[\"preprocessing\"][\"computed_outlier_fill_value\"])\n\n    def forward(self, v: torch.Tensor) -> torch.Tensor:\n        outliers = self.zscore_transformer.transform_inference(v).abs().gt(self.outlier_threshold)\n        v_masked = torch.masked_fill(v, outliers, torch.nan)\n\n        v = torch.nan_to_num(v_masked, nan=self.computed_outlier_fill_value)\n        return v.to(dtype=torch.float32)\n\n\nclass _NumberPreprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.computed_fill_value = float(metadata[\"preprocessing\"][\"computed_fill_value\"])\n        self.numeric_transformer = get_transformer(metadata, metadata[\"preprocessing\"])\n\n        # Optional outlier replacement\n        self.outlier_replacer = None\n        if metadata[\"preprocessing\"].get(\"outlier_strategy\") is not None:\n            self.outlier_replacer = _OutlierReplacer(metadata)\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if not torch.jit.isinstance(v, torch.Tensor):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        v = torch.nan_to_num(v, nan=self.computed_fill_value)\n        v = v.to(dtype=torch.float32)\n\n        # Handle outliers if needed\n        if self.outlier_replacer is not None:\n            v = self.outlier_replacer(v)\n\n        return self.numeric_transformer.transform_inference(v)\n\n\nclass _NumberPostprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.numeric_transformer = get_transformer(metadata, metadata[\"preprocessing\"])\n        self.predictions_key = PREDICTIONS\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n\n        return {self.predictions_key: self.numeric_transformer.inverse_transform_inference(predictions)}\n\n\nclass _NumberPredict(PredictModule):\n    def __init__(self, clip):\n        super().__init__()\n        self.clip = clip\n\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n        predictions = logits\n\n        if self.clip is not None:\n            predictions = torch.clamp(logits, self.clip[0], self.clip[1])\n            logger.debug(f\"  clipped_predictions: {predictions}\")\n\n        return {self.predictions_key: predictions, self.logits_key: logits}\n\n\nclass NumberFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return NUMBER\n\n    @staticmethod\n    def cast_column(column, backend):\n        return backend.df_engine.df_lib.to_numeric(column, errors=\"coerce\").astype(np.float32)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        numeric_transformer: NumberTransformer = get_from_registry(\n            preprocessing_parameters.get(\"normalization\", None),\n            numeric_transformation_registry,\n        )\n\n        params = numeric_transformer.fit_transform_params(column, backend)\n\n        # Ensure mean and std are computed if we're removing outliers\n        outlier_strategy = preprocessing_parameters.get(\"outlier_strategy\")\n        if outlier_strategy is not None and (\"mean\" not in params or \"std\" not in params):\n            params.update(ZScoreTransformer.fit_transform_params(column, backend))\n\n        return params\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        # Had to replace normalize() function due to issue #1911\n        # this comment is to provide context for the change.\n        # original code\n        # def normalize(series: pd.Series) -> pd.Series:\n        #     series = series.copy()\n        #     numeric_transformer = get_transformer(metadata[feature_config[NAME]], preprocessing_parameters)\n        #     series.update(numeric_transformer.transform(series.values))\n        #     return series\n\n        def normalize(series: pd.Series) -> pd.Series:\n            _feature_metadata = copy.deepcopy(metadata[feature_config[NAME]])\n            _feature_metadata.update({NAME: feature_config[NAME]})\n\n            # retrieve request numeric transformer\n            numeric_transformer = get_transformer(_feature_metadata, preprocessing_parameters)\n\n            # transform input numeric values with specified transformer\n            transformed_values = numeric_transformer.transform(series.values)\n\n            # return transformed values with same index values as original series.\n            return pd.Series(transformed_values, index=series.index)\n\n        input_series = input_df[feature_config[COLUMN]].astype(np.float32)\n        proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_partitions(\n            input_series, normalize, meta=input_series\n        )\n\n        return proc_df\n\n\nclass NumberInputFeature(NumberFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: NumberInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n        input_feature_config.encoder.input_size = self.input_shape[-1]\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype == torch.float32 or inputs.dtype == torch.float64\n        assert len(inputs.shape) == 1 or (len(inputs.shape) == 2 and inputs.shape[1] == 1)\n\n        if len(inputs.shape) == 1:\n            inputs = inputs[:, None]\n        inputs_encoded = self.encoder_obj(inputs)\n\n        return inputs_encoded\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self.encoder_obj.output_shape)\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    def get_schema_cls():\n        return NumberInputFeatureConfig\n\n    def create_sample_input(self, batch_size: int = 2):\n        return torch.rand([batch_size])\n\n    @classmethod\n    def get_preproc_input_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"float32\"\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _NumberPreprocessing(metadata)\n\n\nclass NumberOutputFeature(NumberFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: NumberOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.clip = output_feature_config.clip\n        super().__init__(output_feature_config, output_features, **kwargs)\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):  # hidden\n        hidden = inputs[HIDDEN]\n        return self.decoder_obj(hidden)\n\n    def create_predict_module(self) -> PredictModule:\n        if getattr(self, \"clip\", None) and not (isinstance(self.clip, (list, tuple)) and len(self.clip) == 2):\n            raise ValueError(\n                f\"The clip parameter of {self.feature_name} is {self.clip}. \"\n                f\"It must be a list or a tuple of length 2.\"\n            )\n        return _NumberPredict(getattr(self, \"clip\", None))\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, LOGITS}\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.decoder_obj.config.input_size])\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.float32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        pass\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, metadata):\n        # no overall stats, just return empty dictionary\n        return {}\n\n    def postprocess_predictions(\n        self,\n        predictions,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in predictions:\n            # as needed convert predictions make to original value space\n            numeric_transformer = get_from_registry(\n                metadata[\"preprocessing\"].get(\"normalization\", None),\n                numeric_transformation_registry,\n            )(**metadata)\n            predictions[predictions_col] = predictions[predictions_col].map(\n                lambda pred: numeric_transformer.inverse_transform(pred)\n            )\n\n        return predictions\n\n    @staticmethod\n    def get_schema_cls():\n        return NumberOutputFeatureConfig\n\n    @classmethod\n    def get_postproc_output_dtype(cls, metadata: TrainingSetMetadataDict) -> str:\n        return \"float32\"\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _NumberPostprocessing(metadata)\n"
  },
  {
    "path": "ludwig/features/sequence_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport logging\nfrom functools import partial\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import (\n    COLUMN,\n    LAST_PREDICTIONS,\n    LENGTHS,\n    NAME,\n    PREDICTIONS,\n    PROBABILITIES,\n    PROBABILITY,\n    PROC_COLUMN,\n    SEQUENCE,\n)\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.features.feature_utils import compute_sequence_probability, compute_token_probabilities\nfrom ludwig.schema.features.sequence_feature import SequenceInputFeatureConfig, SequenceOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.math_utils import softmax\nfrom ludwig.utils.strings_utils import (\n    build_sequence_matrix,\n    create_vocabulary,\n    SpecialSymbol,\n    START_SYMBOL,\n    STOP_SYMBOL,\n    UNKNOWN_SYMBOL,\n)\nfrom ludwig.utils.tokenizers import get_tokenizer_from_registry\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass _SequencePreprocessing(torch.nn.Module):\n    \"\"\"Torchscript-enabled version of preprocessing done by SequenceFeatureMixin.add_feature_data.\"\"\"\n\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.lowercase = metadata[\"preprocessing\"][\"lowercase\"]\n        self.tokenizer_type = metadata[\"preprocessing\"][\"tokenizer\"]\n        self.tokenizer = get_tokenizer_from_registry(self.tokenizer_type)(\n            pretrained_model_name_or_path=metadata[\"preprocessing\"].get(\"pretrained_model_name_or_path\", None)\n        )\n\n        if not isinstance(self.tokenizer, torch.nn.Module):\n            raise ValueError(f\"tokenizer must be a torch.nn.Module, got {self.tokenizer}\")\n\n        self.padding_symbol = metadata[\"preprocessing\"][\"padding_symbol\"]\n        self.unknown_symbol = metadata[\"preprocessing\"][\"unknown_symbol\"]\n        self.start_symbol = START_SYMBOL\n        self.stop_symbol = STOP_SYMBOL\n        self.max_sequence_length = int(metadata[\"max_sequence_length\"])\n        self.unit_to_id = metadata[\"str2idx\"]\n        self.computed_fill_value = metadata[\"preprocessing\"][\"computed_fill_value\"]\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        \"\"\"Takes a list of strings and returns a tensor of token ids.\"\"\"\n        if not torch.jit.isinstance(v, list[str]):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        futures: list[torch.jit.Future[torch.Tensor]] = []\n        for sequence in v:\n            futures.append(\n                torch.jit.fork(\n                    self._process_sequence,\n                    sequence,\n                )\n            )\n\n        sequence_matrix = []\n        for future in futures:\n            sequence_matrix.append(torch.jit.wait(future))\n\n        return torch.stack(sequence_matrix)\n\n    def _process_sequence(self, sequence: str) -> torch.Tensor:\n        sequence = self.computed_fill_value if sequence == \"nan\" else sequence\n\n        # If tokenizer is HF, we defer lowercase transformation to the tokenizer.\n        if self.lowercase and self.tokenizer_type != \"hf_tokenizer\":\n            sequence_str: str = sequence.lower()\n        else:\n            sequence_str: str = sequence\n\n        sequence_vector = torch.full([self.max_sequence_length], self.unit_to_id[self.padding_symbol])\n\n        if self.tokenizer_type == \"hf_tokenizer\":\n            # Handles start, stop, and unknown symbols implicitly\n            unit_sequence = self.tokenizer(sequence)\n            assert torch.jit.isinstance(unit_sequence, list[int])\n            # Ensures that the sequence lengths are aligned between the input and output tensors.\n            sequence_length = min(len(unit_sequence), self.max_sequence_length)\n            sequence_vector[:sequence_length] = torch.tensor(unit_sequence)[:sequence_length]\n            return sequence_vector\n\n        # If tokenizer is not HF, we manually convert tokens to IDs and insert start, stop, and unknown symbols.\n        unit_sequence = self.tokenizer(sequence_str)\n        assert torch.jit.isinstance(unit_sequence, list[str])\n\n        sequence_vector[0] = self.unit_to_id[self.start_symbol]\n        if len(unit_sequence) + 1 < self.max_sequence_length:\n            sequence_length = len(unit_sequence)\n            sequence_vector[len(unit_sequence) + 1] = self.unit_to_id[self.stop_symbol]\n        else:\n            sequence_length = self.max_sequence_length - 1\n\n        for i in range(sequence_length):\n            curr_unit = unit_sequence[i]\n            if curr_unit in self.unit_to_id:\n                curr_id = self.unit_to_id[curr_unit]\n            else:\n                curr_id = self.unit_to_id[self.unknown_symbol]\n            sequence_vector[i + 1] = curr_id\n        return sequence_vector\n\n\nclass _SequencePostprocessing(torch.nn.Module):\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.max_sequence_length = int(metadata[\"max_sequence_length\"])\n        self.idx2str = metadata[\"idx2str\"]\n        self.unknown_symbol = UNKNOWN_SYMBOL\n        self.predictions_key = PREDICTIONS\n        self.probabilities_key = PROBABILITIES\n        self.probability_key = PROBABILITY\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        pred_predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        pred_probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key)\n\n        predictions: list[list[str]] = []\n        for sequence in pred_predictions:\n            sequence_predictions: list[str] = []\n            for i in range(self.max_sequence_length):\n                unit_id = int(sequence[i].item())\n                if unit_id < len(self.idx2str):\n                    unit_prediction = self.idx2str[unit_id]\n                else:\n                    unit_prediction = self.unknown_symbol\n                sequence_predictions.append(unit_prediction)\n            predictions.append(sequence_predictions)\n\n        probabilities, _ = torch.max(pred_probabilities, dim=-1)\n        probability = torch.sum(torch.log(probabilities.clamp(min=1e-10)), dim=-1)\n\n        return {\n            self.predictions_key: predictions,\n            self.probabilities_key: probabilities,\n            self.probability_key: probability,\n        }\n\n\nclass _SequencePredict(PredictModule):\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n        probabilities = torch.softmax(logits, -1)\n        predictions = torch.argmax(logits, -1)\n\n        # predictions: [batch_size, sequence_length]\n        # probabilities: [batch_size, sequence_length, vocab_size]\n        # logits: [batch_size, sequence_length, vocab_size]\n        return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits}\n\n\nclass SequenceFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return SEQUENCE\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column.astype(str)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        vocabulary = create_vocabulary(\n            column,\n            preprocessing_parameters[\"tokenizer\"],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            num_most_frequent=preprocessing_parameters[\"most_common\"],\n            vocab_file=preprocessing_parameters[\"vocab_file\"],\n            unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n            padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n            ngram_size=preprocessing_parameters[\"ngram_size\"],\n            processor=backend.df_engine,\n        )\n        logger.info(\n            f\"Max length of feature '{column.name}': {vocabulary.max_sequence_length} (without start and stop symbols)\"\n        )\n\n        # Use sequence_length if provided, otherwise use max length found in dataset.\n        if preprocessing_parameters[\"sequence_length\"] is not None:\n            logger.info(\n                f\"Setting max length to sequence_length={preprocessing_parameters['sequence_length']} provided in \"\n                f\"preprocessing parameters\"\n            )\n            max_sequence_length = preprocessing_parameters[\"sequence_length\"]\n        else:\n            max_sequence_length = vocabulary.max_sequence_length\n            logger.info(f\"Setting max length using dataset: {max_sequence_length} (including start and stop symbols)\")\n\n            # If max_sequence_length is None, then use the max length found in the dataset.\n            if (\n                preprocessing_parameters[\"max_sequence_length\"] is not None\n                and preprocessing_parameters[\"max_sequence_length\"] < max_sequence_length\n            ):\n                logger.info(\n                    f\"Truncating max length with max_sequence_length={preprocessing_parameters['max_sequence_length']} \"\n                    f\"from preprocessing parameters\"\n                )\n                max_sequence_length = preprocessing_parameters[\"max_sequence_length\"]\n\n        logger.info(f\"Max sequence length is {max_sequence_length} for feature '{column.name}'\")\n        return {\n            \"idx2str\": vocabulary.vocab,\n            \"str2idx\": vocabulary.str2idx,\n            \"str2freq\": vocabulary.str2freq,\n            \"vocab_size\": len(vocabulary.vocab),\n            \"max_sequence_length\": max_sequence_length,\n        }\n\n    @staticmethod\n    def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend):\n        sequence_data = build_sequence_matrix(\n            sequences=column,\n            inverse_vocabulary=metadata[\"str2idx\"],\n            tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n            length_limit=metadata[\"max_sequence_length\"],\n            padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n            padding=preprocessing_parameters[\"padding\"],\n            unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            tokenizer_vocab_file=preprocessing_parameters[\"vocab_file\"],\n            processor=backend.df_engine,\n        )\n        return sequence_data\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        sequence_data = SequenceInputFeature.feature_data(\n            input_df[feature_config[COLUMN]],\n            metadata[feature_config[NAME]],\n            preprocessing_parameters,\n            backend,\n        )\n        proc_df[feature_config[PROC_COLUMN]] = sequence_data\n        return proc_df\n\n\nclass SequenceInputFeature(SequenceFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: SequenceInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs: torch.Tensor, mask=None):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.int8, inputs.dtype, torch.int16, torch.int32, torch.int64]\n        assert len(inputs.shape) == 2\n        inputs_exp = inputs.type(torch.int32)\n        inputs_mask = torch.not_equal(inputs, SpecialSymbol.PADDING.value)\n        lengths = torch.sum(inputs_mask.type(torch.int32), dim=1)\n        encoder_output = self.encoder_obj(inputs_exp, mask=inputs_mask)\n        encoder_output[LENGTHS] = lengths\n        return encoder_output\n\n    @property\n    def input_dtype(self):\n        return torch.int32\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.vocab = feature_metadata[\"idx2str\"]\n        feature_config.encoder.vocab_size = len(feature_metadata[\"idx2str\"])\n        feature_config.encoder.max_sequence_length = feature_metadata[\"max_sequence_length\"]\n\n    @staticmethod\n    def get_schema_cls():\n        return SequenceInputFeatureConfig\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.encoder_obj.config.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SequencePreprocessing(metadata)\n\n\nclass SequenceOutputFeature(SequenceFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: SequenceOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        super().__init__(output_feature_config, output_features, **kwargs)\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs: dict[str, torch.Tensor], target=None):\n        return self.decoder_obj(inputs, target=target)\n\n    def create_predict_module(self) -> PredictModule:\n        return _SequencePredict()\n\n    def get_prediction_set(self):\n        return self.decoder_obj.get_prediction_set()\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.int32\n\n    @property\n    def input_shape(self) -> torch.Size:\n        # Dummy implementation.\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.decoder_obj.config.max_sequence_length])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.decoder.vocab_size = feature_metadata[\"vocab_size\"]\n        feature_config.decoder.max_sequence_length = feature_metadata[\"max_sequence_length\"]\n        if isinstance(feature_config.loss.class_weights, (list, tuple)):\n            if len(feature_config.loss.class_weights) != feature_config.decoder.vocab_size:\n                raise ValueError(\n                    f\"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with \"\n                    f\"the number of classes ({feature_config.decoder.vocab_size}) for feature {feature_config.column}. \"\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and their order and consider there needs to be a weight \"\n                    \"for the <UNK> and <PAD> class too.\"\n                )\n\n        if isinstance(feature_config.loss.class_weights, dict):\n            if feature_metadata[\"str2idx\"].keys() != feature_config.loss.class_weights.keys():\n                raise ValueError(\n                    f\"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with \"\n                    f'the classes ({feature_metadata[\"str2idx\"].keys()}) of feature {feature_config.column}. '\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and consider there needs to be a weight \"\n                    \"for the <UNK> class too.\"\n                )\n            else:\n                class_weights = feature_config.loss.class_weights\n                idx2str = feature_metadata[\"idx2str\"]\n                class_weights_list = [class_weights[s] for s in idx2str]\n                feature_config.loss.class_weights = class_weights_list\n\n        if feature_config.loss.class_similarities_temperature > 0:\n            if feature_config.loss.class_similarities is not None:\n                similarities = feature_config.loss.class_similarities\n                temperature = feature_config.loss.class_similarities_temperature\n\n                curr_row = 0\n                first_row_length = 0\n                is_first_row = True\n                for row in similarities:\n                    if is_first_row:\n                        first_row_length = len(row)\n                        is_first_row = False\n                        curr_row += 1\n                    else:\n                        curr_row_length = len(row)\n                        if curr_row_length != first_row_length:\n                            raise ValueError(\n                                \"The length of row {} of the class_similarities \"\n                                \"of {} is {}, different from the length of \"\n                                \"the first row {}. All rows must have \"\n                                \"the same length.\".format(\n                                    curr_row, feature_config.column, curr_row_length, first_row_length\n                                )\n                            )\n                        else:\n                            curr_row += 1\n                all_rows_length = first_row_length\n\n                if all_rows_length != len(similarities):\n                    raise ValueError(\n                        f\"The class_similarities matrix of {feature_config.column} has \"\n                        f\"{len(similarities)} rows and {all_rows_length} columns, \"\n                        \"their number must be identical.\"\n                    )\n\n                if all_rows_length != feature_config.decoder.vocab_size:\n                    raise ValueError(\n                        f\"The size of the class_similarities matrix of {feature_config.column} is \"\n                        f\"{all_rows_length}, different from the number of classes \"\n                        f\"({feature_config.decoder.vocab_size}). Check the metadata JSON file to see the classes \"\n                        \"and their order and \"\n                        \"consider <UNK> and <PAD> class too.\"\n                    )\n\n                similarities = np.array(similarities, dtype=np.float32)\n                for i in range(len(similarities)):\n                    similarities[i, :] = softmax(similarities[i, :], temperature=temperature)\n                feature_config.loss.class_similarities = similarities\n            else:\n                raise ValueError(\n                    \"class_similarities_temperature > 0, \"\n                    \"but no class_similarities are provided \"\n                    f\"for feature {feature_config.column}\"\n                )\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        # TODO(Justin): Add a confusion matrix, see\n        # https://github.com/ludwig-ai/ludwig/blob/tf-legacy/ludwig/features/sequence_feature.py#L411\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        lengths_col = f\"{self.feature_name}_{LENGTHS}\"\n        if predictions_col in result:\n            if \"idx2str\" in metadata:\n\n                def idx2str(row):\n                    pred = row[predictions_col]\n                    length = metadata[\"max_sequence_length\"]\n                    return [\n                        metadata[\"idx2str\"][token] if token < len(metadata[\"idx2str\"]) else UNKNOWN_SYMBOL\n                        for token in [pred[i] for i in range(length)]\n                    ]\n\n                result[predictions_col] = result.apply(idx2str, axis=1)\n\n        last_preds_col = f\"{self.feature_name}_{LAST_PREDICTIONS}\"\n        if last_preds_col in result:\n            if \"idx2str\" in metadata:\n\n                def last_idx2str(last_pred):\n                    if last_pred < len(metadata[\"idx2str\"]):\n                        return metadata[\"idx2str\"][last_pred]\n                    return UNKNOWN_SYMBOL\n\n                result[last_preds_col] = result[last_preds_col].map(last_idx2str)\n\n        probs_col = f\"{self.feature_name}_{PROBABILITIES}\"\n        prob_col = f\"{self.feature_name}_{PROBABILITY}\"\n        if probs_col in result:\n            # currently does not return full probabilties because usually it is huge:\n            # dataset x length x classes\n            # TODO: add a mechanism for letting the user decide to save it\n            result[probs_col] = result[probs_col].map(compute_token_probabilities)\n            result[prob_col] = result[probs_col].map(\n                partial(\n                    compute_sequence_probability,\n                    max_sequence_length=metadata[\"max_sequence_length\"],\n                    return_log_prob=True,\n                )\n            )\n\n        if lengths_col in result:\n            del result[lengths_col]\n\n        return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SequencePostprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return SequenceOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/set_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import Any\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROBABILITIES, PROC_COLUMN, SET\nfrom ludwig.features.base_feature import BaseFeatureMixin, InputFeature, OutputFeature, PredictModule\nfrom ludwig.features.feature_utils import set_str_to_idx\nfrom ludwig.schema.features.set_feature import SetInputFeatureConfig, SetOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.strings_utils import create_vocabulary, UNKNOWN_SYMBOL\nfrom ludwig.utils.tokenizers import get_tokenizer_from_registry, TORCHSCRIPT_COMPATIBLE_TOKENIZERS\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass _SetPreprocessing(torch.nn.Module):\n    \"\"\"Torchscript-enabled version of preprocessing done by SetFeatureMixin.add_feature_data.\n\n    If is_bag is true, forward returns a vector for each sample indicating counts of each token. Else, forward returns a\n    multi-hot vector for each sample indicating presence of each token.\n    \"\"\"\n\n    def __init__(self, metadata: TrainingSetMetadataDict, is_bag: bool = False):\n        super().__init__()\n        if metadata[\"preprocessing\"][\"tokenizer\"] not in TORCHSCRIPT_COMPATIBLE_TOKENIZERS:\n            raise ValueError(\n                f\"{metadata['preprocessing']['tokenizer']} is not supported by torchscript. Please use \"\n                f\"one of {TORCHSCRIPT_COMPATIBLE_TOKENIZERS}.\"\n            )\n\n        self.lowercase = metadata[\"preprocessing\"][\"lowercase\"]\n        self.tokenizer = get_tokenizer_from_registry(metadata[\"preprocessing\"][\"tokenizer\"])()\n        self.vocab_size = metadata[\"vocab_size\"]\n        self.unknown_symbol = UNKNOWN_SYMBOL\n        self.unit_to_id = metadata[\"str2idx\"]\n        self.is_bag = is_bag\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        \"\"\"Takes a list of strings and returns a tensor of counts for each token.\"\"\"\n        if not torch.jit.isinstance(v, list[str]):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        if self.lowercase:\n            sequences = [sequence.lower() for sequence in v]\n        else:\n            sequences = v\n\n        unit_sequences = self.tokenizer(sequences)\n        # refines type of unit_sequences from Any to List[List[str]]\n        assert torch.jit.isinstance(unit_sequences, list[list[str]]), \"unit_sequences is not a list of lists.\"\n\n        set_matrix = torch.zeros(len(unit_sequences), self.vocab_size, dtype=torch.float32)\n        for sample_idx, unit_sequence in enumerate(unit_sequences):\n            sequence_length = len(unit_sequence)\n            for i in range(sequence_length):\n                curr_unit = unit_sequence[i]\n                if curr_unit in self.unit_to_id:\n                    curr_id = self.unit_to_id[curr_unit]\n                else:\n                    curr_id = self.unit_to_id[self.unknown_symbol]\n\n                if self.is_bag:\n                    set_matrix[sample_idx][curr_id] += 1\n                else:\n                    set_matrix[sample_idx][curr_id] = 1\n\n        return set_matrix\n\n\nclass _SetPostprocessing(torch.nn.Module):\n    \"\"\"Torchscript-enabled version of postprocessing done by SetFeatureMixin.add_feature_data.\"\"\"\n\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.idx2str = {i: v for i, v in enumerate(metadata[\"idx2str\"])}\n        self.predictions_key = PREDICTIONS\n        self.probabilities_key = PROBABILITIES\n        self.unk = UNKNOWN_SYMBOL\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        probabilities = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.probabilities_key)\n\n        inv_preds: list[list[str]] = []\n        filtered_probs: list[torch.Tensor] = []\n        for sample_idx, sample in enumerate(predictions):\n            sample_preds: list[str] = []\n            pos_sample_idxs: list[int] = []\n            pos_class_idxs: list[int] = []\n            for class_idx, is_positive in enumerate(sample):\n                if is_positive == 1:\n                    sample_preds.append(self.idx2str.get(class_idx, self.unk))\n                    pos_sample_idxs.append(sample_idx)\n                    pos_class_idxs.append(class_idx)\n            inv_preds.append(sample_preds)\n            filtered_probs.append(probabilities[pos_sample_idxs, pos_class_idxs])\n\n        return {\n            self.predictions_key: inv_preds,\n            self.probabilities_key: filtered_probs,\n        }\n\n\nclass _SetPredict(PredictModule):\n    def __init__(self, threshold):\n        super().__init__()\n        self.threshold = threshold\n\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n        probabilities = torch.sigmoid(logits)\n\n        predictions = torch.greater_equal(probabilities, self.threshold)\n        predictions = predictions.type(torch.int64)\n\n        return {self.predictions_key: predictions, self.probabilities_key: probabilities, self.logits_key: logits}\n\n\nclass SetFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return SET\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column.astype(str)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        vocabulary = create_vocabulary(\n            column,\n            preprocessing_parameters[\"tokenizer\"],\n            num_most_frequent=preprocessing_parameters[\"most_common\"],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            add_special_symbols=False,\n            processor=backend.df_engine,\n        )\n        return {\n            \"idx2str\": vocabulary.vocab,\n            \"str2idx\": vocabulary.str2idx,\n            \"str2freq\": vocabulary.str2freq,\n            \"vocab_size\": len(vocabulary.str2idx),\n            \"max_set_size\": vocabulary.max_sequence_length,\n        }\n\n    @staticmethod\n    def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend):\n        def to_dense(x):\n            feature_vector = set_str_to_idx(x, metadata[\"str2idx\"], preprocessing_parameters[\"tokenizer\"])\n\n            set_vector = np.zeros((len(metadata[\"str2idx\"]),))\n            set_vector[feature_vector] = 1\n            return set_vector.astype(np.bool_)\n\n        return backend.df_engine.map_objects(column, to_dense)\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        proc_df[feature_config[PROC_COLUMN]] = SetFeatureMixin.feature_data(\n            input_df[feature_config[COLUMN]],\n            metadata[feature_config[NAME]],\n            preprocessing_parameters,\n            backend,\n        )\n        return proc_df\n\n\nclass SetInputFeature(SetFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: SetInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.bool, torch.int64, torch.float32]\n\n        encoder_output = self.encoder_obj(inputs)\n\n        return encoder_output\n\n    @property\n    def input_dtype(self):\n        return torch.bool\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([len(self.encoder_obj.config.vocab)])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.vocab = feature_metadata[\"idx2str\"]\n\n    @staticmethod\n    def get_schema_cls():\n        return SetInputFeatureConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SetPreprocessing(metadata)\n\n\nclass SetOutputFeature(SetFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: SetOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.threshold = output_feature_config.threshold\n        super().__init__(output_feature_config, output_features, **kwargs)\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):  # hidden\n        hidden = inputs[HIDDEN]\n        return self.decoder_obj(hidden)\n\n    def metric_kwargs(self) -> dict[str, Any]:\n        return {\"threshold\": self.threshold}\n\n    def create_predict_module(self) -> PredictModule:\n        return _SetPredict(self.threshold)\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, PROBABILITIES, LOGITS}\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.bool\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.decoder_obj.input_shape\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.decoder_obj.config.num_classes])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.decoder.num_classes = feature_metadata[\"vocab_size\"]\n        if isinstance(feature_config.loss.class_weights, (list, tuple)):\n            if len(feature_config.loss.class_weights) != feature_config.decoder.num_classes:\n                raise ValueError(\n                    f\"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with \"\n                    f\"the number of classes ({feature_config.decoder.num_classes}) for feature {feature_config.name}. \"\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and their order and consider there needs to be a weight \"\n                    \"for the <UNK> and <PAD> class too.\"\n                )\n\n        if isinstance(feature_config.loss.class_weights, dict):\n            if feature_metadata[\"str2idx\"].keys() != feature_config.loss.class_weights.keys():\n                raise ValueError(\n                    f\"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with \"\n                    f'the classes ({feature_metadata[\"str2idx\"].keys()}) of feature {feature_config.name}. '\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and consider there needs to be a weight \"\n                    \"for the <UNK> and <PAD> class too.\"\n                )\n            else:\n                class_weights = feature_config.loss.class_weights\n                idx2str = feature_metadata[\"idx2str\"]\n                class_weights_list = [class_weights[s] for s in idx2str]\n                feature_config.loss.class_weights = class_weights_list\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        # no overall stats, just return empty dictionary\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in result:\n\n            def idx2str(pred_set):\n                return [metadata[\"idx2str\"][i] for i, pred in enumerate(pred_set) if pred]\n\n            result[predictions_col] = result[predictions_col].map(idx2str)\n\n        probabilities_col = f\"{self.feature_name}_{PROBABILITIES}\"\n        if probabilities_col in result:\n\n            def get_prob(prob_set):\n                # Cast to float32 because empty np.array objects are np.float64, causing mismatch errors during saving.\n                return np.array([prob for prob in prob_set if prob >= self.threshold], dtype=np.float32)\n\n            result[probabilities_col] = result[probabilities_col].map(get_prob)\n\n        return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SetPostprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return SetOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/text_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom functools import partial\n\nimport numpy as np\nimport torch\nfrom torch import Tensor\nfrom transformers import PreTrainedTokenizer\n\nfrom ludwig.constants import (\n    COLUMN,\n    IGNORE_INDEX_TOKEN_ID,\n    LAST_PREDICTIONS,\n    LENGTHS,\n    NAME,\n    PREDICTIONS,\n    PREPROCESSING,\n    PROBABILITIES,\n    PROBABILITY,\n    PROC_COLUMN,\n    RESPONSE,\n    TEXT,\n)\nfrom ludwig.features.base_feature import BaseFeatureMixin, OutputFeature\nfrom ludwig.features.feature_utils import compute_sequence_probability, compute_token_probabilities\nfrom ludwig.features.sequence_feature import (\n    _SequencePostprocessing,\n    _SequencePreprocessing,\n    SequenceInputFeature,\n    SequenceOutputFeature,\n)\nfrom ludwig.modules.metric_registry import get_metric_tensor_input\nfrom ludwig.schema.features.text_feature import TextInputFeatureConfig, TextOutputFeatureConfig\nfrom ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.math_utils import softmax\nfrom ludwig.utils.strings_utils import (\n    build_sequence_matrix,\n    create_vocabulary,\n    get_tokenizer,\n    SpecialSymbol,\n    UNKNOWN_SYMBOL,\n    Vocabulary,\n)\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_decoded_targets_and_predictions(\n    targets: Tensor,\n    predictions: dict[str, Tensor],\n    tokenizer: PreTrainedTokenizer,\n) -> tuple[list[str], list[str]]:\n    \"\"\"Returns the decoded targets and predictions, accounting for IGNORE_INDEX_TOKEN_ID.\"\"\"\n    # Ensure targets and predictions are on the same device\n    pred_tensor = predictions[PREDICTIONS]\n    if targets.device != pred_tensor.device:\n        targets = targets.to(pred_tensor.device)\n    sanitized_targets = torch.where(targets != IGNORE_INDEX_TOKEN_ID, targets, tokenizer.pad_token_id)\n    sanitized_predictions = torch.where(\n        targets != IGNORE_INDEX_TOKEN_ID,\n        pred_tensor,\n        tokenizer.pad_token_id,\n    )\n    decoded_targets = tokenizer.batch_decode(sanitized_targets, skip_special_tokens=True)\n    decoded_predictions = tokenizer.batch_decode(sanitized_predictions, skip_special_tokens=True)\n    return decoded_targets, decoded_predictions\n\n\ndef _get_metadata_reconciled_max_sequence_length(\n    preprocessing_parameters: dict, vocabulary: Vocabulary\n) -> tuple[int, int]:\n    \"\"\"Reconciles the different ways sequence length can be specified in preprocessing parameters.\n\n    If the max sequence length is explicitly specified, we use the minimum of the true maximum sequence length and\n    the explicitly specified value. If the explicitly specified value is less than the true maximum sequence length, we\n    log a warning.\n\n    If the max sequence length is not specified, we use the true maximum sequence length.\n\n    Returns:\n        Tuple(max_sequence_length, sequence_length_99ptile).\n    \"\"\"\n    # For sequence features with a fixed length specified by `sequence_length`, use this as the max_sequence_length.\n    if preprocessing_parameters[\"sequence_length\"] is not None:\n        return preprocessing_parameters[\"sequence_length\"], preprocessing_parameters[\"sequence_length\"]\n\n    # Max sequence length is explicitly set. Use this as the max_sequence_length.\n    if preprocessing_parameters[\"max_sequence_length\"] is not None:\n        if preprocessing_parameters[\"max_sequence_length\"] < vocabulary.max_sequence_length:\n            logger.warning(\n                f\"The max sequence length of the data, {vocabulary.max_sequence_length}, is longer than the max \"\n                f\"sequence length set in the config, {preprocessing_parameters['max_sequence_length']}. Note that this \"\n                \"will truncate all examples to max_sequence_length=\"\n                f\"{preprocessing_parameters['max_sequence_length']}.\"\n            )\n        return (\n            min(vocabulary.max_sequence_length, preprocessing_parameters[\"max_sequence_length\"]),\n            min(vocabulary.sequence_length_99ptile, preprocessing_parameters[\"max_sequence_length\"]),\n        )\n\n    # Max sequence length is None. Use the max sequence length of the data.\n    return vocabulary.max_sequence_length, vocabulary.sequence_length_99ptile\n\n\nclass TextFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return TEXT\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column.astype(str)\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        \"\"\"Returns all metadata for the given text feature.\n\n        Raises:\n            ValueError, if the tokenized prompt template is longer than the max sequence length.\n        \"\"\"\n        prompt_template = config.get(\"prompt\", {}).get(\"template\", \"\")\n        vocabulary: Vocabulary = create_vocabulary(\n            column,\n            tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n            num_most_frequent=preprocessing_parameters[\"most_common\"],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            vocab_file=preprocessing_parameters[\"vocab_file\"],\n            unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n            padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n            pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n            ngram_size=preprocessing_parameters[\"ngram_size\"],\n            compute_idf=preprocessing_parameters[\"compute_idf\"],\n            processor=backend.df_engine,\n            prompt_template=prompt_template,\n        )\n        # Note: The vocabulary's max_sequence_length includes the prompt template, which is merged into the column prior\n        # to computing feature metadata.\n        logger.info(\n            f\"Max length of feature '{column.name}': {vocabulary.max_sequence_length} (without start and stop symbols)\"\n        )\n\n        max_sequence_length, max_sequence_length_99ptile = _get_metadata_reconciled_max_sequence_length(\n            preprocessing_parameters, vocabulary\n        )\n\n        if is_input_feature and max_sequence_length < vocabulary.prompt_template_num_tokens:\n            raise ValueError(\n                f\"The input feature's max sequence length ({max_sequence_length}) is shorter than the prompt template \"\n                f\"length ({vocabulary.prompt_template_num_tokens}). This will truncate all unique information. \"\n                \"Consider making the template shorter or increasing the input feature's max sequence length to a \"\n                f\"value >> {vocabulary.prompt_template_num_tokens}.\"\n            )\n\n        logger.info(f\"Max sequence length is {max_sequence_length} for feature '{column.name}'\")\n\n        return {\n            \"idx2str\": vocabulary.vocab,\n            \"str2idx\": vocabulary.str2idx,\n            \"str2freq\": vocabulary.str2freq,\n            \"str2idf\": vocabulary.str2idf,\n            \"vocab_size\": len(vocabulary.vocab),\n            \"max_sequence_length\": max_sequence_length,\n            \"max_sequence_length_99ptile\": max_sequence_length_99ptile,\n            \"pad_idx\": vocabulary.pad_idx,\n            \"padding_symbol\": vocabulary.padding_symbol,\n            \"unknown_symbol\": vocabulary.unknown_symbol,\n            \"prompt_template_num_tokens\": vocabulary.prompt_template_num_tokens,\n        }\n\n    @staticmethod\n    def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend) -> np.ndarray:\n        # TODO(1891): Remove backward compatibility hack once all models have been retrained with Ludwig after\n        # https://github.com/ludwig-ai/ludwig/pull/1859.\n        prefix = \"\"\n        padding_symbol_metadata_key = \"padding_symbol\"\n        unknown_symbol_metadata_key = \"unknown_symbol\"\n        if \"str2idx\" not in metadata:\n            prefix = \"word_\"\n            padding_symbol_metadata_key = \"word_pad_symbol\"\n            unknown_symbol_metadata_key = \"word_unk_symbol\"\n\n        # ensure preprocessing param values match the metadata determined from dataset\n        preprocessing_parameters[\"padding_symbol\"] = metadata[padding_symbol_metadata_key]\n        preprocessing_parameters[\"unknown_symbol\"] = metadata[unknown_symbol_metadata_key]\n        if preprocessing_parameters[\"fill_value\"] == UNKNOWN_SYMBOL:\n            preprocessing_parameters[\"fill_value\"] = preprocessing_parameters[\"unknown_symbol\"]\n        if (\n            \"computed_fill_value\" in preprocessing_parameters\n            and preprocessing_parameters[\"computed_fill_value\"] == UNKNOWN_SYMBOL\n        ):\n            preprocessing_parameters[\"computed_fill_value\"] = preprocessing_parameters[\"unknown_symbol\"]\n\n        sequences = column\n\n        return build_sequence_matrix(\n            sequences=sequences,\n            inverse_vocabulary=metadata[f\"{prefix}str2idx\"],\n            tokenizer_type=preprocessing_parameters[f\"{prefix}tokenizer\"],\n            length_limit=metadata[f\"{prefix}max_sequence_length\"],\n            padding_symbol=metadata[padding_symbol_metadata_key],\n            padding=preprocessing_parameters[\"padding\"],\n            unknown_symbol=metadata[unknown_symbol_metadata_key],\n            lowercase=preprocessing_parameters[\"lowercase\"],\n            tokenizer_vocab_file=preprocessing_parameters[f\"{prefix}vocab_file\"],\n            pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n            processor=backend.df_engine,\n        )\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        proc_df[feature_config[PROC_COLUMN]] = TextFeatureMixin.feature_data(\n            input_df[feature_config[COLUMN]],\n            metadata[feature_config[NAME]],\n            preprocessing_parameters,\n            backend,\n        )\n        return proc_df\n\n\nclass TextInputFeature(TextFeatureMixin, SequenceInputFeature):\n    def __init__(self, input_feature_config: TextInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs)\n\n    def forward(self, inputs, mask=None):\n        assert isinstance(inputs, torch.Tensor)\n        assert (\n            inputs.dtype == torch.int8\n            or inputs.dtype == torch.int16\n            or inputs.dtype == torch.int32\n            or inputs.dtype == torch.int64\n        )\n        assert len(inputs.shape) == 2\n\n        inputs_mask = torch.not_equal(inputs, SpecialSymbol.PADDING.value)\n\n        inputs_exp = inputs.type(torch.int32)\n        lengths = torch.sum(inputs_mask.type(torch.int32), dim=1)\n        encoder_output = self.encoder_obj(inputs_exp, mask=inputs_mask)\n        encoder_output[LENGTHS] = lengths\n\n        return encoder_output\n\n    @property\n    def input_dtype(self):\n        return torch.int32\n\n    @property\n    def input_shape(self):\n        return torch.Size([self.encoder_obj.config.max_sequence_length])\n\n    def update_config_after_module_init(self, feature_config):\n        feature_config.encoder = self.encoder_obj.config\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.vocab = feature_metadata[\"idx2str\"]\n        feature_config.encoder.vocab_size = len(feature_metadata[\"idx2str\"])\n        feature_config.encoder.max_sequence_length = feature_metadata[\"max_sequence_length\"]\n        feature_config.encoder.pad_idx = feature_metadata[\"pad_idx\"]\n        feature_config.encoder.num_tokens = len(feature_metadata[\"idx2str\"])\n        feature_config.encoder.str2freq = feature_metadata[\"str2freq\"]\n        feature_config.encoder.str2idf = feature_metadata[\"str2idf\"]\n        feature_config.encoder.skip = feature_metadata[PREPROCESSING].get(\"cache_encoder_embeddings\", False)\n\n    @staticmethod\n    def get_schema_cls():\n        return TextInputFeatureConfig\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SequencePreprocessing(metadata)\n\n\nclass TextOutputFeature(TextFeatureMixin, SequenceOutputFeature):\n    def __init__(\n        self,\n        output_feature_config: TextOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        super().__init__(output_feature_config, output_features, **kwargs)\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.int32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.decoder_obj.config.max_sequence_length])\n\n    def update_metrics(\n        self,\n        targets: Tensor,\n        predictions: dict[str, Tensor],\n        tokenizer: PreTrainedTokenizer | None = None,\n    ) -> None:\n        \"\"\"Updates metrics with the given targets and predictions.\n\n        If decoded_targets and decoded_predictions are provided, as through LLM model types, then additional\n        response-based metrics like BLEU and ROUGE are also computed.\n\n        Args:\n            targets: Tensor with target values for this output feature.\n            predictions: Dict of tensors returned by predictions().\n        \"\"\"\n        if tokenizer is not None:\n            # Decode the targets and predictions to compute response-based metrics using the initialized tokenizer.\n            decoded_targets, decoded_predictions = get_decoded_targets_and_predictions(targets, predictions, tokenizer)\n\n        for metric_name, metric_fn in self._metric_functions.items():\n            prediction_key = get_metric_tensor_input(metric_name)\n            try:\n                if prediction_key == RESPONSE:\n                    if tokenizer is not None:\n                        # RESPONSE metrics cannot be computed if decoded texts are not provided.\n                        # Decoded texts are only provided using the LLM model type.\n                        if decoded_targets is not None and decoded_predictions is not None:\n                            # Move metric function to the device of the predictions.\n                            # For CUDA, it can be computed on any of the GPUs since it uses allgather to collect\n                            # the results from all GPUs and compute the final metric.\n                            # We use 'predictions' as the key since it is always present in the predictions dict.\n                            device = \"cuda\" if predictions[\"predictions\"].is_cuda else \"cpu\"\n                            metric_fn = metric_fn.to(device)\n                            if metric_name == \"bleu\":\n                                # BLEU takes in targets as a list.\n                                metric_fn.update(decoded_predictions, [decoded_targets])\n                            else:\n                                metric_fn.update(decoded_predictions, decoded_targets)\n                else:\n                    metric_fn = metric_fn.to(predictions[prediction_key].device)\n                    metric_fn.update(predictions[prediction_key].detach(), targets)\n            except Exception as e:\n                logger.info(f\"Ran into error when calculating metric {metric_name}. Skipping. The error is: {e}\")\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.decoder.vocab_size = feature_metadata[\"vocab_size\"]\n        feature_config.decoder.max_sequence_length = feature_metadata[\"max_sequence_length\"]\n        if isinstance(feature_config.loss.class_weights, (list, tuple)):\n            # [0, 0] for UNK and PAD\n            feature_config.loss.class_weights = [0, 0] + feature_config.loss.class_weights\n            if len(feature_config.loss.class_weights) != feature_config.decoder.vocab_size:\n                raise ValueError(\n                    f\"The length of class_weights ({len(feature_config.loss.class_weights)}) is not compatible with \"\n                    f\"the number of classes ({feature_config.decoder.vocab_size})\"\n                )\n\n        if isinstance(feature_config.loss.class_weights, dict):\n            if feature_metadata[\"str2idx\"].keys() != feature_config.loss.class_weights.keys():\n                raise ValueError(\n                    f\"The class_weights keys ({feature_config.loss.class_weights.keys()}) are not compatible with \"\n                    f'the classes ({feature_metadata[\"str2idx\"].keys()}) of feature {feature_config.column}. '\n                    \"Check the metadata JSON file to see the classes \"\n                    \"and consider there needs to be a weight \"\n                    \"for the <UNK> class too.\"\n                )\n            else:\n                class_weights = feature_config.loss.class_weights\n                idx2str = feature_metadata[\"idx2str\"]\n                class_weights_list = [class_weights[s] for s in idx2str]\n                feature_config.loss.class_weights = class_weights_list\n\n        if feature_config.loss.class_similarities_temperature > 0:\n            if feature_config.class_similarities:\n                distances = feature_config.class_similarities\n                temperature = feature_config.loss.class_similarities_temperature\n                for i in range(len(distances)):\n                    distances[i, :] = softmax(distances[i, :], temperature=temperature)\n                feature_config.loss.class_similarities = distances\n            else:\n                raise ValueError(\n                    \"class_similarities_temperature > 0,\"\n                    \"but no class similarities are provided \"\n                    \"for feature {}\".format(feature_config.column)\n                )\n\n    @staticmethod\n    def calculate_overall_stats(\n        predictions,\n        targets,\n        train_set_metadata,\n    ):\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        # todo: refactor to reuse SequenceOutputFeature.postprocess_predictions\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n\n        tokenizer = None\n        if metadata[\"preprocessing\"][\"tokenizer\"] == \"hf_tokenizer\":\n            tokenizer = get_tokenizer(\n                metadata[\"preprocessing\"][\"tokenizer\"],\n                metadata[\"preprocessing\"][\"vocab_file\"],\n                metadata[\"preprocessing\"][\"pretrained_model_name_or_path\"],\n            )\n\n        if predictions_col in result:\n            token_col = result[predictions_col]\n\n            def idx2str(pred):\n                if tokenizer is None:\n                    return [\n                        metadata[\"idx2str\"][token] if token < len(metadata[\"idx2str\"]) else UNKNOWN_SYMBOL\n                        for token in pred\n                    ]\n                # Decode each token ID individually. In transformers 5.x, batch_decode\n                # on a 1D array treats it as a single sequence rather than individual tokens.\n                return [tokenizer.tokenizer.decode([int(token_id)], skip_special_tokens=True) for token_id in pred]\n\n            result[predictions_col] = token_col.map(idx2str)\n\n            # Add additional response column that represents the predicted text output\n            # as a single string instead of a list of tokens.\n            def idx2response(pred):\n                if tokenizer is None:\n                    # This works because we treat each word as a token.\n                    return \" \".join(\n                        [\n                            metadata[\"idx2str\"][token] if token < len(metadata[\"idx2str\"]) else UNKNOWN_SYMBOL\n                            for token in pred\n                        ]\n                    )\n                return tokenizer.tokenizer.decode(pred, skip_special_tokens=True)\n\n            result[f\"{self.feature_name}_response\"] = token_col.map(idx2response)\n\n        last_preds_col = f\"{self.feature_name}_{LAST_PREDICTIONS}\"\n        if last_preds_col in result:\n\n            def last_idx2str(last_pred):\n                if last_pred < len(metadata[\"idx2str\"]):\n                    return metadata[\"idx2str\"][last_pred]\n                return UNKNOWN_SYMBOL\n\n            result[last_preds_col] = result[last_preds_col].map(last_idx2str)\n\n        probs_col = f\"{self.feature_name}_{PROBABILITIES}\"\n        prob_col = f\"{self.feature_name}_{PROBABILITY}\"\n\n        # \"Summarizes\" the `result`'s probability-related output:\n        # - result[probs_col]:\n        #       Each row is now a list of \"max\" probabilities. Each element is the probability of the argmax token for\n        #       the given time step.\n        #\n        #       Note that we intentionally do not return full list of probabilties for each time step because the output\n        #       of postprocess_predictions is saved to disk and the full probability distribution can be huge,\n        #       especially for large vocab sizes:\n        #           dataset_size x sequence_length x vocab_size\n        #\n        #       TODO: Add a mechanism that lets the user save the full probability distribution if they want.\n        # - result[prob_col]:\n        #       Each row is the overall probability of the sequence. This is the product of the max probabilities over\n        #       all time steps.\n        if probs_col in result:\n            # result[probs_col]: From PredictModule, each row has a list of size (sequence_length) of a list of\n            # probabiltiies of (vocab_size). compute_token_probabilities gets the maximum probability per timestep.\n            result[probs_col] = result[probs_col].map(compute_token_probabilities)\n            result[prob_col] = result[probs_col].map(\n                partial(\n                    compute_sequence_probability,\n                    max_sequence_length=metadata[\"max_sequence_length\"],\n                    return_log_prob=True,\n                ),\n            )\n\n        lengths_col = f\"{self.feature_name}_{LENGTHS}\"\n        if lengths_col in result:\n            del result[lengths_col]\n\n        return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _SequencePostprocessing(metadata)\n\n    @staticmethod\n    def get_schema_cls():\n        return TextOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/timeseries_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom typing import TYPE_CHECKING\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROC_COLUMN, TIMESERIES\nfrom ludwig.features.base_feature import BaseFeatureMixin, OutputFeature, PredictModule\nfrom ludwig.features.sequence_feature import SequenceInputFeature\nfrom ludwig.features.vector_feature import _VectorPostprocessing, _VectorPredict\nfrom ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig, TimeseriesOutputFeatureConfig\nfrom ludwig.types import FeatureMetadataDict, ModelConfigDict, PreprocessingConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.tokenizers import get_tokenizer_from_registry, TORCHSCRIPT_COMPATIBLE_TOKENIZERS\nfrom ludwig.utils.types import Series, TorchscriptPreprocessingInput\n\nif TYPE_CHECKING:\n    from ludwig.backend.base import Backend\n\nlogger = logging.getLogger(__name__)\n\n\ndef create_time_delay_embedding(\n    series: Series, window_size: int, horizon: int, padding_value: int, backend: \"Backend\"\n) -> Series:\n    \"\"\"Time delay embedding from:\n\n    https://towardsdatascience.com/machine-learning-for-forecasting-transformations-and-feature-extraction-bbbea9de0ac2\n\n    Args:\n        series: Column-major timeseries data.\n        window_size: Size of the lookback sliding window for timeseries inputs.\n        horizon: Size of the forward-looking horizon for timeseries outputs.\n        padding_value: Value to pad out the window when there is not enough data around the observation.\n\n    Returns:\n        A column of timeseries window arrays in row-major format for training.\n    \"\"\"\n    # Replace default fill value of \"\" with nan as we will be assuming numeric values here\n    series = series.replace(\"\", np.nan)\n\n    # Create the list of shifts we want to perform over the series.\n    # For backwards looking shifts, we want to include the current element, while for forward looking shifts we do not.\n    # Example:\n    #   window_size=3, horizon=0 --> shift_offsets=[2, 1, 0]\n    #   window_size=0, horizon=2 --> shift_offsets=[-1, -2]\n    shift_offsets = list(range(window_size - 1, -(horizon + 1), -1))\n    shifts = [series.shift(i) for i in shift_offsets]\n    df = backend.df_engine.df_lib.concat(shifts, axis=1)\n    df.columns = [f\"__tmp_column_{j}\" for j in shift_offsets]\n    return df.apply(lambda x: np.nan_to_num(np.array(x.tolist()).astype(np.float32), nan=padding_value), axis=1)\n\n\nclass _TimeseriesPreprocessing(torch.nn.Module):\n    \"\"\"Torchscript-enabled version of preprocessing done by TimeseriesFeatureMixin.add_feature_data.\"\"\"\n\n    def __init__(self, metadata: TrainingSetMetadataDict):\n        super().__init__()\n        if metadata[\"preprocessing\"][\"tokenizer\"] not in TORCHSCRIPT_COMPATIBLE_TOKENIZERS:\n            raise ValueError(\n                f\"{metadata['preprocessing']['tokenizer']} is not supported by torchscript. Please use \"\n                f\"one of {TORCHSCRIPT_COMPATIBLE_TOKENIZERS}.\"\n            )\n        self.tokenizer = get_tokenizer_from_registry(metadata[\"preprocessing\"][\"tokenizer\"])()\n        self.padding = metadata[\"preprocessing\"][\"padding\"]\n        self.padding_value = float(metadata[\"preprocessing\"][\"padding_value\"])\n        self.max_timeseries_length = int(metadata[\"max_timeseries_length\"])\n        self.computed_fill_value = metadata[\"preprocessing\"][\"computed_fill_value\"]\n\n    def _process_str_sequence(self, sequence: list[str], limit: int) -> torch.Tensor:\n        float_sequence = [float(s) for s in sequence[:limit]]\n        return torch.tensor(float_sequence)\n\n    def _nan_to_fill_value(self, v: torch.Tensor) -> torch.Tensor:\n        if v.isnan().any():\n            tokenized_fill_value = self.tokenizer(self.computed_fill_value)\n            # refines type of sequences from Any to List[str]\n            assert torch.jit.isinstance(tokenized_fill_value, list[str])\n            return self._process_str_sequence(tokenized_fill_value, self.max_timeseries_length)\n        return v\n\n    def forward_list_of_tensors(self, v: list[torch.Tensor]) -> torch.Tensor:\n        v = [self._nan_to_fill_value(v_i) for v_i in v]\n\n        if self.padding == \"right\":\n            timeseries_matrix = torch.nn.utils.rnn.pad_sequence(v, batch_first=True, padding_value=self.padding_value)\n            timeseries_matrix = timeseries_matrix[:, : self.max_timeseries_length]\n        else:\n            reversed_timeseries = [torch.flip(v_i[: self.max_timeseries_length], dims=(0,)) for v_i in v]\n            reversed_timeseries_padded = torch.nn.utils.rnn.pad_sequence(\n                reversed_timeseries, batch_first=True, padding_value=self.padding_value\n            )\n            timeseries_matrix = torch.flip(reversed_timeseries_padded, dims=(1,))\n        return timeseries_matrix\n\n    def forward_list_of_strs(self, v: list[str]) -> torch.Tensor:\n        v = [self.computed_fill_value if s == \"nan\" else s for s in v]\n\n        sequences = self.tokenizer(v)\n        # refines type of sequences from Any to List[List[str]]\n        assert torch.jit.isinstance(sequences, list[list[str]]), \"sequences is not a list of lists.\"\n\n        timeseries_matrix = torch.full(\n            [len(sequences), self.max_timeseries_length], self.padding_value, dtype=torch.float32\n        )\n        for sample_idx, str_sequence in enumerate(sequences):\n            limit = min(len(str_sequence), self.max_timeseries_length)\n            float_sequence = self._process_str_sequence(str_sequence, limit)\n            if self.padding == \"right\":\n                timeseries_matrix[sample_idx][:limit] = float_sequence\n            else:  # if self.padding == 'left\n                timeseries_matrix[sample_idx][self.max_timeseries_length - limit :] = float_sequence\n        return timeseries_matrix\n\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        \"\"\"Takes a list of float values and creates a padded torch.Tensor.\"\"\"\n        if torch.jit.isinstance(v, list[torch.Tensor]):\n            return self.forward_list_of_tensors(v)\n        if torch.jit.isinstance(v, list[str]):\n            return self.forward_list_of_strs(v)\n        raise ValueError(f\"Unsupported input: {v}\")\n\n\nclass TimeseriesFeatureMixin(BaseFeatureMixin):\n    @staticmethod\n    def type():\n        return TIMESERIES\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        window_size = preprocessing_parameters.get(\"window_size\", 0) or preprocessing_parameters.get(\"horizon\", 0)\n        if window_size > 0:\n            # Column-major data\n            return {\"max_timeseries_length\": window_size}\n\n        column = column.astype(str)\n        tokenizer = get_tokenizer_from_registry(preprocessing_parameters[\"tokenizer\"])()\n        max_length = 0\n        for timeseries in column:\n            processed_line = tokenizer(timeseries)\n            max_length = max(max_length, len(processed_line))\n        max_length = min(preprocessing_parameters[\"timeseries_length_limit\"], max_length)\n\n        return {\"max_timeseries_length\": max_length}\n\n    @staticmethod\n    def build_matrix(timeseries, tokenizer_name, length_limit, padding_value, padding, backend):\n        tokenizer = get_tokenizer_from_registry(tokenizer_name)()\n\n        ts_vectors = backend.df_engine.map_objects(\n            timeseries, lambda ts: np.nan_to_num(np.array(tokenizer(ts)).astype(np.float32), nan=padding_value)\n        )\n\n        max_length = backend.df_engine.compute(ts_vectors.map(len).max())\n        if max_length < length_limit:\n            logger.debug(f\"max length of {tokenizer_name}: {max_length} < limit: {length_limit}\")\n        max_length = length_limit\n\n        def pad(vector):\n            padded = np.full((max_length,), padding_value, dtype=np.float32)\n            limit = min(vector.shape[0], max_length)\n            if padding == \"right\":\n                padded[:limit] = vector[:limit]\n            else:  # if padding == 'left\n                padded[max_length - limit :] = vector[:limit]\n            return padded\n\n        return backend.df_engine.map_objects(ts_vectors, pad)\n\n    @staticmethod\n    def feature_data(column, metadata, preprocessing_parameters: PreprocessingConfigDict, backend):\n        padding_value = preprocessing_parameters[\"padding_value\"]\n\n        window_size = preprocessing_parameters.get(\"window_size\", 0)\n        horizon = preprocessing_parameters.get(\"horizon\", 0)\n        if window_size > 0 or horizon > 0:\n            # Column-major data. Convert the column into the row-major embedding\n            return create_time_delay_embedding(column, window_size, horizon, padding_value, backend)\n\n        timeseries_data = TimeseriesFeatureMixin.build_matrix(\n            column,\n            preprocessing_parameters[\"tokenizer\"],\n            metadata[\"max_timeseries_length\"],\n            padding_value,\n            preprocessing_parameters[\"padding\"],\n            backend,\n        )\n        return timeseries_data\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        proc_df[feature_config[PROC_COLUMN]] = TimeseriesFeatureMixin.feature_data(\n            input_df[feature_config[COLUMN]].astype(str),\n            metadata[feature_config[NAME]],\n            preprocessing_parameters,\n            backend,\n        )\n        return proc_df\n\n\nclass TimeseriesInputFeature(TimeseriesFeatureMixin, SequenceInputFeature):\n    def __init__(self, input_feature_config: TimeseriesInputFeatureConfig, encoder_obj=None, **kwargs):\n        # add required sequence encoder parameters for time series\n        input_feature_config.encoder.embedding_size = 1\n        input_feature_config.encoder.should_embed = False\n\n        # SequenceInputFeauture's constructor initializes the encoder.\n        super().__init__(input_feature_config, encoder_obj=encoder_obj, **kwargs)\n\n    def forward(self, inputs, mask=None):\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.float16, torch.float32, torch.float64]\n        assert len(inputs.shape) == 2\n\n        inputs_exp = inputs.type(torch.float32)\n        encoder_output = self.encoder_obj(inputs_exp, mask=mask)\n\n        return encoder_output\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.encoder_obj.input_shape\n\n    @property\n    def input_dtype(self):\n        return torch.float32\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.input_size = feature_metadata[\"max_timeseries_length\"]\n        feature_config.encoder.max_sequence_length = feature_metadata[\"max_timeseries_length\"]\n\n    @staticmethod\n    def get_schema_cls():\n        return TimeseriesInputFeatureConfig\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _TimeseriesPreprocessing(metadata)\n\n\nclass TimeseriesOutputFeature(TimeseriesFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: TimeseriesOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.horizon = output_feature_config.horizon\n        super().__init__(output_feature_config, output_features, **kwargs)\n        output_feature_config.decoder.output_size = self.horizon\n\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):  # hidden\n        hidden = inputs[HIDDEN]\n        return self.decoder_obj(hidden)\n\n    def loss_kwargs(self):\n        return self.loss.to_dict()\n\n    def metric_kwargs(self):\n        return dict(num_outputs=self.output_shape[0])\n\n    def create_predict_module(self) -> PredictModule:\n        return _VectorPredict()\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, LOGITS}\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.float32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.horizon])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.horizon = feature_metadata[\"max_timeseries_length\"]\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        # no overall stats, just return empty dictionary\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in result:\n            result[predictions_col] = result[predictions_col].map(lambda pred: pred.tolist())\n        return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _VectorPostprocessing()\n\n    @staticmethod\n    def get_schema_cls():\n        return TimeseriesOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/features/vector_feature.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport numpy as np\nimport torch\n\nfrom ludwig.constants import COLUMN, HIDDEN, LOGITS, NAME, PREDICTIONS, PROC_COLUMN, VECTOR\nfrom ludwig.features.base_feature import InputFeature, OutputFeature, PredictModule\nfrom ludwig.schema.features.vector_feature import VectorInputFeatureConfig, VectorOutputFeatureConfig\nfrom ludwig.types import (\n    FeatureMetadataDict,\n    FeaturePostProcessingOutputDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nlogger = logging.getLogger(__name__)\n\n\nclass _VectorPreprocessing(torch.nn.Module):\n    def forward(self, v: TorchscriptPreprocessingInput) -> torch.Tensor:\n        if torch.jit.isinstance(v, torch.Tensor):\n            out = v\n        elif torch.jit.isinstance(v, list[torch.Tensor]):\n            out = torch.stack(v)\n        elif torch.jit.isinstance(v, list[str]):\n            vectors = []\n            for sample in v:\n                vector = torch.tensor([float(x) for x in sample.split()], dtype=torch.float32)\n                vectors.append(vector)\n            out = torch.stack(vectors)\n        else:\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        if out.isnan().any():\n            raise ValueError(\"Scripted NaN handling not implemented for Vector feature\")\n        return out\n\n\nclass _VectorPostprocessing(torch.nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.predictions_key = PREDICTIONS\n        self.logits_key = LOGITS\n\n    def forward(self, preds: dict[str, torch.Tensor], feature_name: str) -> FeaturePostProcessingOutputDict:\n        predictions = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.predictions_key)\n        logits = output_feature_utils.get_output_feature_tensor(preds, feature_name, self.logits_key)\n\n        return {self.predictions_key: predictions, self.logits_key: logits}\n\n\nclass _VectorPredict(PredictModule):\n    def forward(self, inputs: dict[str, torch.Tensor], feature_name: str) -> dict[str, torch.Tensor]:\n        logits = output_feature_utils.get_output_feature_tensor(inputs, feature_name, self.logits_key)\n\n        return {self.predictions_key: logits, self.logits_key: logits}\n\n\nclass VectorFeatureMixin:\n    @staticmethod\n    def type():\n        return VECTOR\n\n    @staticmethod\n    def cast_column(column, backend):\n        return column\n\n    @staticmethod\n    def get_feature_meta(\n        config: ModelConfigDict,\n        column,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        is_input_feature: bool,\n    ) -> FeatureMetadataDict:\n        return {\"preprocessing\": preprocessing_parameters}\n\n    @staticmethod\n    def add_feature_data(\n        feature_config,\n        input_df,\n        proc_df,\n        metadata,\n        preprocessing_parameters: PreprocessingConfigDict,\n        backend,\n        skip_save_processed_input,\n    ):\n        \"\"\"Expects all the vectors to be of the same size.\n\n        The vectors need to be whitespace delimited strings. Missing values are not handled.\n        \"\"\"\n        if len(input_df[feature_config[COLUMN]]) == 0:\n            raise ValueError(\"There are no vectors in the dataset provided\")\n\n        # Convert the string of features into a numpy array\n        try:\n            proc_df[feature_config[PROC_COLUMN]] = backend.df_engine.map_objects(\n                input_df[feature_config[COLUMN]], lambda x: np.array(x.split(), dtype=np.float32)\n            )\n        except ValueError:\n            logger.error(\n                \"Unable to read the vector data. Make sure that all the vectors\"\n                \" are of the same size and do not have missing/null values.\"\n            )\n            raise\n\n        # Determine vector size\n        vector_size = backend.df_engine.compute(proc_df[feature_config[PROC_COLUMN]].map(len).max())\n        vector_size_param = preprocessing_parameters.get(\"vector_size\")\n        if vector_size_param is not None:\n            # TODO(travis): do we even need a user param for vector size if we're going to auto-infer it in all\n            # cases? Is this only useful as a sanity check for the user to make sure their data conforms to\n            # expectations?\n            if vector_size != vector_size_param:\n                raise ValueError(\n                    \"The user provided value for vector size ({}) does not \"\n                    \"match the value observed in the data: {}\".format(preprocessing_parameters, vector_size)\n                )\n        else:\n            logger.debug(f\"Detected vector size: {vector_size}\")\n\n        metadata[feature_config[NAME]][\"vector_size\"] = vector_size\n        return proc_df\n\n\nclass VectorInputFeature(VectorFeatureMixin, InputFeature):\n    def __init__(self, input_feature_config: VectorInputFeatureConfig, encoder_obj=None, **kwargs):\n        super().__init__(input_feature_config, **kwargs)\n\n        # input_feature_config.encoder.input_size = input_feature_config.encoder.vector_size\n        if encoder_obj:\n            self.encoder_obj = encoder_obj\n        else:\n            self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)\n\n    def forward(self, inputs: torch.Tensor) -> torch.Tensor:\n        assert isinstance(inputs, torch.Tensor)\n        assert inputs.dtype in [torch.float32, torch.float64]\n        assert len(inputs.shape) == 2\n\n        inputs_encoded = self.encoder_obj(inputs)\n\n        return inputs_encoded\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.encoder_obj.config.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.encoder_obj.output_shape\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.encoder.input_size = feature_metadata[\"vector_size\"]\n\n    @staticmethod\n    def create_preproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _VectorPreprocessing()\n\n    @staticmethod\n    def get_schema_cls():\n        return VectorInputFeatureConfig\n\n\nclass VectorOutputFeature(VectorFeatureMixin, OutputFeature):\n    def __init__(\n        self,\n        output_feature_config: VectorOutputFeatureConfig | dict,\n        output_features: dict[str, OutputFeature],\n        **kwargs,\n    ):\n        self.vector_size = output_feature_config.vector_size\n        super().__init__(output_feature_config, output_features, **kwargs)\n        output_feature_config.decoder.output_size = self.vector_size\n\n        self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)\n        self._setup_loss()\n        self._setup_metrics()\n\n    def logits(self, inputs, **kwargs):  # hidden\n        hidden = inputs[HIDDEN]\n        return self.decoder_obj(hidden)\n\n    def metric_kwargs(self):\n        return dict(num_outputs=self.output_shape[0])\n\n    def create_predict_module(self) -> PredictModule:\n        return _VectorPredict()\n\n    def get_prediction_set(self):\n        return {PREDICTIONS, LOGITS}\n\n    @classmethod\n    def get_output_dtype(cls):\n        return torch.float32\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.vector_size])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @staticmethod\n    def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):\n        feature_config.vector_size = feature_metadata[\"vector_size\"]\n\n    @staticmethod\n    def calculate_overall_stats(predictions, targets, train_set_metadata):\n        # no overall stats, just return empty dictionary\n        return {}\n\n    def postprocess_predictions(\n        self,\n        result,\n        metadata,\n    ):\n        predictions_col = f\"{self.feature_name}_{PREDICTIONS}\"\n        if predictions_col in result:\n            result[predictions_col] = result[predictions_col].map(lambda pred: pred.tolist())\n        return result\n\n    @staticmethod\n    def create_postproc_module(metadata: TrainingSetMetadataDict) -> torch.nn.Module:\n        return _VectorPostprocessing()\n\n    @staticmethod\n    def get_schema_cls():\n        return VectorOutputFeatureConfig\n"
  },
  {
    "path": "ludwig/forecast.py",
    "content": "import argparse\nimport logging\nimport sys\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n\ndef forecast_cli(\n    model_path: str,\n    dataset: str | dict | pd.DataFrame = None,\n    data_format: str | None = None,\n    horizon: int = 1,\n    output_directory: str | None = None,\n    output_format: str = \"parquet\",\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    logging_level: int = logging.INFO,\n    **kwargs,\n) -> None:\n    \"\"\"Loads pre-trained model to forecast on the provided dataset.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used in the prediction.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.\n    :param horizon: How many samples into the future to forecast.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the forecasted values.\n    :param output_format: (str) format of the output dataset.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param logging_level: (int) Log level that will be sent to stderr.\n\n    # Returns\n\n    :return: ('None')\n    \"\"\"\n    model = LudwigModel.load(\n        model_path,\n        logging_level=logging_level,\n        backend=backend,\n        callbacks=callbacks,\n    )\n    model.forecast(\n        dataset=dataset,\n        data_format=data_format,\n        horizon=horizon,\n        output_directory=output_directory,\n        output_format=output_format,\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model and uses it to forecast\",\n        prog=\"ludwig forecast\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    parser.add_argument(\n        \"-n\", \"--horizon\", help=\"horizon, or number of steps in the future to forecast\", type=int, default=1\n    )\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\"--dataset\", help=\"input data file path\", required=True)\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\",\n            \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n\n    # -------------------------\n    # Output results parameters\n    # -------------------------\n    parser.add_argument(\n        \"-od\", \"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\"\n    )\n\n    parser.add_argument(\n        \"-of\",\n        \"--output_format\",\n        help=\"format to write the output dataset\",\n        default=\"parquet\",\n        choices=[\n            \"csv\",\n            \"parquet\",\n        ],\n    )\n\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"forecast\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.forecast\")\n\n    args.backend = initialize_backend(args.backend)\n    if args.backend.is_coordinator():\n        print_ludwig(\"Forecast\", LUDWIG_VERSION)\n        logger.info(f\"Dataset path: {args.dataset}\")\n        logger.info(f\"Model path: {args.model_path}\")\n        logger.info(\"\")\n\n    forecast_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/globals.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nLUDWIG_VERSION = \"0.11.2\"\n\nMODEL_FILE_NAME = \"model\"\nMODEL_WEIGHTS_FILE_NAME = \"model_weights\"\nMODEL_HYPERPARAMETERS_FILE_NAME = \"model_hyperparameters.json\"\nTRAIN_SET_METADATA_FILE_NAME = \"training_set_metadata.json\"\nTRAINING_PROGRESS_TRACKER_FILE_NAME = \"training_progress.json\"\nTRAINING_CHECKPOINTS_DIR_PATH = \"training_checkpoints\"\n\nTEST_STATISTICS_FILE_NAME = \"test_statistics.json\"\n\nDESCRIPTION_FILE_NAME = \"description.json\"\n\nPREDICTIONS_PARQUET_FILE_NAME = \"predictions.parquet\"\nPREDICTIONS_SHAPES_FILE_NAME = \"predictions.shapes.json\"\n\nTRAINING_PREPROC_FILE_NAME = \"training.hdf5\"\n\nHYPEROPT_STATISTICS_FILE_NAME = \"hyperopt_statistics.json\"\n\nCONFIG_YAML = \"config.yaml\"\n\nDISABLE_PROGRESSBAR = False\n\n\ndef set_disable_progressbar(value):\n    global DISABLE_PROGRESSBAR\n    DISABLE_PROGRESSBAR = value\n\n\ndef is_progressbar_disabled():\n    return DISABLE_PROGRESSBAR\n"
  },
  {
    "path": "ludwig/hyperopt/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/hyperopt/execution.py",
    "content": "import contextlib\nimport copy\nimport datetime\nimport glob\nimport json\nimport logging\nimport os\nimport shutil\nimport sys\nimport tempfile\nimport threading\nimport time\nimport traceback\nimport uuid\nfrom collections.abc import Callable\nfrom functools import lru_cache\nfrom inspect import signature\nfrom pathlib import Path\nfrom typing import Any\n\nimport ray\nfrom ray import tune\nfrom ray.tune import ExperimentAnalysis, PlacementGroupFactory, register_trainable, Stopper\nfrom ray.tune.schedulers.resource_changing_scheduler import DistributeResources, ResourceChangingScheduler\nfrom ray.tune.search import BasicVariantGenerator, ConcurrencyLimiter, SEARCH_ALG_IMPORT\nfrom ray.tune.utils import wait_for_gpu\nfrom ray.util.queue import Queue as RayQueue\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import initialize_backend, RAY\nfrom ludwig.backend.ray import initialize_ray\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import MAXIMIZE, TEST, TRAINER, TRAINING, TYPE, VALIDATION\nfrom ludwig.hyperopt.results import HyperoptResults, TrialResults\nfrom ludwig.hyperopt.search_algos import get_search_algorithm\nfrom ludwig.hyperopt.utils import load_json_values, substitute_parameters\nfrom ludwig.modules.metric_modules import get_best_function\nfrom ludwig.schema.model_types.utils import merge_with_defaults\nfrom ludwig.utils import metric_utils\nfrom ludwig.utils.data_utils import hash_dict, NumpyEncoder\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import has_remote_protocol, safe_move_file\nfrom ludwig.utils.misc_utils import get_from_registry\n\nlogger = logging.getLogger(__name__)\n\n\ndef _patch_bohb_configspace_conversion():\n    \"\"\"Monkey-patch TuneBOHB.convert_search_space for ConfigSpace 1.x compatibility.\n\n    ConfigSpace 1.x removed the `q` (quantization) parameter from hyperparameter classes.\n    Ray Tune's BOHB integration still passes `q=...`, so we patch the converter to drop it.\n    \"\"\"\n    try:\n        # Check if ConfigSpace 1.x (no 'q' parameter)\n        import inspect\n        import math\n\n        import ConfigSpace\n        from ray.tune.search.bohb.bohb_search import TuneBOHB\n        from ray.tune.search.sample import Categorical, Float, Integer, LogUniform, Normal, Quantized, Uniform\n        from ray.tune.search.variant_generator import parse_spec_vars\n        from ray.tune.utils import flatten_dict\n\n        sig = inspect.signature(ConfigSpace.UniformFloatHyperparameter.__init__)\n        if \"q\" in sig.parameters:\n            return  # Old ConfigSpace, no patching needed\n\n        @staticmethod\n        def convert_search_space(spec):\n            resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)\n            if grid_vars:\n                raise ValueError(\n                    \"Grid search parameters cannot be automatically converted \" \"to a TuneBOHB search space.\"\n                )\n            spec = flatten_dict(spec, prevent_delimiter=True)\n            resolved_vars, domain_vars, grid_vars = parse_spec_vars(spec)\n\n            def resolve_value(par, domain):\n                quantize = None\n                sampler = domain.get_sampler()\n                if isinstance(sampler, Quantized):\n                    quantize = sampler.q\n                    sampler = sampler.sampler\n\n                if isinstance(domain, Float):\n                    if isinstance(sampler, LogUniform):\n                        lower = domain.lower\n                        upper = domain.upper\n                        if quantize:\n                            lower = math.ceil(domain.lower / quantize) * quantize\n                            upper = math.floor(domain.upper / quantize) * quantize\n                        return ConfigSpace.UniformFloatHyperparameter(par, lower=lower, upper=upper, log=True)\n                    elif isinstance(sampler, Uniform):\n                        lower = domain.lower\n                        upper = domain.upper\n                        if quantize:\n                            lower = math.ceil(domain.lower / quantize) * quantize\n                            upper = math.floor(domain.upper / quantize) * quantize\n                        return ConfigSpace.UniformFloatHyperparameter(par, lower=lower, upper=upper, log=False)\n                    elif isinstance(sampler, Normal):\n                        return ConfigSpace.hyperparameters.NormalFloatHyperparameter(\n                            par, mu=sampler.mean, sigma=sampler.sd, log=False\n                        )\n                elif isinstance(domain, Integer):\n                    if isinstance(sampler, LogUniform):\n                        lower = domain.lower\n                        upper = domain.upper\n                        if quantize:\n                            lower = math.ceil(domain.lower / quantize) * quantize\n                            upper = math.floor(domain.upper / quantize) * quantize\n                        else:\n                            upper -= 1\n                        return ConfigSpace.UniformIntegerHyperparameter(par, lower=lower, upper=upper, log=True)\n                    elif isinstance(sampler, Uniform):\n                        lower = domain.lower\n                        upper = domain.upper\n                        if quantize:\n                            lower = math.ceil(domain.lower / quantize) * quantize\n                            upper = math.floor(domain.upper / quantize) * quantize\n                        else:\n                            upper -= 1\n                        return ConfigSpace.UniformIntegerHyperparameter(par, lower=lower, upper=upper, log=False)\n                elif isinstance(domain, Categorical):\n                    if isinstance(sampler, Uniform):\n                        return ConfigSpace.CategoricalHyperparameter(par, choices=domain.categories)\n\n                raise ValueError(\n                    \"TuneBOHB does not support parameters of type \"\n                    \"`{}` with samplers of type `{}`\".format(type(domain).__name__, type(domain.sampler).__name__)\n                )\n\n            cs = ConfigSpace.ConfigurationSpace()\n            for path, domain in domain_vars:\n                par = \"/\".join(str(p) for p in path)\n                value = resolve_value(par, domain)\n                cs.add_hyperparameter(value)\n            return cs\n\n        TuneBOHB.convert_search_space = convert_search_space\n        logger.info(\"Patched TuneBOHB.convert_search_space for ConfigSpace 1.x compatibility\")\n\n    except ImportError:\n        pass  # BOHB not installed\n\n\n_patch_bohb_configspace_conversion()\n\n\ntry:\n    from ludwig.backend.ray import RayBackend\n\n    # TODO: refactor this into an interface\n    def _is_ray_backend(backend) -> bool:\n        if isinstance(backend, str):\n            return backend == RAY\n        return isinstance(backend, RayBackend)\n\nexcept ImportError as e:\n    logger.warning(\n        f\"ImportError (execution.py) failed to import RayBackend with error: \\n\\t{e}. \"\n        \"The LocalBackend will be used instead. If you want to use the RayBackend, please install ludwig[distributed].\"\n    )\n\n    class RayBackend:\n        pass\n\n    def _is_ray_backend(backend) -> bool:\n        return False\n\n\ndef identity(x):\n    return x\n\n\ndef _get_relative_checkpoints_dir_parts(path: Path):\n    return path.parts[-2:]\n\n\n# Follwing disabled at the moment, expect to be re-enabled pending https://github.com/ludwig-ai/ludwig/issues/2039\ndef ray_resource_allocation_function(\n    trial_runner: \"trial_runner.TrialRunner\",  # noqa\n    trial: \"Trial\",  # noqa\n    result: dict[str, Any],\n    scheduler: \"ResourceChangingScheduler\",\n):\n    \"\"\"Determine resources to allocate to running trials.\"\"\"\n    pgf = DistributeResources(trial_runner, trial, result, scheduler)\n    # restore original base trial resources\n\n    # create bundles\n    if scheduler.base_trial_resources.required_resources.get(\"GPU\", 0):\n        bundles = [{\"CPU\": 1, \"GPU\": 1}] * int(pgf.required_resources[\"GPU\"])\n    else:\n        bundles = [{\"CPU\": 1}] * (int(pgf.required_resources[\"CPU\"] - 0.001))\n    # we can't set Trial actor's CPUs to 0 so we just go very low\n    bundles = [{\"CPU\": 0.001}] + bundles\n    pgf = PlacementGroupFactory(bundles)\n    return pgf\n\n\ndef _create_tune_checkpoint(save_path):\n    \"\"\"Create a Ray Tune Checkpoint from a model save path.\"\"\"\n\n    def ignore_dot_files(src, files):\n        return [f for f in files if f.startswith(\".\")]\n\n    tmpdir = tempfile.mkdtemp()\n    checkpoint_model = os.path.join(tmpdir, \"model\")\n    if os.path.exists(save_path):\n        copy_id = uuid.uuid4()\n        tmp_dst = f\"{checkpoint_model}.{copy_id}.tmp\"\n        shutil.copytree(save_path, tmp_dst, ignore=ignore_dot_files)\n        try:\n            os.rename(tmp_dst, checkpoint_model)\n        except Exception:\n            shutil.rmtree(tmp_dst)\n\n    return tune.Checkpoint.from_directory(tmpdir)\n\n\nclass RayTuneExecutor:\n    def __init__(\n        self,\n        parameters: dict,\n        output_feature: str,\n        metric: str,\n        goal: str,\n        split: str,\n        search_alg: dict | None = None,\n        cpu_resources_per_trial: int = None,\n        gpu_resources_per_trial: int = None,\n        kubernetes_namespace: str = None,\n        time_budget_s: int | float | datetime.timedelta = None,\n        max_concurrent_trials: int | None = None,\n        num_samples: int = 1,\n        scheduler: dict | None = None,\n        **kwargs,\n    ) -> None:\n        if ray is None:\n            raise ImportError(\"ray module is not installed. To install it, try running pip install ray\")\n        self.output_feature = output_feature\n        self.metric = metric\n        self.split = split\n        initialize_ray()\n        self.search_space, self.decode_ctx = self._get_search_space(parameters)\n        self.num_samples = num_samples\n        self.goal = goal\n        self.search_algorithm = get_search_algorithm(search_alg)\n        self.scheduler = None if scheduler is None else tune.create_scheduler(scheduler[TYPE], **scheduler)\n        self.output_feature = output_feature\n        self.metric = metric\n        self.split = split\n        self.trial_id = 0\n        self.cpu_resources_per_trial = cpu_resources_per_trial\n        self.gpu_resources_per_trial = gpu_resources_per_trial\n        self.kubernetes_namespace = kubernetes_namespace\n        self.time_budget_s = time_budget_s\n        self.max_concurrent_trials = max_concurrent_trials\n        self.sync_config = None\n        self.sync_client = None\n        # Head node is the node to which all checkpoints are synced if running on a K8s cluster.\n        self.head_node_ip = ray.util.get_node_ip_address()\n\n    def _get_search_space(self, parameters: dict) -> tuple[dict, dict]:\n        \"\"\"Encode search space parameters as JSON with context for decoding.\"\"\"\n        config = {}\n        ctx = {}\n        for param, values in parameters.items():\n            # Encode list and dict types as JSON encoded strings to\n            # workaround type limitations of the underlying frameworks\n            values = self.encode_values(param, values, ctx)\n\n            param_search_type = values[\"space\"].lower()\n            if hasattr(tune, param_search_type):\n                param_search_space = getattr(tune, param_search_type)\n            else:\n                raise ValueError(f\"'{param_search_type}' is not a supported Ray Tune search space\")\n\n            param_search_input_args = {}\n            param_search_space_sig = signature(param_search_space)\n            for arg in param_search_space_sig.parameters.values():\n                if arg.name in values:\n                    param_search_input_args[arg.name] = values[arg.name]\n                else:\n                    if arg.default is arg.empty:\n                        raise ValueError(f\"Parameter '{arg}' not defined for {param}\")\n            config[param] = param_search_space(**param_search_input_args)\n        return config, ctx\n\n    @staticmethod\n    def encode_values(param: str, values: dict, ctx: dict) -> dict:\n        \"\"\"JSON encodes any search spaces whose values are lists / dicts.\n\n        Only applies to grid search and choice options.  See here for details:\n\n        https://docs.ray.io/en/master/tune/api_docs/search_space.html#random-distributions-api\n        \"\"\"\n        values = values.copy()\n        for key in [\"values\", \"categories\"]:\n            if key in values and not isinstance(values[key][0], (int, float)):\n                values[key] = [json.dumps(v) for v in values[key]]\n                ctx[param] = json.loads\n        return values\n\n    @staticmethod\n    def decode_values(config: dict, ctx: dict) -> dict:\n        \"\"\"Decode config values with the decode function in the context.\n\n        Uses the identity function if no encoding is needed.\n        \"\"\"\n        return {key: ctx.get(key, identity)(value) for key, value in config.items()}\n\n    def _has_metric(self, stats, split):\n        if not stats:\n            return False\n\n        if split is not None:\n            if split not in stats:\n                return False\n            stats = stats[split]\n\n        if self.output_feature not in stats:\n            return False\n        stats = stats[self.output_feature]\n\n        if self.metric not in stats:\n            return False\n        stats = stats[self.metric]\n        return len(stats) > 0\n\n    def _has_eval_metric(self, stats):\n        if stats is None:\n            return False\n\n        if self.output_feature not in stats:\n            return False\n        stats = stats[self.output_feature]\n\n        for metric_part in self.metric.split(\".\"):\n            if not isinstance(stats, dict) or metric_part not in stats:\n                return False\n            stats = stats[metric_part]\n        return isinstance(stats, float)\n\n    def get_metric_score(self, train_stats) -> float:\n        if self._has_metric(train_stats, VALIDATION):\n            logger.info(\"Returning metric score from training (validation) statistics\")\n            return self.get_metric_score_from_train_stats(train_stats, VALIDATION)\n        elif self._has_metric(train_stats, TRAINING):\n            logger.info(\"Returning metric score from training split statistics, \" \"as no validation was given\")\n            return self.get_metric_score_from_train_stats(train_stats, TRAINING)\n        else:\n            raise RuntimeError(\"Unable to obtain metric score from missing training (validation) statistics\")\n\n    def get_metric_score_from_eval_stats(self, eval_stats) -> float | list:\n        stats = eval_stats[self.output_feature]\n        for metric_part in self.metric.split(\".\"):\n            if isinstance(stats, dict):\n                if metric_part in stats:\n                    stats = stats[metric_part]\n                else:\n                    raise ValueError(f\"Evaluation statistics do not contain the metric {self.metric}\")\n            else:\n                raise ValueError(f\"Evaluation statistics do not contain the metric {self.metric}\")\n\n        if not isinstance(stats, float):\n            raise ValueError(f\"The metric {self.metric} in evaluation statistics is not a numerical value: {stats}\")\n        return stats\n\n    def get_metric_score_from_train_stats(self, train_stats, select_split=None) -> float:\n        select_split = select_split or VALIDATION\n\n        # grab the results of the model with highest validation test performance\n        train_valiset_stats = train_stats[select_split]\n\n        validation_field_result = train_valiset_stats[self.output_feature]\n        best_function = get_best_function(self.metric)\n\n        # results of the model with highest validation test performance\n        epoch_best_validation_metric, best_validation_metric = best_function(\n            enumerate(validation_field_result[self.metric]), key=lambda pair: pair[1]\n        )\n\n        return best_validation_metric\n\n    def sort_hyperopt_results(self, hyperopt_results):\n        return sorted(\n            hyperopt_results, key=lambda hp_res: hp_res.metric_score, reverse=self.hyperopt_sampler.goal == MAXIMIZE\n        )\n\n    @property\n    def _cpu_resources_per_trial_non_none(self):\n        return self.cpu_resources_per_trial if self.cpu_resources_per_trial is not None else 1\n\n    @property\n    def _gpu_resources_per_trial_non_none(self):\n        return self.gpu_resources_per_trial if self.gpu_resources_per_trial is not None else 0\n\n    def _get_remote_checkpoint_dir(self, trial_dir: Path) -> str | tuple[str, str] | None:\n        \"\"\"Get the path to remote checkpoint directory.\"\"\"\n        if self.sync_config is None:\n            return None\n\n        if self.sync_config.upload_dir is not None:\n            # Cloud storage sync config\n            remote_checkpoint_dir = os.path.join(\n                self.sync_config.upload_dir, *_get_relative_checkpoints_dir_parts(trial_dir)\n            )\n            return remote_checkpoint_dir\n        elif self.kubernetes_namespace is not None:\n            # Kubernetes sync config. Returns driver node name and path.\n            # When running on kubernetes, each trial is rsynced to the node running the main process.\n            node_name = self._get_kubernetes_node_address_by_ip()(self.head_node_ip)\n            return (node_name, trial_dir)\n        else:\n            logger.warning(\n                \"Checkpoint syncing disabled as syncing is only supported to remote cloud storage or on Kubernetes \"\n                \"clusters is supported. To use syncing, set the kubernetes_namespace in the config or use a cloud URI \"\n                \"as the output directory.\"\n            )\n            return None\n\n    @lru_cache(maxsize=1)\n    def _get_kubernetes_node_address_by_ip(self) -> Callable:\n        \"\"\"Returns a method to get the node name by IP address within a K8s cluster.\"\"\"\n        assert self.kubernetes_namespace is not None\n        from ray.tune.integration.kubernetes import KubernetesSyncer\n\n        # Initialized with null local and remote directories as we only need to use get_node_address_by_ip.\n        kubernetes_syncer = KubernetesSyncer(None, None)\n\n        return kubernetes_syncer.get_node_address_by_ip\n\n    # For specified [stopped] trial, remove checkpoint marker on any partial checkpoints\n    @staticmethod\n    def _remove_partial_checkpoints(trial_path: str):\n        marker_paths = glob.glob(os.path.join(glob.escape(trial_path), \"checkpoint_*/.is_checkpoint\"))\n        for marker_path in marker_paths:\n            chkpt_dir = os.path.dirname(marker_path)\n            metadata_file = glob.glob(os.path.join(glob.escape(chkpt_dir), \"*.tune_metadata\"))\n            # glob.glob: filenames starting with a dot are special cases\n            # that are not matched by '*' and '?' patterns.\n            metadata_file += glob.glob(os.path.join(glob.escape(chkpt_dir), \".tune_metadata\"))\n            metadata_file = list(set(metadata_file))  # avoid duplication\n            if len(metadata_file) < 1:\n                # Remove checkpoint marker on incomplete directory\n                os.remove(marker_path)\n\n    @contextlib.contextmanager\n    def _get_best_model_path(self, trial_or_path, analysis: ExperimentAnalysis) -> str:\n        # Accept either a Trial object or a path string\n        from ray.tune.experiment.trial import Trial\n\n        if isinstance(trial_or_path, str):\n            trial_path = trial_or_path\n        else:\n            trial_path = trial_or_path.local_path\n\n        remote_checkpoint_dir = self._get_remote_checkpoint_dir(Path(trial_path))\n        if remote_checkpoint_dir is not None and self.sync_client is not None:\n            self.sync_client.sync_down(remote_checkpoint_dir, trial_path)\n            self.sync_client.wait_or_retry()\n        self._remove_partial_checkpoints(trial_path)  # needed by get_best_checkpoint\n\n        # get_best_checkpoint requires a Trial object in Ray 2.x\n        if isinstance(trial_or_path, Trial):\n            trial = trial_or_path\n        else:\n            # Try to find the trial by matching its path\n            trial = None\n            for t in analysis.trials:\n                if t.local_path and t.local_path.rstrip(\"/\") == trial_path.rstrip(\"/\"):\n                    trial = t\n                    break\n\n        try:\n            if trial is not None:\n                checkpoint = analysis.get_best_checkpoint(trial)\n            else:\n                checkpoint = None\n        except Exception:\n            logger.warning(\n                f\"Cannot get best model path for {trial_path} due to exception below:\" f\"\\n{traceback.format_exc()}\"\n            )\n            yield None\n            return\n\n        if checkpoint is not None:\n            with checkpoint.as_directory() as path:\n                yield path\n        else:\n            yield checkpoint\n\n    @staticmethod\n    def _evaluate_best_model(\n        trial,\n        trial_path,\n        best_model_path,\n        dataset,\n        data_format,\n        skip_save_unprocessed_output,\n        skip_save_predictions,\n        skip_save_eval_stats,\n        gpus,\n        gpu_memory_limit,\n        allow_parallel_threads,\n        backend,\n        debug,\n    ):\n        model_path = os.path.join(best_model_path, \"model\")\n        if not os.path.isdir(model_path):\n            logger.warning(\n                f\"Best model path {model_path} does not exist or is incomplete. \"\n                \"This can happen when time budget expires mid-checkpoint. Skipping evaluation.\"\n            )\n            return\n        best_model = LudwigModel.load(\n            model_path,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            from_checkpoint=True,\n        )\n        if best_model.config[TRAINER][\"eval_batch_size\"]:\n            batch_size = best_model.config[TRAINER][\"eval_batch_size\"]\n        else:\n            batch_size = best_model.config[TRAINER][\"batch_size\"]\n        try:\n            eval_stats, _, _ = best_model.evaluate(\n                dataset=dataset,\n                data_format=data_format,\n                batch_size=batch_size,\n                output_directory=trial_path,\n                skip_save_unprocessed_output=skip_save_unprocessed_output,\n                skip_save_predictions=skip_save_predictions,\n                skip_save_eval_stats=skip_save_eval_stats,\n                collect_predictions=False,\n                collect_overall_stats=True,\n                return_type=\"dict\",\n                debug=debug,\n            )\n            trial[\"eval_stats\"] = json.dumps(eval_stats, cls=NumpyEncoder)\n        except NotImplementedError:\n            logger.warning(\n                \"Skipping evaluation as the necessary methods are not \"\n                \"supported. Full exception below:\\n\"\n                f\"{traceback.format_exc()}\"\n            )\n\n    def _run_experiment(\n        self,\n        config,\n        checkpoint_dir,\n        hyperopt_dict,\n        decode_ctx,\n        is_using_ray_backend=False,\n    ):\n        # Ray Tune redirects stdout/stderr through a Tee object that may not\n        # implement isatty(), which ray.data's progress bar code requires.\n        # Patch it to avoid AttributeError.\n        for stream in (sys.stdout, sys.stderr):\n            if not hasattr(stream, \"isatty\"):\n                stream.isatty = lambda: False\n\n        for gpu_id in ray.get_gpu_ids():\n            # Previous trial may not have freed its memory yet, so wait to avoid OOM\n            wait_for_gpu(gpu_id)\n\n        # Some config values may be JSON encoded as strings, so decode them here\n        config = self.decode_values(config, decode_ctx)\n\n        # Remove mlflow injected config parameters: https://github.com/ludwig-ai/ludwig/issues/2288\n        if \"mlflow\" in config:\n            del config[\"mlflow\"]\n\n        trial_id = tune.get_context().get_trial_id()\n        trial_dir = Path(tune.get_context().get_trial_dir())\n\n        modified_config = substitute_parameters(copy.deepcopy(hyperopt_dict[\"config\"]), config)\n\n        modified_config = merge_with_defaults(modified_config)\n\n        hyperopt_dict[\"config\"] = modified_config\n        hyperopt_dict[\"experiment_name \"] = f'{hyperopt_dict[\"experiment_name\"]}_{trial_id}'\n        hyperopt_dict[\"output_directory\"] = str(trial_dir)\n\n        tune_executor = self\n        if is_using_ray_backend:\n            ray_queue = RayQueue(actor_options={\"num_cpus\": 0})\n        else:\n            ray_queue = None\n\n        def report(progress_tracker, save_path=None):\n            # The progress tracker's metrics are nested dictionaries of TrainerMetrics: feature_name -> metric_name ->\n            # List[TrainerMetric], with one entry per training checkpoint, according to steps_per_checkpoint.\n            # We reduce the dictionary of TrainerMetrics to a simple list of floats for interfacing with Ray Tune.\n            train_stats = {\n                TRAINING: metric_utils.reduce_trainer_metrics_dict(progress_tracker.train_metrics),\n                VALIDATION: metric_utils.reduce_trainer_metrics_dict(progress_tracker.validation_metrics),\n                TEST: metric_utils.reduce_trainer_metrics_dict(progress_tracker.test_metrics),\n            }\n\n            metric_score = tune_executor.get_metric_score(train_stats)\n            report_kwargs = {\n                \"metrics\": {\n                    \"parameters\": json.dumps(config, cls=NumpyEncoder),\n                    \"metric_score\": metric_score,\n                    \"training_stats\": json.dumps(train_stats, cls=NumpyEncoder),\n                    \"eval_stats\": \"{}\",\n                    \"trial_id\": tune.get_context().get_trial_id(),\n                    \"trial_dir\": str(tune.get_context().get_trial_dir()),\n                }\n            }\n            if save_path is not None:\n                report_kwargs[\"checkpoint\"] = _create_tune_checkpoint(save_path)\n            tune.report(**report_kwargs)\n\n        class RayTuneReportCallback(Callback):\n            def __init__(self):\n                super().__init__()\n                self.last_steps = 0\n                self.resume_ckpt_dir = None\n\n            def _get_remote_checkpoint_dir(self) -> str | tuple[str, str] | None:\n                # sync client has to be recreated to avoid issues with serialization\n                return tune_executor._get_remote_checkpoint_dir(trial_dir)\n\n            def _checkpoint_progress(self, trainer, progress_tracker, save_path) -> None:\n                \"\"\"Checkpoints the progress tracker.\"\"\"\n                if is_using_ray_backend:\n                    # Pass the save_path directly through the queue. On single-node clusters,\n                    # the trial driver and training workers share the same filesystem.\n                    # For multi-node, the checkpoint should be on shared storage.\n                    ray_queue.put((progress_tracker, save_path))\n                    return\n                # For non-Ray backend, report metrics + checkpoint together\n                report(progress_tracker, save_path=save_path)\n\n            def on_train_start(self, model, config: dict[str, Any], config_fp: str | None):\n                if is_using_ray_backend and checkpoint_dir:\n                    # Store the checkpoint directory path for syncing to the trainer worker.\n                    self.resume_ckpt_dir = checkpoint_dir\n\n            def on_trainer_train_setup(self, trainer, save_path, is_coordinator):\n                # Check local rank before manipulating files, as otherwise there will be a race condition\n                # between multiple workers running on the same node.\n                if self.resume_ckpt_dir is not None and trainer.local_rank == 0:\n                    # Resume from a previous checkpoint by syncing files from the checkpoint\n                    # directory to the save_path.\n                    ckpt_path = self.resume_ckpt_dir\n                    # Attempt an atomic move from the ckpt_path to the save_path\n                    # This may first require removing the existing save_path\n                    tmp_path = save_path + \".tmp\"\n                    if os.path.exists(save_path):\n                        os.rename(save_path, tmp_path)\n\n                    try:\n                        model_path = os.path.join(ckpt_path, \"model\")\n                        if os.path.exists(model_path):\n                            safe_move_file(model_path, save_path)\n                        elif os.path.exists(ckpt_path):\n                            safe_move_file(ckpt_path, save_path)\n                    except Exception:\n                        # Rollback from partial changes. Remove the save_path\n                        # and move the original save_path back.\n                        if os.path.exists(save_path):\n                            shutil.rmtree(save_path)\n                        if os.path.exists(tmp_path):\n                            os.rename(tmp_path, save_path)\n                        raise\n\n                    # Cleanup the backup save_path as it's no longer needed\n                    if os.path.exists(tmp_path):\n                        shutil.rmtree(tmp_path)\n\n                # Sync all workers here before continuing to training\n                trainer.barrier()\n\n            def on_eval_end(self, trainer, progress_tracker, save_path):\n                progress_tracker.tune_checkpoint_num += 1\n                self.last_steps = progress_tracker.steps\n                self._checkpoint_progress(trainer, progress_tracker, save_path)\n\n            def on_trainer_train_teardown(self, trainer, progress_tracker, save_path, is_coordinator):\n                if is_coordinator and progress_tracker.steps > self.last_steps:\n                    # Note: Calling tune.report in both on_eval_end() and here can cause multiprocessing issues\n                    # for some ray samplers if not steps have happened since the last eval.\n                    self._checkpoint_progress(trainer, progress_tracker, save_path)\n\n        callbacks = hyperopt_dict.get(\"callbacks\") or []\n        hyperopt_dict[\"callbacks\"] = callbacks + [RayTuneReportCallback()]\n\n        # set tune resources\n        if is_using_ray_backend:\n            resources = tune.get_context().get_trial_resources()\n            # check if we are using at least 1 gpu per trial\n            use_gpu = bool(self._gpu_resources_per_trial_non_none)\n            # get the resources assigned to the current trial\n            num_gpus = resources.required_resources.get(\"GPU\", 0)\n            num_cpus = resources.required_resources.get(\"CPU\", 1) if num_gpus == 0 else 0\n\n            distributed_kwargs = {\n                \"num_workers\": int(num_gpus) if use_gpu else 1,\n                \"use_gpu\": use_gpu,\n                \"resources_per_worker\": {\n                    \"CPU\": num_cpus,\n                    \"GPU\": 1 if use_gpu else 0,\n                },\n            }\n            hyperopt_dict[\"backend\"].set_distributed_kwargs(**distributed_kwargs)\n\n            logger.debug(f\"Trial distributed kwargs: {distributed_kwargs}\")\n\n        stats = []\n        thread_error = [None]  # Use list to allow mutation from nested function\n\n        def _run():\n            try:\n                train_stats, eval_stats = run_experiment(\n                    **hyperopt_dict,\n                    model_resume_path=checkpoint_dir,\n                    parameters=config,\n                )\n                stats.append((train_stats, eval_stats))\n            except Exception as e:\n                thread_error[0] = e\n                logger.error(f\"Error in hyperopt trial thread: {e}\")\n\n        if is_using_ray_backend:\n            # We have to pull the results to the trial actor\n            # from worker actors, as the Tune session is running\n            # only on the trial actor\n            thread = threading.Thread(target=_run)\n            thread.daemon = True\n            thread.start()\n\n            def check_queue():\n                qsize = ray_queue.qsize()\n                if qsize:\n                    results = ray_queue.get_nowait_batch(qsize)\n                    for progress_tracker, save_path in results:\n                        report(progress_tracker, save_path=save_path)\n\n            while thread.is_alive():\n                thread.join(timeout=0)\n                check_queue()\n                time.sleep(0.1)\n            thread.join()\n            check_queue()\n        else:\n            # remove threading overhead\n            _run()\n\n        if thread_error[0] is not None:\n            raise RuntimeError(f\"Experiment failed: {thread_error[0]}\") from thread_error[0]\n        if not stats:\n            raise RuntimeError(\"Experiment did not complete.\")\n        train_stats, eval_stats = stats.pop()\n\n        metric_score = self.get_metric_score(train_stats)\n        tune.report(\n            metrics={\n                \"parameters\": json.dumps(config, cls=NumpyEncoder),\n                \"metric_score\": metric_score,\n                \"training_stats\": json.dumps(train_stats, cls=NumpyEncoder),\n                \"eval_stats\": json.dumps(eval_stats, cls=NumpyEncoder),\n                \"trial_id\": tune.get_context().get_trial_id(),\n                \"trial_dir\": str(tune.get_context().get_trial_dir()),\n            }\n        )\n\n    def execute(\n        self,\n        config,\n        dataset=None,\n        training_set=None,\n        validation_set=None,\n        test_set=None,\n        training_set_metadata=None,\n        data_format=None,\n        experiment_name=\"hyperopt\",\n        model_name=\"run\",\n        resume=None,\n        skip_save_training_description=False,\n        skip_save_training_statistics=False,\n        skip_save_model=False,\n        skip_save_progress=False,\n        skip_save_log=False,\n        skip_save_processed_input=True,\n        skip_save_unprocessed_output=False,\n        skip_save_predictions=False,\n        skip_save_eval_stats=False,\n        output_directory=\"results\",\n        gpus=None,\n        gpu_memory_limit=None,\n        allow_parallel_threads=True,\n        callbacks=None,\n        tune_callbacks=None,\n        backend=None,\n        random_seed=default_random_seed,\n        debug=False,\n        hyperopt_log_verbosity=3,\n        **kwargs,\n    ) -> HyperoptResults:\n        if isinstance(dataset, str) and not has_remote_protocol(dataset) and not os.path.isabs(dataset):\n            dataset = os.path.abspath(dataset)\n\n        # Ray Tune / PyArrow requires absolute paths or URIs for storage_path\n        if not has_remote_protocol(output_directory) and not os.path.isabs(output_directory):\n            output_directory = os.path.abspath(output_directory)\n\n        if isinstance(backend, str):\n            backend = initialize_backend(backend)\n\n        if gpus is not None:\n            raise ValueError(\n                \"Parameter `gpus` is not supported when using Ray Tune. \"\n                \"Configure GPU resources with Ray and set `gpu_resources_per_trial` in your \"\n                \"hyperopt config.\"\n            )\n\n        if gpu_memory_limit is None and 0 < self._gpu_resources_per_trial_non_none < 1:\n            # Enforce fractional GPU utilization\n            gpu_memory_limit = self.gpu_resources_per_trial\n\n        hyperopt_dict = dict(\n            config=config,\n            dataset=dataset,\n            training_set=training_set,\n            validation_set=validation_set,\n            test_set=test_set,\n            training_set_metadata=training_set_metadata,\n            data_format=data_format,\n            experiment_name=experiment_name,\n            model_name=model_name,\n            eval_split=self.split,\n            skip_save_training_description=skip_save_training_description,\n            skip_save_training_statistics=skip_save_training_statistics,\n            skip_save_model=skip_save_model,\n            skip_save_progress=skip_save_progress,\n            skip_save_log=skip_save_log,\n            skip_save_processed_input=skip_save_processed_input,\n            skip_save_unprocessed_output=skip_save_unprocessed_output,\n            skip_save_predictions=skip_save_predictions,\n            skip_save_eval_stats=skip_save_eval_stats,\n            output_directory=output_directory,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n            backend=backend,\n            random_seed=random_seed,\n            debug=debug,\n        )\n\n        mode = \"min\" if self.goal != MAXIMIZE else \"max\"\n        metric = \"metric_score\"\n        # if random seed not set, use Ludwig seed\n        self.search_algorithm.check_for_random_seed(random_seed)\n        if self.search_algorithm.search_alg_dict is not None:\n            if TYPE not in self.search_algorithm.search_alg_dict:\n                candiate_search_algs = [search_alg for search_alg in SEARCH_ALG_IMPORT.keys()]\n                logger.warning(\n                    \"WARNING: search_alg type parameter missing, using 'variant_generator' as default. \"\n                    f\"These are possible values for the type parameter: {candiate_search_algs}.\"\n                )\n                search_alg = None\n            else:\n                search_alg_type = self.search_algorithm.search_alg_dict[TYPE]\n                search_alg = tune.create_searcher(\n                    search_alg_type, metric=metric, mode=mode, **self.search_algorithm.search_alg_dict\n                )\n        else:\n            search_alg = None\n\n        if self.max_concurrent_trials:\n            assert (\n                self.max_concurrent_trials > 0\n            ), f\"`max_concurrent_trials` must be greater than 0, got {self.max_concurrent_trials}\"\n            if isinstance(search_alg, BasicVariantGenerator) or search_alg is None:\n                search_alg = BasicVariantGenerator(max_concurrent=self.max_concurrent_trials)\n            elif isinstance(search_alg, ConcurrencyLimiter):\n                raise ValueError(\n                    \"You have specified `max_concurrent_trials`, but the search \"\n                    \"algorithm is already a `ConcurrencyLimiter`. FIX THIS \"\n                    \"by setting `max_concurrent_trials=None`.\"\n                )\n            else:\n                search_alg = ConcurrencyLimiter(search_alg, max_concurrent=self.max_concurrent_trials)\n\n        resources_per_trial = {\n            \"cpu\": self._cpu_resources_per_trial_non_none,\n            \"gpu\": self._gpu_resources_per_trial_non_none,\n        }\n\n        def run_experiment_trial(config, local_hyperopt_dict, checkpoint_dir=None):\n            return self._run_experiment(\n                config,\n                checkpoint_dir,\n                local_hyperopt_dict,\n                self.decode_ctx,\n                _is_ray_backend(backend),\n            )\n\n        tune_config = {}\n        _tune_callbacks = list(tune_callbacks or [])\n        for callback in callbacks or []:\n            run_experiment_trial, tune_config = callback.prepare_ray_tune(\n                run_experiment_trial,\n                tune_config,\n                _tune_callbacks,\n            )\n        tune_callbacks = _tune_callbacks\n\n        if _is_ray_backend(backend):\n            # for now, we do not do distributed training on cpu (until spread scheduling is implemented for Ray Train)\n            # but we do want to enable it when GPUs are specified\n            resources_per_trial = PlacementGroupFactory(\n                [{}] + ([{\"CPU\": 0, \"GPU\": 1}] * self._gpu_resources_per_trial_non_none)\n                if self._gpu_resources_per_trial_non_none\n                else [{}] + [{\"CPU\": self._cpu_resources_per_trial_non_none}]\n            )\n\n        if has_remote_protocol(output_directory):\n            # In Ray 2.x, remote storage is handled via RunConfig storage_path\n            self.sync_config = tune.SyncConfig()\n            self.sync_client = None\n            # output_directory will be used as storage_path\n        elif self.kubernetes_namespace:\n            logger.warning(\n                \"Kubernetes-specific syncing is no longer supported in Ray 2.x. \"\n                \"Use cloud storage (S3, GCS) as the output directory instead.\"\n            )\n\n        run_experiment_trial_params = tune.with_parameters(run_experiment_trial, local_hyperopt_dict=hyperopt_dict)\n\n        @ray.remote\n        def _register(name, trainable):\n            register_trainable(name, trainable)\n\n        ray.get(_register.remote(f\"trainable_func_f{hash_dict(config).decode('ascii')}\", run_experiment_trial_params))\n\n        # Note that resume=\"AUTO\" will attempt to resume the experiment if possible, and\n        # otherwise will start a new experiment:\n        # https://docs.ray.io/en/latest/tune/tutorials/tune-stopping.html\n        should_resume = \"AUTO\" if resume is None else resume\n\n        # If the output directory is an S3 path and AWS_ENDPOINT_URL is set,\n        # configure a custom S3 filesystem for Ray Tune. We use fsspec's s3fs\n        # wrapped in PyArrow's FSSpecHandler because PyArrow's native S3 C++\n        # client doesn't read AWS_ENDPOINT_URL and its chunked transfer encoding\n        # is incompatible with some S3-compatible stores (e.g. MinIO).\n        storage_filesystem = None\n        if output_directory and str(output_directory).startswith(\"s3://\"):\n            endpoint_url = os.environ.get(\"AWS_ENDPOINT_URL\")\n            if endpoint_url:\n                import pyarrow.fs\n                import s3fs\n\n                s3 = s3fs.S3FileSystem(\n                    endpoint_url=endpoint_url,\n                    key=os.environ.get(\"AWS_ACCESS_KEY_ID\"),\n                    secret=os.environ.get(\"AWS_SECRET_ACCESS_KEY\"),\n                )\n                storage_filesystem = pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(s3))\n                # When storage_filesystem is set, storage_path must be a plain\n                # path (bucket/key...), not a URI (s3://bucket/key...).\n                output_directory = str(output_directory).removeprefix(\"s3://\")\n\n        try:\n            analysis = tune.run(\n                f\"trainable_func_f{hash_dict(config).decode('ascii')}\",\n                name=experiment_name,\n                config={\n                    **self.search_space,\n                    **tune_config,\n                },\n                scheduler=self.scheduler,\n                search_alg=search_alg,\n                num_samples=self.num_samples,\n                checkpoint_config=tune.CheckpointConfig(num_to_keep=1),\n                max_failures=1,  # retry a trial failure once\n                resources_per_trial=resources_per_trial,\n                time_budget_s=self.time_budget_s,\n                sync_config=self.sync_config,\n                storage_path=output_directory,\n                storage_filesystem=storage_filesystem,\n                metric=metric,\n                mode=mode,\n                trial_name_creator=lambda trial: f\"trial_{trial.trial_id}\",\n                trial_dirname_creator=lambda trial: f\"trial_{trial.trial_id}\",\n                callbacks=tune_callbacks,\n                stop=CallbackStopper(callbacks),\n                verbose=hyperopt_log_verbosity,\n                resume=should_resume,\n                log_to_file=True,\n            )\n        except Exception as e:\n            # Explicitly raise a RuntimeError if an error is encountered during a Ray trial.\n            # NOTE: Cascading the exception with \"raise _ from e\" still results in hanging.\n            raise RuntimeError(f\"Encountered Ray Tune error: {e}\")\n\n        if \"metric_score\" in analysis.results_df.columns:\n            ordered_trials = analysis.results_df.sort_values(\"metric_score\", ascending=self.goal != MAXIMIZE)\n\n            # Catch nans in edge case where the trial doesn't complete\n            temp_ordered_trials = []\n            for kwargs in ordered_trials.to_dict(orient=\"records\"):\n                for key in [\"parameters\", \"training_stats\", \"eval_stats\"]:\n                    if isinstance(kwargs[key], float):\n                        kwargs[key] = {}\n                temp_ordered_trials.append(kwargs)\n\n            # Trials w/empty eval_stats fields & non-empty training_stats fields ran intermediate\n            # tune.report call(s) but were terminated before reporting eval_stats from post-train\n            # evaluation (e.g., trial stopped due to time budget or relatively poor performance.)\n            # For any such trials, run model evaluation for the best model in that trial & record\n            # results in ordered_trials which is returned & is persisted in hyperopt_statistics.json.\n            for trial in temp_ordered_trials:\n                if trial[\"eval_stats\"] == \"{}\" and trial[\"training_stats\"] != \"{}\":\n                    # Evaluate the best model on the eval_split, which is validation_set\n                    if validation_set is not None and validation_set.size > 0:\n                        trial_path = trial[\"trial_dir\"]\n                        with self._get_best_model_path(trial_path, analysis) as best_model_path:\n                            if best_model_path is not None:\n                                try:\n                                    self._evaluate_best_model(\n                                        trial,\n                                        trial_path,\n                                        best_model_path,\n                                        validation_set,\n                                        data_format,\n                                        skip_save_unprocessed_output,\n                                        skip_save_predictions,\n                                        skip_save_eval_stats,\n                                        gpus,\n                                        gpu_memory_limit,\n                                        allow_parallel_threads,\n                                        backend,\n                                        debug,\n                                    )\n                                except Exception:\n                                    logger.warning(\n                                        f\"Failed to evaluate best model for trial {trial_path}. \"\n                                        \"This can happen with incomplete checkpoints from early stopping. \"\n                                        f\"Full exception:\\n{traceback.format_exc()}\"\n                                    )\n                            else:\n                                logger.warning(\"Skipping evaluation as no model checkpoints were available\")\n                    else:\n                        logger.warning(\"Skipping evaluation as no validation set was provided\")\n\n            ordered_trials = [TrialResults.from_dict(load_json_values(kwargs)) for kwargs in temp_ordered_trials]\n        else:\n            logger.warning(\"No trials reported results; check if time budget lower than epoch latency\")\n            ordered_trials = []\n\n        return HyperoptResults(ordered_trials=ordered_trials, experiment_analysis=analysis)\n\n\nclass CallbackStopper(Stopper):\n    \"\"\"Ray Tune Stopper that triggers the entire job to stop if one callback returns True.\"\"\"\n\n    def __init__(self, callbacks: list[Callback] | None):\n        self.callbacks = callbacks or []\n\n    def __call__(self, trial_id, result):\n        return False\n\n    def stop_all(self):\n        for callback in self.callbacks:\n            if callback.should_stop_hyperopt():\n                return True\n        return False\n\n\ndef get_build_hyperopt_executor(executor_type):\n    return get_from_registry(executor_type, executor_registry)\n\n\nexecutor_registry = {\"ray\": RayTuneExecutor}\n\n\ndef set_values(params: dict[str, Any], model_dict: dict[str, Any]):\n    for key, value in params.items():\n        if isinstance(value, dict):\n            for sub_key, sub_value in value.items():\n                if key not in model_dict:\n                    model_dict[key] = dict()\n                model_dict[key][sub_key] = sub_value\n        else:\n            model_dict[key] = value\n\n\ndef run_experiment(\n    config,\n    parameters=None,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    training_set_metadata=None,\n    data_format=None,\n    experiment_name=\"hyperopt\",\n    model_name=\"run\",\n    model_resume_path=None,\n    eval_split=VALIDATION,\n    skip_save_training_description=False,\n    skip_save_training_statistics=False,\n    skip_save_model=False,\n    skip_save_progress=False,\n    skip_save_log=False,\n    skip_save_processed_input=False,\n    skip_save_unprocessed_output=False,\n    skip_save_predictions=False,\n    skip_save_eval_stats=False,\n    output_directory=\"results\",\n    gpus=None,\n    gpu_memory_limit=None,\n    allow_parallel_threads=True,\n    callbacks=None,\n    backend=None,\n    random_seed=default_random_seed,\n    debug=False,\n    **kwargs,\n):\n    for callback in callbacks or []:\n        callback.on_hyperopt_trial_start(parameters)\n\n    # Collect training and validation losses and metrics\n    # & append it to `results`\n    model = LudwigModel(\n        config=config,\n        backend=backend,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n    )\n\n    eval_stats, train_stats, _, _ = model.experiment(\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        experiment_name=experiment_name,\n        model_name=model_name,\n        model_resume_path=model_resume_path,\n        eval_split=eval_split,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        output_directory=output_directory,\n        skip_collect_predictions=True,\n        skip_collect_overall_stats=False,\n        random_seed=random_seed,\n        debug=debug,\n    )\n\n    for callback in callbacks or []:\n        callback.on_hyperopt_trial_end(parameters)\n\n    return train_stats, eval_stats\n\n\ndef _run_experiment_unary(kwargs):\n    \"\"\"Unary function is needed by Fiber to map a list of args.\"\"\"\n    return run_experiment(**kwargs)\n"
  },
  {
    "path": "ludwig/hyperopt/results.py",
    "content": "# !/usr/bin/env python\n\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom dataclasses_json import dataclass_json\n\ntry:\n    from ray.tune import ExperimentAnalysis\nexcept ImportError:\n    ExperimentAnalysis = Any\n\n\n@dataclass_json\n@dataclass\nclass TrialResults:\n    parameters: dict\n    metric_score: float\n    training_stats: dict\n    eval_stats: dict\n\n\n@dataclass\nclass HyperoptResults:\n    ordered_trials: list[TrialResults]\n    experiment_analysis: ExperimentAnalysis\n"
  },
  {
    "path": "ludwig/hyperopt/run.py",
    "content": "import copy\nimport logging\nimport os\nfrom pprint import pformat\n\nimport pandas as pd\nimport yaml\nfrom tabulate import tabulate\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import Backend, initialize_backend, LocalBackend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import (\n    AUTO,\n    COMBINED,\n    EXECUTOR,\n    GOAL,\n    HYPEROPT,\n    LOSS,\n    MAX_CONCURRENT_TRIALS,\n    METRIC,\n    NAME,\n    OUTPUT_FEATURES,\n    PARAMETERS,\n    PREPROCESSING,\n    SEARCH_ALG,\n    SPLIT,\n    TEST,\n    TRAINING,\n    TYPE,\n    VALIDATION,\n)\nfrom ludwig.data.split import get_splitter\nfrom ludwig.hyperopt.results import HyperoptResults\nfrom ludwig.hyperopt.utils import (\n    log_warning_if_all_grid_type_parameters,\n    print_hyperopt_results,\n    save_hyperopt_stats,\n    should_tune_preprocessing,\n    update_hyperopt_params_with_defaults,\n)\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom ludwig.utils.dataset_utils import generate_dataset_statistics\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import makedirs, open_file\n\ntry:\n    from ray.tune import Callback as TuneCallback\n\n    from ludwig.backend.ray import RayBackend\nexcept ImportError:\n    TuneCallback = object\n\n    class RayBackend:\n        pass\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef hyperopt(\n    config: str | dict,\n    dataset: str | dict | pd.DataFrame = None,\n    training_set: str | dict | pd.DataFrame = None,\n    validation_set: str | dict | pd.DataFrame = None,\n    test_set: str | dict | pd.DataFrame = None,\n    training_set_metadata: str | dict = None,\n    data_format: str = None,\n    experiment_name: str = \"hyperopt\",\n    model_name: str = \"run\",\n    resume: bool | None = None,\n    skip_save_training_description: bool = False,\n    skip_save_training_statistics: bool = False,\n    skip_save_model: bool = False,\n    skip_save_progress: bool = False,\n    skip_save_log: bool = False,\n    skip_save_processed_input: bool = True,\n    skip_save_unprocessed_output: bool = False,\n    skip_save_predictions: bool = False,\n    skip_save_eval_stats: bool = False,\n    skip_save_hyperopt_statistics: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    tune_callbacks: list[TuneCallback] = None,\n    backend: Backend | str = None,\n    random_seed: int = default_random_seed,\n    hyperopt_log_verbosity: int = 3,\n    **kwargs,\n) -> HyperoptResults:\n    \"\"\"This method performs an hyperparameter optimization.\n\n    # Inputs\n\n    :param config: (Union[str, dict]) config which defines\n        the different parameters of the model, features, preprocessing and\n        training.  If `str`, filepath to yaml configuration file.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used in the experiment.\n        If it has a split column, it will be used for splitting (0 for train,\n        1 for validation, 2 for test), otherwise the dataset will be\n        randomly split.\n    :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing training data.\n    :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing validation data.\n    :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing test data.\n    :param training_set_metadata: (Union[str, dict], default: `None`)\n        metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n        dataset created the first time an input file is used in the same\n        directory with the same name and a '.meta.json' extension.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'df'`, `'dict'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param experiment_name: (str, default: `'experiment'`) name for\n        the experiment.\n    :param model_name: (str, default: `'run'`) name of the model that is\n        being used.\n    :param resume: (bool) If true, continue hyperopt from the state of the previous\n        run in the output directory with the same experiment name. If false, will create\n        new trials, ignoring any previous state, even if they exist in the output_directory.\n        By default, will attempt to resume if there is already an existing experiment with\n        the same name, and will create new trials if not.\n    :param skip_save_training_description: (bool, default: `False`) disables\n        saving the description JSON file.\n    :param skip_save_training_statistics: (bool, default: `False`) disables\n        saving training statistics JSON file.\n    :param skip_save_model: (bool, default: `False`) disables\n        saving model weights and hyperparameters each time the model\n        improves. By default Ludwig saves model weights after each epoch\n        the validation metric improves, but if the model is really big\n        that can be time consuming. If you do not want to keep\n        the weights and just find out what performance a model can get\n        with a set of hyperparameters, use this parameter to skip it,\n        but the model will not be loadable later on and the returned model\n        will have the weights obtained at the end of training, instead of\n        the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n        progress each epoch. By default Ludwig saves weights and stats\n        after each epoch for enabling resuming of training, but if\n        the model is really big that can be time consuming and will uses\n        twice as much space, use this parameter to skip it, but training\n        cannot be resumed later on.\n    :param skip_save_log: (bool, default: `False`) disables saving\n        TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n        but if it is not needed turning it off can slightly increase the\n        overall speed.\n    :param skip_save_processed_input: (bool, default: `False`) if input\n        dataset is provided it is preprocessed and cached by saving an HDF5\n        and JSON files to avoid running the preprocessing again. If this\n        parameter is `False`, the HDF5 and JSON file are not saved.\n    :param skip_save_unprocessed_output: (bool, default: `False`) by default\n        predictions and their probabilities are saved in both raw\n        unprocessed numpy files containing tensors and as postprocessed\n        CSV files (one for each output feature). If this parameter is True,\n        only the CSV ones are saved and the numpy ones are skipped.\n    :param skip_save_predictions: (bool, default: `False`) skips saving test\n        predictions CSV files.\n    :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n        statistics JSON file.\n    :param skip_save_hyperopt_statistics: (bool, default: `False`) skips saving\n        hyperopt stats file.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param random_seed: (int: default: 42) random seed used for weights\n        initialization, splits and any other random function.\n    :param hyperopt_log_verbosity: (int: default: 3) controls verbosity of\n        ray tune log messages.  Valid values: 0 = silent, 1 = only status updates,\n        2 = status and brief trial results, 3 = status and detailed trial results.\n\n    # Return\n\n    :return: (List[dict]) List of results for each trial, ordered by\n        descending performance on the target metric.\n    \"\"\"\n    from ludwig.hyperopt.execution import get_build_hyperopt_executor, RayTuneExecutor\n\n    # check if config is a path or a dict\n    if isinstance(config, str):  # assume path\n        with open_file(config, \"r\") as def_file:\n            config_dict = yaml.safe_load(def_file)\n    else:\n        config_dict = config\n\n    if HYPEROPT not in config_dict:\n        raise ValueError(\"Hyperopt Section not present in config\")\n\n    # backwards compatibility\n    upgraded_config = upgrade_config_dict_to_latest_version(config_dict)\n\n    # Initialize config object\n    config_obj = ModelConfig.from_dict(upgraded_config)\n\n    # Retain pre-merged config for hyperopt schema generation\n    premerged_config = copy.deepcopy(upgraded_config)\n\n    # Get full config with defaults\n    full_config = config_obj.to_dict()  # TODO (Connor): Refactor to use config object\n\n    hyperopt_config = full_config[HYPEROPT]\n\n    # Explicitly default to a local backend to avoid picking up Ray\n    # backend from the environment.\n    backend = backend or config_dict.get(\"backend\") or \"local\"\n    backend = initialize_backend(backend)\n\n    update_hyperopt_params_with_defaults(hyperopt_config)\n\n    # Check if all features are grid type parameters and log UserWarning if needed\n    log_warning_if_all_grid_type_parameters(hyperopt_config)\n\n    # Infer max concurrent trials\n    if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:\n        hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)\n        logger.info(f\"Setting max_concurrent_trials to {hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS]}\")\n\n    # Print hyperopt config\n    logger.info(\"Hyperopt Config\")\n    logger.info(pformat(hyperopt_config, indent=4))\n    logger.info(\"\\n\")\n\n    search_alg = hyperopt_config[SEARCH_ALG]\n    executor = hyperopt_config[EXECUTOR]\n    parameters = hyperopt_config[PARAMETERS]\n    split = hyperopt_config[SPLIT]\n    output_feature = hyperopt_config[\"output_feature\"]\n    metric = hyperopt_config[METRIC]\n    goal = hyperopt_config[GOAL]\n\n    ######################\n    # check validity of output_feature / metric/ split combination\n    ######################\n    splitter = get_splitter(**full_config[PREPROCESSING][\"split\"])\n    if split == TRAINING:\n        if training_set is None and not splitter.has_split(0):\n            raise ValueError(\n                'The data for the specified split for hyperopt \"{}\" '\n                \"was not provided, \"\n                \"or the split amount specified in the preprocessing section \"\n                \"of the config is not greater than 0\".format(split)\n            )\n    elif split == VALIDATION:\n        if validation_set is None and not splitter.has_split(1):\n            raise ValueError(\n                'The data for the specified split for hyperopt \"{}\" '\n                \"was not provided, \"\n                \"or the split amount specified in the preprocessing section \"\n                \"of the config is not greater than 0\".format(split)\n            )\n    elif split == TEST:\n        if test_set is None and not splitter.has_split(2):\n            raise ValueError(\n                'The data for the specified split for hyperopt \"{}\" '\n                \"was not provided, \"\n                \"or the split amount specified in the preprocessing section \"\n                \"of the config is not greater than 0\".format(split)\n            )\n    else:\n        raise ValueError(\n            'unrecognized hyperopt split \"{}\". ' \"Please provide one of: {}\".format(split, {TRAINING, VALIDATION, TEST})\n        )\n    if output_feature == COMBINED:\n        if metric != LOSS:\n            raise ValueError('The only valid metric for \"combined\" output feature is \"loss\"')\n    else:\n        output_feature_names = {of[NAME] for of in full_config[OUTPUT_FEATURES]}\n        if output_feature not in output_feature_names:\n            raise ValueError(\n                'The output feature specified for hyperopt \"{}\" '\n                \"cannot be found in the config. \"\n                'Available ones are: {} and \"combined\"'.format(output_feature, output_feature_names)\n            )\n\n    hyperopt_executor = get_build_hyperopt_executor(executor[TYPE])(\n        parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor\n    )\n\n    # Explicitly default to a local backend to avoid picking up Ray\n    # backend from the environment.\n    backend = backend or config_dict.get(\"backend\") or \"local\"\n    backend = initialize_backend(backend)\n    if not (\n        isinstance(backend, LocalBackend)\n        or (isinstance(hyperopt_executor, RayTuneExecutor) and isinstance(backend, RayBackend))\n    ):\n        raise ValueError(\n            \"Hyperopt requires using a `local` backend at this time, or \" \"`ray` backend with `ray` executor.\"\n        )\n\n    for callback in callbacks or []:\n        callback.on_hyperopt_init(experiment_name)\n\n    if not should_tune_preprocessing(full_config):\n        # preprocessing is not being tuned, so generate it once before starting trials\n        for callback in callbacks or []:\n            callback.on_hyperopt_preprocessing_start(experiment_name)\n\n        model = LudwigModel(\n            config=full_config,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n\n        training_set, validation_set, test_set, training_set_metadata = model.preprocess(\n            dataset=dataset,\n            training_set=training_set,\n            validation_set=validation_set,\n            test_set=test_set,\n            training_set_metadata=training_set_metadata,\n            data_format=data_format,\n            skip_save_processed_input=skip_save_processed_input,\n            random_seed=random_seed,\n        )\n        dataset = None\n\n        dataset_statistics = generate_dataset_statistics(training_set, validation_set, test_set)\n\n        logger.info(\"\\nDataset Statistics\")\n        logger.info(tabulate(dataset_statistics, headers=\"firstrow\", tablefmt=\"fancy_grid\"))\n\n        for callback in callbacks or []:\n            callback.on_hyperopt_preprocessing_end(experiment_name)\n\n    for callback in callbacks or []:\n        callback.on_hyperopt_start(experiment_name)\n\n    hyperopt_results = hyperopt_executor.execute(\n        premerged_config,\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        experiment_name=experiment_name,\n        model_name=model_name,\n        resume=resume,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        output_directory=output_directory,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n        tune_callbacks=tune_callbacks,\n        backend=backend,\n        random_seed=random_seed,\n        hyperopt_log_verbosity=hyperopt_log_verbosity,\n        **kwargs,\n    )\n\n    if backend.is_coordinator():\n        print_hyperopt_results(hyperopt_results)\n\n        if not skip_save_hyperopt_statistics:\n            with backend.storage.artifacts.use_credentials():\n                results_directory = os.path.join(output_directory, experiment_name)\n                makedirs(results_directory, exist_ok=True)\n\n                hyperopt_stats = {\n                    \"hyperopt_config\": hyperopt_config,\n                    \"hyperopt_results\": [t.to_dict() for t in hyperopt_results.ordered_trials],\n                }\n\n                save_hyperopt_stats(hyperopt_stats, results_directory)\n                logger.info(f\"Hyperopt stats saved to: {results_directory}\")\n\n    for callback in callbacks or []:\n        callback.on_hyperopt_end(experiment_name)\n        callback.on_hyperopt_finish(experiment_name)\n\n    logger.info(\"Finished hyperopt\")\n\n    return hyperopt_results\n"
  },
  {
    "path": "ludwig/hyperopt/search_algos.py",
    "content": "import logging\nfrom abc import ABC\nfrom importlib import import_module\n\nfrom ludwig.constants import TYPE\nfrom ludwig.utils.misc_utils import get_from_registry\n\nlogger = logging.getLogger(__name__)\n\n\ndef _is_package_installed(package_name: str, search_algo_name: str) -> bool:\n    try:\n        import_module(package_name)\n        return True\n    except ImportError:\n        raise ImportError(\n            f\"Search algorithm {search_algo_name} requires package {package_name}, however package is not installed.\"\n            \" Please refer to Ray Tune documentation for packages required for this search algorithm.\"\n        )\n\n\nclass SearchAlgorithm(ABC):\n    def __init__(self, search_alg_dict: dict) -> None:\n        self.search_alg_dict = search_alg_dict\n        self.random_seed_attribute_name = None\n\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        if self.random_seed_attribute_name not in self.search_alg_dict:\n            self.search_alg_dict[self.random_seed_attribute_name] = ludwig_random_seed\n\n\nclass BasicVariantSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"random_state\"\n\n\nclass HyperoptSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"hyperopt\", \"hyperopt\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"random_state_seed\"\n\n\nclass BOHBSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"hpbandster\", \"bohb\")\n        _is_package_installed(\"ConfigSpace\", \"bohb\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"seed\"\n\n\nclass AxSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"sqlalchemy\", \"ax\")\n        _is_package_installed(\"ax\", \"ax\")\n        super().__init__(search_alg_dict)\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\nclass BayesOptSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"bayes_opt\", \"bayesopt\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"random_state\"\n\n\nclass BlendsearchSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"flaml\", \"blendsearch\")\n        super().__init__(search_alg_dict)\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\nclass CFOSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"flaml\", \"cfo\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"seed\"\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\nclass DragonflySA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"dragonfly\", \"dragonfly\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"random_state_seed\"\n\n\nclass HEBOSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"hebo\", \"hebo\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"random_state_seed\"\n\n\nclass SkoptSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"skopt\", \"skopt\")\n        super().__init__(search_alg_dict)\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\nclass NevergradSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"nevergrad\", \"nevergrad\")\n        super().__init__(search_alg_dict)\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\nclass OptunaSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"optuna\", \"optuna\")\n        super().__init__(search_alg_dict)\n        self.random_seed_attribute_name = \"seed\"\n\n\nclass ZooptSA(SearchAlgorithm):\n    def __init__(self, search_alg_dict: dict) -> None:\n        _is_package_installed(\"zoopt\", \"zoopt\")\n        super().__init__(search_alg_dict)\n\n    # override parent method, this search algorithm does not support\n    # setting random seed\n    def check_for_random_seed(self, ludwig_random_seed: int) -> None:\n        pass\n\n\ndef get_search_algorithm(search_algo):\n    search_algo_name = search_algo.get(TYPE, None)\n    return get_from_registry(search_algo_name, search_algo_registry)(search_algo)\n\n\nsearch_algo_registry = {\n    None: BasicVariantSA,\n    \"variant_generator\": BasicVariantSA,\n    \"random\": BasicVariantSA,\n    \"hyperopt\": HyperoptSA,\n    \"bohb\": BOHBSA,\n    \"ax\": AxSA,\n    \"bayesopt\": BayesOptSA,\n    \"blendsearch\": BlendsearchSA,\n    \"cfo\": CFOSA,\n    \"dragonfly\": DragonflySA,\n    \"hebo\": HEBOSA,\n    \"skopt\": SkoptSA,\n    \"nevergrad\": NevergradSA,\n    \"optuna\": OptunaSA,\n    \"zoopt\": ZooptSA,\n}\n"
  },
  {
    "path": "ludwig/hyperopt/utils.py",
    "content": "import copy\nimport dataclasses\nimport json\nimport logging\nimport os\nimport warnings\nfrom typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUTO,\n    COMBINED,\n    EXECUTOR,\n    GOAL,\n    GRID_SEARCH,\n    HYPEROPT,\n    INPUT_FEATURES,\n    LOSS,\n    MAX_CONCURRENT_TRIALS,\n    METRIC,\n    MINIMIZE,\n    NAME,\n    NUM_SAMPLES,\n    OUTPUT_FEATURES,\n    PARAMETERS,\n    PREPROCESSING,\n    RAY,\n    SPACE,\n    SPLIT,\n    TYPE,\n    VALIDATION,\n)\nfrom ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME\nfrom ludwig.hyperopt.results import HyperoptResults, TrialResults\nfrom ludwig.types import HyperoptConfigDict, ModelConfigDict\nfrom ludwig.utils.data_utils import save_json\nfrom ludwig.utils.misc_utils import (\n    get_class_attributes,\n    get_from_registry,\n    merge_dict,\n    set_default_value,\n    set_default_values,\n)\nfrom ludwig.utils.print_utils import print_boxed\n\nlogger = logging.getLogger(__name__)\n\n\ndef print_hyperopt_results(hyperopt_results: HyperoptResults):\n    print_boxed(\"HYPEROPT RESULTS\", print_fun=logger.info)\n    for trial_results in hyperopt_results.ordered_trials:\n        if not isinstance(trial_results.metric_score, str):\n            logger.info(f\"score: {trial_results.metric_score:.6f} | parameters: {trial_results.parameters}\")\n    logger.info(\"\")\n\n\ndef save_hyperopt_stats(hyperopt_stats, hyperopt_dir_name):\n    hyperopt_stats_fn = os.path.join(hyperopt_dir_name, HYPEROPT_STATISTICS_FILE_NAME)\n    save_json(hyperopt_stats_fn, hyperopt_stats)\n\n\ndef load_json_value(v):\n    try:\n        return json.loads(v)\n    except Exception as e:\n        logger.warning(f\"While loading json, encountered exception: {e}\")\n        return v\n\n\n# define set containing names to return for TrialResults\nTRIAL_RESULTS_NAMES_SET = {f.name for f in dataclasses.fields(TrialResults)}\n\n\ndef load_json_values(d):\n    # ensure metric_score is a string for the json load to eliminate extraneous exception message\n    d[\"metric_score\"] = str(d[\"metric_score\"])\n\n    # load only data required for TrialResults\n    return {k: load_json_value(v) for k, v in d.items() if k in TRIAL_RESULTS_NAMES_SET}\n\n\ndef should_tune_preprocessing(config):\n    parameters = config[HYPEROPT][PARAMETERS]\n    for param_name in parameters.keys():\n        if f\"{PREPROCESSING}.\" in param_name:\n            return True\n    return False\n\n\ndef parameter_to_dict(name, value):\n    if name == \".\":\n        # Parameter name \".\", means top-level config\n        return value\n\n    parameter_dict = {}\n    curr_dict = parameter_dict\n    name_list = name.split(\".\")\n    for i, name_elem in enumerate(name_list):\n        if i == len(name_list) - 1:\n            curr_dict[name_elem] = value\n        else:\n            name_dict = curr_dict.get(name_elem, {})\n            curr_dict[name_elem] = name_dict\n            curr_dict = name_dict\n    return parameter_dict\n\n\ndef feature_list_to_dict(config: ModelConfigDict) -> ModelConfigDict:\n    input_features_dict = {}\n    for feature in config[INPUT_FEATURES]:\n        input_features_dict[feature[NAME]] = feature\n\n    output_features_dict = {}\n    for feature in config[OUTPUT_FEATURES]:\n        output_features_dict[feature[NAME]] = feature\n\n    config = copy.copy(config)\n    config[INPUT_FEATURES] = input_features_dict\n    config[OUTPUT_FEATURES] = output_features_dict\n    return config\n\n\ndef feature_dict_to_list(config: ModelConfigDict) -> ModelConfigDict:\n    # This works because Python dicts are order-preserving, so we do not need to\n    # do anything special to map from a key in the dict to an index in a list\n    input_features_list = []\n    for feature in config[INPUT_FEATURES].values():\n        input_features_list.append(feature)\n\n    output_features_list = []\n    for feature in config[OUTPUT_FEATURES].values():\n        output_features_list.append(feature)\n\n    config = copy.copy(config)\n    config[INPUT_FEATURES] = input_features_list\n    config[OUTPUT_FEATURES] = output_features_list\n    return config\n\n\ndef substitute_parameters(\n    config: ModelConfigDict,\n    parameters: dict[str, Any],\n):\n    \"\"\"Update Ludwig config with parameters sampled from the Hyperopt sampler.\"\"\"\n\n    # Collect the sets of names for each feature grouping so we can map feature names to\n    # groups\n    input_feature_names = {feature[NAME] for feature in config[INPUT_FEATURES]}\n    output_feature_names = {feature[NAME] for feature in config[OUTPUT_FEATURES]}\n\n    # Features in the user config are provided as a list, but in hyperopt we reference\n    # features by name, so convert temporarily to a dict to simplify the mergep process.\n    config = feature_list_to_dict(config)\n\n    # Merge parameters into the user configuration in order. As such, if there are conflicting\n    # params, the later params will take precedence.\n    for name, value in parameters.items():\n        # User params are provided as <feature_name>.<param>, but we group input / output features\n        # together during the merge to make it easier and unambiguous to convert back and forth\n        # TODO(travis): we should revisit the user format here, as it silently breaks situations\n        # where the user has a feature named \"trainer\", \"combiner\", etc.\n        prefix = name.split(\".\")[0]\n        if prefix in input_feature_names:\n            name = f\"{INPUT_FEATURES}.{name}\"\n        elif prefix in output_feature_names:\n            name = f\"{OUTPUT_FEATURES}.{name}\"\n\n        param_dict = parameter_to_dict(name, value)\n        config = merge_dict(config, param_dict)\n\n    # Now that all features have been merged, convert back to the original list format.\n    config = feature_dict_to_list(config)\n\n    return config\n\n\n@DeveloperAPI\ndef get_num_duplicate_trials(hyperopt_config: HyperoptConfigDict) -> int:\n    \"\"\"Returns the number of duplicate trials that will be created.\n\n    Duplicate trials are only created when there are grid type parameters and num_samples > 1.\n    \"\"\"\n    num_samples = hyperopt_config[EXECUTOR].get(NUM_SAMPLES, 1)\n    if num_samples == 1:\n        return 0\n\n    total_grid_search_trials = 1\n    for _, param_info in hyperopt_config[PARAMETERS].items():\n        if param_info.get(SPACE, None) == GRID_SEARCH:\n            total_grid_search_trials *= len(param_info.get(\"values\", []))\n\n    num_duplicate_trials = (total_grid_search_trials * num_samples) - total_grid_search_trials\n    return num_duplicate_trials\n\n\ndef log_warning_if_all_grid_type_parameters(hyperopt_config: HyperoptConfigDict) -> None:\n    \"\"\"Logs warning if all parameters have a grid type search space and num_samples > 1 since this will result in\n    duplicate trials being created.\"\"\"\n    num_duplicate_trials = get_num_duplicate_trials(hyperopt_config)\n    if num_duplicate_trials == 0:\n        return\n\n    num_samples = hyperopt_config[EXECUTOR].get(NUM_SAMPLES, 1)\n    warnings.warn(\n        \"All hyperopt parameters in Ludwig config are using grid_search space, but number of samples \"\n        f\"({num_samples}) is greater than 1. This will result in {num_duplicate_trials} duplicate trials being \"\n        \"created. Consider setting `num_samples` to 1 in the hyperopt executor to prevent trial duplication.\",\n        RuntimeWarning,\n    )\n\n\ndef update_hyperopt_params_with_defaults(hyperopt_params: HyperoptConfigDict) -> None:\n    \"\"\"Updates user's Ludwig config with default hyperopt parameters.\"\"\"\n    from ludwig.hyperopt.execution import executor_registry\n\n    set_default_value(hyperopt_params, EXECUTOR, {})\n    set_default_value(hyperopt_params, SPLIT, VALIDATION)\n    set_default_value(hyperopt_params, \"output_feature\", COMBINED)\n    set_default_value(hyperopt_params, METRIC, LOSS)\n    set_default_value(hyperopt_params, GOAL, MINIMIZE)\n\n    set_default_values(\n        hyperopt_params[EXECUTOR],\n        {TYPE: RAY, NUM_SAMPLES: 1, MAX_CONCURRENT_TRIALS: AUTO},\n    )\n\n    if hyperopt_params[EXECUTOR].get(\"trial_driver_resources\") is None:\n        hyperopt_params[EXECUTOR][\"trial_driver_resources\"] = {\"CPU\": 1, \"GPU\": 0}\n\n    executor = get_from_registry(hyperopt_params[EXECUTOR][TYPE], executor_registry)\n    executor_defaults = {k: v for k, v in executor.__dict__.items() if k in get_class_attributes(executor)}\n    set_default_values(\n        hyperopt_params[EXECUTOR],\n        executor_defaults,\n    )\n"
  },
  {
    "path": "ludwig/hyperopt_cli.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport sys\n\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.hyperopt.run import hyperopt\nfrom ludwig.utils.data_utils import load_config_from_str, load_yaml\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n\ndef hyperopt_cli(\n    config: str | dict,\n    dataset: str = None,\n    training_set: str = None,\n    validation_set: str = None,\n    test_set: str = None,\n    training_set_metadata: str = None,\n    data_format: str = None,\n    experiment_name: str = \"experiment\",\n    model_name: str = \"run\",\n    # model_load_path=None,\n    # model_resume_path=None,\n    skip_save_training_description: bool = False,\n    skip_save_training_statistics: bool = False,\n    skip_save_model: bool = False,\n    skip_save_progress: bool = False,\n    skip_save_log: bool = False,\n    skip_save_processed_input: bool = False,\n    skip_save_unprocessed_output: bool = False,\n    skip_save_predictions: bool = False,\n    skip_save_eval_stats: bool = False,\n    skip_save_hyperopt_statistics: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    random_seed: int = default_random_seed,\n    hyperopt_log_verbosity: int = 3,\n    **kwargs,\n):\n    \"\"\"Searches for optimal hyperparameters.\n\n    # Inputs\n\n    :param config: (Union[str, dict]) in-memory representation of\n            config or string path to a YAML config file.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used for training.\n        If it has a split column, it will be used for splitting (0 for train,\n        1 for validation, 2 for test), otherwise the dataset will be\n        randomly split.\n    :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing training data.\n    :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing validation data.\n    :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing test data.\n    :param training_set_metadata: (Union[str, dict], default: `None`)\n        metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n        dataset created the first time an input file is used in the same\n        directory with the same name and a '.meta.json' extension.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param experiment_name: (str, default: `'experiment'`) name for\n        the experiment.\n    :param model_name: (str, default: `'run'`) name of the model that is\n        being used.\n    :param skip_save_training_description: (bool, default: `False`) disables\n        saving the description JSON file.\n    :param skip_save_training_statistics: (bool, default: `False`) disables\n        saving training statistics JSON file.\n    :param skip_save_model: (bool, default: `False`) disables\n        saving model weights and hyperparameters each time the model\n        improves. By default Ludwig saves model weights after each epoch\n        the validation metric improves, but if the model is really big\n        that can be time consuming. If you do not want to keep\n        the weights and just find out what performance a model can get\n        with a set of hyperparameters, use this parameter to skip it,\n        but the model will not be loadable later on and the returned model\n        will have the weights obtained at the end of training, instead of\n        the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n        progress each epoch. By default Ludwig saves weights and stats\n        after each epoch for enabling resuming of training, but if\n        the model is really big that can be time consuming and will uses\n        twice as much space, use this parameter to skip it, but training\n        cannot be resumed later on.\n    :param skip_save_log: (bool, default: `False`) disables saving\n        TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n        but if it is not needed turning it off can slightly increase the\n        overall speed.\n    :param skip_save_processed_input: (bool, default: `False`) if input\n        dataset is provided it is preprocessed and cached by saving an HDF5\n        and JSON files to avoid running the preprocessing again. If this\n        parameter is `False`, the HDF5 and JSON file are not saved.\n    :param skip_save_unprocessed_output: (bool, default: `False`) by default\n        predictions and their probabilities are saved in both raw\n        unprocessed numpy files containing tensors and as postprocessed\n        CSV files (one for each output feature). If this parameter is True,\n        only the CSV ones are saved and the numpy ones are skipped.\n    :param skip_save_predictions: (bool, default: `False`) skips saving test\n        predictions CSV files\n    :param skip_save_eval_stats: (bool, default: `False`) skips saving test\n        statistics JSON file\n    :param skip_save_hyperopt_statistics: (bool, default: `False`) skips saving\n        hyperopt stats file.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param random_seed: (int: default: 42) random seed used for weights\n        initialization, splits and any other random function.\n    :param hyperopt_log_verbosity: (int: default: 3) Controls verbosity of ray tune log messages.  Valid values:\n        0 = silent, 1 = only status updates, 2 = status and brief trial\n        results, 3 = status and detailed trial results.\n\n    # Return\n    :return\" (`None`)\n    \"\"\"\n    return hyperopt(\n        config=config,\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        experiment_name=experiment_name,\n        model_name=model_name,\n        # model_load_path=model_load_path,\n        # model_resume_path=model_resume_path,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        skip_save_hyperopt_statistics=skip_save_hyperopt_statistics,\n        output_directory=output_directory,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n        backend=backend,\n        random_seed=random_seed,\n        hyperopt_log_verbosity=hyperopt_log_verbosity,\n        **kwargs,\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script searches for optimal Hyperparameters\",\n        prog=\"ludwig hyperopt\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # -------------------\n    # Hyperopt parameters\n    # -------------------\n    parser.add_argument(\n        \"-sshs\",\n        \"--skip_save_hyperopt_statistics\",\n        help=\"skips saving hyperopt statistics file\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # ----------------------------\n    # Experiment naming parameters\n    # ----------------------------\n    parser.add_argument(\n        \"--output_directory\",\n        type=str,\n        default=\"results\",\n        help=\"directory that contains the results\",\n    )\n    parser.add_argument(\"--experiment_name\", type=str, default=\"hyperopt\", help=\"experiment name\")\n    parser.add_argument(\"--model_name\", type=str, default=\"run\", help=\"name for the model\")\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\n        \"--dataset\",\n        help=\"input data file path. \"\n        \"If it has a split column, it will be used for splitting \"\n        \"(0: train, 1: validation, 2: test), \"\n        \"otherwise the dataset will be randomly split\",\n    )\n    parser.add_argument(\"--training_set\", help=\"input train data file path\")\n    parser.add_argument(\"--validation_set\", help=\"input validation data file path\")\n    parser.add_argument(\"--test_set\", help=\"input test data file path\")\n\n    parser.add_argument(\n        \"--training_set_metadata\",\n        help=\"input metadata JSON file path. An intermediate preprocessed file \"\n        \"containing the mappings of the input file created \"\n        \"the first time a file is used, in the same directory \"\n        \"with the same name and a .json extension\",\n    )\n\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n\n    parser.add_argument(\n        \"-sspi\",\n        \"--skip_save_processed_input\",\n        help=\"skips saving intermediate HDF5 and JSON files\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    config = parser.add_mutually_exclusive_group(required=True)\n    config.add_argument(\n        \"-c\",\n        \"--config\",\n        type=load_yaml,\n        help=\"Path to the YAML file containing the model configuration\",\n    )\n    config.add_argument(\n        \"-cs\",\n        \"--config_str\",\n        dest=\"config\",\n        type=load_config_from_str,\n        help=\"JSON or YAML serialized string of the model configuration\",\n    )\n\n    parser.add_argument(\n        \"-mlp\",\n        \"--model_load_path\",\n        help=\"path of a pretrained model to load as initialization\",\n    )\n    parser.add_argument(\n        \"-mrp\",\n        \"--model_resume_path\",\n        help=\"path of the model directory to resume training of\",\n    )\n    parser.add_argument(\n        \"-sstd\",\n        \"--skip_save_training_description\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving the description JSON file\",\n    )\n    parser.add_argument(\n        \"-ssts\",\n        \"--skip_save_training_statistics\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving training statistics JSON file\",\n    )\n    parser.add_argument(\n        \"-ssm\",\n        \"--skip_save_model\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving weights each time the model improves. \"\n        \"By default Ludwig saves  weights after each epoch \"\n        \"the validation metric (improves, but  if the model is really big \"\n        \"that can be time consuming. If you do not want to keep \"\n        \"the weights and just find out what performance a model can get \"\n        \"with a set of hyperparameters, use this parameter to skip it\",\n    )\n    parser.add_argument(\n        \"-ssp\",\n        \"--skip_save_progress\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving weights after each epoch. By default ludwig saves \"\n        \"weights after each epoch for enabling resuming of training, but \"\n        \"if the model is really big that can be time consuming and will \"\n        \"save twice as much space, use this parameter to skip it\",\n    )\n    parser.add_argument(\n        \"-ssl\",\n        \"--skip_save_log\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving TensorBoard logs. By default Ludwig saves \"\n        \"logs for the TensorBoard, but if it is not needed turning it off \"\n        \"can slightly increase the overall speed\",\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-rs\",\n        \"--random_seed\",\n        type=int,\n        default=42,\n        help=\"a random seed that is going to be used anywhere there is a call \"\n        \"to a random number generator: data splitting, parameter \"\n        \"initialization and training set shuffling\",\n    )\n    parser.add_argument(\n        \"-hlv\",\n        \"--hyperopt_log_verbosity\",\n        type=int,\n        default=3,\n        choices=[0, 1, 2, 3],\n        help=\"Controls verbosity of ray tune log messages.  Valid values: \"\n        \"0 = silent, 1 = only status updates, 2 = status and brief trial \"\n        \"results, 3 = status and detailed trial results.\",\n    )\n    parser.add_argument(\"-g\", \"--gpus\", nargs=\"+\", type=int, default=None, help=\"list of gpus to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"hyperopt\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.hyperopt\")\n\n    args.backend = initialize_backend(args.backend or args.config.get(\"backend\"))\n    if args.backend.is_coordinator():\n        print_ludwig(\"Hyperopt\", LUDWIG_VERSION)\n\n    hyperopt_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/model_export/base_model_exporter.py",
    "content": "from abc import ABC, abstractmethod\n\nimport torch\n\n\nclass LudwigTorchWrapper(torch.nn.Module):\n    \"\"\"Base class that establishes the contract for exporting to different file formats.\"\"\"\n\n    def __init__(self, model):\n        super().__init__()\n        self.model = model\n\n    def forward(self, x):\n        return self.model({\"image_path\": x})\n\n\nclass BaseModelExporter(ABC):\n    @abstractmethod\n    def export(self, model_path, export_path, export_args_override):\n        pass\n\n    @abstractmethod\n    def check_model_export(self, path):\n        pass\n"
  },
  {
    "path": "ludwig/model_export/onnx_exporter.py",
    "content": "import os\n\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.model_export.base_model_exporter import BaseModelExporter, LudwigTorchWrapper\n\n\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nclass OnnxExporter(BaseModelExporter):\n    \"\"\"Class that abstracts the convertion of torch to onnx.\"\"\"\n\n    def export(self, model_path, export_path, output_model_name):\n        ludwig_model = LudwigModel.load(model_path)\n        model = LudwigTorchWrapper(ludwig_model.model)  # Wrap the model\n        model.eval()  # inference mode, is this needed.. I think onnx export does this for us\n\n        width = ludwig_model.config[\"input_features\"][0][\"preprocessing\"][\"width\"]\n        height = ludwig_model.config[\"input_features\"][0][\"preprocessing\"][\"height\"]\n        example_input = torch.randn(1, 3, width, height, requires_grad=True)\n\n        torch.onnx.export(\n            model,\n            example_input,\n            os.path.join(export_path, output_model_name),\n            opset_version=18,\n            export_params=True,\n            do_constant_folding=True,\n            input_names=[\"input\"],\n            output_names=[\"combiner_hidden_1\", \"output\", \"combiner_hidden_2\"],\n        )\n\n    def check_model_export(self, path):\n        import onnx\n\n        onnx_model = onnx.load(path)\n        onnx.checker.check_model(onnx_model)\n"
  },
  {
    "path": "ludwig/models/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/models/base.py",
    "content": "import contextlib\nimport logging\nfrom abc import ABCMeta, abstractmethod\nfrom collections import OrderedDict\nfrom typing import Any\n\nimport numpy as np\nimport torch\nimport torchmetrics\n\nfrom ludwig.combiners.combiners import Combiner\nfrom ludwig.constants import COMBINED, LOSS, NAME\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.features.base_feature import create_passthrough_input_feature, InputFeature, ModuleWrapper, OutputFeature\nfrom ludwig.features.feature_registries import get_input_type_registry, get_output_type_registry\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.modules.metric_modules import LudwigMetric\nfrom ludwig.modules.training_hooks import TrainingHook\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig, FeatureCollection\nfrom ludwig.utils.algorithms_utils import topological_sort_feature_dependencies\nfrom ludwig.utils.metric_utils import get_scalar_from_ludwig_metric\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import DEVICE, LudwigModule, reg_loss\nfrom ludwig.utils.types import TorchDevice\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseModel(LudwigModule, metaclass=ABCMeta):\n    \"\"\"Base model for use in LudwigModule.\n\n    Implementations of this class should implement the following methods:\n    - type()\n    - forward()\n    \"\"\"\n\n    @staticmethod\n    @abstractmethod\n    def type() -> str:\n        \"\"\"Returns the model type.\"\"\"\n\n    def __init__(self, random_seed: int = None):\n        self._random_seed = random_seed\n\n        # TODO: with change to misc_utils.set_random_seed() this may be redundant\n        #       seems to be required for test_api.py::test_api_training_determinism\n        if random_seed is not None:\n            torch.random.manual_seed(random_seed)\n\n        super().__init__()\n\n        self.input_features = self.create_feature_dict()\n        self.output_features = self.create_feature_dict()\n\n        # ================ Combined loss metric ================\n        self._eval_loss_metric = ModuleWrapper(torchmetrics.MeanMetric())\n        self._eval_additional_losses_metrics = ModuleWrapper(torchmetrics.MeanMetric())\n\n        # ================ Training Hook Handles ================\n        self._forward_hook_handles: list[TrainingHook] = []\n\n    def create_feature_dict(self) -> LudwigFeatureDict:\n        \"\"\"Creates and returns a LudwigFeatureDict.\"\"\"\n        return LudwigFeatureDict()\n\n    def to_device(self, device):\n        return self.to(device)\n\n    def metrics_to_device(self, device: str):\n        self._eval_loss_metric.module = self._eval_loss_metric.module.to(device)\n        self._eval_additional_losses_metrics.module = self._eval_additional_losses_metrics.module.to(device)\n        for feature in self.output_features.values():\n            feature._eval_loss_metric.module = feature._eval_loss_metric.module.to(device)\n\n    @classmethod\n    def build_inputs(cls, input_feature_configs: FeatureCollection[BaseInputFeatureConfig]) -> dict[str, InputFeature]:\n        \"\"\"Builds and returns input features in topological order.\"\"\"\n        input_features = OrderedDict()\n        input_features_def = topological_sort_feature_dependencies(input_feature_configs.to_list())\n        for input_feature_def in input_features_def:\n            input_features[input_feature_def[NAME]] = cls.build_single_input(\n                getattr(input_feature_configs, input_feature_def[NAME]), input_features\n            )\n        return input_features\n\n    @staticmethod\n    def build_single_input(\n        feature_config: BaseInputFeatureConfig, other_input_features: dict[str, InputFeature] | None\n    ) -> InputFeature:\n        \"\"\"Builds a single input feature from the input feature definition.\"\"\"\n        logger.debug(f\"Input {feature_config.type} feature {feature_config.name}\")\n\n        encoder_obj = None\n        if feature_config.tied is not None:\n            tied_input_feature_name = feature_config.tied\n            if tied_input_feature_name in other_input_features:\n                encoder_obj = other_input_features[tied_input_feature_name].encoder_obj\n\n        return create_input_feature(feature_config, encoder_obj)\n\n    @classmethod\n    def build_outputs(\n        cls, output_feature_configs: FeatureCollection[BaseOutputFeatureConfig], combiner: Combiner\n    ) -> dict[str, OutputFeature]:\n        \"\"\"Builds and returns output features in topological order.\"\"\"\n        output_features_def = topological_sort_feature_dependencies(output_feature_configs.to_list())\n        output_features = {}\n\n        for output_feature_def in output_features_def:\n            # TODO(Justin): Check that the semantics of input_size align with what the combiner's output shape returns\n            # for seq2seq.\n            setattr(getattr(output_feature_configs, output_feature_def[NAME]), \"input_size\", combiner.output_shape[-1])\n            output_features[output_feature_def[NAME]] = cls.build_single_output(\n                getattr(output_feature_configs, output_feature_def[NAME]), output_features\n            )\n        return output_features\n\n    @staticmethod\n    def build_single_output(\n        feature_config: BaseOutputFeatureConfig, output_features: dict[str, OutputFeature] | None\n    ) -> OutputFeature:\n        \"\"\"Builds a single output feature from the output feature definition.\"\"\"\n        logger.debug(f\"Output {feature_config.type} feature {feature_config.name}\")\n        output_feature_class = get_from_registry(feature_config.type, get_output_type_registry())\n        output_feature_obj = output_feature_class(feature_config, output_features=output_features)\n        return output_feature_obj\n\n    def get_model_inputs(self):\n        \"\"\"Returns a dict of feature name -> sample model input.\"\"\"\n        device = next(self.parameters()).device\n        inputs = {\n            input_feature_name: input_feature.create_sample_input().to(device)\n            for input_feature_name, input_feature in self.input_features.items()\n        }\n        return inputs\n\n    def get_model_size(self) -> int:\n        \"\"\"Returns total number of parameters in model.\"\"\"\n        model_tensors = self.collect_weights()\n        total_size = 0\n        for tnsr in model_tensors:\n            total_size += tnsr[1].detach().cpu().numpy().size\n        return total_size\n\n    def to_torchscript(self, device: TorchDevice | None = None):\n        \"\"\"Converts the ECD model as a TorchScript model.\"\"\"\n        if device is None:\n            device = DEVICE\n\n        self.eval()\n        model_inputs = self.get_model_inputs()\n\n        model_to_script = self.to(device)\n        model_inputs_to_script = {k: v.to(device) for k, v in model_inputs.items()}\n        # We set strict=False to enable dict inputs and outputs.\n        return torch.jit.trace(model_to_script, model_inputs_to_script, strict=False)\n\n    def save_torchscript(self, save_path, device: TorchDevice | None = None):\n        \"\"\"Saves the ECD model as a TorchScript model.\"\"\"\n        if device is None:\n            device = DEVICE\n\n        traced = self.to_torchscript(device)\n        traced.save(save_path)\n\n    @property\n    def input_shape(self):\n        \"\"\"Returns the shape of the model's input.\"\"\"\n        # TODO(justin): Remove dummy implementation. Make input_shape and output_shape functions.\n        return torch.Size([1, 1])\n\n    @abstractmethod\n    def forward(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n        mask=None,\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Forward pass of the model.\n\n        Args:\n            inputs: Inputs to the model. Can be a dictionary of input names to\n                input tensors or a tuple of (inputs, targets) where inputs is\n                a dictionary of input names to input tensors and targets is a\n                dictionary of target names to target tensors.\n            mask: A mask for the inputs.\n\n        Returns:\n            A dictionary of output {feature name}::{tensor_name} -> output tensor.\n        \"\"\"\n\n    def predictions(self, inputs):\n        \"\"\"Returns the model's predictions for the given inputs.\"\"\"\n        outputs = self(inputs)\n        return self.outputs_to_predictions(outputs)\n\n    def outputs_to_predictions(self, outputs: dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:\n        \"\"\"Returns the model's predictions given the raw model outputs.\"\"\"\n        predictions = {}\n        for of_name in self.output_features:\n            predictions[of_name] = self.output_features.get(of_name).predictions(outputs, of_name)\n        return predictions\n\n    def evaluation_step(self, inputs, targets):\n        \"\"\"Predict the inputs and update evaluation metrics.\"\"\"\n        predictions = self.predictions(inputs)\n        self.update_metrics(targets, predictions)\n        return predictions\n\n    def predict_step(self, inputs):\n        \"\"\"Predict the inputs.\"\"\"\n        return self.predictions(inputs)\n\n    def train_loss(\n        self,\n        targets,\n        predictions,\n        regularization_type: str | None = None,\n        regularization_lambda: float | None = None,\n    ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:\n        \"\"\"Computes the training loss for the model.\n\n        Args:\n            targets: A dictionary of target names to target tensors.\n            predictions: A dictionary of output names to output tensors.\n            regularization_type: One of 'l1', 'l2', 'l1_l2', or None.\n            regularization_lambda: The regularization lambda.\n\n        Returns:\n            A tuple of the loss tensor and a dictionary of loss for every\n            output feature.\n        \"\"\"\n        train_loss = 0\n        of_train_losses = {}\n        for of_name, of_obj in self.output_features.items():\n            of_train_loss = of_obj.train_loss(targets[of_name], predictions, of_name)\n            train_loss += of_obj.loss.weight * of_train_loss\n            of_train_losses[of_name] = of_train_loss\n\n        additional_losses = self.losses()\n        if additional_losses:\n            train_loss += torch.sum(torch.stack(additional_losses))  # other losses\n\n        # Add regularization loss\n        if regularization_type is not None and regularization_lambda != 0:\n            train_loss += reg_loss(self, regularization_type, l1=regularization_lambda, l2=regularization_lambda)\n\n        return train_loss, of_train_losses\n\n    def eval_loss(self, targets, predictions):\n        \"\"\"Computes all evaluation losses for the model given targets and predictions.\n\n        Args:\n            targets: A dictionary of target names to target tensors.\n            predictions: A dictionary of output names to output tensors.\n\n        Returns:\n            A tuple of loss values for eval losses and additional losses.\n        \"\"\"\n        eval_loss = 0\n        for of_name, of_obj in self.output_features.items():\n            of_eval_loss = of_obj.eval_loss(targets[of_name], predictions[of_name])\n            eval_loss += of_obj.loss.weight * of_eval_loss\n\n        additional_loss = 0\n        additional_losses = self.losses()\n        if additional_losses:\n            additional_loss = torch.sum(torch.stack(additional_losses))  # other losses\n\n        return eval_loss, additional_loss\n\n    def update_metrics(self, targets, predictions):\n        \"\"\"Updates the model's metrics given targets and predictions.\"\"\"\n        for of_name, of_obj in self.output_features.items():\n            of_obj.update_metrics(targets[of_name], predictions[of_name])\n\n        eval_loss, additional_losses = self.eval_loss(targets, predictions)\n        self.eval_loss_metric.update(eval_loss)\n        self.eval_additional_losses_metrics.update(additional_losses)\n\n    @property\n    def eval_loss_metric(self) -> LudwigMetric:\n        return self._eval_loss_metric.module\n\n    @eval_loss_metric.setter\n    def eval_loss_metric(self, value: LudwigMetric) -> None:\n        self._eval_loss_metric.module = value\n\n    @property\n    def eval_additional_losses_metrics(self) -> LudwigMetric:\n        return self._eval_additional_losses_metrics.module\n\n    def get_metrics(self) -> dict[str, dict[str, float]]:\n        \"\"\"Returns a dictionary of metrics for each output feature of the model.\"\"\"\n        all_of_metrics = {}\n        for of_name, of_obj in self.output_features.items():\n            all_of_metrics[of_name] = of_obj.get_metrics()\n        all_of_metrics[COMBINED] = {\n            LOSS: get_scalar_from_ludwig_metric(self.eval_loss_metric)\n            + get_scalar_from_ludwig_metric(self.eval_additional_losses_metrics)\n        }\n        return all_of_metrics\n\n    def reset_metrics(self):\n        \"\"\"Resets the model's metrics.\"\"\"\n        for of_obj in self.output_features.values():\n            of_obj.reset_metrics()\n        self.eval_loss_metric.reset()\n\n    def collect_weights(self, tensor_names=None, **kwargs):\n        \"\"\"Returns named parameters filtered against `tensor_names` if not None.\"\"\"\n        if not tensor_names:\n            return self.named_parameters()\n\n        # Check for bad tensor names.\n        weight_names = {name for name, _ in self.named_parameters()}\n        for name in tensor_names:\n            if name not in weight_names:\n                raise ValueError(f'Requested tensor name filter \"{name}\" not present in the model graph')  # noqa: E713\n\n        # Apply filter.\n        tensor_set = set(tensor_names)\n        return [named_param for named_param in self.named_parameters() if named_param[0] in tensor_set]\n\n    def unskip(self):\n        \"\"\"Converts all skipped features into their fully encoded versions.\"\"\"\n\n    @abstractmethod\n    def save(self, save_path: str):\n        \"\"\"Saves the model to the given path.\"\"\"\n\n    @abstractmethod\n    def load(self, save_path: str):\n        \"\"\"Loads the model from the given path.\"\"\"\n\n    @abstractmethod\n    def get_args(self):\n        \"\"\"Returns init arguments for constructing this model.\"\"\"\n\n    @contextlib.contextmanager\n    def use_generation_config(self, generation_config: dict[str, Any]):\n        if generation_config is not None:\n            raise NotImplementedError(f\"{self.__class__.__name__} does not support generation_config. \")\n        yield\n\n    def _activate_forward_hooks(self):\n        \"\"\"Activates/registers forward hooks for the model.\"\"\"\n\n    def _deactivate_forward_hooks(self) -> None:\n        \"\"\"Deactivates/de-registers forward hooks for the model (if needed).\"\"\"\n        for handle in self._forward_hook_handles:\n            handle.deactivate_hook()\n\n\ndef create_input_feature(feature_config: BaseInputFeatureConfig, encoder_obj: Encoder | None) -> InputFeature:\n    input_feature_cls = get_from_registry(feature_config.type, get_input_type_registry())\n    input_feature = input_feature_cls(feature_config, encoder_obj=encoder_obj)\n    if not feature_config.encoder.skip:\n        return input_feature\n    return create_passthrough_input_feature(input_feature, feature_config)\n"
  },
  {
    "path": "ludwig/models/calibrator.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport numpy as np\n\nfrom ludwig.backend import Backend\nfrom ludwig.models.ecd import ECD\n\n\nclass Calibrator:\n    \"\"\"Calibrator calibrates the output probabilities of a model.\"\"\"\n\n    def __init__(self, model: ECD, backend: Backend, batch_size: int = 128):\n        self.model = model\n        self.backend = backend\n        self.batch_size = batch_size\n\n    def calibration_enabled(self):\n        \"\"\"Calibration is enabled if the config requests calibration for any output feature.\n\n        If no output features have calibration enabled, the calibration phase should be skipped.\n        \"\"\"\n        return any(o.calibration_module is not None for o in self.model.output_features.values())\n\n    def train_calibration(self, dataset, dataset_name: str):\n        \"\"\"Calibrates model output probabilities on validation set after training.\n\n        This works well for most datasets, though it may fail for some difficult or extremely imbalanced datasets.\n        \"\"\"\n        if not self.calibration_enabled():\n            # Early out if no output features have calibration enabled.\n            return\n        with self.backend.create_predictor(self.model, batch_size=self.batch_size) as predictor:\n            metrics, predictions = predictor.batch_evaluation(\n                dataset, collect_predictions=True, collect_logits=True, dataset_name=dataset_name\n            )\n\n        dataset_df = dataset.to_df()\n        for output_feature in self.model.output_features.values():\n            if output_feature.calibration_module is not None:\n                feature_logits_key = f\"{output_feature.feature_name}_logits\"\n                if feature_logits_key in predictions:\n                    feature_logits = self.backend.df_engine.compute(predictions[feature_logits_key])\n                    feature_labels = self.backend.df_engine.compute(dataset_df[output_feature.proc_column])\n                    output_feature.calibration_module.train_calibration(\n                        np.stack(feature_logits.values, axis=0), np.stack(feature_labels.values, axis=0)\n                    )\n"
  },
  {
    "path": "ludwig/models/ecd.py",
    "content": "import logging\nimport os\n\nimport numpy as np\nimport torch\n\nfrom ludwig.accounting.used_tokens import get_used_tokens_for_ecd\nfrom ludwig.combiners.combiners import create_combiner\nfrom ludwig.constants import MODEL_ECD, MODEL_LLM, USED_TOKENS\nfrom ludwig.globals import MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.models.base import BaseModel\nfrom ludwig.schema.model_types.ecd import ECDModelConfig\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.augmentation_utils import AugmentationPipelines\nfrom ludwig.utils.data_utils import clear_data_cache\nfrom ludwig.utils.fs_utils import open_file\nfrom ludwig.utils.state_dict_backward_compatibility import update_state_dict\nfrom ludwig.utils.torch_utils import get_torch_device\n\nlogger = logging.getLogger(__name__)\n\n\nclass ECD(BaseModel):\n    @staticmethod\n    def type() -> str:\n        return MODEL_ECD\n\n    def __init__(\n        self,\n        config_obj: ECDModelConfig,\n        random_seed=None,\n        **_kwargs,\n    ):\n        self.config_obj = config_obj\n        self._random_seed = random_seed\n\n        super().__init__(random_seed=self._random_seed)\n\n        # ================ Inputs ================\n        try:\n            self.input_features.update(self.build_inputs(input_feature_configs=self.config_obj.input_features))\n        except KeyError as e:\n            raise KeyError(\n                f\"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}\"\n            ) from e\n\n        # ================ Combiner ================\n        logger.debug(f\"Combiner {self.config_obj.combiner.type}\")\n        self.combiner = create_combiner(self.config_obj.combiner, input_features=self.input_features)\n\n        # ================ Outputs ================\n        self.output_features.update(\n            self.build_outputs(output_feature_configs=self.config_obj.output_features, combiner=self.combiner)\n        )\n\n        # After constructing all layers, clear the cache to free up memory\n        clear_data_cache()\n\n    def prepare_for_training(self):\n        # 1/10/23: For parity with how the LLM model type sets up adapters and quantization, LLM encoders should call\n        # `prepare_for_training` at training time rather than at initialization. This loop searches for input features\n        # using the LLM encoder and calls `prepare_for_training` on those encoders only. No other changes should be\n        # made to the ECD model itself or any other encoders.\n        for feature in self.config_obj.input_features:\n            encoder_type = feature.encoder.type\n            if encoder_type == MODEL_LLM:\n                feature_name = feature.name\n                encoder = self.input_features.get(feature_name)\n                encoder.prepare_for_training()\n\n    def encode(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n    ):\n        # Convert inputs to tensors.\n        for input_feature_name, input_values in inputs.items():\n            if not isinstance(input_values, torch.Tensor):\n                inputs[input_feature_name] = torch.from_numpy(input_values)\n            else:\n                inputs[input_feature_name] = input_values\n\n        encoder_outputs = {}\n        for input_feature_name, input_values in inputs.items():\n            encoder = self.input_features.get(input_feature_name)\n            encoder_output = encoder(input_values)\n            encoder_outputs[input_feature_name] = encoder_output\n\n        return encoder_outputs\n\n    def combine(self, encoder_outputs):\n        return self.combiner(encoder_outputs)\n\n    def decode(self, combiner_outputs, targets, mask):\n        # Invoke output features.\n        output_logits = {}\n        output_last_hidden = {}\n        for output_feature_name, output_feature in self.output_features.items():\n            # Use the presence or absence of targets to signal training or prediction.\n            target = targets[output_feature_name] if targets is not None else None\n            decoder_outputs = output_feature(combiner_outputs, output_last_hidden, mask=mask, target=target)\n\n            # Add decoder outputs to overall output dictionary.\n            for decoder_output_name, tensor in decoder_outputs.items():\n                output_feature_utils.set_output_feature_tensor(\n                    output_logits, output_feature_name, decoder_output_name, tensor\n                )\n\n            # Save the hidden state of the output feature (for feature dependencies).\n            output_last_hidden[output_feature_name] = decoder_outputs[\"last_hidden\"]\n        return output_logits\n\n    def forward(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n        mask=None,\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Forward pass of the model.\n\n        Args:\n            inputs: Inputs to the model. Can be a dictionary of input names to\n                input tensors or a tuple of (inputs, targets) where inputs is\n                a dictionary of input names to input tensors and targets is a\n                dictionary of target names to target tensors.\n            mask: A mask for the inputs.\n\n        Returns:\n            A dictionary of output {feature name}::{tensor_name} -> output tensor.\n        \"\"\"\n\n        if isinstance(inputs, tuple):\n            inputs, targets = inputs\n            # Convert targets to tensors.\n            for target_feature_name, target_value in targets.items():\n                if not isinstance(target_value, torch.Tensor):\n                    targets[target_feature_name] = torch.from_numpy(target_value)\n                else:\n                    targets[target_feature_name] = target_value\n        else:\n            targets = None\n\n        assert list(inputs.keys()) == self.input_features.keys()\n\n        encoder_outputs = self.encode(inputs)\n        combiner_outputs = self.combine(encoder_outputs)\n        decoder_outputs = self.decode(combiner_outputs, targets, mask)\n\n        # Compute the number of used tokens.\n        decoder_outputs[USED_TOKENS] = get_used_tokens_for_ecd(inputs, targets)\n        return decoder_outputs\n\n    def unskip(self):\n        for k in self.input_features.keys():\n            self.input_features.set(k, self.input_features.get(k).unskip())\n\n    def save(self, save_path):\n        \"\"\"Saves the model to the given path.\"\"\"\n        weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME)\n        torch.save(self.state_dict(), weights_save_path)\n        # Ensure the file is fully flushed to disk before any other process reads it\n        with open(weights_save_path, \"rb\") as f:\n            os.fsync(f.fileno())\n\n    def load(self, save_path):\n        \"\"\"Loads the model from the given path.\"\"\"\n        weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME)\n        device = torch.device(get_torch_device())\n        with open_file(weights_save_path, \"rb\") as f:\n            state_dict = torch.load(f, map_location=device)\n            self.load_state_dict(update_state_dict(state_dict))\n\n    def get_args(self):\n        \"\"\"Returns init arguments for constructing this model.\"\"\"\n        return (\n            self.config_obj.input_features.to_list(),\n            self.config_obj.combiner.to_dict(),\n            self.config_obj.output_features.to_list(),\n            self._random_seed,\n        )\n\n    def get_augmentation_pipelines(self) -> AugmentationPipelines:\n        \"\"\"Returns the augmentation pipeline for this model.\"\"\"\n        # dictionary to hold any augmentation pipeline\n        augmentation_pipelines = {}\n\n        # loop through all input features and add their augmentation pipeline to the dictionary\n        for input_feature in self.config_obj.input_features:\n            # if augmentation was specified for this input feature, add AugmentationPipeline to dictionary\n            if input_feature.has_augmentation():\n                # use input feature proc_column as key because that is what is used in the Batcher\n                augmentation_pipelines[input_feature.proc_column] = self.input_features.get(\n                    input_feature.name\n                ).get_augmentation_pipeline()\n\n        return AugmentationPipelines(augmentation_pipelines)\n"
  },
  {
    "path": "ludwig/models/embedder.py",
    "content": "from collections.abc import Callable\n\nimport numpy as np\nimport pandas as pd\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT, MODEL_ECD, NAME, PROC_COLUMN, TYPE\nfrom ludwig.features.feature_registries import get_input_type_registry\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.models.base import BaseModel\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, FeatureCollection\nfrom ludwig.schema.features.utils import get_input_feature_cls\nfrom ludwig.types import FeatureConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils.batch_size_tuner import BatchSizeEvaluator\nfrom ludwig.utils.dataframe_utils import from_numpy_dataset\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import get_torch_device, LudwigModule\n\n\n@DeveloperAPI\nclass Embedder(LudwigModule):\n    def __init__(self, feature_configs: list[FeatureConfigDict], metadata: TrainingSetMetadataDict):\n        super().__init__()\n\n        self.input_features = LudwigFeatureDict()\n\n        input_feature_configs = []\n        for feature in feature_configs:\n            feature_cls = get_from_registry(feature[TYPE], get_input_type_registry())\n\n            # TODO(travis): this assumes ECD is the selected model type. The best solution is to change the\n            # input params from FeatureConfigDict types to BaseInputFeatureConfig types, which will require a\n            # refactor of preprocessing to use the schema, not the dict types.\n            feature_obj = get_input_feature_cls(MODEL_ECD, feature[TYPE]).from_dict(feature)\n            feature_cls.update_config_with_metadata(feature_obj, metadata[feature[NAME]])\n\n            # When running prediction or eval, we need the preprocessing to use the original pretrained\n            # weights, which requires unsetting this field. In the future, we could avoid this by plumbing\n            # through the saved weights and loading them dynamically after building the model.\n            feature_obj.encoder.saved_weights_in_checkpoint = False\n\n            input_feature_configs.append(feature_obj)\n\n        feature_collection = FeatureCollection[BaseInputFeatureConfig](input_feature_configs)\n        try:\n            self.input_features.update(BaseModel.build_inputs(input_feature_configs=feature_collection))\n        except KeyError as e:\n            raise KeyError(\n                f\"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}\"\n            )\n\n    def forward(self, inputs: dict[str, torch.Tensor]):\n        encoder_outputs = {}\n        for input_feature_name, input_values in inputs.items():\n            encoder = self.input_features.get(input_feature_name)\n            encoder_output = encoder(input_values)\n            encoder_outputs[input_feature_name] = encoder_output[ENCODER_OUTPUT]\n        return encoder_outputs\n\n\n@DeveloperAPI\ndef create_embed_batch_size_evaluator(\n    features_to_encode: list[FeatureConfigDict], metadata: TrainingSetMetadataDict\n) -> BatchSizeEvaluator:\n    class _EmbedBatchSizeEvaluator(BatchSizeEvaluator):\n        def __init__(self):\n            embedder = Embedder(features_to_encode, metadata)\n            self.device = get_torch_device()\n            self.embedder = embedder.to(self.device)\n            self.embedder.eval()\n\n        def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n            inputs = {\n                input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(self.device)\n                for input_feature_name, input_feature in self.embedder.input_features.items()\n            }\n            with torch.no_grad():\n                self.embedder(inputs)\n\n    return _EmbedBatchSizeEvaluator\n\n\n@DeveloperAPI\ndef create_embed_transform_fn(\n    features_to_encode: list[FeatureConfigDict], metadata: TrainingSetMetadataDict\n) -> Callable:\n    class EmbedTransformFn:\n        def __init__(self):\n            embedder = Embedder(features_to_encode, metadata)\n            self.device = get_torch_device()\n            self.embedder = embedder.to(self.device)\n            self.embedder.eval()\n\n        def __call__(self, df: pd.DataFrame) -> pd.DataFrame:\n            batch = _prepare_batch(df, features_to_encode, metadata)\n            name_to_proc = {i_feat.feature_name: i_feat.proc_column for i_feat in self.embedder.input_features.values()}\n            inputs = {\n                i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device)\n                for i_feat in self.embedder.input_features.values()\n            }\n            with torch.no_grad():\n                encoder_outputs = self.embedder(inputs)\n\n            encoded = {name_to_proc[k]: v.detach().cpu().float().numpy() for k, v in encoder_outputs.items()}\n            output_df = from_numpy_dataset(encoded)\n\n            for c in output_df.columns:\n                df[c] = output_df[c]\n\n            return df\n\n    return EmbedTransformFn\n\n\n# TODO(travis): consolidate with implementation in data/ray.py\ndef _prepare_batch(\n    df: pd.DataFrame, features: list[FeatureConfigDict], metadata: TrainingSetMetadataDict\n) -> dict[str, np.ndarray]:\n    batch = {}\n    for feature in features:\n        c = feature[PROC_COLUMN]\n        if df[c].values.dtype == \"object\":\n            # Ensure columns stacked instead of turned into np.array([np.array, ...], dtype=object) objects\n            batch[c] = np.stack(df[c].values)\n        else:\n            batch[c] = df[c].to_numpy()\n\n    for feature in features:\n        c = feature[PROC_COLUMN]\n        reshape = metadata.get(feature[NAME], {}).get(\"reshape\")\n        if reshape is not None:\n            batch[c] = batch[c].reshape((-1, *reshape))\n\n    return batch\n"
  },
  {
    "path": "ludwig/models/inference.py",
    "content": "import logging\nimport os\nfrom typing import Any, TYPE_CHECKING\n\nimport pandas as pd\nimport torch\nfrom torch import nn\n\nfrom ludwig.constants import NAME, POSTPROCESSOR, PREDICTOR, PREPROCESSOR, TYPE\nfrom ludwig.data.postprocessing import convert_dict_to_df\nfrom ludwig.data.preprocessing import load_metadata\nfrom ludwig.features.feature_registries import get_input_type_registry\nfrom ludwig.features.feature_utils import get_module_dict_key_from_name, get_name_from_module_dict_key\nfrom ludwig.globals import MODEL_HYPERPARAMETERS_FILE_NAME, TRAIN_SET_METADATA_FILE_NAME\nfrom ludwig.types import ModelConfigDict, TrainingSetMetadataDict\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.data_utils import load_json, save_json\nfrom ludwig.utils.inference_utils import get_filename_from_stage, to_inference_module_input_from_dataframe\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.output_feature_utils import get_feature_name_from_concat_name, get_tensor_name_from_concat_name\nfrom ludwig.utils.torch_utils import DEVICE\nfrom ludwig.utils.types import TorchDevice, TorchscriptPreprocessingInput\n\n# Prevents circular import errors from typing.\nif TYPE_CHECKING:\n    from ludwig.models.base import BaseModel\n\nlogger = logging.getLogger(__name__)\n\n\nclass InferenceModule(nn.Module):\n    \"\"\"A nn.Module subclass that wraps the inference preprocessor, predictor, and postprocessor.\"\"\"\n\n    def __init__(\n        self,\n        preprocessor: torch.jit.ScriptModule,\n        predictor: torch.jit.ScriptModule,\n        postprocessor: torch.jit.ScriptModule,\n        config: ModelConfigDict | None = None,\n        training_set_metadata: TrainingSetMetadataDict | None = None,\n    ):\n        super().__init__()\n        self.preprocessor = preprocessor\n        self.predictor = predictor\n        self.postprocessor = postprocessor\n        self.config = config\n        # Do not remove – used by Predibase app\n        self.training_set_metadata = training_set_metadata\n\n    def preprocessor_forward(self, inputs: dict[str, TorchscriptPreprocessingInput]) -> dict[str, torch.Tensor]:\n        \"\"\"Forward pass through the preprocessor.\"\"\"\n        return self.preprocessor(inputs)\n\n    def predictor_forward(self, preproc_inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:\n        \"\"\"Forward pass through the predictor.\n\n        Ensures that the inputs are on the correct device. The outputs are on the same device as self.predictor.\n        \"\"\"\n        for k, v in preproc_inputs.items():\n            preproc_inputs[k] = v.to(self.predictor.device)\n\n        with torch.no_grad():  # Ensure model params do not compute gradients\n            predictions_flattened = self.predictor(preproc_inputs)\n            return predictions_flattened\n\n    def postprocessor_forward(self, predictions_flattened: dict[str, torch.Tensor]) -> dict[str, dict[str, Any]]:\n        \"\"\"Forward pass through the postprocessor.\"\"\"\n        postproc_outputs_flattened: dict[str, Any] = self.postprocessor(predictions_flattened)\n        # Turn flat inputs into nested predictions per feature name\n        postproc_outputs: dict[str, dict[str, Any]] = _unflatten_dict_by_feature_name(postproc_outputs_flattened)\n        return postproc_outputs\n\n    def forward(self, inputs: dict[str, TorchscriptPreprocessingInput]) -> dict[str, dict[str, Any]]:\n        preproc_inputs: dict[str, torch.Tensor] = self.preprocessor_forward(inputs)\n        predictions_flattened: dict[str, torch.Tensor] = self.predictor_forward(preproc_inputs)\n        postproc_outputs: dict[str, dict[str, Any]] = self.postprocessor_forward(predictions_flattened)\n        return postproc_outputs\n\n    @torch.jit.unused\n    def predict(self, dataset: pd.DataFrame, return_type: dict | pd.DataFrame = pd.DataFrame) -> pd.DataFrame | dict:\n        \"\"\"Predict on a batch of data with an interface similar to LudwigModel.predict.\"\"\"\n        inputs = to_inference_module_input_from_dataframe(dataset, self.config, load_paths=True)\n\n        preds = self(inputs)\n\n        if return_type == pd.DataFrame:\n            preds = convert_dict_to_df(preds)\n        return preds, None  # Second return value is for compatibility with LudwigModel.predict\n\n    @torch.jit.unused\n    @classmethod\n    def from_ludwig_model(\n        cls: \"InferenceModule\",\n        model: \"BaseModel\",\n        config: ModelConfigDict,\n        training_set_metadata: TrainingSetMetadataDict,\n        device: TorchDevice | None = None,\n    ):\n        \"\"\"Create an InferenceModule from a trained LudwigModel.\"\"\"\n        if device is None:\n            logger.info(f'No device specified. Loading using device \"{DEVICE}\".')\n            device = DEVICE\n\n        stage_to_module = _init_inference_stages_from_ludwig_model(\n            model, config, training_set_metadata, device=device, scripted=True\n        )\n\n        return cls(\n            stage_to_module[PREPROCESSOR],\n            stage_to_module[PREDICTOR],\n            stage_to_module[POSTPROCESSOR],\n            config=config,\n            training_set_metadata=training_set_metadata,\n        )\n\n    @torch.jit.unused\n    @classmethod\n    def from_directory(\n        cls: \"InferenceModule\",\n        directory: str,\n        device: TorchDevice | None = None,\n    ):\n        \"\"\"Create an InferenceModule from a directory containing a model, config, and training set metadata.\"\"\"\n        if device is None:\n            logger.info(f'No device specified. Loading using device \"{DEVICE}\".')\n            device = DEVICE\n\n        stage_to_module = _init_inference_stages_from_directory(directory, device=device)\n\n        config_path = os.path.join(directory, MODEL_HYPERPARAMETERS_FILE_NAME)\n        config = load_json(config_path) if os.path.exists(config_path) else None\n\n        metadata_path = os.path.join(directory, TRAIN_SET_METADATA_FILE_NAME)\n        training_set_metadata = load_metadata(metadata_path) if os.path.exists(metadata_path) else None\n\n        return cls(\n            stage_to_module[PREPROCESSOR],\n            stage_to_module[PREDICTOR],\n            stage_to_module[POSTPROCESSOR],\n            config=config,\n            training_set_metadata=training_set_metadata,\n        )\n\n\nclass _InferencePreprocessor(nn.Module):\n    \"\"\"Wraps preprocessing modules into a single nn.Module.\n\n    TODO(geoffrey): Implement torchscript-compatible feature_utils.LudwigFeatureDict to replace\n    get_module_dict_key_from_name and get_name_from_module_dict_key usage.\n    \"\"\"\n\n    def __init__(self, config: ModelConfigDict, training_set_metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.preproc_modules = nn.ModuleDict()\n        for feature_config in config[\"input_features\"]:\n            feature_name = feature_config[NAME]\n            feature = get_from_registry(feature_config[TYPE], get_input_type_registry())\n            # prevents collisions with reserved keywords\n            module_dict_key = get_module_dict_key_from_name(feature_name)\n            self.preproc_modules[module_dict_key] = feature.create_preproc_module(training_set_metadata[feature_name])\n\n    def forward(self, inputs: dict[str, TorchscriptPreprocessingInput]) -> dict[str, torch.Tensor]:\n        preproc_inputs = {}\n        for module_dict_key, preproc in self.preproc_modules.items():\n            feature_name = get_name_from_module_dict_key(module_dict_key)\n            preproc_inputs[feature_name] = preproc(inputs[feature_name])\n        return preproc_inputs\n\n\nclass _InferencePredictor(nn.Module):\n    \"\"\"Wraps model forward pass + predictions into a single nn.Module.\n\n    The forward call of this module returns a flattened dictionary in order to support Triton input/output.\n\n    TODO(geoffrey): Implement torchscript-compatible feature_utils.LudwigFeatureDict to replace\n    get_module_dict_key_from_name and get_name_from_module_dict_key usage.\n    \"\"\"\n\n    def __init__(self, model: \"BaseModel\", device: TorchDevice):\n        super().__init__()\n        self.device = torch.device(device)\n        self.model = model.to_torchscript(self.device)\n        self.predict_modules = nn.ModuleDict()\n        for feature_name, feature in model.output_features.items():\n            # prevents collisions with reserved keywords\n            module_dict_key = get_module_dict_key_from_name(feature_name)\n            self.predict_modules[module_dict_key] = feature.prediction_module.to(device=self.device)\n\n    def forward(self, preproc_inputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]:\n        model_outputs = self.model(preproc_inputs)\n        predictions_flattened: dict[str, torch.Tensor] = {}\n        for module_dict_key, predict in self.predict_modules.items():\n            feature_name = get_name_from_module_dict_key(module_dict_key)\n            feature_predictions = predict(model_outputs, feature_name)\n            # Flatten out the predictions to support Triton input/output\n            for predict_key, tensor_values in feature_predictions.items():\n                predict_concat_key = output_feature_utils.get_feature_concat_name(feature_name, predict_key)\n                predictions_flattened[predict_concat_key] = tensor_values\n        return predictions_flattened\n\n\nclass _InferencePostprocessor(nn.Module):\n    \"\"\"Wraps postprocessing modules into a single nn.Module.\n\n    The forward call of this module returns a flattened dictionary in order to support Triton input/output.\n\n    TODO(geoffrey): Implement torchscript-compatible feature_utils.LudwigFeatureDict to replace\n    get_module_dict_key_from_name and get_name_from_module_dict_key usage.\n    \"\"\"\n\n    def __init__(self, model: \"BaseModel\", training_set_metadata: TrainingSetMetadataDict):\n        super().__init__()\n        self.postproc_modules = nn.ModuleDict()\n        for feature_name, feature in model.output_features.items():\n            # prevents collisions with reserved keywords\n            module_dict_key = get_module_dict_key_from_name(feature_name)\n            self.postproc_modules[module_dict_key] = feature.create_postproc_module(training_set_metadata[feature_name])\n\n    def forward(self, predictions_flattened: dict[str, torch.Tensor]) -> dict[str, Any]:\n        postproc_outputs_flattened: dict[str, Any] = {}\n        for module_dict_key, postproc in self.postproc_modules.items():\n            feature_name = get_name_from_module_dict_key(module_dict_key)\n            feature_postproc_outputs = postproc(predictions_flattened, feature_name)\n            # Flatten out the predictions to support Triton input/output\n            for postproc_key, tensor_values in feature_postproc_outputs.items():\n                postproc_concat_key = output_feature_utils.get_feature_concat_name(feature_name, postproc_key)\n                postproc_outputs_flattened[postproc_concat_key] = tensor_values\n        return postproc_outputs_flattened\n\n\ndef save_ludwig_model_for_inference(\n    save_path: str,\n    model: \"BaseModel\",\n    config: ModelConfigDict,\n    training_set_metadata: TrainingSetMetadataDict,\n    device: TorchDevice | None = None,\n    model_only: bool = False,\n) -> None:\n    \"\"\"Saves a LudwigModel (a BaseModel model, config, and training_set_metadata) for inference.\"\"\"\n    if device is None:\n        logger.info(f'No device specified. Saving using device \"{DEVICE}\".')\n        device = DEVICE\n\n    stage_to_filenames = {\n        stage: get_filename_from_stage(stage, device) for stage in [PREPROCESSOR, PREDICTOR, POSTPROCESSOR]\n    }\n\n    stage_to_module = _init_inference_stages_from_ludwig_model(\n        model, config, training_set_metadata, device, scripted=True\n    )\n    if model_only:\n        stage_to_module[PREDICTOR].save(os.path.join(save_path, stage_to_filenames[PREDICTOR]))\n    else:\n        config_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n        if not os.path.exists(config_path):\n            save_json(config_path, config)\n            logger.info(f\"Saved model config to {config_path}\")\n\n        training_set_metadata_path = os.path.join(save_path, TRAIN_SET_METADATA_FILE_NAME)\n        if not os.path.exists(training_set_metadata_path):\n            save_json(training_set_metadata_path, training_set_metadata)\n            logger.info(f\"Saved training set metadata to {training_set_metadata_path}\")\n\n        for stage, module in stage_to_module.items():\n            module.save(os.path.join(save_path, stage_to_filenames[stage]))\n            logger.info(f\"Saved torchscript module for {stage} to {stage_to_filenames[stage]}.\")\n\n\ndef _init_inference_stages_from_directory(\n    directory: str,\n    device: TorchDevice,\n) -> dict[str, torch.nn.Module]:\n    \"\"\"Initializes inference stage modules from directory.\"\"\"\n    stage_to_filenames = {\n        stage: get_filename_from_stage(stage, device) for stage in [PREPROCESSOR, PREDICTOR, POSTPROCESSOR]\n    }\n\n    stage_to_module = {}\n    for stage in [PREPROCESSOR, PREDICTOR, POSTPROCESSOR]:\n        stage_to_module[stage] = torch.jit.load(os.path.join(directory, stage_to_filenames[stage]))\n        print(f\"Loaded torchscript module for {stage} from {stage_to_filenames[stage]}.\")\n    return stage_to_module\n\n\ndef _init_inference_stages_from_ludwig_model(\n    model: \"BaseModel\",\n    config: ModelConfigDict,\n    training_set_metadata: TrainingSetMetadataDict,\n    device: TorchDevice,\n    scripted: bool = True,\n) -> dict[str, torch.nn.Module]:\n    \"\"\"Initializes inference stage modules from a LudwigModel (a BaseModel model, config, and\n    training_set_metadata).\"\"\"\n    preprocessor = _InferencePreprocessor(config, training_set_metadata)\n    predictor = _InferencePredictor(model, device=device)\n    postprocessor = _InferencePostprocessor(model, training_set_metadata)\n\n    stage_to_module = {\n        PREPROCESSOR: preprocessor,\n        PREDICTOR: predictor,\n        POSTPROCESSOR: postprocessor,\n    }\n    if scripted:\n        stage_to_module = {stage: torch.jit.script(module) for stage, module in stage_to_module.items()}\n    return stage_to_module\n\n\ndef _unflatten_dict_by_feature_name(flattened_dict: dict[str, Any]) -> dict[str, dict[str, Any]]:\n    \"\"\"Convert a flattened dictionary of objects to a nested dictionary of outputs per feature name.\"\"\"\n    outputs: dict[str, dict[str, Any]] = {}\n    for concat_key, tensor_values in flattened_dict.items():\n        feature_name = get_feature_name_from_concat_name(concat_key)\n        tensor_name = get_tensor_name_from_concat_name(concat_key)\n        feature_outputs: dict[str, Any] = {}\n        if feature_name not in outputs:\n            outputs[feature_name] = feature_outputs\n        else:\n            feature_outputs = outputs[feature_name]\n        feature_outputs[tensor_name] = tensor_values\n    return outputs\n"
  },
  {
    "path": "ludwig/models/llm.py",
    "content": "import contextlib\nimport logging\nimport os\nfrom typing import Any\n\nimport numpy as np\nimport torch\nfrom transformers import AutoConfig, GenerationConfig\n\nfrom ludwig.accounting.used_tokens import get_used_tokens_for_llm\nfrom ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, MODEL_LLM, PREDICTIONS, TEXT, USED_TOKENS\nfrom ludwig.features.base_feature import ModuleWrapper, OutputFeature\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.features.text_feature import TextOutputFeature\nfrom ludwig.globals import MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.models.base import BaseModel\nfrom ludwig.modules.training_hooks import NEFTuneHook\nfrom ludwig.schema.features.base import BaseOutputFeatureConfig, FeatureCollection\nfrom ludwig.schema.model_types.llm import LLMModelConfig\nfrom ludwig.utils.augmentation_utils import AugmentationPipelines\nfrom ludwig.utils.data_utils import clear_data_cache\nfrom ludwig.utils.llm_quantization_utils import convert_quantized_linear_to_linear\nfrom ludwig.utils.llm_utils import (\n    add_left_padding,\n    generate_merged_ids,\n    get_context_len,\n    get_realigned_target_and_prediction_tensors_for_inference,\n    initialize_adapter,\n    load_pretrained_from_config,\n    pad_target_tensor_for_fine_tuning,\n    remove_left_padding,\n    to_device,\n)\nfrom ludwig.utils.logging_utils import log_once\nfrom ludwig.utils.output_feature_utils import set_output_feature_tensor\nfrom ludwig.utils.tokenizers import HFTokenizer\nfrom ludwig.utils.torch_utils import reg_loss\n\nlogger = logging.getLogger(__name__)\n\n\nclass DictWrapper:\n    \"\"\"Wrapper for a LudwigFeatureDict module that allows for iteration over keys.\n\n    The purpose of this class is to avoid exposing input and output features as modules of the LLM. This is because we\n    only wish to train the underlying model, and having these additional modules can confuse systems like DeepSpeed.\n    \"\"\"\n\n    def __init__(self, obj: LudwigFeatureDict):\n        self.obj = obj\n\n    def get(self, key) -> torch.nn.Module:\n        return self.obj.get(key)\n\n    def set(self, key: str, module: torch.nn.Module) -> None:\n        self.obj.set(key, module)\n\n    def __len__(self) -> int:\n        return len(self.obj)\n\n    def __next__(self) -> None:\n        return next(iter(self.obj))\n\n    def __iter__(self) -> None:\n        return iter(self.obj.keys())\n\n    def keys(self) -> list[str]:\n        return self.obj.keys()\n\n    def values(self) -> list[torch.nn.Module]:\n        return self.obj.values()\n\n    def items(self) -> list[tuple[str, torch.nn.Module]]:\n        return self.obj.items()\n\n    def update(self, modules: dict[str, torch.nn.Module]) -> None:\n        self.obj.update(modules)\n\n\nclass LLM(BaseModel):\n    @staticmethod\n    def type() -> str:\n        return MODEL_LLM\n\n    def __init__(\n        self,\n        config_obj: LLMModelConfig,\n        random_seed=None,\n        _device=None,\n        **_kwargs,\n    ):\n        super().__init__(random_seed=random_seed)\n\n        self.config_obj = config_obj\n        self._random_seed = random_seed\n\n        self.model_name = self.config_obj.base_model\n        self.model_config = AutoConfig.from_pretrained(\n            self.config_obj.base_model,\n            trust_remote_code=self.config_obj.trust_remote_code,\n        )\n\n        self.model = load_pretrained_from_config(self.config_obj, model_config=self.model_config)\n        self.curr_device = next(self.model.parameters()).device\n        logger.info(\"Done.\")\n\n        self.context_len = get_context_len(self.model_config)\n\n        # TODO(Arnav): This needs be more flexible to account for RoPE Scaling\n        # When merging input IDs and target IDs for LLM fine-tuning, we want to make sure that the merged tensor is\n        # not longer than the global maximum sequence length. This is provided in the preprocessing config. We never\n        # want to exceed the maximum possible context length so we also check for that.\n        if self.config_obj.preprocessing.global_max_sequence_length:\n            global_max_sequence_length = self.config_obj.preprocessing.global_max_sequence_length\n            self.global_max_sequence_length = (\n                global_max_sequence_length if global_max_sequence_length <= self.context_len else self.context_len\n            )\n        else:\n            self.global_max_sequence_length = self.context_len\n\n        # Initialize tokenizer\n        self.tokenizer = HFTokenizer(\n            self.config_obj.base_model,\n            trust_remote_code=self.config_obj.trust_remote_code,\n        ).tokenizer\n\n        self._set_generation_config(self.config_obj.generation.to_dict())\n\n        # ================ Inputs ================\n        try:\n            self.input_features.update(self.build_inputs(input_feature_configs=self.config_obj.input_features))\n        except KeyError as e:\n            raise KeyError(\n                f\"An input feature has a name that conflicts with a class attribute of torch's ModuleDict: {e}\"\n            ) from e\n\n        # This is used to store the model inputs during the forward pass when fine-tuning LLMs. This allows us to have\n        # access to the joint model inputs (input_ids and target_ids) when computing metrics. In particular, the target\n        # ids are needed to correctly compute next token softmax cross entropy loss.\n        self.model_inputs = None\n\n        # ================ Outputs ================\n        self.output_feature_type = self.config_obj.output_features[0].type\n\n        self.output_features.update(\n            self.build_outputs(\n                output_feature_configs=self.config_obj.output_features,\n                # Set the input size to the model vocab size instead of the tokenizer vocab size\n                # because the model has additional \"head\" layers that are used to predict the next\n                # token in the sequence. These head layers can add additional dimensions to the\n                # logits tensor, beyond the vocab_size dimension.\n                input_size=self.input_shape[-1] if self.output_feature_type == TEXT else self.model_config.vocab_size,\n            )\n        )\n\n        # Extract the decoder object for the forward pass\n        self._output_feature_decoder = ModuleWrapper(self.output_features.items()[0][1])\n\n        self.attention_masks = None\n\n        clear_data_cache()\n\n    def create_feature_dict(self) -> DictWrapper:\n        return DictWrapper(LudwigFeatureDict())\n\n    @contextlib.contextmanager\n    def use_generation_config(self, generation_config_dict: dict[str, Any] | None = None):\n        \"\"\"Sets the generation config for the model.\"\"\"\n        # Save the original generation config so that we can reset it if/when we change it when self.generation gets is\n        # dynamically mutated during 1-off predict calls after fine-tuning.\n        original_generation_config_dict = self.generation.to_dict()\n        try:\n            # no-op if generation_config is None\n            if generation_config_dict is not None:\n                # unwrap the original generation config, update it with the new generation config\n                new_generation_config_dict = {**original_generation_config_dict, **generation_config_dict}\n                self._set_generation_config(new_generation_config_dict)\n            yield\n        finally:\n            self._set_generation_config(original_generation_config_dict)\n\n    def _set_generation_config(self, new_generation_config_dict: dict[str, Any]):\n        self.generation = GenerationConfig(**new_generation_config_dict)\n        # We need to manually set the pad_token_id to the tokenizer's pad_token_id for certain models like GPT and\n        # CodeLlama to avoid getting an error. This workaround can be found here:\n        # (https://github.com/huggingface/transformers/issues/25353#issuecomment-1669339754)\n        self.generation.pad_token_id = self.tokenizer.pad_token_id\n        self.max_new_tokens = self.generation.max_new_tokens\n        # max input length value copied from FastChat\n        # https://github.com/lm-sys/FastChat/blob/0e958b852a14f4bef5f0e9d7a5e7373477329cf2/fastchat/serve/inference.py#L183  # noqa E501\n        self.max_input_length = self.context_len - self.max_new_tokens - 8\n\n    @property\n    def output_feature_decoder(self) -> OutputFeature:\n        return self._output_feature_decoder.module\n\n    def initialize_adapter(self):\n        \"\"\"If an adapter config is provided, we want to wrap the model with a PEFT model for fine-tuning.\"\"\"\n        if self.config_obj.adapter:\n            if self.config_obj.trainer.type != \"finetune\" and not self.config_obj.adapter.pretrained_adapter_weights:\n                raise ValueError(\n                    \"Adapter config was provided, but trainer type is not set to `finetune`. Either set the trainer to \"\n                    \"`finetune` or remove the adapter config.\"\n                )\n\n            self.model = initialize_adapter(self.model, self.config_obj)\n\n            logger.info(\"==================================================\")\n            logger.info(\"Trainable Parameter Summary For Fine-Tuning\")\n            logger.info(f\"Fine-tuning with adapter: {self.config_obj.adapter.type}\")\n            self.model.print_trainable_parameters()\n            logger.info(\"==================================================\")\n\n    def prepare_for_training(self):\n        # TODO: this implementation will not work if resuming from a previous checkpoint. Need to fix this.\n        if self.config_obj.quantization:\n            self.prepare_for_quantized_training()\n        self.initialize_adapter()\n\n    def prepare_for_quantized_training(self):\n        from peft import prepare_model_for_kbit_training\n\n        self.model = prepare_model_for_kbit_training(self.model, use_gradient_checkpointing=False)\n\n    def to_device(self, device):\n        # Always refresh curr_device from actual parameter location, since\n        # nn.Module.to() can move parameters without updating curr_device.\n        self.curr_device = next(self.model.parameters()).device\n        self.model, device = to_device(self.model, device, self.config_obj, self.curr_device)\n        self.curr_device = device\n        return self\n\n    @classmethod\n    def build_outputs(\n        cls, output_feature_configs: FeatureCollection[BaseOutputFeatureConfig], input_size: int\n    ) -> dict[str, OutputFeature]:\n        \"\"\"Builds and returns output feature.\"\"\"\n        # TODO: only single task currently\n        if len(output_feature_configs) > 1:\n            raise ValueError(\"The LLM model type only supports a single output feature.\")\n\n        output_feature_config = output_feature_configs[0]\n        output_feature_config.input_size = input_size\n\n        output_features = {}\n        output_feature = cls.build_single_output(output_feature_config, output_features)\n        output_features[output_feature_config.name] = output_feature\n\n        return output_features\n\n    def forward(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n        mask=None,\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Produces logits tensor for finetuning the model.\n\n        Args:\n            inputs: Inputs to the model. Can be a dictionary of input names to\n                input tensors or a tuple of (inputs, targets) where inputs is\n                a dictionary of input names to input tensors and targets is a\n                dictionary of target names to target tensors.\n            mask: A mask for the inputs.\n\n        Returns:\n            A dictionary of output {feature name}::{tensor_name} -> output tensor.\n        \"\"\"\n        input_ids, target_ids = self._unpack_inputs(inputs)\n\n        # Generate merged input_id, target_id pairs for the model, and create corresponding attention masks\n        # We save them as class variables so that we can use them when realigning target and prediction tensors\n        self.model_inputs, self.attention_masks = generate_merged_ids(\n            input_ids, target_ids, self.tokenizer, self.global_max_sequence_length\n        )\n\n        # TODO (jeffkinnison): Determine why the 8-bit `SCB` and `CB` matrices are deleted in the forward pass\n        model_outputs = self.model(input_ids=self.model_inputs, attention_mask=self.attention_masks).get(LOGITS)\n\n        if self.output_feature_type != TEXT:\n            # Pass generated tokens through decoder after averaging the token probabilities\n            # This is required for the classification head for the classifier decoder\n            model_outputs = torch.mean(model_outputs, dim=1)\n\n        if self.output_feature_type == TEXT:\n            decoder_outputs = model_outputs\n        else:\n            decoder_outputs = self.output_feature_decoder.decoder_obj(model_outputs)\n\n        # Set the output feature tensor to the decoder outputs (logits)\n        outputs = {}\n        of_name = self.config_obj.output_features[0].name\n        set_output_feature_tensor(outputs, of_name, LOGITS, decoder_outputs)\n\n        # Get predictions, probabilities and logits tensor from the output feature's predictions function\n        outputs = self.output_features.get(of_name).predictions(outputs, of_name)\n\n        # Cast to float32 for metric computation incase we're using deespeed with\n        # reduced precision such as bfloat16.\n        for prediction_key, prediction_tensor in outputs.items():\n            if prediction_key != PREDICTIONS:\n                # Skipping casting it to float32 since the predictions are tokens and they should be int64\n                # (which is already the case)\n                outputs[prediction_key] = prediction_tensor.type(torch.float32)\n\n        # Add token usage.\n        outputs[USED_TOKENS] = get_used_tokens_for_llm(self.model_inputs, self.tokenizer)\n        return outputs\n\n    def generate(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n        mask=None,\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Generates tokens using the model.\"\"\"\n        log_once(f\"For generating text, using: {self.generation}\")\n        input_ids, _ = self._unpack_inputs(inputs)\n\n        with torch.no_grad():\n            input_lengths = []\n            sequences_list = []\n            for input_ids_sample in input_ids:\n                input_ids_sample_no_padding = remove_left_padding(input_ids_sample, self.tokenizer)\n\n                if input_ids_sample_no_padding.shape[1] > self.max_input_length:\n                    logger.warning(\n                        f\"Input length {input_ids_sample_no_padding.shape[1]} is \"\n                        f\"greater than max input length {self.max_input_length}. Truncating.\"\n                    )\n                    input_ids_sample_no_padding = input_ids_sample_no_padding[:, -self.max_input_length :]  # noqa E203\n\n                input_lengths.append(input_ids_sample_no_padding.shape[1])\n\n                # Ensure input_ids are on the same device as the model\n                model_device = next(self.model.parameters()).device\n                input_ids_sample_no_padding = input_ids_sample_no_padding.to(model_device)\n\n                # Generate text using the model\n                model_outputs = self.model.generate(\n                    input_ids=input_ids_sample_no_padding,\n                    attention_mask=mask,\n                    generation_config=self.generation,\n                    return_dict_in_generate=True,\n                    output_scores=True,\n                )\n\n                sequences_list.append(model_outputs.sequences[0])\n\n            # Extract the predictions, probabilities and logits from the model outputs\n            # through the forward pass of the output feature\n            outputs = self.output_feature_decoder.decoder_obj.forward(\n                sequences_list,\n                input_lengths,\n                self.max_new_tokens,\n            )\n\n        return outputs\n\n    def is_merge_and_unload_set(self) -> bool:\n        \"\"\"Check if the \"adapter\" configuration section exists and, if affirmative, that it contains the\n        \"postprocessor\" subsection and the \"merge_adapter_into_base_model\" and \"progressbar\" directives.\n\n        # Return\n\n        :return (bool): whether merge_and_unload should be done.\n        \"\"\"\n        return (\n            self.config_obj.adapter is not None\n            and self.config_obj.adapter.postprocessor is not None\n            and self.config_obj.adapter.postprocessor.merge_adapter_into_base_model\n        )\n\n    def merge_and_unload(self, progressbar: bool = False) -> None:\n        \"\"\"This method merges the LoRa layers into the base model.  This is needed if someone wants to use the base\n        model as a standalone model.  The implementation calls merge_and_unload() of the underlying LoraModel class\n        (in peft).\n\n        Args:\n            progressbar (bool): whether to show a progressbar indicating the unload and merge process\n        \"\"\"\n        from peft import LoraModel\n\n        if isinstance(self.model.base_model, LoraModel):\n            self.model.base_model.merge_and_unload(progressbar=progressbar)\n        else:\n            raise ValueError(\"This operation requires an LLM model trained with a LoRA adapter.\")\n\n    def _unpack_inputs(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n    ) -> tuple[torch.Tensor, torch.Tensor | None]:\n        \"\"\"Converts input tensors to input ids.\"\"\"\n        if isinstance(inputs, tuple):\n            inputs, targets = inputs\n            # Convert targets to tensors.\n            for target_feature_name, target_value in targets.items():\n                if not isinstance(target_value, torch.Tensor):\n                    targets[target_feature_name] = torch.from_numpy(target_value)\n                else:\n                    targets[target_feature_name] = target_value\n        else:\n            targets = None\n\n        assert list(inputs.keys()) == self.input_features.keys()\n\n        input_ids = self.get_input_ids(inputs)\n        target_ids = self.get_target_ids(targets) if targets else None\n\n        return input_ids, target_ids\n\n    def get_input_ids(\n        self,\n        inputs: (\n            dict[str, torch.Tensor] | dict[str, np.ndarray] | tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]\n        ),\n    ) -> torch.Tensor:\n        \"\"\"Returns the input ids for the text feature input.\"\"\"\n        return inputs[self.config_obj.input_features[0].name].type(torch.int32)\n\n    def get_target_ids(self, outputs: dict[str, torch.Tensor]) -> torch.Tensor:\n        \"\"\"Returns the output ids for the text feature output.\"\"\"\n        return outputs[self.config_obj.output_features[0].name].type(torch.int32)\n\n    def update_metrics(self, targets, predictions):\n        \"\"\"Updates the model's metrics given targets and predictions for zero-shot/few-shot.\"\"\"\n        for of_name, of_obj in self.output_features.items():\n            if isinstance(of_obj, TextOutputFeature):\n                # Align the target length with the predictions length to enable text metric evaluation.\n                _targets, _predictions = get_realigned_target_and_prediction_tensors_for_inference(\n                    targets, predictions, of_name, self.tokenizer\n                )\n                of_obj.update_metrics(_targets[of_name], _predictions[of_name], self.tokenizer)\n            else:\n                of_obj.update_metrics(targets[of_name], predictions[of_name])\n\n        # HACK (Tim): get the device of the targets to transfer self.eval_loss_metric to the same device\n        target_device = list(targets.values())[0].device\n\n        eval_loss, additional_losses = self.eval_loss(targets, predictions)\n        self.eval_loss_metric = self.eval_loss_metric.to(target_device)\n        self.eval_loss_metric.update(eval_loss)\n        self.eval_additional_losses_metrics.update(additional_losses)\n\n    def update_metrics_finetune_llm(self, targets, predictions):\n        \"\"\"Updates the model's metrics given targets and predictions for fine-tuning.\"\"\"\n        _targets, _predictions = targets, predictions\n        for of_name, of_obj in self.output_features.items():\n            if isinstance(of_obj, TextOutputFeature):\n                # Update the target tensor to enable text metric evaluation. This pads the target tensor with -100s\n                # to match the prediction length and depends on how much of the target tensor was included in the\n                # forward pass.\n                _targets = self._update_target_tensor_for_finetuning(_targets, _predictions, of_name)\n                if isinstance(of_obj, TextOutputFeature):\n                    of_obj.update_metrics(_targets[of_name], _predictions[of_name], self.tokenizer)\n                else:\n                    of_obj.update_metrics(_targets[of_name], _predictions[of_name])\n                continue\n\n            of_obj.update_metrics(_targets[of_name], _predictions[of_name])\n\n        eval_loss, additional_losses = self.eval_loss(_targets, _predictions)\n        self.eval_loss_metric.update(eval_loss)\n        self.eval_additional_losses_metrics.update(additional_losses)\n\n    def train_loss(\n        self,\n        targets,\n        predictions,\n        regularization_type: str | None = None,\n        regularization_lambda: float | None = None,\n    ) -> tuple[torch.Tensor, dict[str, torch.Tensor]]:\n        \"\"\"Computes the training loss for the model.\n\n        Args:\n            targets: A dictionary of target names to target tensors.\n            predictions: A dictionary of output names to output tensors.\n            regularization_type: One of 'l1', 'l2', 'l1_l2', or None.\n            regularization_lambda: The regularization lambda.\n\n        Returns:\n            A tuple of the loss tensor and a dictionary of loss for every\n            output feature.\n        \"\"\"\n        train_loss = 0\n        of_train_losses = {}\n        for of_name, of_obj in self.output_features.items():\n            _targets, _predictions = targets, predictions\n            if isinstance(of_obj, TextOutputFeature):\n                _predictions = {of_name: _predictions}\n\n                # Update the target tensor to enable text metric evaluation. This pads the target tensor with -100s\n                # to match the prediction length and depends on how much of the target tensor was included in the\n                # forward pass.\n                _targets = self._update_target_tensor_for_finetuning(_targets, _predictions, of_name)\n\n            # TODO(Arnav): Seems like doing this again and going between these format types in unnecessary, but\n            # refactor so that we don't have to do this at a later point.\n            predictions = {}\n            for key, _ in _predictions[of_name].items():\n                set_output_feature_tensor(predictions, of_name, key, _predictions[of_name][key])\n            _predictions = predictions\n\n            of_train_loss = of_obj.train_loss(_targets[of_name], _predictions, of_name)\n            train_loss += of_obj.loss.weight * of_train_loss\n            of_train_losses[of_name] = of_train_loss\n\n        additional_losses = self.losses()\n        if additional_losses:\n            train_loss += torch.sum(torch.stack(additional_losses))  # other losses\n\n        # Add regularization loss\n        if regularization_type is not None and regularization_lambda != 0:\n            train_loss += reg_loss(self, regularization_type, l1=regularization_lambda, l2=regularization_lambda)\n\n        return train_loss, of_train_losses\n\n    def eval_loss(self, targets, predictions):\n        \"\"\"Computes all evaluation losses for the model given targets and predictions.\n\n        Args:\n            targets: A dictionary of target names to target tensors.\n            predictions: A dictionary of output names to output tensors.\n\n        Returns:\n            A tuple of loss values for eval losses and additional losses.\n        \"\"\"\n        eval_loss = 0\n        for of_name, of_obj in self.output_features.items():\n            if isinstance(of_obj, TextOutputFeature):\n                # Align the target length with the predictions length to enable text metric evaluation.\n                _targets, _predictions = get_realigned_target_and_prediction_tensors_for_inference(\n                    targets, predictions, of_name, self.tokenizer\n                )\n                of_eval_loss = of_obj.eval_loss(_targets[of_name], _predictions[of_name])\n            else:\n                # HACK(geoffrey): we need a non-empty loss, so we just fill it with zeros\n                of_eval_loss = torch.tensor(0.0).to(predictions[of_name][LOGITS].device)\n\n            eval_loss += of_obj.loss.weight * of_eval_loss\n\n        additional_loss = 0\n        additional_losses = self.losses()\n        if additional_losses:\n            additional_loss = torch.sum(torch.stack(additional_losses))  # other losses\n\n        return eval_loss, additional_loss\n\n    def outputs_to_predictions(self, outputs: dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:\n        \"\"\"Returns the model's predictions for each output feature.\"\"\"\n        predictions = {}\n        for of_name in self.output_features:\n            # TODO(travis): this will need to change when we support multiple output features\n            predictions[of_name] = outputs\n        return predictions\n\n    def save(self, save_path):\n        \"\"\"Saves the model to the given path.\"\"\"\n        # TODO(travis): use the implementation of trainer itself to decide whether to save the model, to\n        # avoid this hack\n        if self.config_obj.trainer.type != \"none\":\n            weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME)\n            # We initialize the model's generation configuration; otherwise, we get a validation error.\n            self.model.generation_config = self.generation\n            self.model.save_pretrained(weights_save_path)\n        else:\n            logger.info(\"Skipped saving LLM without weight adjustments.\")\n\n    def save_base_model(self, save_path):\n        \"\"\"Saves the base LLM model to the given path.\"\"\"\n        # TODO: see the \"TODO\" statement from \"LLM.save()\" in this module.\n        if self.config_obj.trainer.type != \"none\":\n            weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME)\n            self.model.base_model.save_pretrained(weights_save_path)\n            # While this class initializes the tokenizer (from the base_model) automatically, and hence does not\n            # need to be saved if inference is to be done using LudwigModel.predict(), the rationale for saving the\n            # tokenizer to HuggingFace Hub is to provide access to models fine-tuned and persisted to HuggingFace Hub\n            # using Ludwig at a later time, with the ability to perform inference, independently of Ludwig itself.\n            self.tokenizer.save_pretrained(weights_save_path)\n        else:\n            logger.info(\"Skipped saving LLM without weight adjustments.\")\n\n    def save_dequantized_base_model(self, save_path: str) -> None:\n        \"\"\"Upscales quantized weights of a model to fp16 and saves the result in a folder specified by save_path.\n\n        Args:\n            save_path (str): The path to the folder where the upscaled model weights will be saved.\n\n        Returns:\n            None\n        \"\"\"\n        from peft import PeftModel\n\n        if isinstance(self.model, PeftModel):\n            # Get the base model back by removing all the adapter modules without merging.\n            logger.warning(\n                \"LLM model is currently wrapped in a PeftModel. Removing the adapter layers and saving the base model.\"\n                \"Reload the model via LudwigModel.load() to use your trained adapter layers for inference.\"\n            )\n            self.model = self.model.unload()\n\n        # Dequantize the model weights and cast them to fp16 - replace quantized layers with appropriate\n        # linear layers in-place.\n        logger.info(\"Upscaling quantized weights to fp16...\")\n        convert_quantized_linear_to_linear(self.model)\n        logger.info(\"Done.\")\n\n        # Remove the quantization configuration from the model\n        # The reason we can't delete the quantization config is because it is a property of the model and\n        # HF does some weird serialization of the config that causes an error when trying to access `self.model.config`\n        # after you try and delete a key from the config: TypeError: Object of type dtype is not JSON serializable.\n        self.model.config.quantization_config = {}\n\n        # Override properties of the model to indicate that it is no longer quantized.\n        # This is also necessary to ensure that the model can be saved, otherwise it will raise an error like\n        # \"You are calling `save_pretrained` on a 4-bit converted model. This is currently not supported\"\n        # See: https://github.com/huggingface/transformers/blob/0ad4e7e6dad670a7151aaceb1af3c272a3bf73a8/src/transformers/modeling_utils.py#L2054 # noqa\n        self.model.is_loaded_in_4bit = False\n        self.model.is_loaded_in_8bit = False\n\n        # Save the model\n        logger.info(f\"Saving upscaled model to {save_path}\")\n        self.model.save_pretrained(save_path)\n        logger.info(\"Done.\")\n\n        # Save the tokenizer\n        logger.info(f\"Saving tokenizer to {save_path}\")\n        self.tokenizer.save_pretrained(save_path)\n        logger.info(\"Done.\")\n\n    def load(self, save_path):\n        \"\"\"Loads the model from the given path.\"\"\"\n        weights_save_path = os.path.join(save_path, MODEL_WEIGHTS_FILE_NAME)\n        if self.config_obj.adapter:\n            # Check if the saved weights are merged (no adapter_config.json) or adapter-only\n            adapter_config_path = os.path.join(weights_save_path, \"adapter_config.json\")\n            if os.path.exists(adapter_config_path):\n                from peft import PeftModel  # noqa\n\n                if isinstance(self.model, PeftModel):\n                    # Unwrap and reload PeftModel\n                    self.model = self.model.base_model\n\n                self.model = PeftModel.from_pretrained(self.model, weights_save_path)\n            else:\n                # Weights were already merged (merge_and_unload was done before save),\n                # so load as a regular pretrained model.\n                logger.info(\"Loading merged LoRA weights (no adapter_config.json found).\")\n                self.model = load_pretrained_from_config(\n                    self.config_obj, model_config=self.model_config, weights_save_path=weights_save_path\n                )\n        elif self.config_obj.trainer.type != \"none\":\n            self.model = load_pretrained_from_config(\n                self.config_obj, model_config=self.model_config, weights_save_path=weights_save_path\n            )\n        else:\n            logger.info(\"Skipped loading LLM without weight adjustments.\")\n\n    def get_args(self):\n        \"\"\"Returns init arguments for constructing this model.\"\"\"\n        return (\n            self.config_obj.input_features.to_list(),\n            self.config_obj.output_features.to_list(),\n            self._random_seed,\n        )\n\n    def _update_target_tensor_for_finetuning(\n        self, targets: dict[str, torch.Tensor], predictions: dict[str, torch.Tensor], of_name: str\n    ) -> dict[str, torch.Tensor]:\n        \"\"\"Update target tensor for fine-tuning.\n\n        This method removes left padding from target tensors, adds a eos token to the end of the target tensors,\n        and pads the target tensors with -100 to ensure equal length for loss computation. It then realigns the\n        target tensors with the prediction tensors.\n\n        Args:\n            targets (Dict[str, torch.Tensor]): A dictionary containing the target tensors.\n            predictions (Dict[str, torch.Tensor]): A dictionary containing the predicted tensors.\n            of_name (str): The name of the target tensor.\n\n        Returns:\n            Dict[str, torch.Tensor]: A dictionary containing the updated target tensors aligned with predictions.\n        \"\"\"\n        # Remove left padding from target tensors since we also do this for the model's forward pass when we\n        # concatenate the input_ids with the target_ids. We also need to add the pad token to the end of the\n        # target tensors.\n        targets_without_padding = []\n        lengths = []\n\n        eos_token_tensor = torch.tensor([self.tokenizer.eos_token_id])\n        for target in targets[of_name]:\n            target = remove_left_padding(target, self.tokenizer)[0]\n            target = torch.cat([target, eos_token_tensor.to(device=target.device)], dim=-1).unsqueeze(0)\n            targets_without_padding.append(target)\n            lengths.append(target.shape[1])\n\n        # We need all target tensors to have the same length for the loss computation. We pad the target\n        # tensors with -100 since we want to negate all tokens that are not target_ids during the softmax\n        # cross entropy loss computation. This ensures that the loss is computed only for the target tokens.\n        max_length = max(lengths)\n        for i, target in enumerate(targets_without_padding):\n            targets_without_padding[i] = add_left_padding(\n                targets_without_padding[i][0],\n                max_length,\n                IGNORE_INDEX_TOKEN_ID,\n            )\n\n        targets[of_name] = torch.stack(targets_without_padding, dim=0).to(\n            dtype=targets[of_name].dtype,\n            device=targets[of_name].device,\n        )\n\n        # Re-align target tensors without padding to have equal length before realigning with the prediction\n        # tensors. Padding left with -100 to match the length of the target tensor masks the input ids during\n        # softmax cross entropy loss computation. This ensures that the loss is computed only for the target\n        # token IDs. Examples:\n        # BERTLMHead: https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/bert/modeling_bert.py#L1216-L1219 # noqa\n        # GPTNeoForCausalLM: https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/gpt_neo/modeling_gpt_neo.py#L736 # noqa\n        _targets = pad_target_tensor_for_fine_tuning(targets, predictions, self.model_inputs, of_name)\n\n        return _targets\n\n    def _activate_forward_hooks(self):\n        \"\"\"Activates/registers forward hooks for the model.\"\"\"\n        if not self.config_obj.model_parameters:\n            return\n\n        # Initialize forward hook handles\n        if self.config_obj.model_parameters.neftune_noise_alpha:\n            self._forward_hook_handles.append(\n                NEFTuneHook(neftune_noise_alpha=self.config_obj.model_parameters.neftune_noise_alpha)\n            )\n\n        # Activate forward hooks iteratively\n        for hook in self._forward_hook_handles:\n            # Update the model with the forward hooks in place\n            self.model = hook.activate_hook(self.model)\n\n    @staticmethod\n    def get_augmentation_pipelines() -> AugmentationPipelines:\n        \"\"\"Returns the augmentation pipeline for this model.\"\"\"\n        return AugmentationPipelines({})\n"
  },
  {
    "path": "ludwig/models/predictor.py",
    "content": "import logging\nimport os\nimport sys\nfrom abc import ABC, abstractmethod\nfrom collections import defaultdict, OrderedDict\nfrom pprint import pformat\n\nimport numpy as np\nimport pandas as pd\nimport psutil\nimport torch\nfrom torch import nn\n\nfrom ludwig.constants import COMBINED, LAST_HIDDEN, LOGITS, MODEL_ECD, MODEL_LLM\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.data.utils import convert_to_dict\nfrom ludwig.distributed.base import DistributedStrategy, LocalStrategy\nfrom ludwig.globals import is_progressbar_disabled, PREDICTIONS_PARQUET_FILE_NAME, TEST_STATISTICS_FILE_NAME\nfrom ludwig.models.base import BaseModel\nfrom ludwig.progress_bar import LudwigProgressBar\nfrom ludwig.utils.data_utils import save_csv, save_json\nfrom ludwig.utils.dataframe_utils import from_numpy_dataset\nfrom ludwig.utils.print_utils import repr_ordered_dict\nfrom ludwig.utils.registry import Registry\nfrom ludwig.utils.strings_utils import make_safe_filename\nfrom ludwig.utils.torch_utils import get_torch_device\n\nEXCLUDE_PRED_SET = {LOGITS, LAST_HIDDEN}\nSKIP_EVAL_METRICS = {\"confusion_matrix\", \"roc_curve\"}\nSTATS_SAMPLE_SIZE = 10000\n\nlogger = logging.getLogger(__name__)\n\n\nclass BasePredictor(ABC):\n    @abstractmethod\n    def batch_predict(self, dataset, dataset_name=None):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def predict_single(self, batch):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def batch_collect_activations(self, layer_names, dataset, bucketing_field=None):\n        raise NotImplementedError()\n\n    # Remote implementations may override this\n    def shutdown(self):\n        pass\n\n    # Functions needed to treat Trainer as a context manager\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.shutdown()\n\n\n_predictor_registry = Registry[BasePredictor]()\n\n\ndef register_predictor(model_types: list[str]):\n    def wrap(cls):\n        for model_type in model_types:\n            _predictor_registry[model_type] = cls\n        return cls\n\n    return wrap\n\n\ndef get_predictor_cls(model_type: str) -> type[BasePredictor]:\n    return _predictor_registry[model_type]\n\n\n@register_predictor([MODEL_ECD])\nclass Predictor(BasePredictor):\n    \"\"\"Predictor is a class that uses a model to predict and evaluate.\"\"\"\n\n    def __init__(\n        self,\n        dist_model: nn.Module,\n        batch_size: int = 128,\n        distributed: DistributedStrategy = None,\n        report_tqdm_to_ray: bool = False,\n        model: BaseModel | None = None,\n        remote: bool = False,\n        **kwargs,\n    ):\n        \"\"\"\n        :param dist_model: model to use for prediction, post-wrap for distributed training\n        :param batch_size: batch size to use for prediction\n        :param distributed: distributed strategy to use for prediction\n        :param report_tqdm_to_ray: whether to report tqdm progress to Ray\n        :param model: Ludwig BaseModel before being wrapped for distributed training.\n            Used to call Ludwig helper functions.\n        \"\"\"\n        model = model or dist_model\n        assert isinstance(model, BaseModel)\n\n        self._batch_size = batch_size\n        self._distributed = distributed if distributed is not None else LocalStrategy()\n        self.report_tqdm_to_ray = report_tqdm_to_ray\n\n        device = get_torch_device()\n        self.device = device\n        self.dist_model = dist_model\n        self.model = model\n        self.model.metrics_to_device(device)\n\n        if remote:\n            # Only return results from rank 0 to reduce network overhead\n            self.batch_predict = self._distributed.return_first(self.batch_predict)\n            self.batch_evaluation = self._distributed.return_first(self.batch_evaluation)\n\n    def batch_predict(self, dataset: Dataset, dataset_name: str = None, collect_logits: bool = False):\n        self.dist_model = self._distributed.to_device(self.dist_model)\n        prev_model_training_mode = self.dist_model.training  # store previous model training mode\n        self.dist_model.eval()  # set model to eval mode\n\n        with torch.no_grad():\n            with dataset.initialize_batcher(self._batch_size, should_shuffle=False) as batcher:\n                progress_bar_config = {\n                    \"desc\": \"Prediction\" if dataset_name is None else f\"Prediction {dataset_name: <5.5}\",\n                    \"total\": batcher.steps_per_epoch,\n                    \"file\": sys.stdout,\n                    \"disable\": is_progressbar_disabled(),\n                }\n                progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n                predictions = defaultdict(list)\n                while not batcher.last_batch():\n                    batch = batcher.next_batch()\n                    preds = self._predict(batch)\n                    self._accumulate_preds(\n                        preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET\n                    )\n                    progress_bar.update(1)\n\n                progress_bar.close()\n\n        # consolidate predictions from each batch to a single tensor\n        self._concat_preds(predictions)\n\n        self.dist_model.train(prev_model_training_mode)\n\n        return from_numpy_dataset(predictions)\n\n    def predict_single(self, batch, collect_logits: bool = False):\n        prev_model_training_mode = self.dist_model.training  # store previous model training mode\n        self.dist_model.eval()  # set model to eval mode\n\n        with torch.no_grad():\n            predictions = defaultdict(list)\n            preds = self._predict(batch)\n            self._accumulate_preds(\n                preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET\n            )\n            self._concat_preds(predictions)\n\n        # reset model to its original training mode\n        self.dist_model.train(prev_model_training_mode)\n        return from_numpy_dataset(predictions)\n\n    def _predict(self, batch: dict[str, np.ndarray]) -> dict[str, np.ndarray]:\n        \"\"\"Predict a batch of data.\n\n        Params:\n            model: BaseModel model\n            batch: batch of data\n\n        Returns:\n            predictions: dictionary of predictions\n        \"\"\"\n        inputs = {\n            i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device)\n            for i_feat in self.model.input_features.values()\n        }\n\n        outputs = self._predict_on_inputs(inputs)\n        return self.model.outputs_to_predictions(outputs)\n\n    def _accumulate_preds(self, preds, predictions, exclude_pred_set=EXCLUDE_PRED_SET):\n        # accumulate predictions from batch for each output feature\n        for of_name, of_preds in preds.items():\n            for pred_name, pred_values in of_preds.items():\n                if pred_name not in exclude_pred_set:\n                    key = f\"{of_name}_{pred_name}\"\n                    predictions[key].append(pred_values.detach().cpu())\n\n    def _concat_preds(self, predictions):\n        for key, pred_value_list in predictions.items():\n            # Without detaching, a runtime error is raised since pred_value_list\n            # is a tensor that requires grad.\n            predictions[key] = torch.cat(pred_value_list, dim=0).numpy()\n\n    def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None):\n        \"\"\"Batch evaluate model on dataset.\n\n        Params:\n            dataset (Union[str, dict, pandas.DataFrame]): source containing the entire dataset to be evaluated.\n            collect_predictions: Return model predictions.\n            collect_logits: Return model logits and final layer activations.\n\n        Returns:\n            Tuple of dictionaries of (metrics, predictions). The keys of metrics are determined by the metrics in the\n            model config. The keys of the predictions dictionary depend on which values are requested by the caller:\n            collect_predictions, collect_logits.\n        \"\"\"\n        self.dist_model = self._distributed.to_device(self.dist_model)\n        prev_model_training_mode = self.dist_model.training  # store previous model training mode\n        self.dist_model.eval()  # set model to eval mode\n\n        with torch.no_grad():\n            with dataset.initialize_batcher(\n                self._batch_size, should_shuffle=False, distributed=self._distributed\n            ) as batcher:\n                progress_bar_config = {\n                    \"desc\": \"Evaluation\" if dataset_name is None else f\"Evaluation {dataset_name: <5.5}\",\n                    \"total\": batcher.steps_per_epoch,\n                    \"file\": sys.stdout,\n                    \"disable\": is_progressbar_disabled(),\n                    \"position\": 0,  # Necessary to disable extra new line artifacts in training logs.\n                }\n                progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n\n                predictions = defaultdict(list)\n                eval_steps = (\n                    self.dist_model.config_obj.trainer.eval_steps\n                    if hasattr(self.dist_model, \"config_obj\")\n                    and hasattr(self.dist_model.config_obj.trainer, \"eval_steps\")\n                    else None\n                )\n                eval_steps_counter = 0\n                while not batcher.last_batch():\n                    if eval_steps and eval_steps_counter >= eval_steps:\n                        logger.info(f\"Reached evaluation step {eval_steps}. Ending evaluation.\")\n                        break\n                    batch = batcher.next_batch()\n                    logger.debug(\n                        f\"evaluation for {dataset_name}: obtained next batch \"\n                        f\"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB\"\n                    )\n                    inputs = {\n                        i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(\n                            self.device\n                        )\n                        for i_feat in self.model.input_features.values()\n                    }\n                    targets = {\n                        o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(\n                            self.device\n                        )\n                        for o_feat in self.model.output_features.values()\n                    }\n\n                    outputs = self._predict_on_inputs(inputs)\n                    preds = self.model.outputs_to_predictions(outputs)\n                    self.model.update_metrics(targets, preds)\n\n                    # accumulate predictions from batch for each output feature\n                    if collect_predictions:\n                        self._accumulate_preds(\n                            preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET\n                        )\n\n                    progress_bar.update(1)\n                    eval_steps_counter += 1\n                    if self.is_coordinator():\n                        logger.debug(\n                            f\"evaluation for {dataset_name}: completed batch {progress_bar.total_steps} \"\n                            f\"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB\"\n                        )\n                progress_bar.close()\n\n            # consolidate predictions from each batch to a single tensor\n            if collect_predictions:\n                self._concat_preds(predictions)\n\n            metrics = self.model.get_metrics()\n            self.model.reset_metrics()\n\n            self.dist_model.train(prev_model_training_mode)  # Restores previous model training mode.\n\n            return metrics, from_numpy_dataset(predictions)\n\n    def batch_collect_activations(self, layer_names, dataset, bucketing_field=None):\n        if bucketing_field:\n            raise ValueError(\"BucketedBatcher is not supported yet\")\n\n        self.dist_model = self._distributed.to_device(self.dist_model)\n        prev_model_training_mode = self.dist_model.training  # store previous model training mode\n        self.dist_model.eval()  # set model to eval mode\n\n        with torch.no_grad():\n            with dataset.initialize_batcher(\n                self._batch_size, should_shuffle=False, distributed=self._distributed\n            ) as batcher:\n                progress_bar_config = {\n                    \"desc\": \"Collecting Tensors\",\n                    \"total\": batcher.steps_per_epoch,\n                    \"file\": sys.stdout,\n                    \"disable\": is_progressbar_disabled(),\n                }\n                progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n\n                collected_tensors = []\n                while not batcher.last_batch():\n                    batch = batcher.next_batch()\n\n                    inputs = {\n                        i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(\n                            self.device\n                        )\n                        for i_feat in self.model.input_features.values()\n                    }\n                    outputs = self._predict_on_inputs(inputs)\n                    collected_tensors = [(concat_name, tensor) for concat_name, tensor in outputs.items()]\n                    progress_bar.update(1)\n\n                progress_bar.close()\n\n        self.dist_model.train(prev_model_training_mode)  # Restores previous model training mode.\n\n        return collected_tensors\n\n    def _predict_on_inputs(self, inputs: dict) -> dict:\n        return self.dist_model(inputs)\n\n    def is_coordinator(self):\n        return self._distributed.rank() == 0\n\n\n@register_predictor([MODEL_LLM])\nclass LlmPredictor(Predictor):\n    def _predict_on_inputs(self, inputs: dict) -> dict:\n        return self.dist_model.generate(inputs)\n\n\nclass LlmFineTunePredictor(Predictor):\n    def batch_evaluation(self, dataset, collect_predictions=False, collect_logits=False, dataset_name=None):\n        \"\"\"Batch evaluate model on dataset.\n\n        Params:\n            dataset (Union[str, dict, pandas.DataFrame]): source containing the entire dataset to be evaluated.\n            collect_predictions: Return model predictions.\n            collect_logits: Return model logits and final layer activations.\n\n        Returns:\n            Tuple of dictionaries of (metrics, predictions, input/target/output dictionary). The keys of metrics are\n            determined by the metrics in the model config. The keys of the predictions dictionary depend on which values\n            are requested by the caller: collect_predictions, collect_logits. The keys of the input/target/output\n            dictionary are \"inputs\", \"targets\", and \"outputs\". The values of each of these keys are dictionaries of\n            feature names to lists of tensors. The tensors are the inputs, targets, and outputs for each batch.\n        \"\"\"\n        prev_model_training_mode = self.dist_model.training  # store previous model training mode\n        self.dist_model.eval()  # set model to eval mode\n        example_inputs = defaultdict(list)\n        example_targets = defaultdict(list)\n        example_outputs = defaultdict(list)\n        with torch.no_grad():\n            with dataset.initialize_batcher(\n                self._batch_size, should_shuffle=False, distributed=self._distributed\n            ) as batcher:\n                progress_bar_config = {\n                    \"desc\": \"Evaluation\" if dataset_name is None else f\"Evaluation {dataset_name: <5.5}\",\n                    \"total\": batcher.steps_per_epoch,\n                    \"file\": sys.stdout,\n                    \"disable\": is_progressbar_disabled(),\n                    \"position\": 0,  # Necessary to disable extra new line artifacts in training logs.\n                }\n                progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n\n                predictions = defaultdict(list)\n                eval_steps = (\n                    self.dist_model.config_obj.trainer.eval_steps\n                    if hasattr(self.dist_model, \"config_obj\")\n                    and hasattr(self.dist_model.config_obj.trainer, \"eval_steps\")\n                    else None\n                )\n                eval_steps_counter = 0\n                while not batcher.last_batch():\n                    if eval_steps and eval_steps_counter >= eval_steps:\n                        logger.info(f\"Reached evaluation step {eval_steps}. Ending evaluation.\")\n                        break\n                    batch = batcher.next_batch()\n                    logger.debug(\n                        f\"evaluation for {dataset_name}: obtained next batch \"\n                        f\"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB\"\n                    )\n                    inputs = {\n                        i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(\n                            self.device\n                        )\n                        for i_feat in self.model.input_features.values()\n                    }\n                    targets = {\n                        o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(\n                            self.device\n                        )\n                        for o_feat in self.model.output_features.values()\n                    }\n\n                    outputs = self._predict_on_inputs((inputs, targets))\n                    preds = self.model.outputs_to_predictions(outputs)\n\n                    for key in inputs:\n                        example_inputs[key].extend(inputs[key])\n                    for key in targets:\n                        example_targets[key].extend(targets[key])\n                    for key in preds:\n                        example_outputs[key].extend(preds[key][\"predictions\"])\n\n                    # Need to pass through a custom fine-tune metric function because we need to transform\n                    # the targets into the right format for loss calculation (requires padding with -100s to the left)\n                    # and other tensor alignment.\n                    self.model.update_metrics_finetune_llm(targets, preds)\n\n                    # accumulate predictions from batch for each output feature\n                    if collect_predictions:\n                        self._accumulate_preds(\n                            preds, predictions, exclude_pred_set={LAST_HIDDEN} if collect_logits else EXCLUDE_PRED_SET\n                        )\n\n                    progress_bar.update(1)\n                    eval_steps_counter += 1\n                    if self.is_coordinator():\n                        logger.debug(\n                            f\"evaluation for {dataset_name}: completed batch {progress_bar.total_steps} \"\n                            f\"memory used: {psutil.Process(os.getpid()).memory_info()[0] / 1e6:0.2f}MB\"\n                        )\n\n                progress_bar.close()\n\n            # consolidate predictions from each batch to a single tensor\n            if collect_predictions:\n                for key, pred_value_list in predictions.items():\n                    predictions[key] = torch.cat(pred_value_list, dim=0).detach().cpu().numpy()\n\n            metrics = self.model.get_metrics()\n            self.model.reset_metrics()\n\n            input_target_output_dict = {\n                \"inputs\": example_inputs,\n                \"targets\": example_targets,\n                \"outputs\": example_outputs,\n            }\n\n            self.dist_model.train(prev_model_training_mode)  # Restores previous model training mode.\n            return metrics, from_numpy_dataset(predictions), input_target_output_dict\n\n\ndef calculate_overall_stats(output_features, predictions, dataset, training_set_metadata):\n    overall_stats = {}\n    for of_name, output_feature in output_features.items():\n        feature_metadata = training_set_metadata[output_feature.feature_name]\n        feature_metadata.update(training_set_metadata[output_feature.feature_name])\n\n        feature_df = predictions.loc[:, predictions.columns.str.startswith(of_name)]\n        feature_df = feature_df.rename(columns=lambda c: c[len(of_name) + 1 :])\n\n        target = dataset.loc[:, output_feature.proc_column]\n\n        if not isinstance(feature_df, pd.DataFrame):\n            logger.warning(\n                \"Full computation of stats only supported for pandas dataframes. \"\n                \"Sampling the first 10000 rows of the feature and target dataframes for computing overall stats.\"\n            )\n            feature_df = feature_df.head(n=STATS_SAMPLE_SIZE, npartitions=-1, compute=True)\n            target = target.head(n=STATS_SAMPLE_SIZE, npartitions=-1, compute=True)\n\n        overall_stats[of_name] = output_feature.calculate_overall_stats(\n            feature_df,  # predictions\n            target,\n            feature_metadata,  # output feature metadata\n        )\n    return overall_stats\n\n\ndef save_prediction_outputs(\n    postprocessed_output,\n    output_features,\n    output_directory,\n    backend,\n):\n    backend.df_engine.write_predictions(\n        postprocessed_output, os.path.join(output_directory, PREDICTIONS_PARQUET_FILE_NAME)\n    )\n    if not backend.df_engine.partitioned:\n        # csv can only be written out for unpartitioned df format (i.e., pandas)\n        postprocessed_dict = convert_to_dict(postprocessed_output, output_features)\n        csv_filename = os.path.join(output_directory, \"{}_{}.csv\")\n        for output_field, outputs in postprocessed_dict.items():\n            for output_name, values in outputs.items():\n                save_csv(csv_filename.format(output_field, make_safe_filename(output_name)), values)\n\n\ndef save_evaluation_stats(test_stats, output_directory):\n    test_stats_fn = os.path.join(output_directory, TEST_STATISTICS_FILE_NAME)\n    save_json(test_stats_fn, test_stats)\n\n\ndef print_evaluation_stats(test_stats):\n    for output_field, result in test_stats.items():\n        if output_field != COMBINED or (output_field == COMBINED and len(test_stats) > 2):\n            logger.info(f\"\\n===== {output_field} =====\")\n            for metric in sorted(list(result)):\n                if metric not in SKIP_EVAL_METRICS:\n                    value = result[metric]\n                    if isinstance(value, OrderedDict):\n                        value_repr = repr_ordered_dict(value)\n                    else:\n                        value_repr = pformat(result[metric], indent=2)\n                    logger.info(f\"{metric}: {value_repr}\")\n\n\ndef get_output_columns(output_features, include_logits: bool = False):\n    output_columns = []\n    for of_name, feature in output_features.items():\n        for pred in feature.get_prediction_set():\n            if pred not in EXCLUDE_PRED_SET or (pred == LOGITS and include_logits):\n                output_columns.append(f\"{of_name}_{pred}\")\n    return output_columns\n"
  },
  {
    "path": "ludwig/models/registry.py",
    "content": "import logging\n\nfrom ludwig.constants import MODEL_ECD, MODEL_LLM\nfrom ludwig.models.ecd import ECD\nfrom ludwig.models.llm import LLM\n\nlogger = logging.getLogger(__name__)\n\n\nmodel_type_registry = {\n    MODEL_ECD: ECD,\n    MODEL_LLM: LLM,\n}\n"
  },
  {
    "path": "ludwig/models/retrieval.py",
    "content": "import hashlib\nimport json\nimport os\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable\nfrom typing import Any, TYPE_CHECKING\n\nimport numpy as np\nimport pandas as pd\nfrom tqdm import tqdm\n\nfrom ludwig.vector_index import FAISS, get_vector_index_cls\nfrom ludwig.vector_index.base import VectorIndex\n\nif TYPE_CHECKING:\n    from sentence_transformers import SentenceTransformer\n    from ludwig.backend.base import Backend\n\nfrom ludwig.utils.batch_size_tuner import BatchSizeEvaluator\nfrom ludwig.utils.torch_utils import get_torch_device\n\n\ndef df_checksum(df: pd.DataFrame) -> str:\n    return hashlib.sha1(pd.util.hash_pandas_object(df).values).hexdigest()\n\n\ndef df_to_row_strs(df: pd.DataFrame) -> list[str]:\n    rows = df.to_dict(orient=\"records\")\n    row_strs = [json.dumps(r) for r in rows]\n    return row_strs\n\n\nclass RetrievalModel(ABC):\n    @abstractmethod\n    def create_dataset_index(self, df: pd.DataFrame, backend: \"Backend\", columns_to_index: list[str] | None = None):\n        \"\"\"Creates an index for the dataset.\n\n        If `columns_to_index` is None, all columns are indexed. Otherwise, only the columns in `columns_to_index` are\n        used for indexing, but all columns in `df` are returned in the search results.\n        \"\"\"\n\n    @abstractmethod\n    def search(\n        self, df, backend: \"Backend\", k: int = 10, return_data: bool = False\n    ) -> list[int] | list[dict[str, Any]]:\n        \"\"\"Retrieve the top k results for the given query.\n\n        If `return_data` is True, returns the data associated with the indices. Otherwise, returns the indices.\n        \"\"\"\n\n    @abstractmethod\n    def save_index(self, name: str, cache_directory: str):\n        \"\"\"Saves the index to the cache directory.\"\"\"\n\n    @abstractmethod\n    def load_index(self, name: str, cache_directory: str):\n        \"\"\"Loads the index from the cache directory.\"\"\"\n\n\nclass RandomRetrieval(RetrievalModel):\n    \"\"\"Random retrieval model.\n\n    Gets k random indices from the dataset regardless of the query.\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        self.index = None\n        self.index_data = None\n\n    def create_dataset_index(self, df: pd.DataFrame, backend: \"Backend\", columns_to_index: list[str] | None = None):\n        self.index = np.array(range(len(df)))\n        self.index_data = df\n\n    def search(\n        self, df, backend: \"Backend\", k: int = 10, return_data: bool = False\n    ) -> list[int] | list[dict[str, Any]]:\n        results = []\n        for _ in tqdm(range(len(df))):\n            indices = np.random.choice(self.index, k, replace=False)\n\n            if return_data:\n                result = self.index_data.iloc[indices].to_dict(orient=\"records\")\n            else:\n                result = indices\n            results.append(result)\n        return results\n\n    def save_index(self, name: str, cache_directory: str):\n        index_file_path = os.path.join(cache_directory, name + \".index\")\n        # open file to prevent using the .npy extension\n        # https://numpy.org/doc/stable/reference/generated/numpy.save.html\n        with open(index_file_path, \"wb\") as f:\n            np.save(f, self.index)\n\n        index_data_file_path = os.path.join(cache_directory, name + \"_data.csv\")\n        self.index_data.to_csv(index_data_file_path, index=False)\n\n    def load_index(self, name: str, cache_directory: str):\n        index_file_path = os.path.join(cache_directory, name + \".index\")\n        self.index = np.load(index_file_path)\n\n        index_data_file_path = os.path.join(cache_directory, name + \"_data.csv\")\n        self.index_data = pd.read_csv(index_data_file_path)\n\n\nclass SemanticRetrieval(RetrievalModel):\n    \"\"\"Semantic retrieval model.\n\n    Uses a sentence transformer model to encode the dataset and retrieve the top k most similar results to the query.\n    \"\"\"\n\n    def __init__(self, model_name, **kwargs):\n        self.model_name = model_name\n        self.model = get_semantic_retrieval_model(self.model_name)\n        self.index: VectorIndex = None\n        self.index_data: pd.DataFrame = None\n\n        # best batch size computed during the encoding step\n        self.best_batch_size = None\n\n    def create_dataset_index(self, df: pd.DataFrame, backend: \"Backend\", columns_to_index: list[str] | None = None):\n        if columns_to_index is None:\n            columns_to_index = df.columns\n        df_to_index = df[columns_to_index]\n        row_strs = df_to_row_strs(df_to_index)\n\n        embeddings = self._encode(row_strs, backend)\n        self.index = get_vector_index_cls(FAISS).from_embeddings(embeddings)\n        # Save the entire df so we can return the full row when searching\n        self.index_data = df\n\n    def _encode(self, row_strs: list[str], backend: \"Backend\") -> np.ndarray:\n        # only do this step once\n        if self.best_batch_size is None:\n            self.best_batch_size = backend.tune_batch_size(\n                create_semantic_retrieval_model_evaluator(self.model, row_strs), len(row_strs)\n            )\n\n        transform_fn = create_semantic_retrieval_model_fn(self.model, self.best_batch_size)\n        df = backend.df_engine.from_pandas(pd.DataFrame({\"data\": row_strs}))\n        df = backend.batch_transform(df, self.best_batch_size, transform_fn)\n        df = backend.df_engine.compute(df)\n        embeddings = np.stack(df[\"data\"].values).astype(np.float32)\n        return embeddings\n\n    def search(\n        self, df: pd.DataFrame, backend: \"Backend\", k: int = 10, return_data: bool = False\n    ) -> list[int] | list[dict[str, Any]]:\n        row_strs = df_to_row_strs(df)\n\n        query_vectors = self._encode(row_strs, backend)\n        results = []\n        # TODO(geoffrey): figure out why self.index.search segfaults with larger batch sizes\n        for query_vector in tqdm(query_vectors, total=query_vectors.shape[0]):\n            indices = self.index.search(query_vector.reshape(1, -1), k)\n            if return_data:\n                result = self.index_data.iloc[indices].to_dict(orient=\"records\")\n            else:\n                result = indices\n            results.append(result)\n        return results\n\n    def save_index(self, name: str, cache_directory: str):\n        index_file_path = os.path.join(cache_directory, name + \".index\")\n        self.index.save(index_file_path)\n\n        index_data_file_path = os.path.join(cache_directory, name + \"_data.csv\")\n        self.index_data.to_csv(index_data_file_path, index=False)\n\n    def load_index(self, name: str, cache_directory: str):\n        index_file_path = os.path.join(cache_directory, name + \".index\")\n        self.index = get_vector_index_cls(FAISS).from_path(index_file_path)\n\n        index_data_file_path = os.path.join(cache_directory, name + \"_data.csv\")\n        self.index_data = pd.read_csv(index_data_file_path)\n\n\ndef create_semantic_retrieval_model_evaluator(\n    model: \"SentenceTransformer\", samples: list[str]\n) -> type[BatchSizeEvaluator]:\n    class _RetrievalModelEvaluator(BatchSizeEvaluator):\n        def __init__(self):\n            self.model = model.to(get_torch_device())\n            self.samples = samples\n\n        def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n            self.model.encode(self.samples[:batch_size], batch_size=batch_size, show_progress_bar=False)\n\n    return _RetrievalModelEvaluator\n\n\ndef create_semantic_retrieval_model_fn(\n    model: \"SentenceTransformer\", batch_size: int\n) -> Callable[[pd.DataFrame], np.ndarray]:\n    class _RetrievalModelFn:\n        def __init__(self):\n            self.model = model.to(get_torch_device())\n            self.batch_size = batch_size\n\n        def __call__(self, df: pd.DataFrame) -> np.ndarray:\n            row_strs = df[\"data\"].tolist()\n            result = self.model.encode(row_strs, batch_size=self.batch_size, show_progress_bar=False)\n            df[\"data\"] = result.tolist()\n            return df\n\n    return _RetrievalModelFn\n\n\ndef get_semantic_retrieval_model(model_name: str) -> \"SentenceTransformer\":\n    from sentence_transformers import SentenceTransformer\n\n    return SentenceTransformer(model_name, device=get_torch_device())\n\n\ndef get_retrieval_model(type: str, **kwargs) -> RetrievalModel:\n    if type == \"random\":\n        return RandomRetrieval(**kwargs)\n    elif type == \"semantic\":\n        return SemanticRetrieval(**kwargs)\n    else:\n        raise ValueError(f\"Unsupported retrieval model type: {type}\")\n"
  },
  {
    "path": "ludwig/modules/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/modules/attention_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nfrom torch import nn\nfrom torch.nn import functional as F\n\nfrom ludwig.utils.torch_utils import get_activation, LudwigModule\n\nlogger = logging.getLogger(__name__)\n\n\nclass FeedForwardAttentionReducer(LudwigModule):\n    def __init__(self, input_size, hidden_size=256, activation=\"tanh\"):\n        super().__init__()\n        self.fc_layer1 = nn.Linear(input_size, hidden_size)\n        self.fc_layer1_activation = get_activation(activation)\n        self.fc_layer2 = nn.Linear(hidden_size, 1, bias=False)\n        self.input_shape_var = None\n        self.output_shape_var = None\n\n    def forward(self, inputs, mask=None):\n        # current_inputs shape [b, s, h]\n        self.input_shape_var = inputs.size()[1:]\n        hidden = self.fc_layer1(inputs)  # [b, s, h']\n        hidden = self.fc_layer1_activation(hidden)\n        hidden = self.fc_layer2(hidden)  # [b, s, 1]\n        attention = F.softmax(hidden, dim=1)\n        gated_inputs = torch.sum(attention * inputs, dim=1)\n        self.output_shape_var = gated_inputs.size()[1:]\n        return gated_inputs  # [b, h]\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.input_shape_var\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.output_shape_var\n\n\nclass MultiHeadSelfAttention(LudwigModule):\n    def __init__(self, input_size, hidden_size, num_heads=8):\n        super().__init__()\n        self.embedding_size = hidden_size\n        self.num_heads = num_heads\n        if hidden_size % num_heads != 0:\n            raise ValueError(\n                f\"When using multi-head attention, `hidden_size` ({hidden_size}), should be divisible by \"\n                f\"`num_heads` ({num_heads}). Please update the `transformer` section of the model config.\"\n            )\n        self.projection_dim = hidden_size // num_heads\n        self.query_dense = nn.Linear(input_size, hidden_size)\n        self.key_dense = nn.Linear(input_size, hidden_size)\n        self.value_dense = nn.Linear(input_size, hidden_size)\n        self.combine_heads = nn.Linear(hidden_size, hidden_size)\n\n    def separate_heads(self, inputs, batch_size):\n        inputs = torch.reshape(inputs, (batch_size, -1, self.num_heads, self.projection_dim))\n        return torch.permute(inputs, (0, 2, 1, 3))\n\n    def forward(self, inputs: torch.Tensor, mask=None):\n        # inputs.shape = [batch_size, seq_len, embedding_dim]\n        batch_size = inputs.shape[0]\n        query = self.query_dense(inputs)  # (batch_size, seq_len, h)\n        key = self.key_dense(inputs)  # (batch_size, seq_len, h)\n        value = self.value_dense(inputs)  # (batch_size, seq_len, h)\n        query = self.separate_heads(query, batch_size)  # (batch_size, num_heads, seq_len, projection_dim)\n        key = self.separate_heads(key, batch_size)  # (batch_size, num_heads, seq_len, projection_dim)\n        value = self.separate_heads(value, batch_size)  # (batch_size, num_heads, seq_len, projection_dim)\n        attn_mask = mask if mask is not None else None\n        outputs = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask)\n        outputs = torch.permute(outputs, (0, 2, 1, 3))  # (batch_size, seq_len, num_heads, projection_dim)\n        concat_outputs = torch.reshape(outputs, (batch_size, -1, self.embedding_size))  # (batch_size, seq_len, h)\n        projected_outputs = self.combine_heads(concat_outputs)  # (batch_size, seq_len, h)\n        return projected_outputs\n\n    @property\n    def output_shape(self):\n        return torch.Size([self.embedding_size])\n\n\nclass TransformerBlock(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        max_sequence_length: int,\n        hidden_size: int,\n        num_heads: int,\n        output_size: int,\n        dropout: float = 0.1,\n    ):\n        super().__init__()\n        self.input_size = input_size\n        self.max_sequence_length = max_sequence_length\n        self.hidden_size = hidden_size\n\n        self.self_attention = MultiHeadSelfAttention(input_size, hidden_size, num_heads=num_heads)\n        self.dropout1 = nn.Dropout(dropout)\n        self.layernorm1 = nn.LayerNorm(hidden_size, eps=1e-6)\n        self.fully_connected = nn.Sequential(\n            nn.Linear(input_size, output_size), get_activation(\"relu\"), nn.Linear(output_size, hidden_size)\n        )\n        self.dropout2 = nn.Dropout(dropout)\n        self.layernorm2 = nn.LayerNorm(hidden_size, eps=1e-6)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.input_size])\n\n    def forward(self, inputs, mask=None):\n        # inputs [b, s, h]\n        attn_output = self.self_attention(inputs)  # [b, s, h]\n        attn_output = self.dropout1(attn_output)  # [b, s, h]\n        ln1_output = self.layernorm1(inputs + attn_output)  # [b, s, h]\n        fc_output = self.fully_connected(ln1_output)  # [b, s, h]\n        fc_output = self.dropout2(fc_output)  # [b, s, h]\n        return self.layernorm2(ln1_output + fc_output)  # [b, s, h]\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.hidden_size])\n\n\nclass TransformerStack(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        max_sequence_length: int,\n        hidden_size: int = 256,\n        num_heads: int = 8,\n        output_size: int = 256,\n        num_layers: int = 1,\n        dropout: float = 0.1,\n        **kwargs,\n    ):\n        super().__init__()\n        self.supports_masking = True\n        self.max_sequence_length = max_sequence_length\n        self.input_size = input_size\n        self.hidden_size = hidden_size\n\n        self.layers = nn.ModuleList()\n\n        prior_input_size = input_size\n        for i in range(num_layers):\n            layer = TransformerBlock(\n                input_size=prior_input_size,\n                max_sequence_length=max_sequence_length,\n                hidden_size=hidden_size,\n                num_heads=num_heads,\n                output_size=output_size,\n                dropout=dropout,\n            )\n            self.layers.append(layer)\n            prior_input_size = self.layers[i].output_shape[-1]\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.input_size])\n\n    def forward(self, inputs, mask=None):\n        hidden = inputs\n        for layer in self.layers:\n            hidden = layer(hidden, mask=mask)\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.hidden_size])\n"
  },
  {
    "path": "ludwig/modules/convolutional_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom functools import partial\nfrom typing import Any\n\nimport torch\nimport torch.nn as nn\n\nfrom ludwig.utils.image_utils import get_img_output_shape\nfrom ludwig.utils.torch_utils import get_activation, LudwigModule\n\nlogger = logging.getLogger(__name__)\n\n\nclass Conv1DLayer(LudwigModule):\n    def __init__(\n        self,\n        in_channels=1,\n        out_channels=256,\n        max_sequence_length=None,\n        kernel_size=3,\n        strides=1,\n        padding=\"same\",\n        dilation=1,\n        groups=1,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n        norm=None,\n        norm_params=None,\n        activation=\"relu\",\n        dropout=0,\n        pool_function=\"max\",\n        pool_size=2,\n        pool_strides=None,\n        pool_padding=\"valid\",\n    ):\n        super().__init__()\n\n        self.in_channels = in_channels\n        self.out_channels = out_channels\n        self.max_sequence_length = max_sequence_length\n        self.kernel_size = kernel_size\n        self.stride = strides\n        self.padding = padding\n        self.dilation = dilation\n        self.groups = groups\n        self.pool_size = pool_size\n        if pool_strides is None:\n            self.pool_strides = pool_size\n        else:\n            self.pool_strides = pool_strides\n        if pool_padding == \"same\" and pool_size is not None:\n            self.pool_padding = (self.pool_size - 1) // 2\n        else:\n            self.pool_padding = 0\n\n        self.layers = nn.ModuleList()\n\n        self.layers.append(\n            nn.Conv1d(\n                in_channels=in_channels,\n                out_channels=out_channels,\n                kernel_size=(kernel_size,),\n                stride=(strides,),\n                padding=padding,\n                dilation=(dilation,),\n            )\n        )\n\n        if norm and norm_params is None:\n            norm_params = {}\n        if norm == \"batch\":\n            self.layers.append(nn.BatchNorm1d(num_features=out_channels, **norm_params))\n        elif norm == \"layer\":\n            self.layers.append(nn.LayerNorm(normalized_shape=[out_channels, self.max_sequence_length], **norm_params))\n\n        self.layers.append(get_activation(activation))\n\n        if dropout > 0:\n            self.layers.append(nn.Dropout(dropout))\n\n        if pool_size is not None:\n            pool = nn.MaxPool1d\n            if pool_function in {\"average\", \"avg\", \"mean\"}:\n                pool = nn.AvgPool1d\n            self.layers.append(pool(kernel_size=self.pool_size, stride=self.pool_strides, padding=self.pool_padding))\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n    @property\n    def input_shape(self):\n        \"\"\"Returns the size of the input tensor without the batch dimension.\"\"\"\n        return torch.Size([self.max_sequence_length, self.in_channels])\n\n    def forward(self, inputs, training=None, mask=None):\n        # inputs: [batch_size, seq_size, in_channels]\n        # in Torch nomenclature (N, L, C)\n        hidden = inputs\n\n        # put in torch compatible form [batch_size, in_channels, seq_size]\n        hidden = hidden.transpose(1, 2)\n\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        # revert back to normal form [batch_size, seq_size, out_channels]\n        hidden = hidden.transpose(1, 2)\n\n        return hidden  # (batch_size, seq_size, out_channels)\n\n\nclass Conv1DStack(LudwigModule):\n    def __init__(\n        self,\n        in_channels=1,\n        max_sequence_length=None,\n        layers=None,\n        num_layers=None,\n        default_num_filters=256,\n        default_filter_size=3,\n        default_strides=1,\n        default_padding=\"same\",\n        default_dilation_rate=1,\n        default_use_bias=True,\n        default_weights_initializer=\"xavier_uniform\",\n        default_bias_initializer=\"zeros\",\n        default_norm=None,\n        default_norm_params=None,\n        default_activation=\"relu\",\n        default_dropout=0,\n        default_pool_function=\"max\",\n        default_pool_size=2,\n        default_pool_strides=None,\n        default_pool_padding=\"same\",\n        **kwargs,\n    ):\n        super().__init__()\n\n        self.max_sequence_length = max_sequence_length\n        self.in_channels = in_channels\n\n        if layers is None:\n            if num_layers is None:\n                self.layers = [\n                    {\"filter_size\": 7, \"pool_size\": 3},\n                    {\"filter_size\": 7, \"pool_size\": 3},\n                    {\"filter_size\": 3, \"pool_size\": None},\n                    {\"filter_size\": 3, \"pool_size\": None},\n                    {\"filter_size\": 3, \"pool_size\": None},\n                    {\"filter_size\": 3, \"pool_size\": 3},\n                ]\n            else:\n                self.layers = []\n                for i in range(num_layers):\n                    self.layers.append(\n                        {\n                            \"filter_size\": default_filter_size,\n                            \"num_filters\": default_num_filters,\n                            \"pool_size\": default_pool_size,\n                            \"pool_strides\": default_pool_strides,\n                        }\n                    )\n        else:\n            self.layers = layers\n\n        for layer in self.layers:\n            if \"num_filters\" not in layer:\n                layer[\"num_filters\"] = default_num_filters\n            if \"filter_size\" not in layer:\n                layer[\"filter_size\"] = default_filter_size\n            if \"strides\" not in layer:\n                layer[\"strides\"] = default_strides\n            if \"padding\" not in layer:\n                layer[\"padding\"] = default_padding\n            if \"dilation_rate\" not in layer:\n                layer[\"dilation_rate\"] = default_dilation_rate\n            if \"use_bias\" not in layer:\n                layer[\"use_bias\"] = default_use_bias\n            if \"weights_initializer\" not in layer:\n                layer[\"weights_initializer\"] = default_weights_initializer\n            if \"bias_initializer\" not in layer:\n                layer[\"bias_initializer\"] = default_bias_initializer\n            if \"norm\" not in layer:\n                layer[\"norm\"] = default_norm\n            if \"norm_params\" not in layer:\n                layer[\"norm_params\"] = default_norm_params\n            if \"activation\" not in layer:\n                layer[\"activation\"] = default_activation\n            if \"dropout\" not in layer:\n                layer[\"dropout\"] = default_dropout\n            if \"pool_function\" not in layer:\n                layer[\"pool_function\"] = default_pool_function\n            if \"pool_size\" not in layer:\n                layer[\"pool_size\"] = default_pool_size\n            if \"pool_strides\" not in layer:\n                layer[\"pool_strides\"] = default_pool_strides\n            if \"pool_padding\" not in layer:\n                layer[\"pool_padding\"] = default_pool_padding\n\n        self.stack = nn.ModuleList()\n\n        prior_layer_channels = in_channels\n        l_in = self.max_sequence_length  # torch L_in\n        for i, layer in enumerate(self.layers):\n            logger.debug(f\"   stack layer {i}\")\n            self.stack.append(\n                Conv1DLayer(\n                    in_channels=prior_layer_channels,\n                    out_channels=layer[\"num_filters\"],\n                    max_sequence_length=l_in,\n                    kernel_size=layer[\"filter_size\"],\n                    strides=layer[\"strides\"],\n                    padding=layer[\"padding\"],\n                    dilation=layer[\"dilation_rate\"],\n                    use_bias=layer[\"use_bias\"],\n                    weights_initializer=layer[\"weights_initializer\"],\n                    bias_initializer=layer[\"bias_initializer\"],\n                    norm=layer[\"norm\"],\n                    norm_params=layer[\"norm_params\"],\n                    activation=layer[\"activation\"],\n                    dropout=layer[\"dropout\"],\n                    pool_function=layer[\"pool_function\"],\n                    pool_size=layer[\"pool_size\"],\n                    pool_strides=layer[\"pool_strides\"],\n                    pool_padding=layer[\"pool_padding\"],\n                )\n            )\n\n            # retrieve number of channels from prior layer\n            input_shape = self.stack[i].input_shape\n            output_shape = self.stack[i].output_shape\n\n            logger.debug(f\"{self.__class__.__name__}: \" f\"input_shape {input_shape}, output shape {output_shape}\")\n\n            # pass along shape for the input to the next layer\n            l_in, prior_layer_channels = output_shape\n\n    @property\n    def input_shape(self):\n        \"\"\"Returns the size of the input tensor without the batch dimension.\"\"\"\n        return torch.Size([self.max_sequence_length, self.in_channels])\n\n    def forward(self, inputs, mask=None):\n        hidden = inputs\n\n        # todo: enumerate for debugging, remove after testing\n        for i, layer in enumerate(self.stack):\n            hidden = layer(hidden)\n\n        if hidden.shape[1] == 0:\n            raise ValueError(\n                \"The output of the conv stack has the second dimension \"\n                \"(length of the sequence) equal to 0. \"\n                \"This means that the combination of filter_size, padding, \"\n                \"stride, pool_size, pool_padding and pool_stride reduces \"\n                \"the sequence length more than is possible. \"\n                'Try using \"same\" padding and reducing or eliminating stride '\n                \"and pool.\"\n            )\n\n        return hidden\n\n\nclass ParallelConv1D(LudwigModule):\n    def __init__(\n        self,\n        in_channels=1,\n        max_sequence_length=None,\n        layers=None,\n        default_num_filters=256,\n        default_filter_size=3,\n        default_strides=1,\n        default_padding=\"same\",\n        default_dilation_rate=1,\n        default_use_bias=True,\n        default_weights_initializer=\"xavier_uniform\",\n        default_bias_initializer=\"zeros\",\n        default_norm=None,\n        default_norm_params=None,\n        default_activation=\"relu\",\n        default_dropout=0,\n        default_pool_function=\"max\",\n        default_pool_size=None,\n        default_pool_strides=None,\n        default_pool_padding=\"valid\",\n        **kwargs,\n    ):\n        super().__init__()\n\n        self.in_channels = in_channels\n        self.max_sequence_length = max_sequence_length\n\n        if layers is None:\n            self.layers = [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}]\n        else:\n            self.layers = layers\n\n        for layer in self.layers:\n            if \"num_filters\" not in layer:\n                layer[\"num_filters\"] = default_num_filters\n            if \"filter_size\" not in layer:\n                layer[\"filter_size\"] = default_filter_size\n            if \"strides\" not in layer:\n                layer[\"strides\"] = default_strides\n            if \"padding\" not in layer:\n                layer[\"padding\"] = default_padding\n            if \"dilation_rate\" not in layer:\n                layer[\"dilation_rate\"] = default_dilation_rate\n            if \"use_bias\" not in layer:\n                layer[\"use_bias\"] = default_use_bias\n            if \"weights_initializer\" not in layer:\n                layer[\"weights_initializer\"] = default_weights_initializer\n            if \"bias_initializer\" not in layer:\n                layer[\"bias_initializer\"] = default_bias_initializer\n            if \"norm\" not in layer:\n                layer[\"norm\"] = default_norm\n            if \"norm_params\" not in layer:\n                layer[\"norm_params\"] = default_norm_params\n            if \"activation\" not in layer:\n                layer[\"activation\"] = default_activation\n            if \"dropout\" not in layer:\n                layer[\"dropout\"] = default_dropout\n            if \"pool_function\" not in layer:\n                layer[\"pool_function\"] = default_pool_function\n            if \"pool_size\" not in layer:\n                layer[\"pool_size\"] = default_pool_size\n            if \"pool_strides\" not in layer:\n                layer[\"pool_strides\"] = default_pool_strides\n            if \"pool_padding\" not in layer:\n                layer[\"pool_padding\"] = default_pool_padding\n\n        self.parallel_layers = nn.ModuleList()\n\n        for i, layer in enumerate(self.layers):\n            logger.debug(f\"   parallel layer {i}\")\n            self.parallel_layers.append(\n                Conv1DLayer(\n                    in_channels=self.in_channels,\n                    out_channels=layer[\"num_filters\"],\n                    max_sequence_length=self.max_sequence_length,\n                    kernel_size=layer[\"filter_size\"],\n                    strides=layer[\"strides\"],\n                    padding=layer[\"padding\"],\n                    dilation=layer[\"dilation_rate\"],\n                    use_bias=layer[\"use_bias\"],\n                    weights_initializer=layer[\"weights_initializer\"],\n                    bias_initializer=layer[\"bias_initializer\"],\n                    norm=layer[\"norm\"],\n                    norm_params=layer[\"norm_params\"],\n                    activation=layer[\"activation\"],\n                    dropout=layer[\"dropout\"],\n                    pool_function=layer[\"pool_function\"],\n                    pool_size=layer[\"pool_size\"],\n                    pool_strides=layer[\"pool_strides\"],\n                    pool_padding=layer[\"pool_padding\"],\n                )\n            )\n\n            logger.debug(\n                f\"{self.__class__.__name__} layer {i}, input shape \"\n                f\"{self.parallel_layers[i].input_shape}, output shape \"\n                f\"{self.parallel_layers[i].output_shape}\"\n            )\n\n    @property\n    def input_shape(self) -> torch.Size:\n        \"\"\"Returns the size of the input tensor without the batch dimension.\"\"\"\n        return torch.Size([self.max_sequence_length, self.in_channels])\n\n    def forward(self, inputs, mask=None):\n        # inputs: [batch_size, seq_size, in_channels)\n\n        hidden = inputs\n        hiddens = []\n\n        for layer in self.parallel_layers:\n            hiddens.append(layer(hidden))\n        hidden = torch.cat(hiddens, 2)\n\n        if hidden.shape[1] == 0:\n            raise ValueError(\n                \"The output of the conv stack has the second dimension \"\n                \"(length of the sequence) equal to 0. \"\n                \"This means that the combination of filter_size, padding, \"\n                \"stride, pool_size, pool_padding and pool_stride reduces \"\n                \"the sequence length more than is possible. \"\n                'Try using \"same\" padding and reducing or eliminating stride '\n                \"and pool.\"\n            )\n\n        # (batch_size, seq_size, len(parallel_layers) * out_channels)\n        return hidden\n\n\nclass ParallelConv1DStack(LudwigModule):\n    def __init__(\n        self,\n        in_channels=None,\n        stacked_layers=None,\n        max_sequence_length=None,\n        default_num_filters=64,\n        default_filter_size=3,\n        default_strides=1,\n        default_padding=\"same\",\n        default_dilation_rate=1,\n        default_use_bias=True,\n        default_weights_initializer=\"xavier_uniform\",\n        default_bias_initializer=\"zeros\",\n        default_norm=None,\n        default_norm_params=None,\n        default_activation=\"relu\",\n        default_dropout=0,\n        default_pool_function=\"max\",\n        default_pool_size=None,\n        default_pool_strides=None,\n        default_pool_padding=\"valid\",\n        **kwargs,\n    ):\n        super().__init__()\n\n        self.max_sequence_length = max_sequence_length\n        self.in_channels = in_channels\n\n        if stacked_layers is None:\n            self.stacked_parallel_layers = [\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n                [{\"filter_size\": 2}, {\"filter_size\": 3}, {\"filter_size\": 4}, {\"filter_size\": 5}],\n            ]\n\n        else:\n            self.stacked_parallel_layers = stacked_layers\n\n        for i, parallel_layers in enumerate(self.stacked_parallel_layers):\n            for j in range(len(parallel_layers)):\n                layer = parallel_layers[j]\n                if \"num_filters\" not in layer:\n                    layer[\"num_filters\"] = default_num_filters\n                if \"filter_size\" not in layer:\n                    layer[\"filter_size\"] = default_filter_size\n                if \"strides\" not in layer:\n                    layer[\"strides\"] = default_strides\n                if \"padding\" not in layer:\n                    layer[\"padding\"] = default_padding\n                if \"dilation_rate\" not in layer:\n                    layer[\"dilation_rate\"] = default_dilation_rate\n                if \"use_bias\" not in layer:\n                    layer[\"use_bias\"] = default_use_bias\n                if \"weights_initializer\" not in layer:\n                    layer[\"weights_initializer\"] = default_weights_initializer\n                if \"bias_initializer\" not in layer:\n                    layer[\"bias_initializer\"] = default_bias_initializer\n                if \"norm\" not in layer:\n                    layer[\"norm\"] = default_norm\n                if \"norm_params\" not in layer:\n                    layer[\"norm_params\"] = default_norm_params\n                if \"activation\" not in layer:\n                    layer[\"activation\"] = default_activation\n                if \"dropout\" not in layer:\n                    layer[\"dropout\"] = default_dropout\n                if \"pool_function\" not in layer:\n                    layer[\"pool_function\"] = default_pool_function\n                if \"pool_size\" not in layer:\n                    if i == len(self.stacked_parallel_layers) - 1:\n                        layer[\"pool_size\"] = default_pool_size\n                    else:\n                        layer[\"pool_size\"] = None\n                if \"pool_strides\" not in layer:\n                    layer[\"pool_strides\"] = default_pool_strides\n                if \"pool_padding\" not in layer:\n                    layer[\"pool_padding\"] = default_pool_padding\n\n        self.stack = nn.ModuleList()\n        num_channels = self.in_channels\n        sequence_length = self.max_sequence_length\n        for i, parallel_layers in enumerate(self.stacked_parallel_layers):\n            logger.debug(f\"   stack layer {i}\")\n            self.stack.append(ParallelConv1D(num_channels, sequence_length, layers=parallel_layers))\n\n            logger.debug(\n                f\"{self.__class__.__name__} layer {i}, input shape \"\n                f\"{self.stack[i].input_shape}, output shape \"\n                f\"{self.stack[i].output_shape}\"\n            )\n\n            # set input specification for the layer\n            num_channels = self.stack[i].output_shape[1]\n            sequence_length = self.stack[i].output_shape[0]\n\n    @property\n    def input_shape(self):\n        \"\"\"Returns the size of the input tensor without the batch dimension.\"\"\"\n        return torch.Size([self.max_sequence_length, self.in_channels])\n\n    def forward(self, inputs, mask=None):\n        hidden = inputs\n\n        for layer in self.stack:\n            hidden = layer(hidden)\n\n        if hidden.shape[2] == 0:\n            raise ValueError(\n                \"The output of the conv stack has the second dimension \"\n                \"(length of the sequence) equal to 0. \"\n                \"This means that the combination of filter_size, padding, \"\n                \"stride, pool_size, pool_padding and pool_stride is reduces \"\n                \"the sequence length more than is possible. \"\n                'Try using \"same\" padding and reducing or eliminating stride '\n                \"and pool.\"\n            )\n\n        return hidden\n\n\nclass Conv2DLayer(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        in_channels: int,\n        out_channels: int = 256,\n        kernel_size: int | tuple[int] = 3,\n        stride: int | tuple[int] = 1,\n        padding: int | tuple[int] | str = \"valid\",\n        dilation: int | tuple[int] = 1,\n        groups: int = 1,\n        use_bias: bool = True,\n        padding_mode: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict[str, Any] | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n        pool_function: int = \"max\",\n        pool_kernel_size: int | tuple[int] = None,\n        pool_stride: int | None = None,\n        pool_padding: int | tuple[int] = 0,\n        pool_dilation: int | tuple[int] = 1,\n    ):\n        super().__init__()\n\n        self.layers = torch.nn.ModuleList()\n\n        self._input_shape = (in_channels, img_height, img_width)\n        pool_stride = pool_stride or pool_kernel_size\n\n        self.layers.append(\n            nn.Conv2d(\n                in_channels=in_channels,\n                out_channels=out_channels,\n                kernel_size=kernel_size,\n                stride=stride,\n                padding=padding,\n                dilation=dilation,\n                groups=groups,\n                bias=use_bias,\n                padding_mode=padding_mode,\n            )\n        )\n        out_height, out_width = get_img_output_shape(img_height, img_width, kernel_size, stride, padding, dilation)\n\n        if norm and norm_params is None:\n            norm_params = {}\n        if norm == \"batch\":\n            # Batch norm over channels\n            self.layers.append(nn.BatchNorm2d(num_features=out_channels, **norm_params))\n        elif norm == \"layer\":\n            # Layer norm over image height and width\n            self.layers.append(nn.LayerNorm(normalized_shape=(out_height, out_width), **norm_params))\n\n        self.layers.append(get_activation(activation))\n\n        if dropout > 0:\n            self.layers.append(nn.Dropout(dropout))\n\n        if pool_kernel_size is not None:\n            pool = partial(nn.MaxPool2d, dilation=pool_dilation)\n            if pool_function in {\"average\", \"avg\", \"mean\"}:\n                pool = nn.AvgPool2d\n            self.layers.append(pool(kernel_size=pool_kernel_size, stride=pool_stride, padding=pool_padding))\n            out_height, out_width = get_img_output_shape(\n                img_height=out_height,\n                img_width=out_width,\n                kernel_size=pool_kernel_size,\n                stride=pool_stride,\n                padding=pool_padding,\n                dilation=pool_dilation,\n            )\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = (out_channels, out_height, out_width)\n\n    def forward(self, inputs):\n        hidden = inputs\n\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\nclass Conv2DStack(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        layers: list[dict] | None = None,\n        num_layers: int | None = None,\n        first_in_channels: int | None = None,\n        default_out_channels: int = 256,\n        default_kernel_size: int | tuple[int] = 3,\n        default_stride: int | tuple[int] = 1,\n        default_padding: int | tuple[int] | str = \"valid\",\n        default_dilation: int | tuple[int] = 1,\n        default_groups: int = 1,\n        default_use_bias: bool = True,\n        default_padding_mode: str = \"zeros\",\n        default_norm: str | None = None,\n        default_norm_params: dict[str, Any] | None = None,\n        default_activation: str = \"relu\",\n        default_dropout: int = 0,\n        default_pool_function: int = \"max\",\n        default_pool_kernel_size: int | tuple[int] = 2,\n        default_pool_stride: int | tuple[int] = None,\n        default_pool_padding: int | tuple[int] = 0,\n        default_pool_dilation: int | tuple[int] = 1,\n    ):\n        super().__init__()\n\n        # Confirm that all inputs are consistent\n        first_in_channels = self._check_in_channels(first_in_channels, layers)\n        default_pool_stride = default_pool_stride or default_pool_kernel_size\n        if layers is not None and num_layers is not None:\n            raise Warning(\"Both layers and num_layers are not None.\" \"Default to using layers.\")\n        if (\n            first_in_channels is not None\n            and layers is not None\n            and len(layers) > 0\n            and \"in_channels\" in layers[0]\n            and layers[0][\"in_channels\"] != first_in_channels\n        ):\n            raise Warning(\n                \"Input channels is set via layers[0]['in_channels'] and first_in_channels.\"\n                \"Default to using first_in_channels.\"\n            )\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        if layers is None:\n            if num_layers is None:\n                self.layers = [\n                    {\"out_channels\": 32},\n                    {\"out_channels\": 64},\n                ]\n            else:\n                self.layers = []\n                for i in range(num_layers):\n                    self.layers.append(\n                        {\n                            \"kernel_size\": default_kernel_size,\n                            \"out_channels\": default_out_channels,\n                            \"pool_kernel_size\": default_pool_kernel_size,\n                        }\n                    )\n        else:\n            self.layers = layers\n\n        for layer in self.layers:\n            if \"out_channels\" not in layer:\n                layer[\"out_channels\"] = default_out_channels\n            if \"kernel_size\" not in layer:\n                layer[\"kernel_size\"] = default_kernel_size\n            if \"stride\" not in layer:\n                layer[\"stride\"] = default_stride\n            if \"padding\" not in layer:\n                layer[\"padding\"] = default_padding\n            if \"dilation\" not in layer:\n                layer[\"dilation\"] = default_dilation\n            if \"groups\" not in layer:\n                layer[\"groups\"] = default_groups\n            if \"use_bias\" not in layer:\n                layer[\"use_bias\"] = default_use_bias\n            if \"padding_mode\" not in layer:\n                layer[\"padding_mode\"] = default_padding_mode\n            if \"norm\" not in layer:\n                layer[\"norm\"] = default_norm\n            if \"norm_params\" not in layer:\n                layer[\"norm_params\"] = default_norm_params\n            if \"activation\" not in layer:\n                layer[\"activation\"] = default_activation\n            if \"dropout\" not in layer:\n                layer[\"dropout\"] = default_dropout\n            if \"pool_function\" not in layer:\n                layer[\"pool_function\"] = default_pool_function\n            if \"pool_kernel_size\" not in layer:\n                layer[\"pool_kernel_size\"] = default_pool_kernel_size\n            if \"pool_stride\" not in layer:\n                layer[\"pool_stride\"] = default_pool_stride\n            if \"pool_padding\" not in layer:\n                layer[\"pool_padding\"] = default_pool_padding\n            if \"pool_dilation\" not in layer:\n                layer[\"pool_dilation\"] = default_pool_dilation\n\n        self.stack = torch.nn.ModuleList()\n\n        in_channels = first_in_channels\n        for i, layer in enumerate(self.layers):\n            logger.debug(f\"   stack layer {i}\")\n            self.stack.append(\n                Conv2DLayer(\n                    img_height=img_height,\n                    img_width=img_width,\n                    in_channels=in_channels,\n                    out_channels=layer[\"out_channels\"],\n                    kernel_size=layer[\"kernel_size\"],\n                    stride=layer[\"stride\"],\n                    padding=layer[\"padding\"],\n                    dilation=layer[\"dilation\"],\n                    groups=layer[\"groups\"],\n                    use_bias=layer[\"use_bias\"],\n                    padding_mode=layer[\"padding_mode\"],\n                    norm=layer[\"norm\"],\n                    norm_params=layer[\"norm_params\"],\n                    activation=layer[\"activation\"],\n                    dropout=layer[\"dropout\"],\n                    pool_function=layer[\"pool_function\"],\n                    pool_kernel_size=layer[\"pool_kernel_size\"],\n                    pool_stride=layer[\"pool_stride\"],\n                    pool_padding=layer[\"pool_padding\"],\n                    pool_dilation=layer[\"pool_dilation\"],\n                )\n            )\n            in_channels, img_height, img_width = self.stack[-1].output_shape\n\n        self._output_shape = (in_channels, img_height, img_width)\n\n    def forward(self, inputs):\n        hidden = inputs\n\n        for layer in self.stack:\n            hidden = layer(hidden)\n\n        return hidden\n\n    def _check_in_channels(self, first_in_channels: int | None, layers: list[dict] | None) -> None:\n        \"\"\"Confirms that in_channels for first layer of the stack exists.\"\"\"\n\n        if first_in_channels is not None:\n            return first_in_channels\n        elif layers is not None and len(layers) > 0 and \"in_channels\" in layers[0]:\n            return layers[0][\"in_channels\"]\n        raise ValueError(\n            \"In_channels for first layer should be specified either via \" \"`first_in_channels` or `layers` arguments.\"\n        )\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.size(self._input_shape)\n\n\nclass Conv2DLayerFixedPadding(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        in_channels: int,\n        out_channels=256,\n        kernel_size=3,\n        stride=1,\n        dilation=1,\n        groups=1,\n        use_bias=False,\n    ):\n        super().__init__()\n\n        self.layers = torch.nn.ModuleList()\n        self._input_shape = (in_channels, img_height, img_width)\n\n        padding = \"same\"\n        if stride > 1:\n            padding = (kernel_size - 1) // 2\n\n        self.layers.append(\n            nn.Conv2d(\n                in_channels=in_channels,\n                out_channels=out_channels,\n                kernel_size=kernel_size,\n                stride=stride,\n                padding=padding,\n                dilation=dilation,\n                groups=groups,\n                bias=use_bias,\n            )\n        )\n        img_height, img_width = get_img_output_shape(\n            img_height=img_height,\n            img_width=img_width,\n            kernel_size=kernel_size,\n            stride=stride,\n            padding=padding,\n            dilation=dilation,\n        )\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = (out_channels, img_height, img_width)\n\n    def forward(self, inputs):\n        hidden = inputs\n\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        return hidden\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n\nclass ResNetBlock(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        first_in_channels: int,\n        out_channels: int,\n        stride: int = 1,\n        batch_norm_momentum: float = 0.1,\n        batch_norm_epsilon: float = 0.001,\n        projection_shortcut: LudwigModule | None = None,\n    ):\n        \"\"\"Resnet blocks used for ResNet34 and smaller.\n\n        stride: A single int specifying the stride of the first convolution.\n            The last convolution will have stride of 1.\n        \"\"\"\n        super().__init__()\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        self.conv1 = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=first_in_channels,\n            out_channels=out_channels,\n            kernel_size=3,\n            stride=stride,\n        )\n        in_channels, img_height, img_width = self.conv1.output_shape\n        self.norm1 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        self.relu1 = get_activation(\"relu\")\n\n        self.conv2 = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=out_channels,\n            out_channels=out_channels,\n            kernel_size=3,\n            stride=1,\n        )\n        self.norm2 = nn.BatchNorm2d(num_features=out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        self.relu2 = get_activation(\"relu\")\n\n        for layer in [self.conv1, self.norm1, self.relu1, self.conv2, self.norm2, self.relu2]:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = self.conv2.output_shape\n\n        self.projection_shortcut = projection_shortcut\n        if self.projection_shortcut is not None and self.projection_shortcut.output_shape != self._output_shape:\n            raise ValueError(\n                f\"Output shapes of ResnetBlock and projection_shortcut should \"\n                f\"match but are {self._output_shape} and \"\n                f\"{self.projection_shortcut.output_shape} respectively.\"\n            )\n        if self.projection_shortcut is None and self._input_shape != self._output_shape:\n            self.projection_shortcut = Conv2DLayer(\n                img_height=self._input_shape[1],\n                img_width=self._input_shape[2],\n                in_channels=first_in_channels,\n                out_channels=out_channels,\n                kernel_size=1,\n                stride=stride,\n            )\n\n    def forward(self, inputs):\n        shortcut = inputs\n\n        if self.projection_shortcut is not None:\n            shortcut = self.projection_shortcut(shortcut)\n\n        hidden = self.conv1(inputs)\n        hidden = self.norm1(hidden)\n        hidden = self.relu1(hidden)\n        hidden = self.conv2(hidden)\n        hidden = self.norm2(hidden)\n\n        return self.relu2(hidden + shortcut)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n\n# TODO(shreya): Combine with ResNetBlock by adding a flag.\nclass ResNetBottleneckBlock(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        first_in_channels: int,\n        out_channels: int,\n        stride: int = 1,\n        batch_norm_momentum: float = 0.1,\n        batch_norm_epsilon: float = 0.001,\n        projection_shortcut: LudwigModule | None = None,\n    ):\n        \"\"\"Resnet bottleneck blocks used for ResNet50 and larger.\n\n        stride: A single int specifying the stride of the middle convolution.\n            The first and last convolution will have stride of 1.\n        \"\"\"\n        super().__init__()\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        self.conv1 = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=first_in_channels,\n            out_channels=out_channels,\n            kernel_size=1,\n            stride=1,\n        )\n        in_channels, img_height, img_width = self.conv1.output_shape\n        self.norm1 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        self.relu1 = get_activation(\"relu\")\n\n        self.conv2 = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=in_channels,\n            out_channels=out_channels,\n            kernel_size=3,\n            stride=stride,\n        )\n        in_channels, img_height, img_width = self.conv2.output_shape\n        self.norm2 = nn.BatchNorm2d(num_features=in_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        self.relu2 = get_activation(\"relu\")\n\n        self.conv3 = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=in_channels,\n            out_channels=4 * out_channels,\n            kernel_size=1,\n            stride=1,\n        )\n        self.norm3 = nn.BatchNorm2d(num_features=4 * out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        self.relu3 = get_activation(\"relu\")\n\n        for layer in [\n            self.conv1,\n            self.norm1,\n            self.relu1,\n            self.conv2,\n            self.norm2,\n            self.relu2,\n            self.conv3,\n            self.norm3,\n            self.relu3,\n        ]:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = self.conv3.output_shape\n\n        self.projection_shortcut = projection_shortcut\n        if self.projection_shortcut is not None and self.projection_shortcut.output_shape != self._output_shape:\n            raise ValueError(\n                f\"Output shapes of ResnetBlock and projection_shortcut should \"\n                f\"match but are {self._output_shape} and \"\n                f\"{self.projection_shortcut.output_shape} respectively.\"\n            )\n        if self.projection_shortcut is None and self._input_shape != self._output_shape:\n            self.projection_shortcut = Conv2DLayer(\n                img_height=self._input_shape[1],\n                img_width=self._input_shape[2],\n                in_channels=first_in_channels,\n                out_channels=4 * out_channels,\n                kernel_size=1,\n                stride=stride,\n            )\n\n    def forward(self, inputs):\n        shortcut = inputs\n\n        if self.projection_shortcut is not None:\n            shortcut = self.projection_shortcut(shortcut)\n\n        hidden = self.conv1(inputs)\n        hidden = self.norm1(hidden)\n        hidden = self.relu1(hidden)\n        hidden = self.conv2(hidden)\n        hidden = self.norm2(hidden)\n        hidden = self.relu2(hidden)\n        hidden = self.conv3(hidden)\n        hidden = self.norm3(hidden)\n\n        return self.relu3(hidden + shortcut)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\nclass ResNetBlockLayer(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        first_in_channels: int,\n        out_channels: int,\n        is_bottleneck: bool,\n        block_fn: ResNetBlock | ResNetBottleneckBlock,\n        num_blocks: int,\n        stride: int | tuple[int] = 1,\n        batch_norm_momentum: float = 0.1,\n        batch_norm_epsilon: float = 0.001,\n    ):\n        super().__init__()\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        # Bottleneck blocks end with 4x the number of channels as they start with\n        projection_out_channels = out_channels * 4 if is_bottleneck else out_channels\n        projection_shortcut = Conv2DLayerFixedPadding(\n            img_height=img_height,\n            img_width=img_width,\n            in_channels=first_in_channels,\n            out_channels=projection_out_channels,\n            kernel_size=1,\n            stride=stride,\n        )\n\n        self.layers = torch.nn.ModuleList(\n            [\n                block_fn(\n                    img_height,\n                    img_width,\n                    first_in_channels,\n                    out_channels,\n                    stride,\n                    batch_norm_momentum,\n                    batch_norm_epsilon,\n                    projection_shortcut,\n                )\n            ]\n        )\n        in_channels, img_height, img_width = self.layers[-1].output_shape\n\n        for _ in range(1, num_blocks):\n            self.layers.append(\n                block_fn(\n                    img_height=img_height,\n                    img_width=img_width,\n                    first_in_channels=in_channels,\n                    out_channels=out_channels,\n                    stride=1,\n                    batch_norm_momentum=batch_norm_momentum,\n                    batch_norm_epsilon=batch_norm_epsilon,\n                )\n            )\n            in_channels, img_height, img_width = self.layers[-1].output_shape\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = (in_channels, img_height, img_width)\n\n    def forward(self, inputs):\n        hidden = inputs\n        for layer in self.layers:\n            hidden = layer(hidden)\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\nclass ResNet(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        first_in_channels: int,\n        out_channels: int,\n        resnet_size: int = 34,\n        kernel_size: int | tuple[int] = 7,\n        conv_stride: int | tuple[int] = 2,\n        first_pool_kernel_size: int | tuple[int] = 3,\n        first_pool_stride: int | tuple[int] = 2,\n        block_sizes: list[int] = None,\n        block_strides: list[int | tuple[int]] = None,\n        batch_norm_momentum: float = 0.1,\n        batch_norm_epsilon: float = 0.001,\n    ):\n        \"\"\"Creates a model obtaining an image representation.\n\n        Implements ResNet v2:\n        Identity Mappings in Deep Residual Networks\n        https://arxiv.org/pdf/1603.05027.pdf\n        by Kaiming He, Xiangyu Zhang, Shaoqing Ren, and Jian Sun, Jul 2016.\n\n        Args:\n          resnet_size: A single integer for the size of the ResNet model.\n          is_bottleneck: Use regular blocks or bottleneck blocks.\n          out_channels: The number of filters to use for the first block layer\n            of the model. This number is then doubled for each subsequent block\n            layer.\n          kernel_size: The kernel size to use for convolution.\n          conv_stride: stride size for the initial convolutional layer\n          first_pool_kernel_size: Pool size to be used for the first pooling layer.\n            If none, the first pooling layer is skipped.\n          first_pool_stride: stride size for the first pooling layer. Not used\n            if first_pool_kernel_size is None.\n          block_sizes: A list containing n values, where n is the number of sets of\n            block layers desired. Each value should be the number of blocks in the\n            i-th set.\n          block_strides: List of integers representing the desired stride size for\n            each of the sets of block layers. Should be same length as block_sizes.\n        Raises:\n          ValueError: if invalid version is selected.\n        \"\"\"\n        super().__init__()\n\n        self._input_shape = (first_in_channels, img_height, img_width)\n\n        is_bottleneck = self.get_is_bottleneck(resnet_size, block_sizes)\n        block_class = self.get_block_fn(is_bottleneck)\n        block_sizes, block_strides = self.get_blocks(resnet_size, block_sizes, block_strides)\n\n        self.layers = torch.nn.ModuleList()\n        self.layers.append(\n            Conv2DLayerFixedPadding(\n                img_height=img_height,\n                img_width=img_width,\n                in_channels=first_in_channels,\n                out_channels=out_channels,\n                kernel_size=kernel_size,\n                stride=conv_stride,\n            )\n        )\n        in_channels, img_height, img_width = self.layers[-1].output_shape\n        self.layers.append(\n            nn.BatchNorm2d(num_features=out_channels, eps=batch_norm_epsilon, momentum=batch_norm_momentum)\n        )\n        self.layers.append(get_activation(\"relu\"))\n\n        if first_pool_kernel_size:\n            self.layers.append(nn.MaxPool2d(kernel_size=first_pool_kernel_size, stride=first_pool_stride, padding=1))\n            img_height, img_width = get_img_output_shape(\n                img_height=img_height,\n                img_width=img_width,\n                kernel_size=first_pool_kernel_size,\n                stride=first_pool_stride,\n                padding=1,\n                dilation=1,\n            )\n\n        for i, num_blocks in enumerate(block_sizes):\n            self.layers.append(\n                ResNetBlockLayer(\n                    img_height=img_height,\n                    img_width=img_width,\n                    first_in_channels=in_channels,\n                    out_channels=out_channels,\n                    is_bottleneck=is_bottleneck,\n                    block_fn=block_class,\n                    num_blocks=num_blocks,\n                    stride=block_strides[i],\n                    batch_norm_momentum=batch_norm_momentum,\n                    batch_norm_epsilon=batch_norm_epsilon,\n                )\n            )\n            out_channels *= 2\n            in_channels, img_height, img_width = self.layers[-1].output_shape\n\n        for layer in self.layers:\n            logger.debug(f\"   {layer._get_name()}\")\n\n        self._output_shape = (in_channels, img_height, img_width)\n\n    def get_is_bottleneck(self, resnet_size: int, block_sizes: list[int]) -> bool:\n        if (resnet_size is not None and resnet_size >= 50) or (block_sizes is not None and sum(block_sizes) >= 16):\n            return True\n        return False\n\n    def get_block_fn(self, is_bottleneck: bool) -> ResNetBlock | ResNetBottleneckBlock:\n        if is_bottleneck:\n            return ResNetBottleneckBlock\n        return ResNetBlock\n\n    def get_blocks(self, resnet_size: int, block_sizes: list[int], block_strides: list[int]) -> tuple[list[int]]:\n        if block_sizes is None:\n            block_sizes = get_resnet_block_sizes(resnet_size)\n        if block_strides is None:\n            block_strides = [1] + [2 for _ in range(len(block_sizes) - 1)]\n        return block_sizes, block_strides\n\n    def forward(self, inputs: torch.Tensor) -> torch.Tensor:\n        hidden = inputs\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\n################################################################################\n# The following code for ResNet is adapted from the TensorFlow implementation\n# https://github.com/tensorflow/models/blob/master/official/resnet/resnet_model.py\n################################################################################\n\n################################################################################\n# Convenience functions for building the ResNet model.\n################################################################################\nresnet_choices = {\n    8: [1, 2, 2],\n    14: [1, 2, 2],\n    18: [2, 2, 2, 2],\n    34: [3, 4, 6, 3],\n    50: [3, 4, 6, 3],\n    101: [3, 4, 23, 3],\n    152: [3, 8, 36, 3],\n    200: [3, 24, 36, 3],\n}\n\n\ndef get_resnet_block_sizes(resnet_size):\n    \"\"\"Retrieve the size of each block_layer in the ResNet model.\n\n    The number of block layers used for the Resnet model varies according\n    to the size of the model. This helper grabs the layer set we want, throwing\n    an error if a non-standard size has been selected.\n    Args:\n      resnet_size: The number of convolutional layers needed in the model.\n    Returns:\n      A list of block sizes to use in building the model.\n    Raises:\n      KeyError: if invalid resnet_size is received.\n    \"\"\"\n    try:\n        return resnet_choices[resnet_size]\n    except KeyError:\n        err = \"Could not find layers for selected Resnet size.\\n\" \"Size received: {}; sizes allowed: {}.\".format(\n            resnet_size, resnet_choices.keys()\n        )\n        raise ValueError(err)\n\n\nclass UNetDoubleConvLayer(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        in_channels: int,\n        out_channels: int,\n        norm: str = None,\n    ):\n        \"\"\"Two Conv2d layers, each followed by a ReLU, used for U-Net.\n\n        Args:\n          img_height: the input image height\n          img_width: the input image width\n          in_channels: the number of input channels\n          out_channels: the number of output channels\n          norm: the normalization to be applied\n        \"\"\"\n        super().__init__()\n\n        self.layers = nn.ModuleList()\n\n        self.layers.append(nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1))\n        if norm == \"batch\":\n            self.layers.append(nn.BatchNorm2d(out_channels))\n        self.layers.append(nn.ReLU())\n\n        self.layers.append(nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1))\n        if norm == \"batch\":\n            self.layers.append(nn.BatchNorm2d(out_channels))\n        self.layers.append(nn.ReLU())\n\n        self._input_shape = (in_channels, img_height, img_width)\n        self._output_shape = (out_channels, img_height, img_width)\n\n    def forward(self, inputs):\n        hidden = inputs\n\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\nclass UNetDownStack(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        in_channels: int,\n        norm: str = None,\n        stack_depth: int = 4,\n    ):\n        \"\"\"Creates the contracting downsampling path of a U-Net stack.\n\n        Implements\n        U-Net: Convolutional Networks for Biomedical Image Segmentation\n        https://arxiv.org/abs/1505.04597\n        by Olaf Ronneberger, Philipp Fischer, Thomas Brox, May 2015.\n\n        Args:\n          img_height: the input image height\n          img_width: the input image width\n          in_channels: the number of input channels\n          norm: the normalization to be applied\n          stack_depth: the depth of the unet stack\n        \"\"\"\n        super().__init__()\n\n        self.conv_layers = nn.ModuleList()\n        self.down_layers = nn.ModuleList()\n\n        height = img_height\n        width = img_width\n        in_c = in_channels\n        out_c = 64\n\n        self._input_shape = (in_c, height, width)\n\n        for i in range(stack_depth):\n            self.conv_layers.append(UNetDoubleConvLayer(height, width, in_c, out_c, norm))\n            in_c = out_c\n            out_c = out_c * 2\n\n            self.down_layers.append(nn.MaxPool2d(kernel_size=2, stride=2))\n            height = height // 2\n            width = width // 2\n\n        self.bottleneck = UNetDoubleConvLayer(height, width, in_c, out_c, norm)\n\n        self._output_shape = (out_c, height, width)\n\n    def forward(self, inputs):\n        skips = []  # skip connections\n        hidden = inputs\n\n        for conv_layer, down_layer in zip(self.conv_layers, self.down_layers):\n            hidden = conv_layer(hidden)\n            skips.append(hidden)\n            hidden = down_layer(hidden)\n\n        hidden = self.bottleneck(hidden)\n        return hidden, skips\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n\nclass UNetUpStack(LudwigModule):\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        out_channels: int,\n        norm: str = None,\n        stack_depth: int = 4,\n    ):\n        \"\"\"Creates the expansive upsampling path of a U-Net stack.\n\n        Implements\n        U-Net: Convolutional Networks for Biomedical Image Segmentation\n        https://arxiv.org/abs/1505.04597\n        by Olaf Ronneberger, Philipp Fischer, Thomas Brox, May 2015.\n\n        Args:\n          img_height: the output image height\n          img_width: the output image width\n          out_channels: the number of output classes\n          norm: the normalization to be applied\n          stack_depth: the depth of the unet stack\n        \"\"\"\n        super().__init__()\n\n        self.conv_layers = nn.ModuleList()\n        self.up_layers = nn.ModuleList()\n\n        height = img_height >> stack_depth\n        width = img_width >> stack_depth\n        in_c = 64 << stack_depth\n        out_c = in_c // 2\n\n        self._input_shape = (in_c, height, width)\n\n        for i in range(stack_depth):\n            self.up_layers.append(nn.ConvTranspose2d(in_c, out_c, kernel_size=2, stride=2))\n            height = height * 2\n            width = width * 2\n\n            self.conv_layers.append(UNetDoubleConvLayer(height, width, out_c * 2, out_c, norm))\n            in_c = out_c\n            out_c = out_c // 2\n\n        self.last_conv = nn.Conv2d(in_c, out_channels, kernel_size=1, padding=0)\n\n        self._output_shape = (out_channels, img_height, img_width)\n\n    def forward(self, inputs, skips):\n        hidden = inputs\n\n        for conv_layer, up_layer in zip(self.conv_layers, self.up_layers):\n            hidden = up_layer(hidden)\n            skip = skips.pop()\n            hidden = torch.cat([hidden, skip], axis=1)\n            hidden = conv_layer(hidden)\n\n        hidden = self.last_conv(hidden)\n        return hidden\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n"
  },
  {
    "path": "ludwig/modules/embedding_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nfrom torch import nn\n\nfrom ludwig.constants import TYPE\nfrom ludwig.modules.initializer_modules import get_initializer\nfrom ludwig.utils.data_utils import load_pretrained_embeddings\nfrom ludwig.utils.torch_utils import get_torch_device, LudwigModule\n\nlogger = logging.getLogger(__name__)\n\nDEVICE = get_torch_device()\n\n\ndef embedding_matrix(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str = \"dense\",\n    embeddings_trainable: bool = True,\n    pretrained_embeddings: str | None = None,\n    force_embedding_size: bool = False,\n    embedding_initializer: str | dict | None = None,\n) -> tuple[nn.Module, int]:\n    \"\"\"Returns initialized torch.nn.Embedding module and embedding size.\"\"\"\n\n    vocab_size = len(vocab)\n    if representation == \"dense\":\n        if pretrained_embeddings:\n            embeddings_matrix = load_pretrained_embeddings(pretrained_embeddings, vocab)\n            if embeddings_matrix.shape[-1] != embedding_size:\n                if not force_embedding_size:\n                    embedding_size = embeddings_matrix.shape[-1]\n                    logger.info(f\"Setting embedding size to be equal to {embeddings_matrix.shape[-1]}.\")\n                else:\n                    raise ValueError(\n                        f\"The size of the pretrained embeddings is \"\n                        f\"{embeddings_matrix.shape[-1]}, but the specified \"\n                        f\"embedding_size is {embedding_size}. Please change \"\n                        f\"the embedding_size accordingly.\"\n                    )\n            embedding_initializer_obj = torch.tensor(embeddings_matrix, dtype=torch.float32)\n\n        else:\n            if vocab_size < embedding_size and not force_embedding_size:\n                logger.info(\n                    f\"  embedding_size ({embedding_size}) is greater than \"\n                    f\"vocab_size ({vocab_size}). Setting embedding size to be \"\n                    f\"equal to vocab_size.\"\n                )\n                embedding_size = vocab_size\n\n            if embedding_initializer is not None:\n                embedding_initializer_obj_ref = get_initializer(embedding_initializer)\n            else:\n                embedding_initializer_obj_ref = get_initializer({TYPE: \"uniform\", \"a\": -1.0, \"b\": 1.0})\n            embedding_initializer_obj = embedding_initializer_obj_ref([vocab_size, embedding_size])\n\n        embeddings = embedding_initializer_obj\n\n    elif representation == \"sparse\":\n        embedding_size = vocab_size\n        embeddings = get_initializer(\"identity\")([vocab_size, embedding_size])\n        embeddings.requires_grad = False\n    else:\n        raise Exception(f\"Embedding representation {representation} not supported.\")\n\n    embeddings = nn.Embedding.from_pretrained(embeddings, freeze=not embeddings_trainable)\n    return embeddings, embedding_size\n\n\ndef embedding_matrix_on_device(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str = \"dense\",\n    embeddings_trainable: bool = True,\n    pretrained_embeddings: str | None = None,\n    force_embedding_size: bool = False,\n    embeddings_on_cpu: bool = False,\n    embedding_initializer: str | None = None,\n) -> tuple[nn.Module, int]:\n    embeddings, embedding_size = embedding_matrix(\n        vocab,\n        embedding_size,\n        representation=representation,\n        embeddings_trainable=embeddings_trainable,\n        pretrained_embeddings=pretrained_embeddings,\n        force_embedding_size=force_embedding_size,\n        embedding_initializer=embedding_initializer,\n    )\n    if embeddings_on_cpu:\n        embeddings.to(\"cpu\")\n    elif not embeddings_on_cpu and torch.cuda.is_available():\n        embeddings.to(device=\"cuda\")\n\n    return embeddings, embedding_size\n\n\nclass Embed(LudwigModule):\n    \"\"\"Module to embed Category, Date, and H3 data types.\"\"\"\n\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int,\n        representation: str = \"dense\",\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        force_embedding_size: bool = False,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | dict | None = None,\n    ):\n        super().__init__()\n        self.supports_masking = True\n\n        self.vocab_size = len(vocab)\n        self.embeddings, self.embedding_size = embedding_matrix_on_device(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            embedding_initializer=embedding_initializer,\n        )\n\n        if dropout > 0:\n            self.dropout = torch.nn.Dropout(p=dropout)\n        else:\n            self.dropout = None\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:\n        if inputs.ndim != 2 or inputs.shape[1] != 1:\n            raise RuntimeError(\n                f\"Embed only takes inputs of shape [batch x 1]. Received inputs with size: {inputs.size()}\"\n            )\n        embedded = self.embeddings(inputs.long())\n        embedded = torch.squeeze(embedded, dim=1)\n        if self.dropout:\n            embedded = self.dropout(embedded)\n        return embedded\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([1])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.embedding_size])\n\n\nclass EmbedSet(LudwigModule):\n    \"\"\"Module to embed Set data types, works on multi-hot encoded input.\"\"\"\n\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int,\n        representation: str = \"dense\",\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        force_embedding_size: bool = False,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | dict | None = None,\n        aggregation_function: str = \"sum\",\n    ):\n        super().__init__()\n        self.supports_masking = True\n\n        self.vocab_size = len(vocab)\n        self.embeddings, self.embedding_size = embedding_matrix_on_device(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            embedding_initializer=embedding_initializer,\n        )\n\n        if dropout > 0:\n            self.dropout = torch.nn.Dropout(p=dropout)\n        else:\n            self.dropout = None\n\n        if aggregation_function == \"sum\":\n            self.aggregation_function = torch.sum\n        elif aggregation_function == \"avg\":\n            self.aggregation_function = torch.mean\n        else:\n            raise ValueError(f\"Unsupported aggregation function {aggregation_function}\")\n\n        self.register_buffer(\"vocab_indices\", torch.arange(self.vocab_size))\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:\n        \"\"\"\n        Params:\n            inputs: Boolean multi-hot tensor of size [batch x vocab_size], where\n                    inputs[b, i] indicates that token i is present in sample b.\n        \"\"\"\n        # Convert multi-hot input to input of indices\n        inputs = inputs.int() * self.vocab_indices\n        embedded = self.embeddings(inputs.long())\n        # Mask out the 0th embedding\n        mask = torch.unsqueeze(inputs, -1)\n        embedded = embedded * mask\n        # Sum over all positive tokens\n        embedded = self.aggregation_function(embedded, dim=1)\n        if self.dropout:\n            embedded = self.dropout(embedded)\n        return embedded\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.vocab_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.embedding_size])\n\n    @property\n    def input_dtype(self):\n        return torch.bool\n\n\nclass EmbedWeighted(LudwigModule):\n    \"\"\"Module to embed Bag data type, works on input of token frequencies.\"\"\"\n\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int,\n        representation: str = \"dense\",\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        force_embedding_size: bool = False,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | None = None,\n    ):\n        super().__init__()\n\n        self.embeddings, self.embedding_size = embedding_matrix_on_device(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            embedding_initializer=embedding_initializer,\n        )\n        self.vocab_size = len(vocab)\n\n        if dropout > 0:\n            self.dropout = nn.Dropout(dropout)\n        else:\n            self.dropout = None\n\n        self.register_buffer(\"vocab_indices\", torch.arange(self.vocab_size, dtype=torch.int32))\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None) -> torch.Tensor:\n        \"\"\"\n        Params:\n            inputs: Tensor of frequencies, where inputs[b, i] represents\n                    frequency of token i in sample b of batch.\n        \"\"\"\n        # Convert to multi-hot input\n        signed_input = (inputs != 0).type(torch.int32)\n        multiple_hot_indexes = signed_input * self.vocab_indices\n        embedded = self.embeddings(multiple_hot_indexes)\n        # Mask out the 0th embedding\n        mask = torch.unsqueeze(inputs, -1)\n        weighted_embedded = embedded * mask\n        # Sum over the all the positive indices\n        embedded_reduced = torch.sum(weighted_embedded, dim=1)\n        if self.dropout:\n            embedded_reduced = self.dropout(embedded_reduced)\n        return embedded_reduced\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.vocab_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.embedding_size])\n\n\nclass EmbedSequence(LudwigModule):\n    def __init__(\n        self,\n        vocab: list[str],\n        embedding_size: int,\n        max_sequence_length: int,\n        representation: str = \"dense\",\n        embeddings_trainable: bool = True,\n        pretrained_embeddings: str | None = None,\n        force_embedding_size: bool = False,\n        embeddings_on_cpu: bool = False,\n        dropout: float = 0.0,\n        embedding_initializer: str | None = None,\n    ):\n        super().__init__()\n        self.supports_masking = True\n\n        self.vocab_size = len(vocab)\n        self.max_sequence_length = max_sequence_length\n        self.embeddings, self.embedding_size = embedding_matrix_on_device(\n            vocab,\n            embedding_size,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            embedding_initializer=embedding_initializer,\n        )\n\n        if dropout > 0:\n            self.dropout = nn.Dropout(dropout)\n        else:\n            self.dropout = None\n\n    def forward(self, inputs: torch.Tensor, mask: torch.Tensor | None = None):\n        if inputs.dtype not in [torch.int, torch.long]:\n            raise RuntimeError(\n                f\"Expected tensor of type torch.int or torch.long as input.\" f\"Received {inputs.dtype} instead.\"\n            )\n\n        embedded = self.embeddings(inputs)\n        if self.dropout:\n            embedded = self.dropout(embedded)\n        return embedded\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length, self.embedding_size])\n\n\nclass TokenAndPositionEmbedding(LudwigModule):\n    def __init__(\n        self,\n        max_sequence_length,\n        vocab,\n        embedding_size,\n        representation=\"dense\",\n        embeddings_trainable=True,\n        pretrained_embeddings=None,\n        force_embedding_size=False,\n        embeddings_on_cpu=False,\n        dropout=0.0,\n        embedding_initializer=None,\n    ):\n        super().__init__()\n        self.max_sequence_length = max_sequence_length\n        self.embedding_size = embedding_size\n        self.token_embed = EmbedSequence(\n            vocab=vocab,\n            embedding_size=embedding_size,\n            max_sequence_length=max_sequence_length,\n            representation=representation,\n            embeddings_trainable=embeddings_trainable,\n            pretrained_embeddings=pretrained_embeddings,\n            force_embedding_size=force_embedding_size,\n            embeddings_on_cpu=embeddings_on_cpu,\n            dropout=dropout,\n            embedding_initializer=embedding_initializer,\n        )\n        self.position_embed = nn.Embedding(\n            num_embeddings=max_sequence_length, embedding_dim=self.token_embed.embedding_size\n        )\n        self.register_buffer(\"positions\", torch.arange(0, max_sequence_length))\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.max_sequence_length])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.token_embed.output_shape\n\n    def forward(self, inputs, mask: torch.Tensor | None = None):\n        positions_hidden = self.position_embed(self.positions)\n        token_hidden = self.token_embed(inputs)\n        return token_hidden + positions_hidden\n"
  },
  {
    "path": "ludwig/modules/fully_connected_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom copy import deepcopy\n\nimport torch\nfrom torch.nn import Dropout, Linear, ModuleList\n\nfrom ludwig.modules.normalization_modules import create_norm_layer\nfrom ludwig.utils.torch_utils import activations, initializer_registry, LudwigModule\n\nlogger = logging.getLogger(__name__)\n\n\nclass FCLayer(LudwigModule):\n    \"\"\"A torch.nn.Linear wrapper that declares input and output shapes, and enables the customization of:\n\n    1. how weights and biases are initialized\n    2. normalization (layer and batch)\n    3. activations\n    4. dropout\n    \"\"\"\n\n    def __init__(\n        self,\n        input_size: int,\n        input_rank: int = 2,\n        output_size: int = 256,\n        use_bias: bool = True,\n        weights_initializer: str = \"xavier_uniform\",\n        bias_initializer: str = \"zeros\",\n        norm: str | None = None,\n        norm_params: dict | None = None,\n        activation: str = \"relu\",\n        dropout: float = 0,\n    ):\n        super().__init__()\n\n        self.layers = ModuleList()\n        self.input_size = input_size\n        self.output_size = output_size\n\n        fc = Linear(in_features=input_size, out_features=output_size, bias=use_bias)\n        self.layers.append(fc)\n\n        weights_initializer = initializer_registry[weights_initializer]\n        weights_initializer(fc.weight)\n\n        if use_bias:\n            bias_initializer = initializer_registry[bias_initializer]\n            bias_initializer(fc.bias)\n\n        if norm is not None:\n            norm_params = norm_params or {}\n            self.layers.append(create_norm_layer(norm, input_rank, output_size, **norm_params))\n\n        # Dict for activation objects in pytorch?\n        self.layers.append(activations[activation]())\n\n        if dropout > 0:\n            self.layers.append(Dropout(dropout))\n\n    def forward(self, inputs, mask=None):\n        hidden = inputs\n        for layer in self.layers:\n            hidden = layer(hidden)\n\n        return hidden\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.output_size])\n\n\nclass FCStack(LudwigModule):\n    \"\"\"A stack of FCLayers.\n\n    The specification of each FCLayer is specified by the `layers` dictionary parameter, whose keys correspond with an\n    FCLayer's constructor arguments, i.e.\n\n    [\n        {\"input_size\": 2, \"output_size\": 4},\n        {\"output_size\": 4, \"use_bias\": False},\n    ]\n\n    `default_*` parameters dictate default values to use for each FCLayer, if not specified by `layers`. If `layers` is\n    `None`, then a stack of size `num_layers` of `FCLayer`s configured with all of the `default_*` parameters is used.\n\n    If `layers` is None and `num_layers` is 0, then there are no fully connected layers and this module serves as a\n    trivial passthrough.\n    \"\"\"\n\n    def __init__(\n        self,\n        first_layer_input_size: int,\n        layers: list[dict] | None = None,\n        num_layers: int = 1,\n        default_input_rank: int = 2,\n        default_output_size: int = 256,\n        default_use_bias: bool = True,\n        default_weights_initializer: str = \"xavier_uniform\",\n        default_bias_initializer: str = \"zeros\",\n        default_norm: str | None = None,\n        default_norm_params: dict | None = None,\n        default_activation: str = \"relu\",\n        default_dropout: float = 0,\n        residual: bool = False,\n        **kwargs,\n    ):\n        super().__init__()\n        self.input_size = first_layer_input_size\n\n        self.norm_layer = None\n        if default_norm is not None:\n            norm_params = default_norm_params or {}\n            self.norm_layer = create_norm_layer(default_norm, default_input_rank, self.input_size, **norm_params)\n\n        self.dropout = None\n        if default_dropout > 0:\n            self.dropout = torch.nn.Dropout(default_dropout)\n\n        if layers is None:\n            self.layers = []\n            for i in range(num_layers):\n                self.layers.append({})\n        else:\n            # deep copy the layer definitions so that we don't modify the original\n            self.layers = deepcopy(layers)\n\n        if len(self.layers) > 0 and \"input_size\" not in self.layers[0]:\n            self.layers[0][\"input_size\"] = first_layer_input_size\n        for i, layer in enumerate(self.layers):\n            if i != 0:\n                layer[\"input_size\"] = self.layers[i - 1][\"output_size\"]\n            if \"input_rank\" not in layer:\n                layer[\"input_rank\"] = default_input_rank\n            if \"output_size\" not in layer:\n                layer[\"output_size\"] = default_output_size\n            if \"use_bias\" not in layer:\n                layer[\"use_bias\"] = default_use_bias\n            if \"weights_initializer\" not in layer:\n                layer[\"weights_initializer\"] = default_weights_initializer\n            if \"bias_initializer\" not in layer:\n                layer[\"bias_initializer\"] = default_bias_initializer\n            if \"norm\" not in layer:\n                layer[\"norm\"] = default_norm\n            if \"norm_params\" not in layer:\n                layer[\"norm_params\"] = default_norm_params\n            if \"activation\" not in layer:\n                layer[\"activation\"] = default_activation\n            if \"dropout\" not in layer:\n                layer[\"dropout\"] = default_dropout\n\n        self.stack = ModuleList()\n\n        for i, layer in enumerate(self.layers):\n            self.stack.append(\n                FCLayer(\n                    input_size=layer[\"input_size\"],\n                    input_rank=layer[\"input_rank\"],\n                    output_size=layer[\"output_size\"],\n                    use_bias=layer[\"use_bias\"],\n                    weights_initializer=layer[\"weights_initializer\"],\n                    bias_initializer=layer[\"bias_initializer\"],\n                    norm=layer[\"norm\"],\n                    norm_params=layer[\"norm_params\"],\n                    activation=layer[\"activation\"],\n                    dropout=layer[\"dropout\"],\n                )\n            )\n        self.residual = residual\n\n    def forward(self, inputs, mask=None):\n        hidden = inputs\n\n        if self.norm_layer is not None:\n            hidden = self.norm_layer(hidden)\n\n        if self.dropout is not None:\n            hidden = self.dropout(hidden)\n\n        prev_fc_layer_size = self.input_size\n        for layer in self.stack:\n            out = layer(hidden)\n            if self.residual and layer.output_size == prev_fc_layer_size:\n                hidden = hidden + out\n            else:\n                hidden = out\n            prev_fc_layer_size = layer.layers[0].out_features\n        return hidden\n\n    @property\n    def num_layers(self) -> int:\n        return len(self.layers)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        if len(self.stack) > 0:\n            return self.stack[-1].output_shape\n        return torch.Size([self.input_size])\n"
  },
  {
    "path": "ludwig/modules/initializer_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport torch\n\nfrom ludwig.constants import TYPE\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import initializer_registry\n\n\ndef _create_and_init(init_fn, init_kwargs, *args, **kwargs):\n    t = torch.empty(*args, **kwargs)\n    init_fn(t, **init_kwargs)\n    return t\n\n\ndef get_initializer(parameters):\n    if parameters is None:\n        return lambda *args, **kwargs: _create_and_init(initializer_registry[parameters], {}, *args, **kwargs)\n    elif isinstance(parameters, str):\n        initializer_fun = get_from_registry(parameters, initializer_registry)\n        return lambda *args, **kwargs: _create_and_init(initializer_fun, {}, *args, **kwargs)\n    elif isinstance(parameters, dict):\n        initializer_fun = get_from_registry(parameters[TYPE], initializer_registry)\n        init_kwargs = parameters.copy()\n        del init_kwargs[TYPE]\n        return lambda *args, **kwargs: _create_and_init(initializer_fun, init_kwargs, *args, **kwargs)\n    else:\n        raise ValueError(\n            f\"Initializers parameters should be either strings or dictionaries, \"\n            f\"but the provided parameters are a {type(parameters)}. \"\n            f\"Parameters values: {parameters}\"\n        )\n"
  },
  {
    "path": "ludwig/modules/loss_implementations/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/modules/loss_implementations/corn.py",
    "content": "# Source: https://github.com/Raschka-research-group/coral-pytorch/blob/main/coral_pytorch/losses.py\n# Sebastian Raschka 2020-2021\n# coral_pytorch\n# Author: Sebastian Raschka <sebastianraschka.com>\n#\n# License: MIT\n\nimport torch\nimport torch.nn.functional as F\n\n\ndef corn_loss(logits, y_train, num_classes):\n    \"\"\"Computes the CORN loss described in our forthcoming 'Deep Neural Networks for Rank Consistent Ordinal\n    Regression based on Conditional Probabilities' manuscript.\n\n    Parameters\n    ----------\n    logits : torch.tensor, shape=(num_examples, num_classes-1)\n        Outputs of the CORN layer.\n\n    y_train : torch.tensor, shape=(num_examples)\n        Torch tensor containing the class labels.\n\n    num_classes : int\n        Number of unique class labels (class labels should start at 0).\n\n    Returns\n    ----------\n        loss : torch.tensor\n        A torch.tensor containing a single loss value.\n\n    Examples\n    ----------\n    >>> # Consider 8 training examples\n    >>> _  = torch.manual_seed(123)\n    >>> X_train = torch.rand(8, 99)\n    >>> y_train = torch.tensor([0, 1, 2, 2, 2, 3, 4, 4])\n    >>> NUM_CLASSES = 5\n    >>> #\n    >>> #\n    >>> # def __init__(self):\n    >>> corn_net = torch.nn.Linear(99, NUM_CLASSES-1)\n    >>> #\n    >>> #\n    >>> # def forward(self, X_train):\n    >>> logits = corn_net(X_train)\n    >>> logits.shape\n    torch.Size([8, 4])\n    >>> corn_loss(logits, y_train, NUM_CLASSES)\n    tensor(0.7127, grad_fn=<DivBackward0>)\n    \"\"\"\n    sets = []\n    for i in range(num_classes - 1):\n        label_mask = y_train > i - 1\n        label_tensor = (y_train[label_mask] > i).to(torch.int64)\n        sets.append((label_mask, label_tensor))\n\n    num_examples = 0\n    losses = 0.0\n    for task_index, s in enumerate(sets):\n        train_examples = s[0]\n        train_labels = s[1]\n\n        if len(train_labels) < 1:\n            continue\n\n        num_examples += len(train_labels)\n        pred = logits[train_examples, task_index]\n\n        loss = -torch.sum(F.logsigmoid(pred) * train_labels + (F.logsigmoid(pred) - pred) * (1 - train_labels))\n        losses += loss\n\n    return losses / num_examples\n"
  },
  {
    "path": "ludwig/modules/loss_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport torch\nfrom torch import nn, Tensor\nfrom torch.nn import HuberLoss as _HuberLoss\nfrom torch.nn import L1Loss\nfrom torch.nn import MSELoss as _MSELoss\nfrom torchmetrics.functional import mean_absolute_percentage_error\n\nimport ludwig.utils.loss_utils as utils\nfrom ludwig.constants import LOGITS\nfrom ludwig.modules.loss_implementations.corn import corn_loss\nfrom ludwig.schema.features.loss.loss import (\n    BaseLossConfig,\n    BWCEWLossConfig,\n    CORNLossConfig,\n    HuberLossConfig,\n    MAELossConfig,\n    MAPELossConfig,\n    MSELossConfig,\n    NextTokenSoftmaxCrossEntropyLossConfig,\n    RMSELossConfig,\n    RMSPELossConfig,\n    SequenceSoftmaxCrossEntropyLossConfig,\n    SigmoidCrossEntropyLossConfig,\n    SoftmaxCrossEntropyLossConfig,\n)\nfrom ludwig.utils import strings_utils\nfrom ludwig.utils.registry import Registry\n\n# used for Laplace smoothing for candidate samplers\nEPSILON = 1.0e-10\n\nloss_impl_registry = Registry[type[nn.Module]]()\n\n\ndef register_loss(config_cls: type[BaseLossConfig]):\n    def wrap(cls: type[nn.Module]):\n        loss_impl_registry[config_cls] = cls\n        return cls\n\n    return wrap\n\n\ndef create_loss(config: BaseLossConfig) -> nn.Module:\n    return loss_impl_registry[type(config)](config)\n\n\nclass LogitsInputsMixin:\n    @classmethod\n    def get_loss_inputs(cls):\n        \"\"\"Maps loss to the desired predicted input type.\"\"\"\n        return LOGITS\n\n\n@register_loss(MSELossConfig)\nclass MSELoss(_MSELoss, LogitsInputsMixin):\n    \"\"\"Mean squared error.\"\"\"\n\n    def __init__(self, config: MSELossConfig):\n        super().__init__()\n\n\n@register_loss(MAELossConfig)\nclass MAELoss(L1Loss, LogitsInputsMixin):\n    \"\"\"Mean absolute error.\"\"\"\n\n    def __init__(self, config: MAELossConfig):\n        super().__init__()\n\n\n@register_loss(MAPELossConfig)\nclass MAPELoss(nn.Module, LogitsInputsMixin):\n    \"\"\"Mean absolute error.\"\"\"\n\n    def __init__(self, config: MAPELossConfig):\n        super().__init__()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        return mean_absolute_percentage_error(preds, target)\n\n\n@register_loss(RMSELossConfig)\nclass RMSELoss(nn.Module, LogitsInputsMixin):\n    \"\"\"Root mean square error.\"\"\"\n\n    def __init__(self, config: RMSELossConfig):\n        super().__init__()\n        self.mse = nn.MSELoss()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        return torch.sqrt(self.mse(preds, target))\n\n\n@register_loss(RMSPELossConfig)\nclass RMSPELoss(nn.Module, LogitsInputsMixin):\n    \"\"\"Root mean square percentage error.\"\"\"\n\n    def __init__(self, config: RMSPELossConfig):\n        super().__init__()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        loss = utils.rmspe_loss(target, preds)\n        return loss\n\n\n@register_loss(BWCEWLossConfig)\nclass BWCEWLoss(nn.Module, LogitsInputsMixin):\n    \"\"\"Binary weighted cross entropy loss.\"\"\"\n\n    def __init__(self, config: BWCEWLossConfig):\n        super().__init__()\n        if config.positive_class_weight:\n            self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor([config.positive_class_weight]))\n        else:\n            self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=config.positive_class_weight)\n        self.robust_lambda = config.robust_lambda\n        self.confidence_penalty = config.confidence_penalty\n\n    def forward(self, preds: torch.Tensor, target: torch.Tensor):\n        train_loss = self.loss_fn(preds, target.float())\n        # robust lambda\n        if self.robust_lambda > 0:\n            train_loss = (1 - self.robust_lambda) * train_loss + self.robust_lambda / 2\n\n        train_mean_loss = torch.mean(train_loss)\n\n        # confidence penalty\n        if self.confidence_penalty > 0:\n            probabilities = torch.sigmoid(preds)\n            mean_penalty = utils.mean_confidence_penalty(probabilities, 2)\n            train_mean_loss += self.confidence_penalty * mean_penalty\n\n        return train_mean_loss\n\n\n@register_loss(SoftmaxCrossEntropyLossConfig)\nclass SoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin):\n    def __init__(self, config: SoftmaxCrossEntropyLossConfig):\n        \"\"\"\n        Params:\n            class_weights: List or 1D tensor of length equal to number of classes.\n        \"\"\"\n        super().__init__()\n        if config.class_weights:\n            self.loss_fn = nn.CrossEntropyLoss(weight=torch.Tensor(config.class_weights))\n        else:\n            self.loss_fn = nn.CrossEntropyLoss()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        \"\"\"\n        Params:\n            preds: Tensor of shape [batch x num_classes]\n                          or shape [batch x num_classes x H x W]\n            target: Tensor of shape [batch], where each element is integral\n                between 0 and num_classes.\n                           or shape [batch x H x W], where each element is integral\n                between 0 and num_classes.\n        \"\"\"\n        if len(target.shape) == 1 or len(target.shape) == 3:\n            # Assumes we are providing the target as a single class, rather than a distribution\n            # The target shape can be a 3D tensor [batch x H x W], for image segmentation\n            target = target.long()\n        return self.loss_fn(preds, target)\n\n\n@register_loss(SequenceSoftmaxCrossEntropyLossConfig)\nclass SequenceSoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin):\n    def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig):\n        \"\"\"\n        Params:\n            class_weights: List or 1D tensor of length equal to number of classes.\n        \"\"\"\n        super().__init__()\n        if config.class_weights:\n            self.loss_fn = nn.CrossEntropyLoss(\n                weight=torch.Tensor(config.class_weights), ignore_index=strings_utils.SpecialSymbol.PADDING.value\n            )\n        else:\n            self.loss_fn = nn.CrossEntropyLoss(ignore_index=strings_utils.SpecialSymbol.PADDING.value)\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        \"\"\"\n        Params:\n            preds: Tensor of shape [batch x sequence_length x vocab_size]\n            target: Tensor of shape [batch x sequence_length], where each element is integral between 0 and vocab_size.\n        \"\"\"\n        target = target.long()\n        return self.loss_fn(preds[1:].view(-1, preds.size(-1)), target[1:].view(-1))\n\n\n@register_loss(NextTokenSoftmaxCrossEntropyLossConfig)\nclass NextTokenSoftmaxCrossEntropyLoss(nn.Module, LogitsInputsMixin):\n    def __init__(self, config: NextTokenSoftmaxCrossEntropyLossConfig):\n        super().__init__()\n        self.loss_fn = nn.CrossEntropyLoss()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        \"\"\"\n        Params:\n            preds: Tensor of shape [batch x sequence_length x vocab_size]\n            target: Tensor of shape [batch x sequence_length], where each element is integral between 0 and vocab_size.\n\n        Reference implementation:\n        https://github.com/huggingface/transformers/blob/v4.29.1/src/transformers/models/bert/modeling_bert.py#LL1253C1-L1260C1 # noqa\n        \"\"\"\n        target = target.long()\n        _, _, vocab_size = preds.shape\n        # logits for all tensors except n+1 since each logit tensor at position i represents the log probabilities for\n        # the next token i+1 if we were to do argmax on the logits ensor at position i.\n        shifted_predictions = preds[:, :-1, :]\n        # Shift by 1 since the logits at position 0 in predictions represent the log likelihood of target token 1\n        shifted_targets = target[:, 1:]\n        return self.loss_fn(shifted_predictions.reshape(-1, vocab_size), shifted_targets.reshape(-1))\n\n\n@register_loss(SigmoidCrossEntropyLossConfig)\nclass SigmoidCrossEntropyLoss(nn.Module, LogitsInputsMixin):\n    def __init__(self, config: SigmoidCrossEntropyLossConfig):\n        \"\"\"\n        Params:\n            class_weights: List or 1D tensor of length equal to number of classes.\n        \"\"\"\n        super().__init__()\n        if config.class_weights:\n            self.loss_fn = nn.BCEWithLogitsLoss(pos_weight=torch.Tensor(config.class_weights))\n        else:\n            self.loss_fn = nn.BCEWithLogitsLoss()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        if preds.ndim != 2:\n            raise RuntimeError(\"SigmoidCrossEntropyLoss currently only supported for 2D tensors.\")\n\n        return self.loss_fn(preds.type(torch.float32), target.type(torch.float32))\n\n\n@register_loss(HuberLossConfig)\nclass HuberLoss(_HuberLoss, LogitsInputsMixin):\n    \"\"\"Huber loss.\"\"\"\n\n    def __init__(self, config: HuberLossConfig):\n        super().__init__(delta=config.delta)\n\n\n@register_loss(CORNLossConfig)\nclass CORNLoss(nn.Module, LogitsInputsMixin):\n    \"\"\"CORN loss.\"\"\"\n\n    def __init__(self, config: CORNLossConfig):\n        super().__init__()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        num_classes = preds.shape[1]\n        return corn_loss(preds, target, num_classes=num_classes)\n"
  },
  {
    "path": "ludwig/modules/lr_scheduler.py",
    "content": "import logging\nimport math\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom torch.optim import Optimizer\nfrom torch.optim.lr_scheduler import CosineAnnealingWarmRestarts, LambdaLR, ReduceLROnPlateau, SequentialLR\n\nfrom ludwig.constants import MINIMIZE, TRAINING, VALIDATION\nfrom ludwig.modules.metric_registry import get_metric_objective\nfrom ludwig.schema.lr_scheduler import LRSchedulerConfig\nfrom ludwig.utils.metric_utils import TrainerMetric\nfrom ludwig.utils.trainer_utils import ProgressTracker\n\nlogger = logging.getLogger(__name__)\n\n\nclass ReduceLROnPLateauCappedDecreases(ReduceLROnPlateau):\n    def __init__(self, optimizer: Optimizer, mode: str, reduce_limit: int, factor: float, patience: int):\n        super().__init__(optimizer, mode=mode, factor=factor, patience=patience)\n        self.reduce_limit = reduce_limit\n        self._num_reduce_lr = 0\n\n    def step(self, metrics):\n        if self._num_reduce_lr >= self.reduce_limit:\n            # Already reduced the LR as many times as we will allow\n            return\n\n        return super().step(metrics)\n\n    @property\n    def num_reduce_lr(self) -> int:\n        return self._num_reduce_lr\n\n    def _reduce_lr(self, epoch=None):\n        \"\"\"Overrides the base ReduceLROnPlateau implementation.\"\"\"\n        self._num_reduce_lr += 1\n        self.apply_lr()\n\n    def apply_lr(self):\n        if self._num_reduce_lr == 0:\n            return\n\n        for i, param_group in enumerate(self.optimizer.param_groups):\n            old_lr = float(param_group[\"lr\"])\n            new_lr = max(old_lr * math.pow(self.factor, self._num_reduce_lr), self.min_lrs[i])\n            if old_lr - new_lr > self.eps:\n                param_group[\"lr\"] = new_lr\n                logger.info(f\"From ReduceLROnPLateauCappedDecreases, reducing learning rate to {new_lr}\")\n\n\nclass LRScheduler:\n    def __init__(\n        self,\n        config: LRSchedulerConfig,\n        optimizer: Optimizer,\n        steps_per_checkpoint: int,\n        total_steps: int,\n    ):\n        self.config = config\n        self.optimizer = optimizer\n\n        # Scheduler updated each training step\n        self.step_info = StepInfo(steps_per_checkpoint, total_steps, self.config)\n        self._train_scheduler = get_schedule_with_warmup_and_decay(self.config, self.optimizer, self.step_info)\n\n        # Scheduler updated each eval step\n        self._eval_scheduler = None\n        if self.config.reduce_on_plateau > 0:\n            mode = \"min\" if get_metric_objective(self.config.reduce_eval_metric) == MINIMIZE else \"max\"\n            self._eval_scheduler = ReduceLROnPLateauCappedDecreases(\n                optimizer=self.optimizer,\n                mode=mode,\n                reduce_limit=self.config.reduce_on_plateau,\n                factor=self.config.reduce_on_plateau_rate,\n                patience=self.config.reduce_on_plateau_patience,\n            )\n\n    def step(self):\n        \"\"\"Called every step of training.\"\"\"\n        self._train_scheduler.step()\n\n        if self._eval_scheduler is not None:\n            # We apply this scheduler every eval step, not train step, so we don't want to call step() here.\n            # However, we need to re-apply the LR reduction to the LR from the train scheduler, as the first scheduler\n            # resets the LR back to the base LR.\n            self._eval_scheduler.apply_lr()\n\n    def eval_step(self, progress_tracker: ProgressTracker, validation_field: str):\n        \"\"\"Called every checkpoint evaluation step.\"\"\"\n        if self._eval_scheduler is None:\n            # No reduce on plateau\n            return\n\n        if self.config.reduce_eval_split == TRAINING:\n            split_metrics = progress_tracker.train_metrics\n        elif self.config.reduce_eval_split == VALIDATION:\n            split_metrics = progress_tracker.validation_metrics\n        else:  # if self.config.reduce_eval_split == TEST:\n            split_metrics = progress_tracker.test_metrics\n\n        validation_metric = self.config.reduce_eval_metric\n        last_metric: TrainerMetric = split_metrics[validation_field][validation_metric][-1]\n        last_metric_value = last_metric[-1]\n\n        prev_num_reductions = self._eval_scheduler.num_reduce_lr\n        self._eval_scheduler.step(last_metric_value)\n\n        num_reductions = self._eval_scheduler.num_reduce_lr\n        if num_reductions > prev_num_reductions:\n            # LR reduction -> update progress tracker\n            progress_tracker.last_learning_rate_reduction_steps = progress_tracker.steps\n            progress_tracker.last_learning_rate_reduction = 0\n            progress_tracker.num_reductions_learning_rate += 1\n        else:\n            progress_tracker.last_learning_rate_reduction = (\n                progress_tracker.steps - progress_tracker.last_learning_rate_reduction_steps\n            )\n\n    def state_dict(self) -> dict[str, Any]:\n        return {\n            \"train_scheduler_state\": self._train_scheduler.state_dict(),\n            \"eval_scheduler_state\": self._eval_scheduler.state_dict() if self._eval_scheduler is not None else {},\n        }\n\n    def load_state_dict(self, d: dict[str, Any]):\n        self._train_scheduler.load_state_dict(d[\"train_scheduler_state\"])\n        if self._eval_scheduler is not None:\n            self._eval_scheduler.load_state_dict(d[\"eval_scheduler_state\"])\n\n\nclass StepInfo:\n    \"\"\"Stores the steps_per_checkpoint and total_steps used during the current training run.\n\n    This class is needed by LambdaLR to allow us to update the steps on training init without resetting the entire\n    LRScheduler from scratch (which would result in resetting the optimizer learning rate).\n    \"\"\"\n\n    def __init__(self, steps_per_checkpoint: int, total_steps: int, config: LRSchedulerConfig):\n        self.config = config\n        self.steps_per_checkpoint = steps_per_checkpoint\n        self.num_training_steps = total_steps\n\n        if self.config.warmup_fraction > 0 and self.config.warmup_evaluations > 0:\n            logger.info(\n                \"Both `learning_rate_scheduler.warmup_fraction` and `learning_rate_scheduler.warmup_evaluations` \"\n                \"provided. The larger of the two (as a function of the total training steps) will be used.\"\n            )\n\n        num_warmup_steps = 0\n        if self.config.warmup_fraction > 0:\n            num_warmup_steps = max(self.config.warmup_fraction * self.num_training_steps, num_warmup_steps)\n        if self.config.warmup_evaluations > 0:\n            num_warmup_steps = max(self.config.warmup_evaluations * self.steps_per_checkpoint, num_warmup_steps)\n        self.num_warmup_steps = num_warmup_steps\n\n\ndef get_schedule_with_warmup_and_decay(\n    config: LRSchedulerConfig,\n    optimizer: Optimizer,\n    step_info: StepInfo,\n) -> LambdaLR:\n    \"\"\"Creates a learning rate scheduler that updates each training step.\"\"\"\n    schedulers = []\n\n    # Warmup scheduler.\n    if step_info.num_warmup_steps > 0:\n        warmup_scheduler = LambdaLR(\n            optimizer,\n            lambda current_step: float(current_step) / float(max(1, step_info.num_warmup_steps)),\n        )\n        schedulers.append(warmup_scheduler)\n\n    # Decay scheduler.\n    decay = config.decay\n    decay_scheduler = decay_registry[decay](config, optimizer, step_info)\n    schedulers.append(decay_scheduler)\n\n    if len(schedulers) == 1:\n        # Only one scheduler, so no need to wrap in a SequentialLR.\n        return schedulers[0]\n\n    # Return a SequentialLR that applies the warmup and decay schedulers in order\n    # with the warmup scheduler only applied for the first num_warmup_steps steps.\n    return SequentialLR(optimizer, schedulers=schedulers, milestones=[step_info.num_warmup_steps])\n\n\ndef no_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig):\n    return 1.0\n\n\ndef linear_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig):\n    return max(\n        0.0,\n        float(num_training_steps - num_warmup_steps - current_step)\n        / float(max(1, num_training_steps - num_warmup_steps)),\n    )\n\n\ndef exponential_decay(current_step: int, num_training_steps: int, num_warmup_steps: int, config: LRSchedulerConfig):\n    decay_rate = float(config.decay_rate)\n    decay_steps = float(config.decay_steps)\n    step = float(current_step)\n    exponent = 1 + step / decay_steps\n    if config.staircase:\n        exponent = math.ceil(exponent)\n    return math.pow(decay_rate, exponent)\n\n\ndef wrap_decay_fn(decay_fn: Callable) -> Callable:\n    def init_fn(config: LRSchedulerConfig, optimizer: Optimizer, step_info: StepInfo) -> LambdaLR:\n        return LambdaLR(\n            optimizer,\n            lambda current_step: decay_fn(\n                current_step, step_info.num_training_steps, step_info.num_warmup_steps, config\n            ),\n        )\n\n    return init_fn\n\n\ndef init_cosine_decay(\n    config: LRSchedulerConfig,\n    optimizer: Optimizer,\n    step_info: StepInfo,\n) -> CosineAnnealingWarmRestarts:\n    t_0 = config.t_0\n    if not t_0:\n        t_0 = step_info.steps_per_checkpoint\n    if not t_0:\n        # A scheduler may be initialized with dummy values like at the start of training.\n        # Ensure that t_0 != 0, as this causes an error to be raised.\n        t_0 = 1\n\n    return CosineAnnealingWarmRestarts(\n        optimizer,\n        T_0=t_0,\n        T_mult=config.t_mult or 1,\n        eta_min=config.eta_min or 0,\n    )\n\n\ndecay_registry = {\n    None: wrap_decay_fn(no_decay),\n    \"linear\": wrap_decay_fn(linear_decay),\n    \"exponential\": wrap_decay_fn(exponential_decay),\n    \"cosine\": init_cosine_decay,\n}\n"
  },
  {
    "path": "ludwig/modules/metric_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport sys\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Callable, Generator\nfrom contextlib import contextmanager\nfrom typing import Any\n\nimport torch\nfrom torch import Tensor, tensor\nfrom torchmetrics import MeanAbsoluteError, MeanAbsolutePercentageError\nfrom torchmetrics import MeanMetric as _MeanMetric\nfrom torchmetrics import MeanSquaredError, Metric\nfrom torchmetrics.classification import (\n    BinaryAccuracy,\n    BinaryAUROC,\n    BinaryPrecision,\n    BinaryRecall,\n    BinarySpecificity,\n    MulticlassAccuracy,\n    MulticlassAUROC,\n)\nfrom torchmetrics.functional.regression.r2 import _r2_score_compute, _r2_score_update\nfrom torchmetrics.metric import jit_distributed_available\nfrom torchmetrics.text import BLEUScore, CharErrorRate, WordErrorRate\nfrom torchmetrics.text.perplexity import Perplexity\nfrom torchmetrics.text.rouge import ROUGEScore\n\nfrom ludwig.constants import (  # RESPONSE,\n    ACCURACY,\n    ACCURACY_MICRO,\n    BINARY,\n    BINARY_WEIGHTED_CROSS_ENTROPY,\n    CATEGORY,\n    CATEGORY_DISTRIBUTION,\n    CORN,\n    HITS_AT_K,\n    HUBER,\n    IGNORE_INDEX_TOKEN_ID,\n    IMAGE,\n    JACCARD,\n    LOGITS,\n    LOSS,\n    MAXIMIZE,\n    MEAN_ABSOLUTE_ERROR,\n    MEAN_ABSOLUTE_PERCENTAGE_ERROR,\n    MEAN_SQUARED_ERROR,\n    MINIMIZE,\n    NEXT_TOKEN_PERPLEXITY,\n    NUMBER,\n    PERPLEXITY,\n    PRECISION,\n    PREDICTIONS,\n    PROBABILITIES,\n    R2,\n    RECALL,\n    ROC_AUC,\n    ROOT_MEAN_SQUARED_ERROR,\n    ROOT_MEAN_SQUARED_PERCENTAGE_ERROR,\n    SEQUENCE,\n    SEQUENCE_ACCURACY,\n    SET,\n    SPECIFICITY,\n    TEXT,\n    TIMESERIES,\n    TOKEN_ACCURACY,\n    VECTOR,\n)\nfrom ludwig.distributed import get_current_dist_strategy\nfrom ludwig.modules.loss_modules import (\n    BWCEWLoss,\n    CORNLoss,\n    HuberLoss,\n    NextTokenSoftmaxCrossEntropyLoss,\n    SequenceSoftmaxCrossEntropyLoss,\n    SigmoidCrossEntropyLoss,\n    SoftmaxCrossEntropyLoss,\n)\nfrom ludwig.modules.metric_registry import get_metric_objective, get_metric_registry, register_metric\nfrom ludwig.schema.features.loss.loss import (\n    BWCEWLossConfig,\n    CORNLossConfig,\n    HuberLossConfig,\n    SequenceSoftmaxCrossEntropyLossConfig,\n    SigmoidCrossEntropyLossConfig,\n    SoftmaxCrossEntropyLossConfig,\n)\nfrom ludwig.utils.loss_utils import rmspe_loss\nfrom ludwig.utils.metric_utils import masked_correct_predictions\nfrom ludwig.utils.torch_utils import sequence_length_2D\n\nlogger = logging.getLogger(__name__)\n\n\nclass LudwigMetric(Metric, ABC):\n    @classmethod\n    def can_report(cls, feature: \"OutputFeature\") -> bool:  # noqa: F821\n        return True\n\n    @contextmanager\n    def sync_context(\n        self,\n        dist_sync_fn: Callable | None = None,\n        process_group: Any | None = None,\n        should_sync: bool = True,\n        should_unsync: bool = True,\n        distributed_available: Callable | None = jit_distributed_available,\n    ) -> Generator:\n        \"\"\"Override the behavior of this in the base class to support custom distributed strategies.\"\"\"\n        dist_strategy = get_current_dist_strategy()\n        self.sync(\n            dist_sync_fn=dist_strategy.gather_all_tensors_fn(),\n            process_group=process_group,\n            should_sync=should_sync,\n            distributed_available=dist_strategy.is_available,\n        )\n\n        yield\n\n        self.unsync(should_unsync=self._is_synced and should_unsync)\n\n\n@register_metric(ROOT_MEAN_SQUARED_ERROR, [NUMBER], MINIMIZE, PREDICTIONS)\nclass RMSEMetric(MeanSquaredError, LudwigMetric):\n    \"\"\"Root mean squared error metric.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__(squared=False)\n\n\n@register_metric(PRECISION, [BINARY], MAXIMIZE, PROBABILITIES)\nclass PrecisionMetric(BinaryPrecision, LudwigMetric):\n    \"\"\"Precision metric.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n@register_metric(RECALL, [BINARY], MAXIMIZE, PROBABILITIES)\nclass RecallMetric(BinaryRecall, LudwigMetric):\n    \"\"\"Recall metric.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n@register_metric(ROC_AUC, [BINARY], MAXIMIZE, PROBABILITIES)\nclass BinaryAUROCMetric(BinaryAUROC, LudwigMetric):\n    \"\"\"Area under the receiver operating curve.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        super().update(preds, target.type(torch.int8))\n\n\n@register_metric(ROC_AUC, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PROBABILITIES)\nclass CategoryAUROCMetric(MulticlassAUROC, LudwigMetric):\n    \"\"\"Area under the receiver operating curve.\"\"\"\n\n    def __init__(self, num_classes: int, **kwargs):\n        super().__init__(num_classes=num_classes)\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        if len(target.shape) > 1:\n            target = torch.argmax(target, dim=1)\n        super().update(preds, target)\n\n\n@register_metric(SPECIFICITY, [BINARY], MAXIMIZE, PROBABILITIES)\nclass SpecificityMetric(BinarySpecificity, LudwigMetric):\n    \"\"\"Specificity metric.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\nclass MeanMetric(LudwigMetric):\n    \"\"\"Abstract class for computing mean of metrics.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n        self.avg = _MeanMetric()\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        self.avg.update(self.get_current_value(preds, target))\n\n    def compute(self) -> Tensor:\n        return self.avg.compute()\n\n    def reset(self):\n        super().reset()\n        self.avg.reset()\n\n    @abstractmethod\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        raise NotImplementedError()\n\n\n@register_metric(ROOT_MEAN_SQUARED_PERCENTAGE_ERROR, [NUMBER], MINIMIZE, PREDICTIONS)\nclass RMSPEMetric(MeanMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    \"\"\" Root mean squared percentage error metric. \"\"\"\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return rmspe_loss(target, preds)\n\n\n@register_metric(R2, [NUMBER, VECTOR, TIMESERIES], MAXIMIZE, PREDICTIONS)\nclass R2Score(LudwigMetric):\n    \"\"\"Custom R-squared metric implementation that modifies torchmetrics R-squared implementation to return Nan\n    when there is only sample. This is because R-squared is only defined for two or more samples.\n\n    Custom implementation uses code from torchmetrics v0.9.2's implementation of R2: https://github.com/Lightning-\n    AI/metrics/blob/master/src/torchmetrics/regression/r2.py\n    \"\"\"\n\n    def __init__(\n        self, num_outputs: int = 1, adjusted: int = 0, multioutput: str = \"uniform_average\", **kwargs: Any\n    ) -> None:\n        super().__init__(**kwargs)\n\n        self.num_outputs = num_outputs\n\n        if adjusted < 0 or not isinstance(adjusted, int):\n            raise ValueError(\"`adjusted` parameter should be an integer larger or equal to 0.\")\n        self.adjusted = adjusted\n\n        allowed_multioutput = (\"raw_values\", \"uniform_average\", \"variance_weighted\")\n        if multioutput not in allowed_multioutput:\n            raise ValueError(\n                f\"Invalid input to argument `multioutput`. Choose one of the following: {allowed_multioutput}\"\n            )\n        self.multioutput = multioutput\n\n        self.add_state(\"sum_squared_error\", default=torch.zeros(self.num_outputs), dist_reduce_fx=\"sum\")\n        self.add_state(\"sum_error\", default=torch.zeros(self.num_outputs), dist_reduce_fx=\"sum\")\n        self.add_state(\"residual\", default=torch.zeros(self.num_outputs), dist_reduce_fx=\"sum\")\n        self.add_state(\"total\", default=tensor(0), dist_reduce_fx=\"sum\")\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        \"\"\"Update state with predictions and targets.\n\n        Args:\n            preds: Predictions from model\n            target: Ground truth values\n        \"\"\"\n        sum_squared_error, sum_error, residual, n_obs = _r2_score_update(preds, target)\n\n        self.sum_squared_error += sum_squared_error\n        self.sum_error += sum_error\n        self.residual += residual\n        self.total += n_obs\n\n    def compute(self) -> Tensor:\n        \"\"\"Computes r2 score over the metric states.\"\"\"\n\n        # self.total maps to the number of observations in preds/target computed during update()\n        if self.total <= 1:\n            logger.warning(\n                \"\"\"R-squared (r2) is not defined for one sample. It needs at least two samples. Returning NaN.\"\"\"\n            )\n            return torch.tensor(float(\"nan\"))\n\n        return _r2_score_compute(\n            self.sum_squared_error, self.sum_error, self.residual, self.total, self.adjusted, self.multioutput\n        )\n\n\n@register_metric(LOSS, [], MINIMIZE, LOGITS)\nclass LossMetric(MeanMetric, ABC):\n    def __init__(self):\n        super().__init__()\n\n    @abstractmethod\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        raise NotImplementedError()\n\n    @classmethod\n    def can_report(cls, feature: \"OutputFeature\") -> bool:  # noqa: F821\n        return False\n\n\n@register_metric(BINARY_WEIGHTED_CROSS_ENTROPY, [BINARY], MINIMIZE, LOGITS)\nclass BWCEWLMetric(LossMetric):\n    \"\"\"Binary Weighted Cross Entropy Weighted Logits Score Metric.\"\"\"\n\n    def __init__(self, config: BWCEWLossConfig, **kwargs):\n        super().__init__()\n        self.loss_function = BWCEWLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return self.loss_function(preds, target)\n\n\n@register_metric(\"softmax_cross_entropy\", [CATEGORY, CATEGORY_DISTRIBUTION, IMAGE], MINIMIZE, LOGITS)\nclass SoftmaxCrossEntropyMetric(LossMetric):\n    def __init__(self, config: SoftmaxCrossEntropyLossConfig, **kwargs):\n        super().__init__()\n        self.softmax_cross_entropy_function = SoftmaxCrossEntropyLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor):\n        return self.softmax_cross_entropy_function(preds, target)\n\n\n@register_metric(\"sequence_softmax_cross_entropy\", [SEQUENCE, TEXT], MINIMIZE, LOGITS)\nclass SequenceSoftmaxCrossEntropyMetric(LossMetric):\n    def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig, **kwargs):\n        super().__init__()\n        self.sequence_softmax_cross_entropy_function = SequenceSoftmaxCrossEntropyLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor):\n        return self.sequence_softmax_cross_entropy_function(preds, target)\n\n\n@register_metric(\"next_token_softmax_cross_entropy\", [SEQUENCE, TEXT], MINIMIZE, LOGITS)\nclass NextTokenSoftmaxCrossEntropyMetric(LossMetric):\n    def __init__(self, config: SequenceSoftmaxCrossEntropyLossConfig, **kwargs):\n        super().__init__()\n        self.next_token_softmax_cross_entropy_function = NextTokenSoftmaxCrossEntropyLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor):\n        return self.next_token_softmax_cross_entropy_function(preds, target)\n\n\n@register_metric(\"sigmoid_cross_entropy\", [SET], MINIMIZE, LOGITS)\nclass SigmoidCrossEntropyMetric(LossMetric):\n    def __init__(self, config: SigmoidCrossEntropyLossConfig, **kwargs):\n        super().__init__()\n        self.sigmoid_cross_entropy_function = SigmoidCrossEntropyLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return self.sigmoid_cross_entropy_function(preds, target)\n\n\n@register_metric(TOKEN_ACCURACY, [SEQUENCE, TEXT], MAXIMIZE, PREDICTIONS)\nclass TokenAccuracyMetric(MeanMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        target = target.type(preds.dtype)\n        target_sequence_length = sequence_length_2D(target)\n        masked_correct_preds = masked_correct_predictions(target, preds, target_sequence_length)\n        return torch.mean(masked_correct_preds)\n\n\n@register_metric(SEQUENCE_ACCURACY, [SEQUENCE, TEXT], MAXIMIZE, PREDICTIONS)\nclass SequenceAccuracyMetric(MeanMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return torch.sum(torch.all(preds == target, dim=1)) / target.size()[0]\n\n\n@register_metric(PERPLEXITY, [SEQUENCE, TEXT], MINIMIZE, PROBABILITIES)\nclass PerplexityMetric(Perplexity, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__(ignore_index=IGNORE_INDEX_TOKEN_ID)\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        super().update(preds, target.type(torch.int64))\n\n\n@register_metric(NEXT_TOKEN_PERPLEXITY, [SEQUENCE, TEXT], MINIMIZE, PROBABILITIES)\nclass NextTokenPerplexityMetric(MeanMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n        self.next_token_softmax_cross_entropy_function = NextTokenSoftmaxCrossEntropyLoss({})\n\n    def get_current_value(self, preds: Tensor, target: Tensor):\n        # Perplexity can be represented as the exponential of the cross-entropy loss.\n        # https://towardsdatascience.com/perplexity-in-language-models-87a196019a94\n        # We can't use torchmetrics perplexity because it calculates normal cross-entropy\n        # loss as opposed to shifted cross entropy loss.\n        shifted_loss = self.next_token_softmax_cross_entropy_function(preds, target)\n        return torch.exp(shifted_loss)\n\n\n# @register_metric(\"bleu\", [TEXT], MAXIMIZE, RESPONSE)\n# https://github.com/ludwig-ai/ludwig/issues/3953\nclass BLEUScoreMetric(BLEUScore, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n# @register_metric(\"rouge\", [TEXT], MAXIMIZE, RESPONSE)\n# https://github.com/ludwig-ai/ludwig/issues/3953\nclass ROUGEScoreMetric(ROUGEScore, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n# @register_metric(\"word_error_rate\", [TEXT], MINIMIZE, RESPONSE)\n# https://github.com/ludwig-ai/ludwig/issues/3953\nclass WordErrorRateMetric(WordErrorRate, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n# @register_metric(\"char_error_rate\", [TEXT], MINIMIZE, RESPONSE)\n# https://github.com/ludwig-ai/ludwig/issues/3953\nclass CharErrorRateMetric(CharErrorRate, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n@register_metric(ACCURACY, [BINARY], MAXIMIZE, PREDICTIONS)\nclass Accuracy(BinaryAccuracy, LudwigMetric):\n    \"\"\"R-squared metric.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n\n@register_metric(ACCURACY, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PREDICTIONS)\nclass CategoryAccuracy(MulticlassAccuracy, LudwigMetric):\n    def __init__(self, num_classes: int, **kwargs):\n        super().__init__(num_classes=num_classes)\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        if len(target.shape) > 1:\n            target = torch.argmax(target, dim=1)\n        super().update(preds, target.type(torch.long))\n\n\n@register_metric(ACCURACY_MICRO, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, PREDICTIONS)\nclass CategoryAccuracyMicro(MulticlassAccuracy, LudwigMetric):\n    def __init__(self, num_classes: int, **kwargs):\n        super().__init__(num_classes=num_classes, average=\"micro\")\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        if len(target.shape) > 1:\n            target = torch.argmax(target, dim=1)\n        super().update(preds, target.type(torch.long))\n\n\n@register_metric(HITS_AT_K, [CATEGORY, CATEGORY_DISTRIBUTION], MAXIMIZE, LOGITS)\nclass HitsAtKMetric(MulticlassAccuracy, LudwigMetric):\n    def __init__(self, num_classes: int, top_k: int, **kwargs):\n        super().__init__(num_classes=num_classes, top_k=top_k, **kwargs)\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        if len(target.shape) > 1:\n            target = torch.argmax(target, dim=1)\n        super().update(preds, target.type(torch.long))\n\n    @classmethod\n    def can_report(cls, feature: \"OutputFeature\") -> bool:  # noqa: F821\n        return feature.num_classes > feature.top_k\n\n\n@register_metric(MEAN_ABSOLUTE_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS)\nclass MAEMetric(MeanAbsoluteError, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        super().update(preds.detach(), target)\n\n\n@register_metric(MEAN_SQUARED_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS)\nclass MSEMetric(MeanSquaredError, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        super().update(preds, target)\n\n\n@register_metric(MEAN_ABSOLUTE_PERCENTAGE_ERROR, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS)\nclass MAPEMetric(MeanAbsolutePercentageError, LudwigMetric):\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def update(self, preds: Tensor, target: Tensor) -> None:\n        super().update(preds, target)\n\n\n@register_metric(JACCARD, [SET], MAXIMIZE, PROBABILITIES)\nclass JaccardMetric(MeanMetric):\n    def __init__(self, threshold: float = 0.5, **kwargs):\n        super().__init__()\n        self.threshold = threshold\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        # notation: b is batch size and nc is number of unique elements in the set\n        # preds: shape [b, nc] probabilities for each class\n        # target: shape [b, nc] bit-mapped set representation\n        preds = torch.greater_equal(preds, self.threshold)  # now bit-mapped set\n        target = target.type(torch.bool)\n\n        intersection = torch.sum(torch.logical_and(target, preds).type(torch.float32), dim=-1)\n        union = torch.sum(torch.logical_or(target, preds).type(torch.float32), dim=-1)\n\n        return intersection / union  # shape [b]\n\n\n@register_metric(HUBER, [NUMBER, VECTOR, TIMESERIES], MINIMIZE, PREDICTIONS)\nclass HuberMetric(LossMetric):\n    def __init__(\n        self,\n        config: HuberLossConfig,\n        **kwargs,\n    ):\n        super().__init__()\n        self.loss_function = HuberLoss(config=config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return self.loss_function(preds, target)\n\n\n@register_metric(CORN, [CATEGORY], MINIMIZE, PREDICTIONS)\nclass CORNMetric(LossMetric):\n    def __init__(\n        self,\n        config: CORNLossConfig,\n        **kwargs,\n    ):\n        super().__init__()\n        self.loss_function = CORNLoss(config=config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor) -> Tensor:\n        return self.loss_function(preds, target)\n\n\ndef get_metric_cls(metric_name: str) -> type[LudwigMetric]:\n    return get_metric_registry()[metric_name]\n\n\ndef get_improved_fn(metric: str) -> Callable:\n    if get_metric_objective(metric) == MINIMIZE:\n        return lambda x, y: x < y\n    else:\n        return lambda x, y: x > y\n\n\ndef get_initial_validation_value(metric: str) -> float:\n    # Use finite floats instead of inf/-inf so that training_progress.json\n    # is valid JSON (RFC 8259). sys.float_info.max (~1.8e308) is larger than\n    # any real metric value, so comparison semantics are identical.\n    if get_metric_objective(metric) == MINIMIZE:\n        return sys.float_info.max\n    else:\n        return -sys.float_info.max\n\n\ndef get_best_function(metric: str) -> Callable:\n    if get_metric_objective(metric) == MINIMIZE:\n        return min\n    else:\n        return max\n"
  },
  {
    "path": "ludwig/modules/metric_registry.py",
    "content": "from typing import Literal, TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import LOGITS, MAXIMIZE, MINIMIZE, PREDICTIONS, PROBABILITIES, RESPONSE\nfrom ludwig.utils.registry import Registry\n\nif TYPE_CHECKING:\n    from ludwig.modules.metric_modules import LudwigMetric\n\n\nmetric_feature_type_registry = Registry()\nmetric_registry = Registry()\nmetric_objective_registry = Registry()\nmetric_tensor_input_registry = Registry()\n\n\ndef register_metric(\n    name: str,\n    feature_types: str | list[str],\n    objective: Literal[MINIMIZE, MAXIMIZE],\n    output_feature_tensor_name: Literal[PREDICTIONS, PROBABILITIES, LOGITS],\n):\n    \"\"\"Registers a metric class.\n\n    Args:\n        name: The name of the metric. Used in metric reporting and in the config.\n        feature_types: The feature types that this metric can be used with.\n        objective: The objective of the metric. Either MINIMIZE or MAXIMIZE.\n        output_feature_tensor_name: Name of the tensor from output_feature::predictions() that should be used as input.\n            For example: PREDICTIONS would be used for accuracy metrics while LOGITS would be used for loss metrics.\n    \"\"\"\n    if isinstance(feature_types, str):\n        feature_types = [feature_types]\n\n    def wrap(cls):\n        for feature_type in feature_types:\n            feature_registry = metric_feature_type_registry.get(feature_type, {})\n            feature_registry[name] = cls\n            metric_feature_type_registry[feature_type] = feature_registry\n        metric_registry[name] = cls\n        metric_objective_registry[name] = objective\n        metric_tensor_input_registry[name] = output_feature_tensor_name\n        return cls\n\n    return wrap\n\n\ndef get_metric_classes(feature_type: str) -> dict[str, \"LudwigMetric\"]:\n    return metric_feature_type_registry[feature_type]\n\n\ndef get_metric_cls(feature_type: str, name: str) -> \"LudwigMetric\":\n    return metric_feature_type_registry[feature_type][name]\n\n\n@DeveloperAPI\ndef get_metric_feature_type_registry() -> Registry:\n    return metric_feature_type_registry\n\n\n@DeveloperAPI\ndef get_metric_registry() -> Registry:\n    return metric_registry\n\n\n@DeveloperAPI\ndef get_metric(metric_name: str) -> \"LudwigMetric\":  # noqa\n    return get_metric_registry()[metric_name]\n\n\n@DeveloperAPI\ndef get_metrics_for_type(feature_type: str) -> dict[str, \"LudwigMetric\"]:  # noqa\n    return get_metric_feature_type_registry()[feature_type]\n\n\n@DeveloperAPI\ndef get_metric_names_for_type(feature_type: str) -> list[str]:\n    return sorted(list(get_metric_feature_type_registry()[feature_type].keys()))\n\n\n@DeveloperAPI\ndef get_metric_objective(metric_name: str) -> Literal[MINIMIZE, MAXIMIZE]:\n    return metric_objective_registry[metric_name]\n\n\n@DeveloperAPI\ndef get_metric_tensor_input(metric_name: str) -> Literal[PREDICTIONS, PROBABILITIES, LOGITS, RESPONSE]:\n    return metric_tensor_input_registry[metric_name]\n"
  },
  {
    "path": "ludwig/modules/mlp_mixer_modules.py",
    "content": "# Copyright (c) 2021 Linux Foundation\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport torch\nimport torch.nn as nn\n\nfrom ludwig.utils.torch_utils import LudwigModule\n\n\nclass MLP(LudwigModule):\n    def __init__(\n        self,\n        in_features: int | tuple[int],\n        hidden_size: int,\n        out_features: int | tuple[int] = None,\n        dropout: float = 0.0,\n    ):\n        super().__init__()\n\n        out_features = out_features or in_features\n\n        self._input_shape = in_features\n        self._output_shape = out_features\n\n        self.linear1 = nn.Linear(in_features=in_features, out_features=hidden_size)\n        self.linear2 = nn.Linear(in_features=hidden_size, out_features=out_features)\n        self.dropout1 = nn.Dropout(dropout)\n        self.dropout2 = nn.Dropout(dropout)\n\n    def forward(self, inputs, **kwargs):\n        hidden = self.dropout1(nn.functional.gelu(self.linear1(inputs)))\n        return self.dropout2(self.linear2(hidden))\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self._input_shape])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self._output_shape])\n\n\nclass MixerBlock(LudwigModule):\n    def __init__(self, embed_size: int, n_patches: int, token_dim: int, channel_dim: int, dropout: float = 0.0):\n        super().__init__()\n        self._input_shape = (n_patches, embed_size)\n        self._output_shape = (n_patches, embed_size)\n\n        self.mlp1 = MLP(in_features=n_patches, hidden_size=token_dim, dropout=dropout)\n\n        self.mlp2 = MLP(in_features=embed_size, hidden_size=channel_dim, dropout=dropout)\n\n        self.layernorm1 = nn.LayerNorm(normalized_shape=embed_size)\n        self.layernorm2 = nn.LayerNorm(normalized_shape=embed_size)\n\n    def forward(self, inputs: torch.Tensor, **kwargs):\n        assert inputs.shape[1:] == self.input_shape\n\n        hidden = inputs\n        hidden = self.layernorm1(hidden).transpose(1, 2)\n        hidden = self.mlp1(hidden).transpose(1, 2)\n\n        mid = hidden + inputs\n\n        hidden = self.layernorm2(mid)\n        hidden = self.mlp2(hidden)\n\n        output = hidden + mid\n        assert output.shape[1:] == self.output_shape\n        return output\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size(self._output_shape)\n\n\nclass MLPMixer(LudwigModule):\n    \"\"\"MLPMixer.\n\n    Implements\n    MLP-Mixer: An all-MLP Architecture for Vision\n    https://arxiv.org/abs/2105.01601\n    \"\"\"\n\n    def __init__(\n        self,\n        img_height: int,\n        img_width: int,\n        in_channels: int,\n        patch_size: int = 16,\n        embed_size: int = 512,\n        token_size: int = 2048,\n        channel_dim: int = 256,\n        num_layers: int = 8,\n        dropout: float = 0.0,\n        avg_pool: bool = True,\n    ):\n        super().__init__()\n        assert (img_height % patch_size == 0) and (img_width % patch_size == 0)\n\n        self._input_shape = (in_channels, img_height, img_width)\n        n_patches = int(img_height * img_width / (patch_size**2))\n\n        self.patch_conv = nn.Conv2d(\n            in_channels=in_channels, out_channels=embed_size, kernel_size=patch_size, stride=patch_size\n        )\n\n        self.mixer_blocks = nn.ModuleList(\n            [\n                MixerBlock(\n                    embed_size=embed_size,\n                    n_patches=n_patches,\n                    token_dim=token_size,\n                    channel_dim=channel_dim,\n                    dropout=dropout,\n                )\n                for _ in range(num_layers)\n            ]\n        )\n\n        self.layer_norm = nn.LayerNorm(normalized_shape=embed_size)\n\n        self.avg_pool = avg_pool\n        if self.avg_pool:\n            self._output_shape = torch.Size((embed_size,))\n        else:\n            self._output_shape = torch.Size((n_patches, embed_size))\n\n    def forward(self, inputs: torch.Tensor) -> torch.Tensor:\n        assert inputs.shape[1:] == self.input_shape\n        hidden = self.patch_conv(inputs)\n        hidden = hidden.flatten(2).transpose(1, 2)\n\n        for mixer_block in self.mixer_blocks:\n            hidden = mixer_block(hidden)\n        hidden = self.layer_norm(hidden)\n\n        if self.avg_pool:\n            hidden = torch.mean(hidden, dim=1)\n\n        assert hidden.shape[1:] == self.output_shape\n\n        return hidden\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size(self._input_shape)\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self._output_shape\n"
  },
  {
    "path": "ludwig/modules/normalization_modules.py",
    "content": "import logging\n\nimport numpy as np\nimport torch\nfrom torch.nn import BatchNorm1d, BatchNorm2d, LayerNorm, Module\n\nfrom ludwig.utils.torch_utils import LudwigModule\n\nlogger = logging.getLogger(__name__)\n\n\n# implementation adapted from https://github.com/dreamquark-ai/tabnet\nclass GhostBatchNormalization(LudwigModule):\n    def __init__(\n        self, num_features: int, momentum: float = 0.05, epsilon: float = 1e-3, virtual_batch_size: int | None = 128\n    ):\n        super().__init__()\n        self.num_features = num_features\n        self.virtual_batch_size = virtual_batch_size\n        self.bn = torch.nn.BatchNorm1d(num_features, momentum=momentum, eps=epsilon)\n\n    def forward(self, inputs):\n        batch_size = inputs.shape[0]\n\n        if self.training and self.virtual_batch_size:\n            splits = inputs.chunk(int(np.ceil(batch_size / self.virtual_batch_size)), 0)\n\n            if batch_size % self.virtual_batch_size == 1:\n                # Skip batch normalization for the last chunk if it is size 1.\n                logger.warning(\n                    f\"Virtual batch size `{self.virtual_batch_size}` is not a factor of the batch size `{batch_size}`, \"\n                    \"resulting in a chunk of size 1. Skipping batch normalization for the last chunk of size 1.\"\n                )\n\n            if batch_size == 1:\n                logger.warning(\n                    \"Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization.\"\n                    \"Make sure to set `batch_size` to a value greater than 1.\"\n                )\n                # We temporarily set the batch_norm module to eval mode as we can't compute the running statistics\n                # when the batch size is 1.\n                self.bn.eval()\n                splits_with_bn = [self.bn(x) if x.shape[0] >= 1 else x for x in splits]\n                self.bn.train()\n            else:\n                splits_with_bn = [self.bn(x) if x.shape[0] > 1 else x for x in splits]\n\n            return torch.cat(splits_with_bn, 0)\n\n        if batch_size != 1 or not self.training:\n            return self.bn(inputs)\n        return inputs\n\n    @property\n    def moving_mean(self) -> torch.Tensor:\n        return self.bn.running_mean\n\n    @property\n    def moving_variance(self) -> torch.Tensor:\n        return self.bn.running_var\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.num_features])\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.num_features])\n\n\nclass BatchNorm1dOrIdentity(BatchNorm1d):\n    \"\"\"BatchNorm1d or Identity layer if the batch_size is 1.\n\n    Workaround for: https://github.com/pytorch/pytorch/issues/4534\n    \"\"\"\n\n    def forward(self, input: torch.Tensor) -> torch.Tensor:\n        if input.shape[0] == 1:\n            logger.warning(\n                \"Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization.\"\n                \"Make sure to set `batch_size` to a value greater than 1.\"\n            )\n            return input\n        return super().forward(input)\n\n\nclass BatchNorm2dOrIdentity(BatchNorm2d):\n    \"\"\"BatchNorm2d or Identity layer if the batch_size is 1.\n\n    Workaround for: https://github.com/pytorch/pytorch/issues/4534\n    \"\"\"\n\n    def forward(self, input: torch.Tensor) -> torch.Tensor:\n        if input.shape[0] == 1:\n            logger.warning(\n                \"Batch size is 1, but batch normalization requires batch size >= 2. Skipping batch normalization.\"\n                \"Make sure to set `batch_size` to a value greater than 1.\"\n            )\n            return input\n        return super().forward(input)\n\n\nnorm_registry = {\n    \"batch_1d\": BatchNorm1dOrIdentity,\n    \"batch_2d\": BatchNorm2dOrIdentity,\n    \"layer\": LayerNorm,\n    \"ghost\": GhostBatchNormalization,\n}\n\n\ndef create_norm_layer(norm: str, input_rank: int, num_features: int, **norm_params) -> Module:\n    if norm == \"batch\":\n        # We use a different batch norm depending on the input_rank.\n        # TODO(travis): consider moving this behind a general BatchNorm interface to avoid this kludge.\n        if input_rank not in {2, 3}:\n            ValueError(f\"`input_rank` parameter expected to be either 2 or 3, but found {input_rank}.\")\n        norm = f\"{norm}_{input_rank - 1}d\"\n\n    norm_cls = norm_registry.get(norm)\n    if norm_cls is None:\n        raise ValueError(\n            f\"Unsupported value for `norm` param: {norm}. Supported values are: {list(norm_registry.keys())}\"\n        )\n\n    return norm_cls(num_features, **norm_params)\n"
  },
  {
    "path": "ludwig/modules/optimization_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport dataclasses\nfrom typing import Optional, TYPE_CHECKING\n\nimport torch\n\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import LudwigModule\n\nif TYPE_CHECKING:\n    from ludwig.schema.optimizers import BaseOptimizerConfig, GradientClippingConfig\n\n\ndef create_clipper(gradient_clipping_config: Optional[\"GradientClippingConfig\"]):\n    from ludwig.schema.optimizers import GradientClippingConfig\n\n    \"\"\"Utility function that will convert a None-type gradient clipping config to the correct form.\"\"\"\n    if isinstance(gradient_clipping_config, GradientClippingConfig):\n        return gradient_clipping_config\n    # Return default config if provided value is None:\n    return GradientClippingConfig()\n\n\ndef get_optimizer_class_and_kwargs(\n    optimizer_config: \"BaseOptimizerConfig\", learning_rate: float\n) -> tuple[type[torch.optim.Optimizer], dict]:\n    \"\"\"Returns the optimizer class and kwargs for the optimizer.\n\n    :return: Tuple of optimizer class and kwargs for the optimizer.\n    \"\"\"\n    from ludwig.schema.optimizers import optimizer_registry\n\n    # Get the corresponding torch optimizer class for the given config:\n    optimizer_cls = get_from_registry(optimizer_config.type.lower(), optimizer_registry)[0]\n\n    # Create a dict of parameters to be passed to torch (i.e. everything except `type`):\n    if dataclasses.is_dataclass(optimizer_config):\n        config_dict = dataclasses.asdict(optimizer_config)\n    elif hasattr(optimizer_config, \"to_dict\"):\n        config_dict = optimizer_config.to_dict()\n    else:\n        config_dict = vars(optimizer_config)\n    cls_kwargs = {field: value for field, value in config_dict.items() if field != \"type\"}\n    cls_kwargs[\"lr\"] = learning_rate\n\n    return optimizer_cls, cls_kwargs\n\n\ndef create_optimizer(\n    model: LudwigModule,\n    optimizer_config: \"BaseOptimizerConfig\",\n    learning_rate: float,\n) -> torch.optim.Optimizer:\n    \"\"\"Returns a ready-to-use torch optimizer instance based on the given optimizer config.\n\n    :param model: Underlying Ludwig model\n    :param learning_rate: Initial learning rate for the optimizer\n    :param optimizer_config: Instance of `ludwig.modules.optimization_modules.BaseOptimizerConfig`.\n    :return: Initialized instance of a torch optimizer.\n    \"\"\"\n    # Make sure the optimizer is compatible with the available resources:\n    if (optimizer_config.is_paged or optimizer_config.is_8bit) and (\n        not torch.cuda.is_available() or torch.cuda.device_count() == 0\n    ):\n        raise ValueError(\n            \"Cannot use a paged or 8-bit optimizer on a non-GPU machine. \"\n            \"Please use a different optimizer or run on a machine with a GPU.\"\n        )\n\n    optimizer_cls, optimizer_kwargs = get_optimizer_class_and_kwargs(optimizer_config, learning_rate)\n    return optimizer_cls(model.parameters(), **optimizer_kwargs)\n"
  },
  {
    "path": "ludwig/modules/recurrent_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the 'License');\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\nfrom torch.nn import GRU, LSTM, RNN\n\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import LudwigModule\n\nlogger = logging.getLogger(__name__)\n\nrnn_layers_registry = {\n    \"rnn\": RNN,\n    \"gru\": GRU,\n    \"lstm\": LSTM,\n}\n\n\nclass RecurrentStack(LudwigModule):\n    def __init__(\n        self,\n        input_size: int = None,\n        hidden_size: int = 256,\n        cell_type: str = \"rnn\",\n        max_sequence_length: int | None = None,\n        num_layers: int = 1,\n        bidirectional: bool = False,\n        use_bias: bool = True,\n        dropout: float = 0.0,\n        **kwargs,\n    ):\n        super().__init__()\n        self.supports_masking = True\n        self.input_size = input_size  # api doc: H_in\n        self.hidden_size = hidden_size  # api doc: H_out\n        self.max_sequence_length = max_sequence_length  # api doc: L (sequence length)\n\n        rnn_layer_class = get_from_registry(cell_type, rnn_layers_registry)\n\n        rnn_params = {\"num_layers\": num_layers, \"bias\": use_bias, \"dropout\": dropout, \"bidirectional\": bidirectional}\n\n        # Delegate recurrent params to PyTorch's RNN/GRU/LSTM implementations.\n        self.layers = rnn_layer_class(input_size, hidden_size, batch_first=True, **rnn_params)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        if self.max_sequence_length:\n            return torch.Size([self.max_sequence_length, self.input_size])\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        hidden_size = self.hidden_size * (2 if self.layers.bidirectional else 1)\n        if self.max_sequence_length:\n            return torch.Size([self.max_sequence_length, hidden_size])\n        return torch.Size([hidden_size])\n\n    def forward(self, inputs: torch.Tensor, mask=None):\n        hidden, final_state = self.layers(inputs)\n\n        if isinstance(final_state, tuple):\n            # lstm cell type\n            final_state = final_state[0][-1], final_state[1][-1]\n        else:\n            # rnn or gru cell type\n            final_state = final_state[-1]\n\n        return hidden, final_state\n"
  },
  {
    "path": "ludwig/modules/reduction_modules.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\n\nimport torch\n\nfrom ludwig.modules.attention_modules import FeedForwardAttentionReducer\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.torch_utils import LudwigModule, sequence_length_3D\n\nlogger = logging.getLogger(__name__)\n\n\nclass SequenceReducer(LudwigModule):\n    \"\"\"Reduces the sequence dimension of an input tensor according to the specified reduce_mode.  Any additional\n    kwargs are passed on to the reduce mode's constructor.  If using reduce_mode==\"attention\", the input_size kwarg\n    must also be specified.\n\n    A sequence is a tensor of 2 or more dimensions, where the shape is [batch size x sequence length x ...].\n\n    :param reduce_mode: The reduction mode, one of {\"last\", \"sum\", \"mean\", \"max\", \"concat\", \"attention\", \"none\"}\n    :param max_sequence_length The maximum sequence length.  Only used for computation of shapes - inputs passed\n                               at runtime may have a smaller sequence length.\n    :param encoding_size The size of each sequence element/embedding vector, or None if input is a sequence of scalars.\n    \"\"\"\n\n    def __init__(self, reduce_mode: str = None, max_sequence_length: int = 256, encoding_size: int = None, **kwargs):\n        super().__init__()\n        # save as private variable for debugging\n        self._reduce_mode = reduce_mode\n        self._max_sequence_length = max_sequence_length\n        self._encoding_size = encoding_size\n        # If embedding size specified and mode is attention, use embedding size as attention module input size\n        # unless the input_size kwarg is provided.\n        if reduce_mode == \"attention\" and encoding_size and \"input_size\" not in kwargs:\n            kwargs[\"input_size\"] = encoding_size\n        # use registry to find required reduction function\n        self._reduce_obj = get_from_registry(reduce_mode, reduce_mode_registry)(**kwargs)\n\n    def forward(self, inputs, mask=None):\n        \"\"\"Forward pass of reducer.\n\n        :param inputs: A tensor of 2 or more dimensions, where the shape is [batch size x sequence length x ...].\n        :param mask: A mask tensor of 2 dimensions [batch size x sequence length].  Not yet implemented.\n\n        :return: The input after applying the reduction operation to sequence dimension.\n        \"\"\"\n        return self._reduce_obj(inputs, mask=mask)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        \"\"\"Returns size of the input tensor without the batch dimension.\"\"\"\n        if self._encoding_size is None:\n            return torch.Size([self._max_sequence_length])\n        else:\n            return torch.Size([self._max_sequence_length, self._encoding_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        \"\"\"Returns size of the output tensor without the batch dimension.\"\"\"\n        input_shape = self.input_shape\n        if self._reduce_mode in {None, \"none\", \"None\"}:\n            return input_shape\n        elif self._reduce_mode == \"concat\":\n            if len(input_shape) > 1:\n                return input_shape[:-2] + (input_shape[-1] * input_shape[-2],)\n            return input_shape\n        else:\n            return input_shape[1:]  # Reduce sequence dimension.\n\n\nclass ReduceLast(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        # inputs: [batch_size, seq_size, hidden_size]\n        batch_size = inputs.shape[0]\n        # gather the correct outputs from the the RNN outputs (the outputs after sequence_length are all 0s)\n        # todo: review for generality\n        sequence_length = sequence_length_3D(inputs) - 1\n        sequence_length[sequence_length < 0] = 0\n        gathered = inputs[torch.arange(batch_size), sequence_length.type(torch.int64)]\n        return gathered\n\n\nclass ReduceSum(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        return torch.sum(inputs, dim=1)\n\n\nclass ReduceMean(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        return torch.mean(inputs, dim=1)\n\n\nclass ReduceMax(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        return torch.amax(inputs, dim=1)\n\n\nclass ReduceConcat(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        if inputs.dim() > 2:\n            return inputs.reshape(-1, inputs.shape[-1] * inputs.shape[-2])\n        return inputs\n\n\nclass ReduceNone(torch.nn.Module):\n    def forward(self, inputs, mask=None):\n        return inputs\n\n\nreduce_mode_registry = {\n    \"last\": ReduceLast,\n    \"sum\": ReduceSum,\n    \"mean\": ReduceMean,\n    \"avg\": ReduceMean,\n    \"max\": ReduceMax,\n    \"concat\": ReduceConcat,\n    \"attention\": FeedForwardAttentionReducer,\n    # TODO: Simplify this.\n    \"none\": ReduceNone,\n    \"None\": ReduceNone,\n    None: ReduceNone,\n}\n"
  },
  {
    "path": "ludwig/modules/tabnet_modules.py",
    "content": "import torch\nimport torch.nn as nn\n\nfrom ludwig.modules.normalization_modules import GhostBatchNormalization\nfrom ludwig.utils.entmax import Entmax15, EntmaxBisect, Sparsemax\nfrom ludwig.utils.torch_utils import LudwigModule\n\n\nclass TabNet(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        size: int,\n        output_size: int,\n        num_steps: int = 1,\n        num_total_blocks: int = 4,\n        num_shared_blocks: int = 2,\n        relaxation_factor: float = 1.5,\n        bn_momentum: float = 0.3,\n        bn_epsilon: float = 1e-3,\n        bn_virtual_bs: int | None = None,\n        sparsity: float = 1e-5,\n        entmax_mode: str = \"sparsemax\",\n        entmax_alpha: float = 1.5,\n    ):\n        \"\"\"TabNet Will output a vector of size output_dim.\n\n        Args:\n            input_size: concatenated size of input feature encoder outputs\n            size: Embedding feature dimension\n            output_size: Output dimension for TabNet\n            num_steps: Total number of steps.\n            num_total_blocks: Total number of feature transformer blocks.\n            num_shared_blocks: Number of shared feature transformer blocks.\n            relaxation_factor: >1 will allow features to be used more than once.\n            bn_momentum: Batch normalization, momentum.\n            bn_epsilon: Batch normalization, epsilon.\n            bn_virtual_bs: Virtual batch ize for ghost batch norm.\n            entmax_mode: Entmax is a sparse family of probability mapping which generalizes softmax and sparsemax.\n                         entmax_mode controls the sparsity.  One of {\"sparsemax\", \"entmax15\", \"constant\", \"adaptive\"}.\n            entmax_alpha: Must be a number between 1.0 and 2.0.  If entmax_mode is \"adaptive\", entmax_alpha is used\n                          as the initial value for the learnable parameter.\n        \"\"\"\n        super().__init__()\n        self.input_size = input_size\n        self.size = size\n        self.output_size = output_size\n        self.num_steps = num_steps\n        self.bn_virtual_bs = bn_virtual_bs\n        self.relaxation_factor = relaxation_factor\n        self.sparsity = torch.tensor(sparsity)\n        self.batch_norm = nn.BatchNorm1d(input_size, momentum=bn_momentum, eps=bn_epsilon)\n\n        kargs = {\n            \"num_total_blocks\": num_total_blocks,\n            \"num_shared_blocks\": num_shared_blocks,\n            \"bn_momentum\": bn_momentum,\n            \"bn_epsilon\": bn_epsilon,\n            \"bn_virtual_bs\": bn_virtual_bs,\n        }\n\n        # first feature transformer block is built first\n        # to get the shared blocks\n        self.feature_transforms = nn.ModuleList([FeatureTransformer(input_size, size + output_size, **kargs)])\n        self.attentive_transforms = nn.ModuleList([None])\n        for i in range(num_steps):\n            self.feature_transforms.append(\n                FeatureTransformer(\n                    input_size,\n                    size + output_size,\n                    **kargs,\n                    shared_fc_layers=self.feature_transforms[0].shared_fc_layers,\n                )\n            )\n            # attentive transformers are initialized in build\n            # because their outputs size depends on the number\n            # of features that we determine by looking at the\n            # last dimension of the input tensor\n            self.attentive_transforms.append(\n                AttentiveTransformer(\n                    size, input_size, bn_momentum, bn_epsilon, bn_virtual_bs, entmax_mode, entmax_alpha\n                )\n            )\n        self.final_projection = nn.Linear(output_size, output_size)\n\n        # Register tensors to be used in forward pass. This is needed in order to move these tensors\n        # to the correct device (GPU/CPU) during the forward pass.\n        self.register_buffer(\"out_accumulator\", torch.zeros(output_size))\n        self.register_buffer(\"aggregated_mask\", torch.zeros(input_size))\n        self.register_buffer(\"prior_scales\", torch.ones(input_size))\n\n    def forward(self, features: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, list[torch.Tensor]]:\n        if features.dim() != 2:\n            raise ValueError(f\"Expecting incoming tensor to be dim 2, \" f\"instead dim={features.dim()}\")\n\n        # shape notation\n        # i_s: input_size\n        # s: size\n        # o_s: output_size\n        # b_s: batch_size\n        batch_size = features.shape[0]  # b_s\n        # Tile out_accumulator, aggregated_mask, and prior_scales to add batch dimension.\n        out_accumulator = torch.tile(self.out_accumulator, (batch_size, 1))\n        aggregated_mask = torch.tile(self.aggregated_mask, (batch_size, 1))\n        prior_scales = torch.tile(self.prior_scales, (batch_size, 1))\n        masks = []\n        total_entropy = 0.0\n\n        if batch_size != 1 or not self.training:\n            # Skip batch normalization training if the batch size is 1.\n            features = self.batch_norm(features)  # [b_s, i_s]\n        elif batch_size == 1:\n            # We temporarily set the batch_norm module to eval mode as we can't compute the running statistics\n            # when the batch size is 1.\n            self.batch_norm.eval()\n            features = self.batch_norm(features)  # [b_s, i_s]\n            self.batch_norm.train()\n        masked_features = features\n\n        x = self.feature_transforms[0](masked_features)  # [b_s, s + o_s]\n\n        for step_i in range(1, self.num_steps + 1):\n            #########################\n            # Attentive Transformer #\n            #########################\n            # x in following is shape [b_s, s]\n            mask_values = self.attentive_transforms[step_i](x[:, self.output_size :], prior_scales)  # [b_s, i_s]\n\n            # relaxation factor 1 forces the feature to be only used once\n            prior_scales = prior_scales * (self.relaxation_factor - mask_values)  # [b_s, i_s]\n\n            # entropy is used to penalize the amount of sparsity\n            # in feature selection\n            if self.sparsity.item() != 0.0:\n                total_entropy += (\n                    torch.mean(torch.sum(-mask_values * torch.log(mask_values + 0.00001), dim=1)) / self.num_steps\n                )\n\n            masks.append(torch.unsqueeze(torch.unsqueeze(mask_values, 0), 3))  # [1, b_s, i_s, 1]\n\n            #######################\n            # Feature Transformer #\n            #######################\n            masked_features = torch.multiply(mask_values, features)\n\n            x = self.feature_transforms[step_i](masked_features)  # [b_s, s + o_s]\n\n            # x in following is shape [b_s, o_s]\n            out = nn.functional.relu(x[:, : self.output_size])  # [b_s, o_s]\n            out_accumulator += out\n\n            # Aggregated masks are used for visualization of the\n            # feature importance attributes.\n            scale = torch.sum(out, dim=1, keepdim=True) / self.num_steps\n            aggregated_mask += mask_values * scale  # [b_s, i_s]\n\n        final_output = self.final_projection(out_accumulator)  # [b_s, o_s]\n\n        sparsity_loss = torch.multiply(self.sparsity, total_entropy)\n        self.update_loss(\"sparsity_loss\", sparsity_loss)\n\n        return final_output, aggregated_mask, masks\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.output_size])\n\n\nclass FeatureBlock(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        size: int,\n        apply_glu: bool = True,\n        bn_momentum: float = 0.1,\n        bn_epsilon: float = 1e-3,\n        bn_virtual_bs: int = None,\n        shared_fc_layer: LudwigModule = None,\n    ):\n        super().__init__()\n        self.input_size = input_size\n        self.apply_glu = apply_glu\n        self.size = size\n        units = size * 2 if apply_glu else size\n\n        # Initialize fc_layer before assigning to shared layer for torchscript compatibilty\n        self.fc_layer = nn.Linear(input_size, units, bias=False)\n        if shared_fc_layer is not None:\n            assert shared_fc_layer.weight.shape == self.fc_layer.weight.shape\n            self.fc_layer = shared_fc_layer\n\n        self.batch_norm = GhostBatchNormalization(\n            units, virtual_batch_size=bn_virtual_bs, momentum=bn_momentum, epsilon=bn_epsilon\n        )\n\n    def forward(self, inputs):\n        # shape notation\n        # i_s: input_size\n        # s: size\n        # u: units\n        # b_s: batch_size\n\n        # inputs shape [b_s, i_s]\n        hidden = self.fc_layer(inputs)  # [b_s, u]\n        hidden = self.batch_norm(hidden)  # [b_s, u]\n        if self.apply_glu:\n            hidden = nn.functional.glu(hidden, dim=-1)  # [bs, s]\n        return hidden  # [b_s, 2*s] if apply_glu else [b_s, s]\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n\nclass AttentiveTransformer(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        size: int,\n        bn_momentum: float = 0.1,\n        bn_epsilon: float = 1e-3,\n        bn_virtual_bs: int = None,\n        entmax_mode: str = \"sparsemax\",\n        entmax_alpha: float = 1.5,\n    ):\n        super().__init__()\n        self.input_size = input_size\n        self.size = size\n        self.entmax_mode = entmax_mode\n        if entmax_mode == \"adaptive\":\n            self.register_buffer(\"trainable_alpha\", torch.tensor(entmax_alpha, requires_grad=True))\n        else:\n            self.trainable_alpha = entmax_alpha\n\n        if self.entmax_mode == \"sparsemax\":\n            self.entmax_module = Sparsemax()\n        elif self.entmax_mode == \"entmax15\":\n            self.entmax_module = Entmax15()\n        else:\n            self.entmax_module = EntmaxBisect(alpha=self.trainable_alpha)\n\n        self.feature_block = FeatureBlock(\n            input_size,\n            size,\n            bn_momentum=bn_momentum,\n            bn_epsilon=bn_epsilon,\n            bn_virtual_bs=bn_virtual_bs,\n            apply_glu=False,\n        )\n\n    def forward(self, inputs, prior_scales):\n        # shape notation\n        # i_s: input_size\n        # s: size\n        # b_s: batch_size\n\n        # inputs shape [b_s, i_s], prior_scales shape [b_s, s]\n        hidden = self.feature_block(inputs)  # [b_s, s]\n        hidden = hidden * prior_scales  # [b_s, s]\n\n        # removing the mean to try to avoid numerical instability\n        # https://github.com/tensorflow/addons/issues/2314\n        # https://github.com/tensorflow/tensorflow/pull/21183/files\n        # In (Arik and Pfister, 2019), they call the logits z.\n        # The mean(logits) can be substracted from logits to make the algorithm\n        # more numerically stable. the instability in this algorithm comes mostly\n        # from the z_cumsum. Substacting the mean will cause z_cumsum to be close\n        # to zero.\n        # hidden = hidden - tf.math.reduce_mean(hidden, axis=1)[:, tf.newaxis]\n        return self.entmax_module(hidden)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.size])\n\n\n# adapted and modified from:\n# https://github.com/ostamand/tensorflow-tabnet/blob/master/tabnet/models/transformers.py\nclass FeatureTransformer(LudwigModule):\n    def __init__(\n        self,\n        input_size: int,\n        size: int,\n        shared_fc_layers: list | None = None,\n        num_total_blocks: int = 4,\n        num_shared_blocks: int = 2,\n        bn_momentum: float = 0.1,\n        bn_epsilon: float = 1e-3,\n        bn_virtual_bs: int = None,\n    ):\n        super().__init__()\n        if shared_fc_layers is None:\n            shared_fc_layers = []\n        self.input_size = input_size\n        self.num_total_blocks = num_total_blocks\n        self.num_shared_blocks = num_shared_blocks\n        self.size = size\n\n        kwargs = {\n            \"bn_momentum\": bn_momentum,\n            \"bn_epsilon\": bn_epsilon,\n            \"bn_virtual_bs\": bn_virtual_bs,\n        }\n\n        # build blocks\n        self.blocks = nn.ModuleList()\n        for n in range(num_total_blocks):\n            # Ensure the sizes fed into FeatureBlock are correct regardless of presence of shared_fc_layer\n            if n == 0:\n                in_features = input_size\n            else:\n                in_features = size\n\n            if shared_fc_layers and n < len(shared_fc_layers):\n                self.blocks.append(FeatureBlock(in_features, size, **kwargs, shared_fc_layer=shared_fc_layers[n]))\n            else:\n                self.blocks.append(FeatureBlock(in_features, size, **kwargs))\n\n    def forward(self, inputs: torch.Tensor) -> torch.Tensor:\n        # shape notation\n        # i_s: input_size\n        # s: size\n        # b_s: batch_size\n\n        # inputs shape [b_s, i_s]\n        hidden = self.blocks[0](inputs)  # [b_s, s]\n        for n in range(1, self.num_total_blocks):\n            hidden = (self.blocks[n](hidden) + hidden) * (0.5**0.5)  # [b_s, s]\n        return hidden  # [b_s, s]\n\n    @property\n    def shared_fc_layers(self):\n        return [self.blocks[i].fc_layer for i in range(self.num_shared_blocks)]\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return torch.Size([self.size])\n"
  },
  {
    "path": "ludwig/modules/training_hooks.py",
    "content": "import logging\nfrom abc import ABC, abstractmethod\n\nimport torch\n\nlogger = logging.getLogger(__name__)\n\n\nclass TrainingHook(ABC):\n    \"\"\"A base class for training hooks in PyTorch.\n\n    This class provides a template for implementing custom training hooks\n    that can be activated, deactivated, and maintain a handle to the hook.\n\n    Attributes:\n        _hook_handle (Optional[torch.utils.hooks.RemovableHandle]): A handle to the\n            registered forward hook, initially set to None.\n    \"\"\"\n\n    def __init__(self, *args, **kwargs) -> None:\n        self._hook_handle = None\n\n    @abstractmethod\n    def hook_fn(self, module: torch.nn.Module, inputs: torch.tensor, outputs: torch.Tensor) -> torch.Tensor:\n        \"\"\"Abstract method to be implemented by subclasses. This is the method that defines the custom behavior of\n        the training hook during a forward pass for the specified module.\n\n        Args:\n            module (nn.Module): The PyTorch module for which the hook is activated.\n            inputs (torch.Tensor): The input to the module during the forward pass.\n            outputs (torch.Tensor): The output from the module during the forward pass.\n\n        Returns:\n            torch.Tensor: The output tensor from the module.\n\n        Raises:\n            NotImplementedError: If the method is not implemented in a subclass.\n        \"\"\"\n\n    def activate_hook(self, module: torch.nn.Module) -> torch.nn.Module:\n        \"\"\"Activates the training hook for a given module.\n\n        Args:\n            module (nn.Module): The PyTorch module for which the hook is activated.\n\n        Returns:\n            nn.Module: The input module with the training hook activated.\n        \"\"\"\n        self._hook_handle = module.register_forward_hook(self.hook_fn)\n        return module\n\n    def deactivate_hook(self):\n        \"\"\"Deactivates and removes the training hook.\"\"\"\n        if self._hook_handle is not None:\n            self._hook_handle.remove()\n            self._hook_handle = None\n\n\nclass NEFTuneHook(TrainingHook):\n    def __init__(self, *args, **kwargs) -> None:\n        super().__init__(*args, **kwargs)\n        self.neftune_noise_alpha = kwargs.get(\"neftune_noise_alpha\")\n\n    def hook_fn(self, module: torch.nn.Module, input: torch.Tensor, output: torch.Tensor) -> torch.Tensor:\n        \"\"\"Implements the NEFTune forward pass for the model using forward hooks. Note this works only for\n        torch.nn. Embedding layers. This method is slightly adapted from the original source code that can be found\n        here: https://github.com/neelsjain/NEFTune.\n\n        The input tensor is ignored since the noise is added to the output of the embedding layer.\n\n        Returns:\n            torch.Tensor: The output tensor from the module.\n        \"\"\"\n        if module.training:\n            dims = torch.tensor(output.size(1) * output.size(2))\n            mag_norm = module.neftune_noise_alpha / torch.sqrt(dims)\n            output = output + torch.zeros_like(output).uniform_(-mag_norm, mag_norm)\n        return output\n\n    def activate_hook(self, module: torch.nn.Module) -> torch.nn.Module:\n        \"\"\"Activates the neftune as presented in this code and paper:\n\n        Code: https://github.com/neelsjain/NEFTune\n        Paper: https://arxiv.org/abs/2310.05914\n\n        Args:\n            module (nn.Module): The PyTorch module for which the hook is activated.\n\n        Returns:\n            nn.Module: The input module with the training hook activated.\n        \"\"\"\n        from peft import PeftModel\n\n        if isinstance(module, PeftModel):\n            embeddings = module.base_model.model.get_input_embeddings()\n        else:\n            embeddings = module.get_input_embeddings()\n\n        embeddings.neftune_noise_alpha = self.neftune_noise_alpha\n        self._hook_handle = embeddings.register_forward_hook(self.hook_fn)\n\n        return module\n"
  },
  {
    "path": "ludwig/predict.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport sys\nfrom ast import literal_eval\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import FULL, TEST, TRAINING, VALIDATION\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n\ndef predict_cli(\n    model_path: str,\n    dataset: str | dict | pd.DataFrame = None,\n    data_format: str = None,\n    split: str = FULL,\n    batch_size: int = 128,\n    generation_config: str | None = None,\n    skip_save_unprocessed_output: bool = False,\n    skip_save_predictions: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    logging_level: int = logging.INFO,\n    **kwargs,\n) -> None:\n    \"\"\"Loads pre-trained model to make predictions on the provided data set.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used in the prediction.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param split: (str, default: `full`) split on which\n        to perform predictions. Valid values are `'training'`, `'validation'`,\n        `'test'` and `'full'`.\n    :param batch_size: (int, default `128`) size of batches for processing.\n    :param generation_config: (str, default: `None`) a string representing\n        the parameters for generation required to perform predictions with\n        an LLM. The string must be a JSON formatted dictionary with keys from\n        https://huggingface.co/docs/transformers/main_classes/text_generation#transformers.GenerationConfig\n        These will be merged with the generation parameters from the original\n        model config.\n    :param skip_save_unprocessed_output: (bool, default: `False`) by default\n        predictions and their probabilities are saved in both raw\n        unprocessed numpy files containing tensors and as postprocessed\n        CSV files (one for each output feature). If this parameter is True,\n        only the CSV ones are saved and the numpy ones are skipped.\n    :param skip_save_predictions: (bool, default: `False`) skips saving test\n        predictions CSV files\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param logging_level: (int) Log level that will be sent to stderr.\n\n    # Returns\n\n    :return: ('None')\n    \"\"\"\n    model = LudwigModel.load(\n        model_path,\n        logging_level=logging_level,\n        backend=backend,\n        gpus=gpus,\n        gpu_memory_limit=gpu_memory_limit,\n        allow_parallel_threads=allow_parallel_threads,\n        callbacks=callbacks,\n    )\n    model.predict(\n        dataset=dataset,\n        data_format=data_format,\n        split=split,\n        batch_size=batch_size,\n        generation_config=literal_eval(generation_config) if generation_config else None,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        output_directory=output_directory,\n        return_type=\"dict\",\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script loads a pretrained model \" \"and uses it to predict\",\n        prog=\"ludwig predict\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\"--dataset\", help=\"input data file path\", required=True)\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\",\n            \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n    parser.add_argument(\n        \"-s\", \"--split\", default=FULL, choices=[TRAINING, VALIDATION, TEST, FULL], help=\"the split to test the model on\"\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n    parser.add_argument(\"-gc\", \"--generation_config\", help=\"generation config (LLMs only)\", default=None)\n\n    # -------------------------\n    # Output results parameters\n    # -------------------------\n    parser.add_argument(\n        \"-od\", \"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\"\n    )\n    parser.add_argument(\n        \"-ssuo\",\n        \"--skip_save_unprocessed_output\",\n        help=\"skips saving intermediate NPY output files\",\n        action=\"store_true\",\n        default=False,\n    )\n    parser.add_argument(\n        \"-sstp\",\n        \"--skip_save_predictions\",\n        help=\"skips saving predictions CSV files\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # ------------------\n    # Generic parameters\n    # ------------------\n    parser.add_argument(\"-bs\", \"--batch_size\", type=int, default=128, help=\"size of batches\")\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\"-g\", \"--gpus\", type=int, default=0, help=\"list of gpu to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-dpt\",\n        \"--disable_parallel_threads\",\n        action=\"store_false\",\n        dest=\"allow_parallel_threads\",\n        help=\"disable PyTorch from using multithreading for reproducibility\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"predict\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.predict\")\n\n    args.backend = initialize_backend(args.backend)\n    if args.backend.is_coordinator():\n        print_ludwig(\"Predict\", LUDWIG_VERSION)\n        logger.info(f\"Dataset path: {args.dataset}\")\n        logger.info(f\"Model path: {args.model_path}\")\n        logger.info(\"\")\n\n    predict_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/preprocess.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport sys\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.data_utils import load_yaml\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\n\nlogger = logging.getLogger(__name__)\n\n\ndef preprocess_cli(\n    preprocessing_config: str | dict = None,\n    dataset: str | dict | pd.DataFrame = None,\n    training_set: str | dict | pd.DataFrame = None,\n    validation_set: str | dict | pd.DataFrame = None,\n    test_set: str | dict | pd.DataFrame = None,\n    training_set_metadata: str | dict = None,\n    data_format: str = None,\n    random_seed: int = default_random_seed,\n    logging_level: int = logging.INFO,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    **kwargs\n) -> None:\n    \"\"\"*train* defines the entire training procedure used by Ludwig's internals. Requires most of the parameters\n    that are taken into the model. Builds a full ludwig model and performs the training.\n\n    :param preprocessing_config: (Union[str, dict]) in-memory representation of\n            config or string path to a YAML config file.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used for training.\n        If it has a split column, it will be used for splitting (0 for train,\n        1 for validation, 2 for test), otherwise the dataset will be\n        randomly split.\n    :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing training data.\n    :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing validation data.\n    :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing test data.\n    :param training_set_metadata: (Union[str, dict], default: `None`)\n        metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n        dataset created the first time an input file is used in the same\n        directory with the same name and a '.meta.json' extension.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param experiment_name: (str, default: `'experiment'`) name for\n        the experiment.\n    :param model_name: (str, default: `'run'`) name of the model that is\n        being used.\n    :param model_load_path: (str, default: `None`) if this is specified the\n        loaded model will be used as initialization\n        (useful for transfer learning).\n    :param model_resume_path: (str, default: `None`) resumes training of\n        the model from the path specified. The config is restored.\n        In addition to config, training statistics, loss for each\n        epoch and the state of the optimizer are restored such that\n        training can be effectively continued from a previously interrupted\n        training process.\n    :param skip_save_training_description: (bool, default: `False`) disables\n        saving the description JSON file.\n    :param skip_save_training_statistics: (bool, default: `False`) disables\n        saving training statistics JSON file.\n    :param skip_save_model: (bool, default: `False`) disables\n        saving model weights and hyperparameters each time the model\n        improves. By default Ludwig saves model weights after each epoch\n        the validation metric improves, but if the model is really big\n        that can be time consuming. If you do not want to keep\n        the weights and just find out what performance a model can get\n        with a set of hyperparameters, use this parameter to skip it,\n        but the model will not be loadable later on and the returned model\n        will have the weights obtained at the end of training, instead of\n        the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n        progress each epoch. By default Ludwig saves weights and stats\n        after each epoch for enabling resuming of training, but if\n        the model is really big that can be time consuming and will uses\n        twice as much space, use this parameter to skip it, but training\n        cannot be resumed later on.\n    :param skip_save_log: (bool, default: `False`) disables saving\n        TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n        but if it is not needed turning it off can slightly increase the\n        overall speed.\n    :param skip_save_processed_input: (bool, default: `False`) if input\n        dataset is provided it is preprocessed and cached by saving an HDF5\n        and JSON files to avoid running the preprocessing again. If this\n        parameter is `False`, the HDF5 and JSON file are not saved.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param random_seed: (int: default: 42) random seed used for weights\n        initialization, splits and any other random function.\n    :param logging_level: (int) Log level that will be sent to stderr.\n\n    # Return\n\n    :return: (`None`)\n    \"\"\"\n    model = LudwigModel(\n        config=preprocessing_config,\n        logging_level=logging_level,\n        callbacks=callbacks,\n        backend=backend,\n    )\n    model.preprocess(\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        skip_save_processed_input=False,\n        random_seed=random_seed,\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script preprocess a dataset\", prog=\"ludwig preprocess\", usage=\"%(prog)s [options]\"\n    )\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\n        \"--dataset\",\n        help=\"input data file path. \"\n        \"If it has a split column, it will be used for splitting \"\n        \"(0: train, 1: validation, 2: test), \"\n        \"otherwise the dataset will be randomly split\",\n    )\n    parser.add_argument(\"--training_set\", help=\"input train data file path\")\n    parser.add_argument(\"--validation_set\", help=\"input validation data file path\")\n    parser.add_argument(\"--test_set\", help=\"input test data file path\")\n\n    parser.add_argument(\n        \"--training_set_metadata\",\n        help=\"input metadata JSON file path. An intermediate preprocessed file \"\n        \"containing the mappings of the input file created \"\n        \"the first time a file is used, in the same directory \"\n        \"with the same name and a .json extension\",\n    )\n\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    preprocessing_def = parser.add_mutually_exclusive_group(required=True)\n    preprocessing_def.add_argument(\n        \"-pc\",\n        \"--preprocessing_config\",\n        dest=\"preprocessing_config\",\n        type=load_yaml,\n        help=\"YAML file describing the preprocessing. \"\n        \"Ignores --preprocessing_config.\"\n        \"Uses the same format of config, \"\n        \"but ignores encoder specific parameters, \"\n        \"decoder specific parameters, combiner and training parameters\",\n    )\n    preprocessing_def.add_argument(\n        \"-pcs\",\n        \"--preprocessing_config_str\",\n        type=yaml.safe_load,\n        help=\"preproceesing config. \"\n        \"Uses the same format of config, \"\n        \"but ignores encoder specific parameters, \"\n        \"decoder specific parameters, combiner and training parameters\",\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-rs\",\n        \"--random_seed\",\n        type=int,\n        default=42,\n        help=\"a random seed that is going to be used anywhere there is a call \"\n        \"to a random number generator: data splitting, parameter \"\n        \"initialization and training set shuffling\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"preprocess\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.preprocess\")\n\n    args.backend = initialize_backend(args.backend)\n    if args.backend.is_coordinator():\n        print_ludwig(\"Preprocess\", LUDWIG_VERSION)\n\n    preprocess_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/progress_bar.py",
    "content": "import uuid\n\nimport tqdm\n\ntry:\n    import ray.train as rt\nexcept ImportError:\n    rt = None\n\n\nclass LudwigProgressBarActions:\n    CREATE = \"create\"\n    UPDATE = \"update\"\n    CLOSE = \"close\"\n\n\nclass LudwigProgressBar:\n    \"\"\"Class for progress bars that supports distributed progress bars in ray.\n\n    # Inputs\n\n    :param report_to_ray: (bool) use the ray.train.report method\n        to report progress to the ray driver. If false then this behaves as a normal tqdm\n        progress bar\n    :param config: (dict) the tqdm configs used for the progress bar. See https://github.com/tqdm/tqdm#parameters\n        for list of parameters\n    :param is_coordinator: (bool) whether the calling process is the coordinator process.\n\n    # Example usage:\n\n    ```python\n    from ludwig.progress_bar import LudwigProgressBar\n\n    config = {\"total\": 20, \"desc\": \"Sample progress bar\"}\n    pbar = LudwigProgressBar(report_to_ray=False, config=config, is_coordinator=True)\n    for i in range(20):\n        pbar.update(1)\n    pbar.close()\n    ```\n    \"\"\"\n\n    def __init__(\n        self,\n        report_to_ray: bool,\n        config: dict,\n        is_coordinator: bool,\n    ) -> None:\n        \"\"\"Constructor for the LudwigProgressBar class.\n\n        # Inputs\n\n        :param report_to_ray: (bool) use the ray.train.report method\n            to report progress to the ray driver. If false then this behaves as a normal tqdm\n            progress bar\n        :param config: (dict) the tqdm configs used for the progress bar. See https://github.com/tqdm/tqdm#parameters\n            for list of parameters\n        :param is_coordinator: (bool) whether the calling process is the coordinator process.\n\n        # Return\n\n        :return: (None) `None`\n        \"\"\"\n        if report_to_ray and rt is None:\n            raise ValueError(\"Set report_to_ray=True but ray is not installed. Run `pip install ray`\")\n\n        self.id = str(uuid.uuid4())[-8:]\n\n        self.report_to_ray = report_to_ray\n        self.is_coordinator = is_coordinator\n        self.config = config\n\n        self.total_steps = 0\n        self.progress_bar = None\n\n        if not self.report_to_ray:\n            if self.is_coordinator:\n                self.progress_bar = tqdm.tqdm(**config)\n        else:\n            if \"file\" in self.config:\n                self.config.pop(\"file\")\n            # All processes need to call ray.train.report since ray has a lock that blocks\n            # a process when calling report if there are processes that haven't called it. Similar\n            # to a distributed checkpoint. Therefore we pass the flag to the driver.\n            # In Ray 2.x, rt.report() only accepts metrics and checkpoint kwargs,\n            # so we pass progress_bar data inside the metrics dict.\n            rt.report(\n                metrics={\n                    \"progress_bar\": {\n                        \"id\": self.id,\n                        \"config\": self.config,\n                        \"action\": LudwigProgressBarActions.CREATE,\n                        \"is_coordinator\": self.is_coordinator,\n                    }\n                }\n            )\n\n    def set_postfix(self, ordered_dict: dict = None, **kwargs) -> None:\n        \"\"\"Sets the postfix (additional stats) for the progress bar.\"\"\"\n        if self.progress_bar:\n            self.progress_bar.set_postfix(ordered_dict, **kwargs)\n\n    def update(self, steps: int) -> None:\n        \"\"\"Updates the progress bar.\n\n        # Inputs\n\n        :param steps: (int) number of steps to update the progress bar by\n\n        # Return\n\n        :return: (None) `None`\n        \"\"\"\n        self.total_steps += steps\n        if self.progress_bar:\n            self.progress_bar.update(steps)\n        elif self.report_to_ray:\n            rt.report(\n                metrics={\n                    \"progress_bar\": {\n                        \"id\": self.id,\n                        \"update_by\": steps,\n                        \"is_coordinator\": self.is_coordinator,\n                        \"action\": LudwigProgressBarActions.UPDATE,\n                    }\n                }\n            )\n\n    def close(self) -> None:\n        \"\"\"Closes the progress bar.\n\n        # Return\n\n        :return: (None) `None`\n        \"\"\"\n        if self.progress_bar:\n            self.progress_bar.close()\n        elif self.report_to_ray:\n            rt.report(\n                metrics={\n                    \"progress_bar\": {\n                        \"id\": self.id,\n                        \"is_coordinator\": self.is_coordinator,\n                        \"action\": LudwigProgressBarActions.CLOSE,\n                    }\n                }\n            )\n"
  },
  {
    "path": "ludwig/schema/__init__.py",
    "content": "# TODO(travis): figure out why we need these imports to avoid circular import error\nfrom ludwig.schema.combiners.utils import get_combiner_jsonschema  # noqa\nfrom ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema  # noqa\nfrom ludwig.schema.hyperopt import get_hyperopt_jsonschema  # noqa\nfrom ludwig.schema.trainer import get_model_type_jsonschema, get_trainer_jsonschema  # noqa\n"
  },
  {
    "path": "ludwig/schema/combiners/__init__.py",
    "content": "import ludwig.schema.combiners.comparator  # noqa: F401\nimport ludwig.schema.combiners.concat  # noqa: F401\nimport ludwig.schema.combiners.project_aggregate  # noqa: F401\nimport ludwig.schema.combiners.sequence  # noqa: F401\nimport ludwig.schema.combiners.sequence_concat  # noqa: F401\nimport ludwig.schema.combiners.tab_transformer  # noqa: F401\nimport ludwig.schema.combiners.tabnet  # noqa: F401\nimport ludwig.schema.combiners.transformer  # noqa: F401\n"
  },
  {
    "path": "ludwig/schema/combiners/base.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseCombinerConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base combiner config class.\"\"\"\n\n    type: str\n"
  },
  {
    "path": "ludwig/schema/combiners/common_transformer_options.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass CommonTransformerConfig:\n    \"\"\"Common transformer parameter values.\"\"\"\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"Dropout rate for the transformer block.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"dropout\"],\n    )\n\n    transformer_output_size: int = schema_utils.NonNegativeInteger(\n        default=256,\n        description=\"Size of the fully connected layer after self attention in the transformer block. This is usually \"\n        \"the same as `hidden_size` and `embedding_size`.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"transformer_output_size\"],\n    )\n\n    hidden_size: int = schema_utils.NonNegativeInteger(\n        default=256,\n        description=\"The number of hidden units of the TransformerStack as well as the dimension that each incoming \"\n        \"input feature is projected to before feeding to the TransformerStack.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"hidden_size\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of transformer layers.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"num_layers\"],\n    )\n\n    num_heads: int = schema_utils.NonNegativeInteger(\n        default=8,\n        description=\"Number of heads of the self attention in the transformer block.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"num_heads\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | dict = common_fields.BiasInitializerField()\n\n    weights_initializer: str | dict = common_fields.WeightsInitializerField()\n\n    # TODO(#1673): Add conditional logic for fields like this one:\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of stacked fully connected layers (only applies if `reduce_output` is not null).\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"num_fc_layers\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Output size of a fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"output_size\"],\n    )\n\n    norm: str | None = common_fields.NormField()\n\n    norm_params: dict | None = common_fields.NormParamsField()\n\n    fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField()\n\n    fc_dropout: float = common_fields.DropoutField()\n\n    fc_activation: str = schema_utils.ActivationOptions(\n        default=\"relu\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"fc_activation\"],\n    )\n\n    fc_residual: bool = common_fields.ResidualField()\n"
  },
  {
    "path": "ludwig/schema/combiners/comparator.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"comparator\")\n@ludwig_dataclass\nclass ComparatorCombinerConfig(BaseCombinerConfig):\n    \"\"\"Parameters for comparator combiner.\"\"\"\n\n    def __post_init__(self):\n        if self.num_fc_layers == 0 and self.fc_layers is None:\n            raise ConfigValidationError(\n                \"`combiner.type=comparator` requires at least one fully connected layer. \"\n                \"Set `num_fc_layers > 0` or `fc_layers`.\"\n            )\n\n        if not self.entity_1:\n            raise ConfigValidationError(\n                \"`combiner.entity_1` is required and must contain as least one input feature name.\"\n            )\n\n        if not self.entity_2:\n            raise ConfigValidationError(\n                \"`combiner.entity_2` is required and must contain as least one input feature name.\"\n            )\n\n    type: str = schema_utils.ProtectedString(\n        \"comparator\",\n        description=COMBINER_METADATA[\"comparator\"][\"type\"].long_description,\n    )\n\n    entity_1: list[str] = schema_utils.List(\n        default=None,\n        description=(\n            \"The list of input feature names `[feature_1, feature_2, ...]` constituting the first entity to compare. \"\n            \"*Required*.\"\n        ),\n        parameter_metadata=COMBINER_METADATA[\"comparator\"][\"entity_1\"],\n    )\n\n    entity_2: list[str] = schema_utils.List(\n        default=None,\n        description=(\n            \"The list of input feature names `[feature_1, feature_2, ...]` constituting the second entity to compare. \"\n            \"*Required*.\"\n        ),\n        parameter_metadata=COMBINER_METADATA[\"comparator\"][\"entity_2\"],\n    )\n\n    dropout: float = common_fields.DropoutField()\n\n    activation: str = schema_utils.ActivationOptions(default=\"relu\")\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=COMBINER_METADATA[\"comparator\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | dict = common_fields.BiasInitializerField()\n\n    weights_initializer: str | dict = common_fields.WeightsInitializerField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField(default=1)\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Output size of a fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"comparator\"][\"output_size\"],\n    )\n\n    norm: str | None = common_fields.NormField()\n\n    norm_params: dict | None = common_fields.NormParamsField()\n\n    fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField()\n"
  },
  {
    "path": "ludwig/schema/combiners/concat.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"concat\")\n@ludwig_dataclass\nclass ConcatCombinerConfig(BaseCombinerConfig):\n    \"\"\"Parameters for concat combiner.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"concat\",\n        description=COMBINER_METADATA[\"concat\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField()\n\n    activation: str = schema_utils.ActivationOptions(default=\"relu\")\n\n    flatten_inputs: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to flatten input tensors to a vector.\",\n        parameter_metadata=COMBINER_METADATA[\"concat\"][\"flatten_inputs\"],\n    )\n\n    residual: bool = common_fields.ResidualField()\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=COMBINER_METADATA[\"concat\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | dict = common_fields.BiasInitializerField()\n\n    weights_initializer: str | dict = common_fields.WeightsInitializerField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField()\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Output size of a fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"concat\"][\"output_size\"],\n    )\n\n    norm: str | None = common_fields.NormField()\n\n    norm_params: dict | None = common_fields.NormParamsField()\n\n    fc_layers: list[dict[str, Any]] | None = common_fields.FCLayersField()\n"
  },
  {
    "path": "ludwig/schema/combiners/project_aggregate.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"project_aggregate\")\n@ludwig_dataclass\nclass ProjectAggregateCombinerConfig(BaseCombinerConfig):\n    type: str = schema_utils.ProtectedString(\n        \"project_aggregate\",\n        description=COMBINER_METADATA[\"project_aggregate\"][\"type\"].long_description,\n    )\n\n    projection_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"All combiner inputs are projected to this size before being aggregated.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"projection_size\"],\n    )\n\n    residual: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to add residual skip connection between the fully connected layers in the stack.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"residual\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout rate to apply to each fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        default=\"relu\",\n        description=\"Activation to apply to each fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"activation\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"Number of fully connected layers after aggregation.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"num_fc_layers\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Output size of each layer of the stack of fully connected layers.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"output_size\"],\n    )\n\n    norm: str | None = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=\"layer\",\n        description=\"Normalization to apply to each projection and fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"norm\"],\n    )\n\n    norm_params: dict | None = schema_utils.Dict(\n        description=\"Parameters of the normalization to apply to each projection and fully connected layer.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"norm_params\"],\n    )\n\n    fc_layers: list[dict[str, Any]] | None = schema_utils.DictList(\n        description=\"Full specification of the fully connected layers after the aggregation. It should be a list of \"\n        \"dict, each dict representing one layer of the fully connected layer stack. \",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"fc_layers\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layers use a bias vector.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | dict = schema_utils.InitializerOrDict(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias of the projection and for the fully connected layers.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str | dict = schema_utils.InitializerOrDict(\n        default=\"xavier_uniform\",\n        description=\"Initializer to use for the weights of the projection and for the fully connected layers.\",\n        parameter_metadata=COMBINER_METADATA[\"project_aggregate\"][\"weights_initializer\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/sequence.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, SEQUENCE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.sequence_concat import MAIN_SEQUENCE_FEATURE_DESCRIPTION\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\"\"\"\nSEQUENCE encoders that always return 2D [batch_size, hidden_size] tensors, regardless of how they are parameterized.\nThese should never be used with modules that expect 3D tensors, such as the SequenceCombiner.\n\"\"\"\n_2D_SEQUENCE_ENCODERS = [\"embed\"]\n\n\n@DeveloperAPI\n@register_combiner_config(\"sequence\")\n@ludwig_dataclass\nclass SequenceCombinerConfig(BaseCombinerConfig):\n    \"\"\"Parameters for sequence combiner.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"sequence\",\n        description=COMBINER_METADATA[\"sequence\"][\"type\"].long_description,\n    )\n\n    main_sequence_feature: str | None = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=MAIN_SEQUENCE_FEATURE_DESCRIPTION,\n        parameter_metadata=COMBINER_METADATA[\"sequence\"][\"main_sequence_feature\"],\n    )\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=SEQUENCE,\n        default=\"parallel_cnn\",\n        description=\"Encoder to apply to `main_sequence_feature`. The encoder must produce\"\n        \" a tensor of size [batch_size, sequence_length, hidden_size]\",\n        blocklist=_2D_SEQUENCE_ENCODERS,\n    )\n\n    reduce_output: str | None = schema_utils.ReductionOptions(\n        default=None,\n        description=\"Strategy to use to aggregate the embeddings of the items of the set.\",\n        parameter_metadata=COMBINER_METADATA[\"sequence\"][\"reduce_output\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/sequence_concat.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\nMAIN_SEQUENCE_FEATURE_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\"\"\"\n\n\n@DeveloperAPI\n@register_combiner_config(\"sequence_concat\")\n@ludwig_dataclass\nclass SequenceConcatCombinerConfig(BaseCombinerConfig):\n    \"\"\"Parameters for sequence concat combiner.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"sequence_concat\"\n\n    type: str = schema_utils.ProtectedString(\n        \"sequence_concat\",\n        description=COMBINER_METADATA[\"sequence_concat\"][\"type\"].long_description,\n    )\n\n    main_sequence_feature: str | None = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=MAIN_SEQUENCE_FEATURE_DESCRIPTION,\n        parameter_metadata=COMBINER_METADATA[\"sequence_concat\"][\"main_sequence_feature\"],\n    )\n\n    reduce_output: str | None = schema_utils.ReductionOptions(\n        default=None,\n        description=\"Strategy to use to aggregate the embeddings of the items of the set.\",\n        parameter_metadata=COMBINER_METADATA[\"sequence_concat\"][\"reduce_output\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/tab_transformer.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.common_transformer_options import CommonTransformerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"tabtransformer\")\n@ludwig_dataclass\nclass TabTransformerCombinerConfig(BaseCombinerConfig, CommonTransformerConfig):\n    \"\"\"Parameters for tab transformer combiner.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"tabtransformer\",\n        description=COMBINER_METADATA[\"tabtransformer\"][\"type\"].long_description,\n    )\n\n    embed_input_feature_name: str | int | None = schema_utils.Embed(\n        description=\"This value controls the size of the embeddings. Valid values are `add` which uses the \"\n        \"`hidden_size` value or an integer that is set to a specific value. In the case of an integer \"\n        \"value, it must be smaller than hidden_size.\",\n        parameter_metadata=COMBINER_METADATA[\"tabtransformer\"][\"embed_input_feature_name\"],\n    )\n\n    reduce_output: str = schema_utils.ReductionOptions(\n        default=\"concat\",\n        description=\"Strategy to use to aggregate the output of the transformer.\",\n        parameter_metadata=COMBINER_METADATA[\"tabtransformer\"][\"reduce_output\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/tabnet.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"tabnet\")\n@ludwig_dataclass\nclass TabNetCombinerConfig(BaseCombinerConfig):\n    \"\"\"Parameters for tabnet combiner.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"tabnet\",\n        description=COMBINER_METADATA[\"tabnet\"][\"type\"].long_description,\n    )\n\n    size: int = schema_utils.PositiveInteger(\n        default=32,\n        description=\"Size of the hidden layers. `N_a` in (Arik and Pfister, 2019).\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"size\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.05,\n        min=0,\n        max=1,\n        description=\"Dropout rate for the transformer block.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"dropout\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Output size of a fully connected layer. `N_d` in (Arik and Pfister, 2019).\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"output_size\"],\n    )\n\n    num_steps: int = schema_utils.NonNegativeInteger(\n        default=3,\n        description=\"Number of steps / repetitions of the the attentive transformer and feature transformer \"\n        \"computations. `N_steps` in (Arik and Pfister, 2019).\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"num_steps\"],\n    )\n\n    num_total_blocks: int = schema_utils.NonNegativeInteger(\n        default=4,\n        description=\"Total number of feature transformer blocks at each step.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"num_total_blocks\"],\n    )\n\n    num_shared_blocks: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"Number of shared feature transformer blocks across the steps.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"num_shared_blocks\"],\n    )\n\n    relaxation_factor: float = schema_utils.FloatRange(\n        default=1.5,\n        description=\"Factor that influences how many times a feature should be used across the steps of computation. \"\n        \"a value of 1 implies it each feature should be use once, a higher value allows for multiple \"\n        \"usages. `gamma` in (Arik and Pfister, 2019).\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"relaxation_factor\"],\n    )\n\n    bn_epsilon: float = schema_utils.FloatRange(\n        default=1e-3,\n        description=\"Epsilon to be added to the batch norm denominator.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"bn_epsilon\"],\n    )\n\n    bn_momentum: float = schema_utils.FloatRange(\n        default=0.05,\n        description=\"Momentum of the batch norm. 1 - `m_B` from the TabNet paper.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"bn_momentum\"],\n    )\n\n    bn_virtual_bs: int | None = schema_utils.PositiveInteger(\n        default=1024,\n        allow_none=True,\n        description=\"Size of the virtual batch size used by ghost batch norm. If null, regular batch norm is used \"\n        \"instead. `B_v` from the TabNet paper.\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"bn_virtual_bs\"],\n    )\n\n    sparsity: float = schema_utils.FloatRange(\n        default=1e-4,\n        description=\"Multiplier of the sparsity inducing loss. `lambda_sparse` in (Arik and Pfister, 2019).\",\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"sparsity\"],\n    )\n\n    entmax_mode: str = schema_utils.StringOptions(\n        [\"entmax15\", \"sparsemax\", \"constant\", \"adaptive\"],\n        default=\"sparsemax\",\n        description=(\n            \"Entmax is a sparse family of probability mapping which generalizes softmax and sparsemax. \"\n            \"`entmax_mode` controls the sparsity\"\n        ),\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"entmax_mode\"],\n    )\n\n    entmax_alpha: float = schema_utils.FloatRange(\n        default=1.5,\n        min=1,\n        max=2,\n        description=(\n            \"Must be a number between 1.0 and 2.0. If entmax_mode is `adaptive`, \"\n            \"`entmax_alpha` is used as the initial value for the learnable parameter. \"\n            \"1 corresponds to softmax, 2 is sparsemax.\"\n        ),\n        parameter_metadata=COMBINER_METADATA[\"tabnet\"][\"entmax_alpha\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/transformer.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.common_transformer_options import CommonTransformerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_combiner_config(\"transformer\")\n@ludwig_dataclass\nclass TransformerCombinerConfig(BaseCombinerConfig, CommonTransformerConfig):\n    \"\"\"Parameters for transformer combiner.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"transformer\",\n        description=COMBINER_METADATA[\"transformer\"][\"type\"].long_description,\n    )\n\n    reduce_output: str | None = schema_utils.ReductionOptions(\n        default=\"mean\",\n        description=\"Strategy to use to aggregate the output of the transformer.\",\n        parameter_metadata=COMBINER_METADATA[\"transformer\"][\"reduce_output\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/combiners/utils.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import TYPE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.metadata import COMBINER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata\nfrom ludwig.utils.registry import Registry\n\nDEFAULT_VALUE = \"concat\"\nDESCRIPTION = \"Select the combiner type.\"\n\ncombiner_config_registry = Registry[type[BaseCombinerConfig]]()\n\n\n@DeveloperAPI\ndef register_combiner_config(name: str):\n    def wrap(cls: type[BaseCombinerConfig]):\n        combiner_config_registry[name] = cls\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_combiner_registry():\n    return combiner_config_registry\n\n\n@DeveloperAPI\ndef get_combiner_jsonschema():\n    \"\"\"Returns a JSON schema structured to only require a `type` key and then conditionally apply a corresponding\n    combiner's field constraints.\"\"\"\n    combiner_types = sorted(list(combiner_config_registry.keys()))\n    parameter_metadata = convert_metadata_to_json(\n        ParameterMetadata.from_dict(\n            {\n                \"commonly_used\": True,\n                \"expected_impact\": 3,\n                \"ui_display_name\": \"Combiner Type\",\n            }\n        )\n    )\n    return {\n        \"type\": \"object\",\n        \"properties\": {\n            \"type\": {\n                \"type\": \"string\",\n                \"enum\": combiner_types,\n                \"enumDescriptions\": get_combiner_descriptions(),\n                \"default\": DEFAULT_VALUE,\n                \"title\": \"combiner_options\",\n                \"description\": DESCRIPTION,\n                \"parameter_metadata\": parameter_metadata,\n            },\n        },\n        \"allOf\": get_combiner_conds(),\n        \"required\": [\"type\"],\n    }\n\n\n@DeveloperAPI\ndef get_combiner_descriptions():\n    \"\"\"This function returns a dictionary of combiner descriptions available at the type selection.\n\n    The process works as follows - 1) Get a dictionary of valid combiners from the combiner config registry,\n    but inverse the key/value pairs since we need to index `valid_combiners` later with an altered version\n    of the combiner config class name. 2) Loop through Combiner Metadata entries, if a metadata entry has a\n    combiner name that matches a valid combiner, add the description metadata to the output dictionary.\n\n    Returns:\n        dict: A dictionary of combiner descriptions.\n    \"\"\"\n    return {k: convert_metadata_to_json(v[TYPE]) for k, v in COMBINER_METADATA.items() if k in combiner_config_registry}\n\n\n@DeveloperAPI\ndef get_combiner_conds() -> list[dict[str, Any]]:\n    \"\"\"Returns a list of if-then JSON clauses for each combiner type in `combiner_registry` and its properties'\n    constraints.\"\"\"\n    combiner_types = sorted(list(combiner_config_registry.keys()))\n    conds = []\n    for combiner_type in combiner_types:\n        combiner_cls = combiner_config_registry[combiner_type]\n        schema_cls = combiner_cls\n        combiner_schema = schema_utils.unload_jsonschema_from_marshmallow_class(schema_cls)\n        combiner_props = combiner_schema[\"properties\"]\n        schema_utils.remove_duplicate_fields(combiner_props)\n        combiner_cond = schema_utils.create_cond({\"type\": combiner_type}, combiner_props)\n        conds.append(combiner_cond)\n    return conds\n\n\nclass CombinerSelection(schema_utils.TypeSelection):\n    def __init__(self):\n        # For registration of all combiners\n        import ludwig.combiners.combiners  # noqa\n\n        super().__init__(registry=combiner_config_registry, default_value=DEFAULT_VALUE, description=DESCRIPTION)\n\n    def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n        return self.registry[key]\n\n    def _jsonschema_type_mapping(self):\n        return get_combiner_jsonschema()\n"
  },
  {
    "path": "ludwig/schema/common_fields.py",
    "content": "from dataclasses import Field\n\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import COMMON_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import ParameterMetadata\nfrom ludwig.utils.torch_utils import initializer_registry\n\n\ndef DropoutField(default: float = 0.0, description: str = None, parameter_metadata: ParameterMetadata = None) -> Field:\n    description = description or \"Default dropout rate applied to fully connected layers.\"\n    full_description = description + (\n        \" Increasing dropout is a common form of regularization to combat overfitting. \"\n        \"The dropout is expressed as the probability of an element to be zeroed out (0.0 means no dropout).\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"dropout\"]\n    return schema_utils.FloatRange(\n        default=default,\n        min=0,\n        max=1,\n        description=full_description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef ResidualField(\n    default: bool = False, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"Whether to add a residual connection to each fully connected layer block. \"\n        \"Requires all fully connected layers to have the same `output_size`.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"residual\"]\n    return schema_utils.Boolean(\n        default=False,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef NumFCLayersField(\n    default: int = 0, description: str = None, parameter_metadata: ParameterMetadata = None, non_zero=False\n) -> Field:\n    assert (not non_zero) or (default > 0 and non_zero)\n\n    description = description or \"Number of stacked fully connected layers to apply.\"\n    full_description = description + (\n        \" Increasing layers adds capacity to the model, enabling it to learn more complex feature interactions.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"num_fc_layers\"]\n\n    # When using a dense encoder, the number of fully connected layers must be strictly greater than 0.\n    if non_zero:\n        return schema_utils.PositiveInteger(\n            default=default, allow_none=False, description=full_description, parameter_metadata=parameter_metadata\n        )\n    return schema_utils.NonNegativeInteger(\n        default=default,\n        allow_none=False,\n        description=full_description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef NormField(\n    default: str | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or \"Default normalization applied at the beginnging of fully connected layers.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"norm\"]\n    return schema_utils.StringOptions(\n        [\"batch\", \"layer\", \"ghost\"],\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef NormParamsField(description: str = None, parameter_metadata: ParameterMetadata = None) -> Field:\n    description = description or \"Default parameters passed to the `norm` module.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"norm_params\"]\n    return schema_utils.Dict(\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef FCLayersField(description: str = None, parameter_metadata: ParameterMetadata = None) -> Field:\n    description = description or (\n        \"List of dictionaries containing the parameters of all the fully connected layers. \"\n        \"The length of the list determines the number of stacked fully connected layers \"\n        \"and the content of each dictionary determines the parameters for a specific layer. \"\n        \"The available parameters for each layer are: `activation`, `dropout`, `norm`, `norm_params`, \"\n        \"`output_size`, `use_bias`, `bias_initializer` and `weights_initializer`. If any of those values \"\n        \"is missing from the dictionary, the default one provided as a standalone parameter will be used instead.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"fc_layers\"]\n    return schema_utils.DictList(\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\nINITIALIZER_SUFFIX = \"\"\"\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\"\"\"\n\n\ndef BiasInitializerField(\n    default: str = \"zeros\", description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    initializers_str = \", \".join([f\"`{i}`\" for i in initializer_registry.keys()])\n    description = description or \"Initializer for the bias vector.\"\n    full_description = f\"{description} Options: {initializers_str}. {INITIALIZER_SUFFIX}\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"bias_initializer\"]\n    return schema_utils.InitializerOrDict(\n        default=default,\n        description=full_description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef WeightsInitializerField(\n    default: str = \"xavier_uniform\", description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    initializers_str = \", \".join([f\"`{i}`\" for i in initializer_registry.keys()])\n    description = description or \"Initializer for the weight matrix.\"\n    full_description = f\"{description} Options: {initializers_str}. {INITIALIZER_SUFFIX}\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"weights_initializer\"]\n    return schema_utils.InitializerOrDict(\n        default=default,\n        description=full_description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef EmbeddingInitializerField(\n    default: str | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or \"Initializer for the embedding matrix.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"embedding_initializer\"]\n    return schema_utils.StringOptions(\n        list(initializer_registry.keys()),\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef EmbeddingSizeField(\n    default: int = 256, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"The maximum embedding size. The actual size will be `min(vocabulary_size, embedding_size)` for \"\n        \"`dense` representations and exactly `vocabulary_size` for the `sparse` encoding, where `vocabulary_size` \"\n        \"is the number of unique strings appearing in the training set input column plus the number of \"\n        \"special tokens (`<UNK>`, `<PAD>`, `<SOS>`, `<EOS>`).\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"embedding_size\"]\n    return schema_utils.PositiveInteger(\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef EmbeddingsOnCPUField(\n    default: bool = False, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"Whether to force the placement of the embedding matrix in regular memory and have the CPU resolve them. \"\n        \"By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster access, \"\n        \"but in some cases the embedding matrix may be too large. This parameter forces the placement of the \"\n        \"embedding matrix in regular memory and the CPU is used for embedding lookup, slightly slowing down the \"\n        \"process as a result of data transfer between CPU and GPU memory.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"embeddings_on_cpu\"]\n    return schema_utils.Boolean(\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef EmbeddingsTrainableField(\n    default: bool = True, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"If `true` embeddings are trained during the training process, if `false` embeddings are fixed. \"\n        \"It may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter \"\n        \"has effect only when `representation` is `dense`; `sparse` one-hot encodings are not trainable.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"embeddings_trainable\"]\n    return schema_utils.Boolean(\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef PretrainedEmbeddingsField(\n    default: str | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"Path to a file containing pretrained embeddings. By default `dense` embeddings are initialized \"\n        \"randomly, but this parameter allows to specify a path to a file containing embeddings in the \"\n        \"[GloVe format](https://nlp.stanford.edu/projects/glove/). When the file containing the embeddings is \"\n        \"loaded, only the embeddings with labels present in the vocabulary are kept, the others are discarded. \"\n        \"If the vocabulary contains strings that have no match in the embeddings file, their embeddings are \"\n        \"initialized with the average of all other embedding plus some random noise to make them different \"\n        \"from each other. This parameter has effect only if `representation` is `dense`.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"pretrained_embeddings\"]\n    return schema_utils.String(\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef MaxSequenceLengthField(\n    default: int | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or \"[internal] Maximum sequence length from preprocessing.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"max_sequence_length\"]\n    return schema_utils.PositiveInteger(\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef VocabField(\n    default: list | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or \"[internal] Vocabulary for the encoder from preprocessing.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"vocab\"]\n    return schema_utils.List(\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef VocabSizeField(\n    default: list | None = None, description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or \"[internal] Size of the vocabulary from preprocessing.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"vocab_size\"]\n    return schema_utils.PositiveInteger(\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef RepresentationField(\n    default: str = \"dense\", description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"Representation of the embedding. `dense` means the embeddings are initialized randomly, \"\n        \"`sparse` means they are initialized to be one-hot encodings.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"representation\"]\n    return schema_utils.StringOptions(\n        [\"dense\", \"sparse\"],\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\ndef ReduceOutputField(\n    default: str | None = \"sum\", description: str = None, parameter_metadata: ParameterMetadata = None\n) -> Field:\n    description = description or (\n        \"How to reduce the output tensor along the `s` sequence length dimension if the rank of the \"\n        \"tensor is greater than 2.\"\n    )\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"reduce_output\"]\n    return schema_utils.ReductionOptions(\n        default=default,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n"
  },
  {
    "path": "ludwig/schema/decoders/__init__.py",
    "content": "# Register all decoders\nimport ludwig.schema.decoders.base\nimport ludwig.schema.decoders.image_decoders  # noqa\nimport ludwig.schema.decoders.llm_decoders  # noqa\nimport ludwig.schema.decoders.sequence_decoders  # noqa\n"
  },
  {
    "path": "ludwig/schema/decoders/base.py",
    "content": "from abc import ABC\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, CATEGORY, MODEL_ECD, MODEL_LLM, NUMBER, SET, TIMESERIES, VECTOR\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.utils import register_decoder_config\nfrom ludwig.schema.metadata import DECODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseDecoderConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Base class for decoders.\"\"\"\n\n    type: str = schema_utils.StringOptions(\n        [\"regressor\", \"classifier\", \"projector\", \"generator\", \"tagger\"],\n        default=None,\n        allow_none=True,\n        description=\"The type of decoder to use.\",\n        parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"type\"],\n    )\n\n    fc_layers: list[dict] = common_fields.FCLayersField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField(\n        description=\"Number of fully-connected layers if `fc_layers` not specified.\"\n    )\n\n    fc_output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Output size of fully connected stack.\",\n        parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_output_size\"],\n    )\n\n    fc_use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector in the fc_stack.\",\n        parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_use_bias\"],\n    )\n\n    fc_weights_initializer: str | dict = schema_utils.OneOfOptionsField(\n        default=\"xavier_uniform\",\n        allow_none=True,\n        description=\"The weights initializer to use for the layers in the fc_stack\",\n        field_options=[\n            schema_utils.InitializerOptions(\n                description=\"Preconfigured initializer to use for the layers in the fc_stack.\",\n                parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_weights_initializer\"],\n            ),\n            schema_utils.Dict(\n                description=\"Custom initializer to use for the layers in the fc_stack.\",\n                parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_weights_initializer\"],\n            ),\n        ],\n        parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_weights_initializer\"],\n    )\n\n    fc_bias_initializer: str | dict = schema_utils.OneOfOptionsField(\n        default=\"zeros\",\n        allow_none=True,\n        description=\"The bias initializer to use for the layers in the fc_stack\",\n        field_options=[\n            schema_utils.InitializerOptions(\n                description=\"Preconfigured bias initializer to use for the layers in the fc_stack.\",\n                parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_bias_initializer\"],\n            ),\n            schema_utils.Dict(\n                description=\"Custom bias initializer to use for the layers in the fc_stack.\",\n                parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_bias_initializer\"],\n            ),\n        ],\n        parameter_metadata=DECODER_METADATA[\"BaseDecoder\"][\"fc_bias_initializer\"],\n    )\n\n    fc_norm: str = common_fields.NormField()\n\n    fc_norm_params: dict = common_fields.NormParamsField()\n\n    fc_activation: str = schema_utils.ActivationOptions(default=\"relu\")\n\n    fc_dropout: float = common_fields.DropoutField()\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass PassthroughDecoderConfig(BaseDecoderConfig):\n    \"\"\"PassthroughDecoderConfig is a dataclass that configures the parameters used for a passthrough decoder.\"\"\"\n\n    @classmethod\n    def module_name(cls):\n        return \"PassthroughDecoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"passthrough\",\n        description=\"The passthrough decoder simply returns the raw numerical values coming from the combiner as \"\n        \"outputs\",\n        parameter_metadata=DECODER_METADATA[\"PassthroughDecoder\"][\"type\"],\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"PassthroughDecoder\"][\"input_size\"],\n    )\n\n\n@DeveloperAPI\n@register_decoder_config(\"regressor\", [BINARY, NUMBER], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass RegressorConfig(BaseDecoderConfig):\n    \"\"\"RegressorConfig is a dataclass that configures the parameters used for a regressor decoder.\"\"\"\n\n    @classmethod\n    def module_name(cls):\n        return \"Regressor\"\n\n    type: str = schema_utils.ProtectedString(\n        \"regressor\",\n        description=DECODER_METADATA[\"Regressor\"][\"type\"].long_description,\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"Regressor\"][\"input_size\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Regressor\"][\"use_bias\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer for the weight matrix.\",\n        parameter_metadata=DECODER_METADATA[\"Regressor\"][\"weights_initializer\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer for the bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Regressor\"][\"bias_initializer\"],\n    )\n\n\n@DeveloperAPI\n@register_decoder_config(\"projector\", [VECTOR, TIMESERIES], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass ProjectorConfig(BaseDecoderConfig):\n    \"\"\"ProjectorConfig is a dataclass that configures the parameters used for a projector decoder.\"\"\"\n\n    @classmethod\n    def module_name(cls):\n        return \"Projector\"\n\n    type: str = schema_utils.ProtectedString(\n        \"projector\",\n        description=DECODER_METADATA[\"Projector\"][\"type\"].long_description,\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"input_size\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the output of the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"output_size\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"use_bias\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer for the weight matrix.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"weights_initializer\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer for the bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"bias_initializer\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        default=None,\n        description=\" Indicates the activation function applied to the output.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"activation\"],\n    )\n\n    multiplier: float = schema_utils.FloatRange(\n        default=1.0,\n        min=0,\n        min_inclusive=False,\n        description=(\n            \"Multiplier to scale the activated outputs by. Useful when setting `activation` to something \"\n            \"that outputs a value between [-1, 1] like tanh to re-scale values back to order of magnitude of \"\n            \"the data you're trying to predict. A good rule of thumb in such cases is to pick a value like \"\n            \"`x * (max - min)` where x is a scalar in the range [1, 2]. For example, if you're trying to predict \"\n            \"something like temperature, it might make sense to pick a multiplier on the order of `100`.\"\n        ),\n    )\n\n    clip: list[int] | tuple[int] = schema_utils.FloatRangeTupleDataclassField(\n        n=2,\n        default=None,\n        allow_none=True,\n        min=0,\n        max=999999999,\n        description=\"Clip the output of the decoder to be within the given range.\",\n        parameter_metadata=DECODER_METADATA[\"Projector\"][\"clip\"],\n    )\n\n\n@DeveloperAPI\n@register_decoder_config(\"classifier\", [CATEGORY, SET], model_types=[MODEL_ECD, MODEL_LLM])\n@ludwig_dataclass\nclass ClassifierConfig(BaseDecoderConfig):\n    @classmethod\n    def module_name(cls):\n        return \"Classifier\"\n\n    type: str = schema_utils.ProtectedString(\n        \"classifier\",\n        description=DECODER_METADATA[\"Classifier\"][\"type\"].long_description,\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"Classifier\"][\"input_size\"],\n    )\n\n    num_classes: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of classes to predict.\",\n        parameter_metadata=DECODER_METADATA[\"Classifier\"][\"num_classes\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Classifier\"][\"use_bias\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer for the weight matrix.\",\n        parameter_metadata=DECODER_METADATA[\"Classifier\"][\"weights_initializer\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer for the bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"Classifier\"][\"bias_initializer\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/decoders/image_decoders.py",
    "content": "from typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import IMAGE, MODEL_ECD\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import register_decoder_config\nfrom ludwig.schema.metadata import DECODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig\n\n\nclass ImageDecoderConfig(BaseDecoderConfig):\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"ImagePreprocessingConfig\"):\n        preprocessing.requires_equal_dimensions = False\n        preprocessing.height = None\n        preprocessing.width = None\n\n\n@DeveloperAPI\n@register_decoder_config(\"unet\", [IMAGE], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass UNetDecoderConfig(ImageDecoderConfig):\n    @staticmethod\n    def module_name():\n        return \"UNetDecoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"unet\",\n        description=DECODER_METADATA[\"UNetDecoder\"][\"type\"].long_description,\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"input_size\"],\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the output image.\",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the output image.\",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"width\"],\n    )\n\n    num_channels: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels in the output image. \",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"num_channels\"],\n    )\n\n    conv_norm: str | None = schema_utils.StringOptions(\n        [\"batch\"],\n        default=\"batch\",\n        allow_none=True,\n        description=\"This is the default norm that will be used for each double conv layer.\" \"It can be null or batch.\",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"conv_norm\"],\n    )\n\n    num_classes: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of classes to predict in the output. \",\n        parameter_metadata=DECODER_METADATA[\"UNetDecoder\"][\"num_classes\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/decoders/llm_decoders.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CATEGORY, MODEL_LLM, TEXT\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import register_decoder_config\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseExtractorDecoderConfig(BaseMarshmallowConfig):\n    tokenizer: str = \"hf_tokenizer\"\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"\",\n        allow_none=True,\n        description=\"Path to the pretrained model or model identifier from huggingface.co/models.\",\n    )\n\n    vocab_file: str = schema_utils.String(\n        default=\"\",\n        allow_none=True,\n        description=\"Path to the vocabulary file.\",\n    )\n\n    max_new_tokens: int = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"Maximum number of new tokens that will be generated.\",\n    )\n\n\n@DeveloperAPI\n@register_decoder_config(\"text_extractor\", [TEXT], model_types=[MODEL_LLM])\n@ludwig_dataclass\nclass TextExtractorDecoderConfig(BaseExtractorDecoderConfig, BaseDecoderConfig):\n    @classmethod\n    def module_name(cls):\n        return \"TextExtractorDecoder\"\n\n    type: str = schema_utils.ProtectedString(\"text_extractor\")\n\n\n@DeveloperAPI\n@register_decoder_config(\"category_extractor\", [CATEGORY], model_types=[MODEL_LLM])\n@ludwig_dataclass\nclass CategoryExtractorDecoderConfig(BaseExtractorDecoderConfig, BaseDecoderConfig):\n    @classmethod\n    def module_name(cls):\n        return \"CategoryExtractorDecoder\"\n\n    type: str = schema_utils.ProtectedString(\"category_extractor\")\n\n    # Match is a dict of label class\n    match: dict[str, dict[str, Any]] = schema_utils.Dict(\n        default=None,\n        allow_none=False,\n        description=\"A dictionary of label classes and their corresponding \"\n        \"match patterns definitions that will be used to parse the output \"\n        \"of the LLM.\",\n    )\n\n    str2idx: dict[str, int] = schema_utils.Dict(\n        default=None,\n        allow_none=True,\n        description=\"A dictionary of label classes and their corresponding \"\n        \"indices that will be used to parse the output of the LLM.\",\n    )\n\n    fallback_label: str = schema_utils.String(\n        default=\"\",\n        allow_none=True,\n        description=\"The label to use if the parser fails to parse the input.\",\n    )\n"
  },
  {
    "path": "ludwig/schema/decoders/sequence_decoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, SEQUENCE, TEXT\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import register_decoder_config\nfrom ludwig.schema.metadata import DECODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_decoder_config(\"generator\", [SEQUENCE, TEXT], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass SequenceGeneratorDecoderConfig(BaseDecoderConfig):\n    @staticmethod\n    def module_name():\n        return \"SequenceGeneratorDecoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"generator\",\n        description=DECODER_METADATA[\"SequenceGeneratorDecoder\"][\"type\"].long_description,\n    )\n\n    vocab_size: int = common_fields.VocabSizeField()\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    cell_type: str = schema_utils.StringOptions(\n        [\"rnn\", \"lstm\", \"gru\"],\n        default=\"gru\",\n        description=\"Type of recurrent cell to use.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceGeneratorDecoder\"][\"cell_type\"],\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceGeneratorDecoder\"][\"input_size\"],\n    )\n\n    reduce_input: str = schema_utils.StringOptions(\n        [\"sum\", \"mean\", \"avg\", \"max\", \"concat\", \"last\"],\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=DECODER_METADATA[\"SequenceGeneratorDecoder\"][\"reduce_input\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked recurrent layers.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceGeneratorDecoder\"][\"num_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_decoder_config(\"tagger\", [SEQUENCE, TEXT], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass SequenceTaggerDecoderConfig(BaseDecoderConfig):\n    @classmethod\n    def module_name(cls):\n        return \"SequenceTaggerDecoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"tagger\",\n        description=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"type\"].long_description,\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"input_size\"],\n    )\n\n    vocab_size: int = common_fields.VocabSizeField()\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    use_attention: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to apply a multi-head self attention layer before prediction.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"use_attention\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"use_bias\"],\n    )\n\n    attention_embedding_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The embedding size of the multi-head self attention layer.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"attention_embedding_size\"],\n    )\n\n    attention_num_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"The number of attention heads in the multi-head self attention layer.\",\n        parameter_metadata=DECODER_METADATA[\"SequenceTaggerDecoder\"][\"attention_num_heads\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/decoders/utils.py",
    "content": "from dataclasses import Field\nfrom typing import Any, TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, TYPE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import DECODER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json\nfrom ludwig.utils.registry import Registry\n\nif TYPE_CHECKING:\n    from ludwig.schema.decoders.base import BaseDecoderConfig\n\n\ndecoder_config_registry = Registry()\n\n\n@DeveloperAPI\ndef register_decoder_config(name: str, features: str | list[str], model_types: list[str] | None = None):\n    if model_types is None:\n        model_types = [MODEL_ECD]\n\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for model_type in model_types:\n            for feature in features:\n                key = (model_type, feature)\n                feature_registry = decoder_config_registry.get(key, {})\n                feature_registry[name] = cls\n                decoder_config_registry[key] = feature_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_decoder_cls(model_type: str, feature: str, name: str):\n    return decoder_config_registry[(model_type, feature)][name]\n\n\n@DeveloperAPI\ndef get_decoder_classes(model_type: str, feature: str) -> dict[str, type[\"BaseDecoderConfig\"]]:\n    return decoder_config_registry[(model_type, feature)]\n\n\n@DeveloperAPI\ndef get_decoder_descriptions(model_type: str, feature_type: str):\n    \"\"\"This function returns a dictionary of decoder descriptions available at the type selection.\n\n    The process works as follows - 1) Get a dictionary of valid decoders from the decoder config registry,\n    but inverse the key/value pairs since we need to index `valid_decoders` later with an altered version\n    of the decoder config class name. 2) Loop through Decoder Metadata entries, if a metadata entry has a\n    decoder name that matches a valid decoder, add the description metadata to the output dictionary.\n\n    Args:\n        model_type (str): The model type to get decoder descriptions for\n        feature_type (str): The feature type to get decoder descriptions for\n    Returns:\n        dict: A dictionary of decoder descriptions\n    \"\"\"\n    output = {}\n    valid_decoders = {\n        cls.module_name() if hasattr(cls, \"module_name\") else None: registered_name\n        for registered_name, cls in get_decoder_classes(model_type, feature_type).items()\n    }\n\n    for k, v in DECODER_METADATA.items():\n        if k in valid_decoders.keys():\n            output[valid_decoders[k]] = convert_metadata_to_json(v[TYPE])\n\n    return output\n\n\n@DeveloperAPI\ndef get_decoder_conds(decoder_classes: dict[str, type[\"BaseDecoderConfig\"]]) -> list[dict[str, Any]]:\n    \"\"\"Returns a JSON schema of conditionals to validate against decoder types for specific feature types.\"\"\"\n    conds = []\n    for decoder_type, decoder_cls in decoder_classes.items():\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(decoder_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        decoder_cond = schema_utils.create_cond(\n            {\"type\": decoder_type},\n            other_props,\n        )\n        conds.append(decoder_cond)\n    return conds\n\n\n@DeveloperAPI\ndef DecoderDataclassField(model_type: str, feature_type: str, default: str) -> Field:\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify a decoder config.\n\n    Returns: Initialized dataclass field that converts an untyped dict with params to a decoder config.\n    \"\"\"\n    decoder_registry = get_decoder_classes(model_type, feature_type)\n\n    class DecoderSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(registry=decoder_registry, default_value=default, allow_str_value=True)\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return decoder_registry[key]\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": list(decoder_registry.keys()),\n                        \"enumDescriptions\": get_decoder_descriptions(model_type, feature_type),\n                        \"default\": default,\n                    },\n                },\n                \"title\": \"decoder_options\",\n                \"allOf\": get_decoder_conds(decoder_registry),\n            }\n\n    return DecoderSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/defaults/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/schema/defaults/base.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseDefaultsConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base defaults config class.\"\"\"\n"
  },
  {
    "path": "ludwig/schema/defaults/ecd.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    DATE,\n    H3,\n    IMAGE,\n    NUMBER,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    VECTOR,\n)\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.defaults.base import BaseDefaultsConfig\nfrom ludwig.schema.defaults.utils import DefaultsDataclassField\nfrom ludwig.schema.features.base import BaseFeatureConfig\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ECDDefaultsConfig(BaseDefaultsConfig):\n    audio: BaseFeatureConfig = DefaultsDataclassField(feature_type=AUDIO)\n\n    bag: BaseFeatureConfig = DefaultsDataclassField(feature_type=BAG)\n\n    binary: BaseFeatureConfig = DefaultsDataclassField(feature_type=BINARY)\n\n    category: BaseFeatureConfig = DefaultsDataclassField(feature_type=CATEGORY)\n\n    date: BaseFeatureConfig = DefaultsDataclassField(feature_type=DATE)\n\n    h3: BaseFeatureConfig = DefaultsDataclassField(feature_type=H3)\n\n    image: BaseFeatureConfig = DefaultsDataclassField(feature_type=IMAGE)\n\n    number: BaseFeatureConfig = DefaultsDataclassField(feature_type=NUMBER)\n\n    sequence: BaseFeatureConfig = DefaultsDataclassField(feature_type=SEQUENCE)\n\n    set: BaseFeatureConfig = DefaultsDataclassField(feature_type=SET)\n\n    text: BaseFeatureConfig = DefaultsDataclassField(feature_type=TEXT)\n\n    timeseries: BaseFeatureConfig = DefaultsDataclassField(feature_type=TIMESERIES)\n\n    vector: BaseFeatureConfig = DefaultsDataclassField(feature_type=VECTOR)\n\n\n@DeveloperAPI\nclass ECDDefaultsField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(ECDDefaultsConfig)\n"
  },
  {
    "path": "ludwig/schema/defaults/llm.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import TEXT\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.defaults.base import BaseDefaultsConfig\nfrom ludwig.schema.defaults.utils import DefaultsDataclassField\nfrom ludwig.schema.features.base import BaseFeatureConfig\nfrom ludwig.schema.features.utils import llm_defaults_config_registry\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass LLMDefaultsConfig(BaseDefaultsConfig):\n    text: BaseFeatureConfig = DefaultsDataclassField(feature_type=TEXT, defaults_registry=llm_defaults_config_registry)\n\n\n@DeveloperAPI\nclass LLMDefaultsField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(LLMDefaultsConfig)\n"
  },
  {
    "path": "ludwig/schema/defaults/utils.py",
    "content": "from dataclasses import field\n\nimport ludwig.schema.utils as schema_utils\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.features.utils import ecd_defaults_config_registry\nfrom ludwig.utils.registry import Registry\n\n\n@DeveloperAPI\ndef DefaultsDataclassField(feature_type: str, defaults_registry: Registry = ecd_defaults_config_registry):\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify a nested default\n    config for a specific feature type.\n\n    Returns: Initialized dataclass field that converts an untyped dict with params to a defaults config.\n    \"\"\"\n\n    class DefaultMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field that deserializes a dict for a valid defaults config from the feature_registry and creates\n        a corresponding JSON schema for external usage.\"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return None\n            if isinstance(value, dict):\n                defaults_class = defaults_registry[feature_type]\n                try:\n                    return defaults_class.Schema().load(value)\n                except (TypeError, ConfigValidationError) as error:\n                    raise ConfigValidationError(f\"Invalid params: {value}, see `{attr}` definition. Error: {error}\")\n            raise ConfigValidationError(f\"Invalid params: {value}\")\n\n        def _jsonschema_type_mapping(self):\n            defaults_cls = defaults_registry[feature_type]\n            props = schema_utils.unload_jsonschema_from_marshmallow_class(defaults_cls)[\"properties\"]\n            return {\n                \"type\": \"object\",\n                \"properties\": props,\n                \"additionalProperties\": False,\n                \"title\": \"defaults_options\",\n            }\n\n    try:\n        defaults_cls = defaults_registry[feature_type]\n        dump_default = defaults_cls.Schema().dump({})\n        load_default = lambda: defaults_cls.Schema().load({})\n\n        return field(\n            metadata={\n                \"marshmallow_field\": DefaultMarshmallowField(\n                    allow_none=False,\n                    dump_default=dump_default,\n                    load_default=load_default,\n                )\n            },\n            default_factory=load_default,\n        )\n    except Exception as e:\n        raise ConfigValidationError(\n            f\"Unsupported feature type: {feature_type}. Allowed: {defaults_registry.keys()}. \" f\"Details: {e}\"\n        )\n"
  },
  {
    "path": "ludwig/schema/encoders/__init__.py",
    "content": "# Register all encoder schemas\nimport ludwig.schema.encoders.bag_encoders\nimport ludwig.schema.encoders.category_encoders\nimport ludwig.schema.encoders.date_encoders\nimport ludwig.schema.encoders.h3_encoders\nimport ludwig.schema.encoders.image\nimport ludwig.schema.encoders.sequence_encoders\nimport ludwig.schema.encoders.set_encoders\nimport ludwig.schema.encoders.text_encoders  # noqa\n"
  },
  {
    "path": "ludwig/schema/encoders/bag_encoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BAG\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_encoder_config(\"embed\", BAG)\n@ludwig_dataclass\nclass BagEmbedWeightedConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"BagEmbedWeighted\"\n\n    type: str = schema_utils.ProtectedString(\n        \"embed\",\n        description=ENCODER_METADATA[\"BagEmbedWeighted\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"activation\"],\n    )\n\n    vocab: list[str] = schema_utils.List(\n        default=None,\n        description=\"Vocabulary of the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"vocab\"],\n    )\n\n    representation: str = schema_utils.StringOptions(\n        [\"dense\", \"sparse\"],\n        default=\"dense\",\n        description=\"The representation of the embedding. Either dense or sparse.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"representation\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=50,\n        description=\"The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for \"\n        \"dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size \"\n        \"is the number of different strings appearing in the training set in the input column (plus 1 for \"\n        \"the unknown token placeholder <UNK>).\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"embedding_size\"],\n    )\n\n    force_embedding_size: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Force the embedding size to be equal to the vocabulary size. This parameter has effect only if \"\n        \"representation is dense.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"force_embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster \"\n        \"access, but in some cases the embedding matrix may be too large. This parameter forces the \"\n        \"placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, \"\n        \"slightly slowing down the process as a result of data transfer between CPU and GPU memory.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"embeddings_on_cpu\"],\n    )\n\n    embeddings_trainable: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true embeddings are trained during the training process, if false embeddings are fixed. It \"\n        \"may be useful when loading pretrained embeddings for avoiding fine tuning them. This parameter \"\n        \"has effect only when representation is dense as sparse one-hot encodings are not trainable.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"embeddings_trainable\"],\n    )\n\n    pretrained_embeddings: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"By default dense embeddings are initialized randomly, but this parameter allows to specify a \"\n        \"path to a file containing embeddings in the GloVe format. When the file containing the \"\n        \"embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, \"\n        \"the others are discarded. If the vocabulary contains strings that have no match in the \"\n        \"embeddings file, their embeddings are initialized with the average of all other embedding plus \"\n        \"some random noise to make them different from each other. This parameter has effect only if \"\n        \"representation is dense.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"pretrained_embeddings\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"weights_initializer\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If output_size is not already specified in fc_layers this is the default output_size that will \"\n        \"be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"This is the number of stacked fully connected layers that the input to the feature passes \"\n        \"through. Their output is projected in the feature's output space.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BagEmbedWeighted\"][\"fc_layers\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/base.py",
    "content": "from abc import ABC\nfrom typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, MODEL_ECD, MODEL_LLM, NUMBER, TEXT, TIMESERIES, VECTOR\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseEncoderConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Base class for encoders.\"\"\"\n\n    type: str\n\n    skip: bool = schema_utils.Boolean(\n        False,\n        \"[internal] Whether to skip encoder and use input as output.\",\n        parameter_metadata=ENCODER_METADATA[\"BaseEncoder\"][\"skip\"],\n    )\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"BasePreprocessingConfig\"):\n        pass\n\n    def is_pretrained(self) -> bool:\n        return False\n\n    def can_cache_embeddings(self) -> bool:\n        return False\n\n\n@DeveloperAPI\n@register_encoder_config(\"passthrough\", [TEXT], model_types=[MODEL_LLM])\n@register_encoder_config(\"passthrough\", [BINARY, NUMBER, VECTOR], model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass PassthroughEncoderConfig(BaseEncoderConfig):\n    \"\"\"PassthroughEncoderConfig is a dataclass that configures the parameters used for a passthrough encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"PassthroughEncoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"passthrough\",\n        description=ENCODER_METADATA[\"PassthroughEncoder\"][\"type\"].long_description,\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"dense\", [BINARY, NUMBER, VECTOR, TIMESERIES])\n@ludwig_dataclass\nclass DenseEncoderConfig(BaseEncoderConfig):\n    \"\"\"DenseEncoderConfig is a dataclass that configures the parameters used for a dense encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"DenseEncoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"dense\",\n        description=ENCODER_METADATA[\"DenseEncoder\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField()\n\n    activation: str = schema_utils.ActivationOptions()\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the dense encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DenseEncoder\"][\"input_size\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Size of the output of the feature.\",\n        parameter_metadata=ENCODER_METADATA[\"DenseEncoder\"][\"output_size\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"DenseEncoder\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | dict = common_fields.BiasInitializerField()\n\n    weights_initializer: str | dict = common_fields.WeightsInitializerField()\n\n    norm: str = common_fields.NormField()\n\n    norm_params: dict = common_fields.NormParamsField()\n\n    num_layers: int = common_fields.NumFCLayersField(default=1, non_zero=True)\n\n    fc_layers: list[dict] = common_fields.FCLayersField()\n"
  },
  {
    "path": "ludwig/schema/encoders/category_encoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CATEGORY, MODEL_ECD\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_encoder_config(\"passthrough\", CATEGORY, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass CategoricalPassthroughEncoderConfig(BaseEncoderConfig):\n    \"\"\"CategoricalPassthroughEncoderConfig is a dataclass that configures the parameters used for a categorical\n    passthrough encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"CategoricalPassthroughEncoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"passthrough\",\n        description=ENCODER_METADATA[\"PassthroughEncoder\"][\"type\"].long_description,\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"dense\", CATEGORY)\n@ludwig_dataclass\nclass CategoricalEmbedConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"CategoricalEmbed\"\n\n    type: str = schema_utils.ProtectedString(\n        \"dense\",\n        description=ENCODER_METADATA[\"CategoricalEmbed\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField()\n\n    vocab: list[str] = common_fields.VocabField()\n\n    embedding_initializer: str = common_fields.EmbeddingInitializerField()\n\n    embedding_size: int = common_fields.EmbeddingSizeField(\n        default=50,\n        description=(\n            \"The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for \"\n            \"dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size \"\n            \"is the number of different strings appearing in the training set in the column the feature is \"\n            \"named after (plus 1 for <UNK>).\"\n        ),\n    )\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n\n@DeveloperAPI\n@register_encoder_config(\"sparse\", CATEGORY)\n@ludwig_dataclass\nclass CategoricalSparseConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"CategorySparse\"\n\n    type: str = schema_utils.ProtectedString(\n        \"sparse\",\n        description=ENCODER_METADATA[\"CategoricalSparse\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField()\n\n    vocab: list[str] = common_fields.VocabField()\n\n    embedding_initializer: str = common_fields.EmbeddingInitializerField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    # TODO(travis): seems like this is not really a valid user option. We should probably just remove these\n    # params entirely and update the encoder implementation.\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField(default=False)\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n\n@DeveloperAPI\n@register_encoder_config(\"onehot\", CATEGORY, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass CategoricalOneHotEncoderConfig(BaseEncoderConfig):\n    \"\"\"CategoricalOneHotEncoderConfig is a dataclass that configures the parameters used for a categorical onehot\n    encoder.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"onehot\",\n        description=\"Type of encoder.\",\n    )\n\n    vocab: list[str] = common_fields.VocabField()\n\n    def can_cache_embeddings(self) -> bool:\n        return True\n"
  },
  {
    "path": "ludwig/schema/encoders/date_encoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DATE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_encoder_config(\"embed\", DATE)\n@ludwig_dataclass\nclass DateEmbedConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"DateEmbed\"\n\n    type: str = schema_utils.ProtectedString(\n        \"embed\",\n        description=ENCODER_METADATA[\"DateEmbed\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"activation\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"weights_initializer\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"The maximum embedding size adopted.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to force the placement of the embedding matrix in regular memory and have the CPU \"\n        \"resolve them.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"embeddings_on_cpu\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If an output_size is not already specified in fc_layers this is the default output_size that \"\n        \"will be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"num_fc_layers\"],\n    )\n\n    # TODO (Connor): Add nesting logic for fc_layers, see fully_connected_module.py\n    fc_layers: list[dict] = schema_utils.DictList(\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateEmbed\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"wave\", DATE)\n@ludwig_dataclass\nclass DateWaveConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"DateWave\"\n\n    type: str = schema_utils.ProtectedString(\n        \"wave\",\n        description=ENCODER_METADATA[\"DateWave\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"activation\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"weights_initializer\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If an output_size is not already specified in fc_layers this is the default output_size that \"\n        \"will be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"num_fc_layers\"],\n    )\n\n    # TODO (Connor): Add nesting logic for fc_layers, see fully_connected_module.py\n    fc_layers: list[dict] = schema_utils.DictList(\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DateWave\"][\"fc_layers\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/h3_encoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import H3\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_encoder_config(\"embed\", H3)\n@ludwig_dataclass\nclass H3EmbedConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"H3Embed\"\n\n    type: str = schema_utils.ProtectedString(\n        \"embed\",\n        description=ENCODER_METADATA[\"H3Embed\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"activation\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"weights_initializer\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"The maximum embedding size adopted.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to force the placement of the embedding matrix in regular memory and have the CPU \"\n        \"resolve them.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"embeddings_on_cpu\"],\n    )\n\n    reduce_output: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the output tensor along the `s` sequence length dimension if the rank of the \"\n        \"tensor is greater than 2.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"reduce_output\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If an output_size is not already specified in fc_layers this is the default output_size that \"\n        \"will be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3Embed\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"weighted_sum\", H3)\n@ludwig_dataclass\nclass H3WeightedSumConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"H3WeightedSum\"\n\n    type: str = schema_utils.ProtectedString(\n        \"weighted_sum\",\n        description=ENCODER_METADATA[\"H3WeightedSum\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"activation\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"weights_initializer\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"The maximum embedding size adopted.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to force the placement of the embedding matrix in regular memory and have the CPU \"\n        \"resolve them.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"embeddings_on_cpu\"],\n    )\n\n    should_softmax: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Determines if the weights of the weighted sum should be passed though a softmax layer before \"\n        \"being used.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"should_softmax\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If an output_size is not already specified in fc_layers this is the default output_size that \"\n        \"will be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"H3WeightedSum\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"rnn\", H3)\n@ludwig_dataclass\nclass H3RNNConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"H3RNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"rnn\",\n        description=ENCODER_METADATA[\"H3RNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout rate\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"dropout\"],\n    )\n\n    recurrent_dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout rate for the recurrent state\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"recurrent_dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        default=\"tanh\",\n        description=\"The activation function to use\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"activation\"],\n    )\n\n    recurrent_activation: str = schema_utils.ActivationOptions(\n        default=\"sigmoid\",\n        description=\"The activation function to use in the recurrent step\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"recurrent_activation\"],\n    )\n\n    cell_type: str = schema_utils.StringOptions(\n        [\"rnn\", \"lstm\", \"lstm_block\", \"ln\", \"lstm_cudnn\", \"gru\", \"gru_block\", \"gru_cudnn\"],\n        default=\"rnn\",\n        description=\"The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `lstm_block`, `lstm`, \"\n        \"`ln`, `lstm_cudnn`, `gru`, `gru_block`, `gru_cudnn`. For reference about the differences between \"\n        \"the cells please refer to PyTorch's documentation. We suggest to use the `block` variants on \"\n        \"CPU and the `cudnn` variants on GPU because of their increased speed. \",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"cell_type\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked recurrent layers.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"num_layers\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"The size of the hidden representation within the transformer block. It is usually the same as \"\n        \"the embedding_size, but if the two values are different, a projection layer will be added before \"\n        \"the first transformer block.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"hidden_size\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"use_bias\"],\n    )\n\n    unit_forget_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, add 1 to the bias of the forget gate at initialization\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"unit_forget_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"weights_initializer\"],\n    )\n\n    recurrent_initializer: str = schema_utils.InitializerOptions(\n        default=\"orthogonal\",\n        description=\"The initializer for recurrent matrix weights\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"recurrent_initializer\"],\n    )\n\n    reduce_output: str = schema_utils.ReductionOptions(\n        default=\"last\",\n        description=\"How to reduce the output tensor along the `s` sequence length dimension if the rank of the \"\n        \"tensor is greater than 2.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"reduce_output\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"The maximum embedding size adopted.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to force the placement of the embedding matrix in regular memory and have the CPU \"\n        \"resolve them.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"embeddings_on_cpu\"],\n    )\n\n    bidirectional: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, two recurrent networks will perform encoding in the forward and backward direction and \"\n        \"their outputs will be concatenated.\",\n        parameter_metadata=ENCODER_METADATA[\"H3RNN\"][\"bidirectional\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/image/__init__.py",
    "content": "import ludwig.schema.encoders.image.base\nimport ludwig.schema.encoders.image.timm  # noqa\nimport ludwig.schema.encoders.image.torchvision  # noqa\n"
  },
  {
    "path": "ludwig/schema/encoders/image/base.py",
    "content": "from typing import Any, TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import IMAGE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.torch_utils import initializer_registry\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig\n\n\nclass ImageEncoderConfig(BaseEncoderConfig):\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"ImagePreprocessingConfig\"):\n        preprocessing.requires_equal_dimensions = False\n        preprocessing.height = None\n        preprocessing.width = None\n\n\n@DeveloperAPI\n@register_encoder_config(\"stacked_cnn\", IMAGE)\n@ludwig_dataclass\nclass Stacked2DCNNConfig(ImageEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"Stacked2DCNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"stacked_cnn\",\n        description=ENCODER_METADATA[\"Stacked2DCNN\"][\"type\"].long_description,\n    )\n\n    conv_dropout: int | None = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout rate\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"conv_dropout\"],\n    )\n\n    conv_activation: str = schema_utils.ActivationOptions(\n        description=\"If an activation is not already specified in conv_layers this is the default activation that \"\n        \"will be used for each layer. It indicates the activation function applied to the output.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"conv_activation\"],\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"width\"],\n    )\n\n    num_channels: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels to use in the encoder. \",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"num_channels\"],\n    )\n\n    out_channels: int | None = schema_utils.NonNegativeInteger(\n        default=32,\n        description=\"Indicates the number of filters, and by consequence the output channels of the 2d convolution. \"\n        \"If out_channels is not already specified in conv_layers this is the default out_channels that \"\n        \"will be used for each layer. \",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"out_channels\"],\n    )\n\n    kernel_size: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=3,\n        description=\"An integer or pair of integers specifying the kernel size. A single integer specifies a square \"\n        \"kernel, while a pair of integers specifies the height and width of the kernel in that order (h, \"\n        \"w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for \"\n        \"each layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=False, description=\"\", default=3),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"kernel_size\"],\n    )\n\n    stride: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=1,\n        description=\"An integer or pair of integers specifying the stride of the convolution along the height and \"\n        \"width. If a stride is not already specified in conv_layers, specifies the default stride of the \"\n        \"2D convolutional kernel that will be used for each layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=False, description=\"\", default=1),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"stride\"],\n    )\n\n    padding_mode: str | None = schema_utils.StringOptions(\n        options=[\"zeros\", \"reflect\", \"replicate\", \"circular\"],\n        default=\"zeros\",\n        description=\"If padding_mode is not already specified in conv_layers, specifies the default padding_mode of \"\n        \"the 2D convolutional kernel that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"padding_mode\"],\n    )\n\n    padding: int | tuple[int] | str | None = schema_utils.OneOfOptionsField(\n        default=\"valid\",\n        allow_none=True,\n        description=\"An int, pair of ints (h, w), or one of ['valid', 'same'] specifying the padding used for\"\n        \"convolution kernels.\",\n        field_options=[\n            schema_utils.NonNegativeInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n            schema_utils.StringOptions(options=[\"valid\", \"same\"], default=\"valid\", allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"padding\"],\n    )\n\n    dilation: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=1,\n        allow_none=True,\n        description=\"An int or pair of ints specifying the dilation rate to use for dilated convolution. If dilation \"\n        \"is not already specified in conv_layers, specifies the default dilation of the 2D convolutional \"\n        \"kernel that will be used for each layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"dilation\"],\n    )\n\n    groups: int | None = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Groups controls the connectivity between convolution inputs and outputs. When groups = 1, each \"\n        \"output channel depends on every input channel. When groups > 1, input and output channels are \"\n        \"divided into groups separate groups, where each output channel depends only on the inputs in its \"\n        \"respective input channel group. in_channels and out_channels must both be divisible by groups.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"groups\"],\n    )\n\n    pool_function: str | None = schema_utils.StringOptions(\n        [\"max\", \"average\", \"avg\", \"mean\"],\n        default=\"max\",\n        description=\"Pooling function to use.\",\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"pool_function\"],\n    )\n\n    pool_kernel_size: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=2,\n        allow_none=True,\n        description=\"An integer or pair of integers specifying the pooling size. If pool_kernel_size is not specified \"\n        \"in conv_layers this is the default value that will be used for each layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"pool_kernel_size\"],\n    )\n\n    pool_stride: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=\"An integer or pair of integers specifying the pooling stride, which is the factor by which the \"\n        \"pooling layer downsamples the feature map. Defaults to pool_kernel_size.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"pool_stride\"],\n    )\n\n    pool_padding: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=0,\n        allow_none=True,\n        description=\"An integer or pair of ints specifying pooling padding (h, w).\",\n        field_options=[\n            schema_utils.NonNegativeInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"pool_padding\"],\n    )\n\n    pool_dilation: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=1,\n        allow_none=True,\n        description=\"An integer or pair of ints specifying pooling dilation rate (h, w).\",\n        field_options=[\n            schema_utils.PositiveInteger(default=None, allow_none=True, description=\"\"),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"pool_dilation\"],\n    )\n\n    output_size: int | None = schema_utils.PositiveInteger(\n        default=128,\n        description=\"If output_size is not already specified in fc_layers this is the default output_size that will \"\n        \"be used for each layer. It indicates the size of the output of a fully connected layer. \",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"output_size\"],\n    )\n\n    conv_use_bias: bool | None = schema_utils.Boolean(\n        default=True,\n        description=\"If bias not already specified in conv_layers, specifies if the 2D convolutional kernel should \"\n        \"have a bias term.\",\n    )\n\n    conv_norm: str | None = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"If a norm is not already specified in conv_layers this is the default norm that will be used for \"\n        \"each layer. It indicates the normalization applied to the activations and can be null, \"\n        \"batch or layer.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"conv_norm\"],\n    )\n\n    conv_norm_params: dict[str, Any] | None = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if conv_norm is either batch or layer. \",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"conv_norm_params\"],\n    )\n\n    num_conv_layers: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of convolutional layers to use in the encoder. \",\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"num_conv_layers\"],\n    )\n\n    conv_layers: list[dict] | None = schema_utils.DictList(\n        default=None,\n        description=\"List of convolutional layers to use in the encoder. \",\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"conv_layers\"],\n    )\n\n    fc_dropout: float | None = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout rate\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_dropout\"],\n    )\n\n    fc_activation: str | None = schema_utils.ActivationOptions(\n        description=\"If an activation is not already specified in fc_layers this is the default activation that will \"\n        \"be used for each layer. It indicates the activation function applied to the output.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_activation\"],\n    )\n\n    fc_use_bias: bool | None = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_use_bias\"],\n    )\n\n    fc_bias_initializer: str | None = schema_utils.StringOptions(\n        sorted(list(initializer_registry.keys())),\n        default=\"zeros\",\n        description=\"Initializer for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_bias_initializer\"],\n    )\n\n    fc_weights_initializer: str | None = schema_utils.StringOptions(\n        sorted(list(initializer_registry.keys())),\n        default=\"xavier_uniform\",\n        description=\"Initializer for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_weights_initializer\"],\n    )\n\n    fc_norm: str | None = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"If a norm is not already specified in fc_layers this is the default norm that will be used for \"\n        \"each layer. It indicates the norm of the output and can be null, batch or layer.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_norm\"],\n    )\n\n    fc_norm_params: dict[str, Any] | None = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either batch or layer. For information on parameters used with batch \"\n        \"see Torch's documentation on batch normalization or for layer see Torch's documentation on layer \"\n        \"normalization.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_norm_params\"],\n    )\n\n    num_fc_layers: int | None | None = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] | None | None = schema_utils.DictList(\n        default=None,\n        description=\"A list of dictionaries containing the parameters of all the fully connected layers. The length \"\n        \"of the list determines the number of stacked fully connected layers and the content of each \"\n        \"dictionary determines the parameters for a specific layer. The available parameters for each \"\n        \"layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and \"\n        \"weights_initializer. If any of those values is missing from the dictionary, the default one \"\n        \"specified as a parameter of the encoder will be used instead. \",\n        parameter_metadata=ENCODER_METADATA[\"Stacked2DCNN\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"_resnet_legacy\", IMAGE)\n@ludwig_dataclass\nclass ResNetConfig(ImageEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"ResNet\"\n\n    type: str = schema_utils.ProtectedString(\n        \"_resnet_legacy\",\n        description=ENCODER_METADATA[\"ResNet\"][\"type\"].long_description,\n    )\n\n    dropout: float | None = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout rate\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"dropout\"],\n    )\n\n    activation: str | None = schema_utils.ActivationOptions(\n        description=\"if an activation is not already specified in fc_layers this is the default activation that will \"\n        \"be used for each layer. It indicates the activation function applied to the output.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"activation\"],\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"width\"],\n    )\n\n    resnet_size: int | None = schema_utils.PositiveInteger(\n        default=50,\n        description=\"The size of the ResNet model to use.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"resnet_size\"],\n    )\n\n    num_channels: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels to use in the encoder. \",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"num_channels\"],\n    )\n\n    out_channels: int | None = schema_utils.NonNegativeInteger(\n        default=32,\n        description=\"Indicates the number of filters, and by consequence the output channels of the 2d convolution. \"\n        \"If out_channels is not already specified in conv_layers this is the default out_channels that \"\n        \"will be used for each layer. \",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"out_channels\"],\n    )\n\n    kernel_size: int | tuple[int] | None = schema_utils.OneOfOptionsField(\n        default=3,\n        allow_none=True,\n        description=\"An integer or pair of integers specifying the kernel size. A single integer specifies a square \"\n        \"kernel, while a pair of integers specifies the height and width of the kernel in that order (h, \"\n        \"w). If a kernel_size is not specified in conv_layers this kernel_size that will be used for \"\n        \"each layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"kernel_size\"],\n    )\n\n    conv_stride: int | tuple[int] = schema_utils.OneOfOptionsField(\n        default=1,\n        allow_none=True,\n        description=\"An integer or pair of integers specifying the stride of the initial convolutional layer.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"conv_stride\"],\n    )\n\n    first_pool_kernel_size: int | tuple[int] = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=\"Pool size to be used for the first pooling layer. If none, the first pooling layer is skipped.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"first_pool_kernel_size\"],\n    )\n\n    first_pool_stride: int | tuple[int] = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=\"Stride for first pooling layer. If null, defaults to first_pool_kernel_size.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=True, description=\"\", default=None),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"first_pool_stride\"],\n    )\n\n    batch_norm_momentum: float = schema_utils.NonNegativeFloat(\n        default=0.9,\n        description=\"Momentum of the batch norm running statistics.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"batch_norm_momentum\"],\n    )\n\n    batch_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=0.001,\n        description=\"Epsilon of the batch norm.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"batch_norm_epsilon\"],\n    )\n\n    use_bias: bool | None = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"use_bias\"],\n    )\n\n    bias_initializer: str | None = schema_utils.StringOptions(\n        sorted(list(initializer_registry.keys())),\n        default=\"zeros\",\n        description=\"initializer for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str | None = schema_utils.StringOptions(\n        sorted(list(initializer_registry.keys())),\n        default=\"xavier_uniform\",\n        description=\"Initializer for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"weights_initializer\"],\n    )\n\n    output_size: int | None = schema_utils.PositiveInteger(\n        default=128,\n        description=\"if output_size is not already specified in fc_layers this is the default output_size that will \"\n        \"be used for each layer. It indicates the size of the output of a fully connected layer. \",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"output_size\"],\n    )\n\n    norm: str | None = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"if a norm is not already specified in fc_layers this is the default norm that will be used for \"\n        \"each layer. It indicates the norm of the output and can be null, batch or layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"norm\"],\n    )\n\n    norm_params: dict[str, Any] | None = schema_utils.Dict(\n        default=None,\n        description=\"parameters used if norm is either batch or layer. For information on parameters used with batch \"\n        \"see Torch's documentation on batch normalization or for layer see Torch's documentation on layer \"\n        \"normalization.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int | None | None = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked fully connected layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] | None | None = schema_utils.DictList(\n        default=None,\n        description=\"A list of dictionaries containing the parameters of all the fully connected layers. The length \"\n        \"of the list determines the number of stacked fully connected layers and the content of each \"\n        \"dictionary determines the parameters for a specific layer. The available parameters for each \"\n        \"layer are: activation, dropout, norm, norm_params, output_size, use_bias, bias_initializer and \"\n        \"weights_initializer. If any of those values is missing from the dictionary, the default one \"\n        \"specified as a parameter of the encoder will be used instead. \",\n        parameter_metadata=ENCODER_METADATA[\"ResNet\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"mlp_mixer\", IMAGE)\n@ludwig_dataclass\nclass MLPMixerConfig(ImageEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"MLPMixer\"\n\n    type: str = schema_utils.ProtectedString(\n        \"mlp_mixer\",\n        description=ENCODER_METADATA[\"MLPMixer\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout rate.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"dropout\"],\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"width\"],\n    )\n\n    num_channels: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels to use in the encoder. \",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"num_channels\"],\n    )\n\n    patch_size: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"The image patch size. Each patch is patch_size² pixels. Must evenly divide the image width and \"\n        \"height.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"patch_size\"],\n    )\n\n    embed_size: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The patch embedding size, the output size of the mixer if avg_pool is true.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"embed_size\"],\n    )\n\n    token_size: int = schema_utils.PositiveInteger(\n        default=2048,\n        description=\"The per-patch embedding size.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"token_size\"],\n    )\n\n    channel_dim: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Number of channels in hidden layer.\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"channel_dim\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"The depth of the network (the number of Mixer blocks).\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"num_layers\"],\n    )\n\n    avg_pool: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, pools output over patch dimension, outputs a vector of shape (embed_size). If false, \"\n        \"the output tensor is of shape (n_patches, embed_size), where n_patches is img_height x img_width \"\n        \"/ patch_size².\",\n        parameter_metadata=ENCODER_METADATA[\"MLPMixer\"][\"avg_pool\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"_vit_legacy\", IMAGE)\n@ludwig_dataclass\nclass ViTConfig(ImageEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"ViT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"_vit_legacy\",\n        description=ENCODER_METADATA[\"ViT\"][\"type\"].long_description,\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"width\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"num_hidden_layers\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooling layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"hidden_size\"],\n    )\n\n    hidden_act: str = schema_utils.StringOptions(\n        [\"relu\", \"gelu\", \"selu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"Hidden layer activation, one of gelu, relu, selu or gelu_new.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout rate for all fully connected layers in the embeddings, encoder, and pooling.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"hidden_dropout_prob\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads in each attention layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"num_attention_heads\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout rate for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the intermediate (i.e., feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"intermediate_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"layer_norm_eps\"],\n    )\n\n    gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"gradient_checkpointing\"],\n    )\n\n    patch_size: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"The image patch size. Each patch is patch_size² pixels. Must evenly divide the image width and \"\n        \"height.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"patch_size\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Is the encoder trainable.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"trainable\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Use pre-trained model weights from Hugging Face.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model: str = schema_utils.String(\n        default=\"google/vit-base-patch16-224\",\n        description=\"The name of the pre-trained model to use.\",\n        parameter_metadata=ENCODER_METADATA[\"ViT\"][\"pretrained_model\"],\n    )\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"ImagePreprocessingConfig\"):\n        \"\"\"If the encoder is not in trainable mode, override the image width and height to be compatible with the\n        pretrained encoder image dimension requirements.\"\"\"\n        if self.requires_equal_dimensions() and self.required_width() != self.required_height():\n            raise ValueError(\"Invalid definition. `required_width` and `required_height` are not equal\")\n\n        preprocessing.requires_equal_dimensions = self.requires_equal_dimensions()\n        if not self.trainable or self.use_pretrained:\n            preprocessing.height = self.required_height()\n            preprocessing.width = self.required_width()\n\n    @classmethod\n    def requires_equal_dimensions(cls) -> bool:\n        return True\n\n    @classmethod\n    def required_width(cls) -> int | None:\n        return 224\n\n    @classmethod\n    def required_height(cls) -> int | None:\n        return 224\n\n    def is_pretrained(self) -> bool:\n        return self.use_pretrained\n\n\n@DeveloperAPI\n@register_encoder_config(\"unet\", IMAGE)\n@ludwig_dataclass\nclass UNetEncoderConfig(ImageEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"UNetEncoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"unet\",\n        description=ENCODER_METADATA[\"UNetEncoder\"][\"type\"].long_description,\n    )\n\n    height: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Height of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"UNetEncoder\"][\"height\"],\n    )\n\n    width: int = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Width of the input image.\",\n        parameter_metadata=ENCODER_METADATA[\"UNetEncoder\"][\"width\"],\n    )\n\n    num_channels: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels in the input image. \",\n        parameter_metadata=ENCODER_METADATA[\"UNetEncoder\"][\"num_channels\"],\n    )\n\n    conv_norm: str | None = schema_utils.StringOptions(\n        [\"batch\"],\n        default=\"batch\",\n        allow_none=True,\n        description=\"This is the default norm that will be used for each double conv layer.\" \"It can be null or batch.\",\n        parameter_metadata=ENCODER_METADATA[\"UNetEncoder\"][\"conv_norm\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/image/timm.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import IMAGE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@ludwig_dataclass\nclass TimmBaseConfig(BaseEncoderConfig):\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Download model weights from pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TimmEncoder\"][\"use_pretrained\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use weights saved in the Ludwig checkpoint instead of pretrained weights.\",\n        parameter_metadata=ENCODER_METADATA[\"TimmEncoder\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the encoder parameters are trainable.\",\n        parameter_metadata=ENCODER_METADATA[\"TimmEncoder\"][\"trainable\"],\n    )\n\n    def is_pretrained(self) -> bool:\n        return self.use_pretrained\n\n\n@DeveloperAPI\n@register_encoder_config(\"timm\", IMAGE)\n@ludwig_dataclass\nclass TimmEncoderConfig(TimmBaseConfig):\n    type: str = schema_utils.ProtectedString(\"timm\", description=\"Type of encoder.\")\n\n    model_name: str = schema_utils.String(\n        default=\"caformer_s18\",\n        description=(\n            \"Name of the timm model to use. Any model from the timm library is supported. \"\n            \"See https://huggingface.co/docs/timm for available models.\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"TimmEncoder\"][\"model_name\"],\n    )\n\n\n# Convenience aliases for MetaFormer variants with curated model_name options\n\nCAFORMER_MODELS = [\n    \"caformer_s18\",\n    \"caformer_s36\",\n    \"caformer_m36\",\n    \"caformer_b36\",\n    \"caformer_s18.sail_in22k_ft_in1k\",\n    \"caformer_s18.sail_in22k_ft_in1k_384\",\n    \"caformer_s36.sail_in22k_ft_in1k\",\n    \"caformer_s36.sail_in22k_ft_in1k_384\",\n    \"caformer_m36.sail_in22k_ft_in1k\",\n    \"caformer_m36.sail_in22k_ft_in1k_384\",\n    \"caformer_b36.sail_in22k_ft_in1k\",\n    \"caformer_b36.sail_in22k_ft_in1k_384\",\n]\n\nCONVFORMER_MODELS = [\n    \"convformer_s18\",\n    \"convformer_s36\",\n    \"convformer_m36\",\n    \"convformer_b36\",\n    \"convformer_s18.sail_in22k_ft_in1k\",\n    \"convformer_s18.sail_in22k_ft_in1k_384\",\n    \"convformer_s36.sail_in22k_ft_in1k\",\n    \"convformer_s36.sail_in22k_ft_in1k_384\",\n    \"convformer_m36.sail_in22k_ft_in1k\",\n    \"convformer_m36.sail_in22k_ft_in1k_384\",\n    \"convformer_b36.sail_in22k_ft_in1k\",\n    \"convformer_b36.sail_in22k_ft_in1k_384\",\n]\n\nPOOLFORMER_MODELS = [\n    \"poolformerv2_s12\",\n    \"poolformerv2_s24\",\n    \"poolformerv2_s36\",\n    \"poolformerv2_m36\",\n    \"poolformerv2_m48\",\n    \"poolformer_s12\",\n    \"poolformer_s24\",\n    \"poolformer_s36\",\n    \"poolformer_m36\",\n    \"poolformer_m48\",\n]\n\n\n@DeveloperAPI\n@register_encoder_config(\"caformer\", IMAGE)\n@ludwig_dataclass\nclass TimmCAFormerEncoderConfig(TimmBaseConfig):\n    type: str = schema_utils.ProtectedString(\"caformer\", description=\"Type of encoder.\")\n\n    model_name: str = schema_utils.StringOptions(\n        CAFORMER_MODELS,\n        default=\"caformer_s18\",\n        allow_none=False,\n        description=(\n            \"CAFormer model variant. Hybrid Conv+Attention MetaFormer achieving SOTA accuracy. \"\n            \"Variants with '.sail_in22k_ft_in1k' are pretrained on ImageNet-21K and finetuned on ImageNet-1K. \"\n            \"Variants with '_384' use 384x384 input resolution.\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"TimmCAFormerEncoder\"][\"model_name\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"convformer\", IMAGE)\n@ludwig_dataclass\nclass TimmConvFormerEncoderConfig(TimmBaseConfig):\n    type: str = schema_utils.ProtectedString(\"convformer\", description=\"Type of encoder.\")\n\n    model_name: str = schema_utils.StringOptions(\n        CONVFORMER_MODELS,\n        default=\"convformer_s18\",\n        allow_none=False,\n        description=(\n            \"ConvFormer model variant. Pure CNN MetaFormer that outperforms ConvNeXt. \"\n            \"Variants with '.sail_in22k_ft_in1k' are pretrained on ImageNet-21K and finetuned on ImageNet-1K.\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"TimmConvFormerEncoder\"][\"model_name\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"poolformer\", IMAGE)\n@ludwig_dataclass\nclass TimmPoolFormerEncoderConfig(TimmBaseConfig):\n    type: str = schema_utils.ProtectedString(\"poolformer\", description=\"Type of encoder.\")\n\n    model_name: str = schema_utils.StringOptions(\n        POOLFORMER_MODELS,\n        default=\"poolformerv2_s12\",\n        allow_none=False,\n        description=(\n            \"PoolFormer model variant. MetaFormer using simple average pooling as token mixer. \"\n            \"V2 variants use StarReLU activation and improved training recipe.\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"TimmPoolFormerEncoder\"][\"model_name\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/image/torchvision.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import IMAGE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@ludwig_dataclass\nclass TVBaseEncoderConfig(BaseEncoderConfig):\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Download model weights from pre-trained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TVBaseEncoder\"][\"use_pretrained\"],\n    )\n\n    model_cache_dir: str | None = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Directory path to cache pretrained model weights.\",\n        parameter_metadata=ENCODER_METADATA[\"TVBaseEncoder\"][\"model_cache_dir\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to save the weights in the checkpoint.\",\n        parameter_metadata=ENCODER_METADATA[\"TVBaseEncoder\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Is the encoder trainable.\",\n        parameter_metadata=ENCODER_METADATA[\"TVBaseEncoder\"][\"trainable\"],\n    )\n\n    def is_pretrained(self) -> bool:\n        return self.use_pretrained\n\n\n@DeveloperAPI\n@register_encoder_config(\"alexnet\", IMAGE)\n@ludwig_dataclass\nclass TVAlexNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"alexnet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"base\"],\n        default=\"base\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVAlexNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"convnext\", IMAGE)\n@ludwig_dataclass\nclass TVConvNeXtEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"convnext\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"tiny\", \"small\", \"base\", \"large\"],\n        default=\"base\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVConvNeXtEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"densenet\", IMAGE)\n@ludwig_dataclass\nclass TVDenseNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"densenet\", description=\"Type of encoder.\")\n\n    model_variant: int = schema_utils.IntegerOptions(\n        [121, 161, 169, 201],\n        default=121,\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVDenseNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"efficientnet\", IMAGE)\n@ludwig_dataclass\nclass TVEfficientNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"efficientnet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"b0\",\n            \"b1\",\n            \"b2\",\n            \"b3\",\n            \"b4\",\n            \"b5\",\n            \"b6\",\n            \"b7\",\n            \"v2_s\",\n            \"v2_m\",\n            \"v2_l\",\n        ],\n        default=\"b0\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVEfficientNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"googlenet\", IMAGE)\n@ludwig_dataclass\nclass TVGoogLeNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"googlenet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"base\"],\n        default=\"base\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVGoogLeNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"inceptionv3\", IMAGE)\n@ludwig_dataclass\nclass TVInceptionV3EncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"inceptionv3\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"base\"],\n        default=\"base\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVGoogLeNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"maxvit\", IMAGE)\n@ludwig_dataclass\nclass TVMaxVitEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"maxvit\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"t\"],\n        default=\"t\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVMNASNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"mnasnet\", IMAGE)\n@ludwig_dataclass\nclass TVMNASNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"mnasnet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"0_5\", \"0_75\", \"1_0\", \"1_3\"],\n        default=\"0_5\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVMNASNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"mobilenetv2\", IMAGE)\n@ludwig_dataclass\nclass TVMobileNetV2EncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"mobilenetv2\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"base\"],\n        default=\"base\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVMobileNetV2Encoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"mobilenetv3\", IMAGE)\n@ludwig_dataclass\nclass TVMobileNetV3EncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"mobilenetv3\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"small\",\n            \"large\",\n        ],\n        default=\"small\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVMobileNetV3Encoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"regnet\", IMAGE)\n@ludwig_dataclass\nclass TVRegNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"regnet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"x_1_6gf\",\n            \"x_16gf\",\n            \"x_32gf\",\n            \"x_3_2gf\",\n            \"x_400mf\",\n            \"x_800mf\",\n            \"x_8gf\",\n            \"y_128gf\",\n            \"y_16gf\",\n            \"y_1_6gf\",\n            \"y_32gf\",\n            \"y_3_2gf\",\n            \"y_400mf\",\n            \"y_800mf\",\n            \"y_8gf\",\n        ],\n        default=\"x_1_6gf\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVRegNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"resnet\", IMAGE)\n@ludwig_dataclass\nclass TVResNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"resnet\", description=\"Type of encoder.\")\n\n    model_variant: int = schema_utils.IntegerOptions(\n        [18, 34, 50, 101, 152],\n        default=50,\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVResNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"resnext\", IMAGE)\n@ludwig_dataclass\nclass TVResNeXtEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"resnext\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\"50_32x4d\", \"101_32x8d\", \"101_64x4d\"],\n        default=\"50_32x4d\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVResNeXtEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"shufflenet_v2\", IMAGE)\n@ludwig_dataclass\nclass TVShuffleNetV2EncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"shufflenet_v2\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"x0_5\",\n            \"x1_0\",\n            \"x1_5\",\n            \"x2_0\",\n        ],\n        default=\"x0_5\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVShuffleNetV2Encoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"squeezenet\", IMAGE)\n@ludwig_dataclass\nclass TVSqueezeNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"squeezenet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"1_0\",\n            \"1_1\",\n        ],\n        default=\"1_0\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVSqueezeNetEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"swin_transformer\", IMAGE)\n@ludwig_dataclass\nclass TVSwinTransformerEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"swin_transformer\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"t\",\n            \"s\",\n            \"b\",\n        ],\n        default=\"t\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVSwinTransformerEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"vit\", IMAGE)\n@ludwig_dataclass\nclass TVViTEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"vit\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"b_16\",\n            \"b_32\",\n            \"l_16\",\n            \"l_32\",\n            \"h_14\",\n        ],\n        default=\"b_16\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVViTEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"vgg\", IMAGE)\n@ludwig_dataclass\nclass TVVGGEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"vgg\", description=\"Type of encoder.\")\n\n    model_variant: int | str = schema_utils.OneOfOptionsField(\n        default=11,\n        description=\"Pretrained model variant to use.\",\n        field_options=[\n            schema_utils.IntegerOptions(\n                [\n                    11,\n                    13,\n                    16,\n                    19,\n                ],\n                default=11,\n                allow_none=False,\n            ),\n            schema_utils.StringOptions(\n                [\n                    \"11_bn\",\n                    \"13_bn\",\n                    \"16_bn\",\n                    \"19_bn\",\n                ],\n                default=\"11_bn\",\n                allow_none=False,\n            ),\n        ],\n        allow_none=False,\n        parameter_metadata=ENCODER_METADATA[\"TVVGGEncoder\"][\"model_variant\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"wide_resnet\", IMAGE)\n@ludwig_dataclass\nclass TVWideResNetEncoderConfig(TVBaseEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"wide_resnet\", description=\"Type of encoder.\")\n\n    model_variant: str = schema_utils.StringOptions(\n        [\n            \"50_2\",\n            \"101_2\",\n        ],\n        default=\"50_2\",\n        allow_none=False,\n        description=\"Pretrained model variant to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TVViTEncoder\"][\"model_variant\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/sequence_encoders.py",
    "content": "from dataclasses import Field\nfrom typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUDIO, SEQUENCE, TEXT, TIMESERIES\nfrom ludwig.schema import common_fields\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.sequence import SequencePreprocessingConfig\n\nCONV_LAYERS_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\"\"\"\n\nNUM_CONV_LAYERS_DESCRIPTION = \"The number of stacked convolutional layers when `conv_layers` is `null`.\"\n\n\ndef NumFiltersField(default: int = 256) -> Field:\n    return schema_utils.PositiveInteger(\n        default=default,\n        description=\"Number of filters, and by consequence number of output channels of the 1d convolution.\",\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"num_filters\"],\n    )\n\n\ndef FilterSizeField(default: int = 3) -> Field:\n    return schema_utils.PositiveInteger(\n        default=default,\n        description=\"Size of the 1d convolutional filter. It indicates how wide the 1d convolutional filter is.\",\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"filter_size\"],\n    )\n\n\ndef PoolFunctionField(default: str = \"max\") -> Field:\n    return schema_utils.ReductionOptions(\n        default=default,\n        description=(\n            \"Pooling function to use. `max` will select the maximum value. Any of `average`, `avg`, or \"\n            \"`mean` will compute the mean value\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"pool_function\"],\n    )\n\n\ndef PoolSizeField(default: int | None = None) -> Field:\n    return schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The default pool_size that will be used for each layer. If a pool_size is not already specified \"\n            \"in conv_layers this is the default pool_size that will be used for each layer. It indicates the size of \"\n            \"the max pooling that will be performed along the `s` sequence dimension after the convolution operation.\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"pool_size\"],\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass SequenceEncoderConfig(BaseEncoderConfig):\n    \"\"\"Base class for sequence encoders.\"\"\"\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"SequencePreprocessingConfig\"):\n        if isinstance(preprocessing, dict):\n            preprocessing[\"cache_encoder_embeddings\"] = False\n        else:\n            preprocessing.cache_encoder_embeddings = False\n\n\n@DeveloperAPI\n@register_encoder_config(\"passthrough\", [TIMESERIES])\n@ludwig_dataclass\nclass SequencePassthroughConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"SequencePassthrough\"\n\n    type: str = schema_utils.ProtectedString(\n        \"passthrough\",\n        description=ENCODER_METADATA[\"SequencePassthrough\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    encoding_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The size of the encoding vector, or None if sequence elements are scalars.\",\n        parameter_metadata=ENCODER_METADATA[\"SequencePassthrough\"][\"encoding_size\"],\n    )\n\n    reduce_output: str = common_fields.ReduceOutputField(default=None)\n\n\n@DeveloperAPI\n@register_encoder_config(\"embed\", [SEQUENCE, TEXT])\n@ludwig_dataclass\nclass SequenceEmbedConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"SequenceEmbed\"\n\n    type: str = schema_utils.ProtectedString(\n        \"embed\",\n        description=ENCODER_METADATA[\"SequenceEmbed\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate applied to the embedding.\")\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField(default=\"uniform\")\n\n    reduce_output: str = common_fields.ReduceOutputField()\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n\n@DeveloperAPI\n@register_encoder_config(\"parallel_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass ParallelCNNConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"ParallelCNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"parallel_cnn\",\n        description=ENCODER_METADATA[\"ParallelCNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate applied to the embedding.\")\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\"\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to embed the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField()\n\n    num_conv_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=NUM_CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"num_conv_layers\"],\n    )\n\n    conv_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for conv_layers\n        default=None,\n        description=CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"conv_layers\"],\n    )\n\n    num_filters: int = NumFiltersField()\n\n    filter_size: int = FilterSizeField()\n\n    pool_function: str = PoolFunctionField()\n\n    pool_size: int = PoolSizeField()\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of parallel fully connected layers to use.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ParallelCNN\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"stacked_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass StackedCNNConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"StackedCNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"stacked_cnn\",\n        description=ENCODER_METADATA[\"StackedCNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate applied to the embedding.\")\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\"\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    num_conv_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=NUM_CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"num_conv_layers\"],\n    )\n\n    conv_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for conv_layers\n        default=None,\n        description=CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"conv_layers\"],\n    )\n\n    num_filters: int = NumFiltersField()\n\n    filter_size: int = FilterSizeField()\n\n    pool_function: str = PoolFunctionField()\n\n    pool_size: int = PoolSizeField()\n\n    strides: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Stride length of the convolution.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"strides\"],\n    )\n\n    padding: str = schema_utils.StringOptions(\n        [\"valid\", \"same\"],\n        default=\"same\",\n        description=\"Padding to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"padding\"],\n    )\n\n    dilation_rate: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Dilation rate to use for dilated convolution.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"dilation_rate\"],\n    )\n\n    pool_strides: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Factor to scale down.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"pool_strides\"],\n    )\n\n    pool_padding: str = schema_utils.StringOptions(\n        [\"valid\", \"same\"],\n        default=\"same\",\n        description=\"Padding to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"pool_padding\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to embed the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField()\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of parallel fully connected layers to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNN\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"stacked_parallel_cnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass StackedParallelCNNConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"StackedParallelCNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"stacked_parallel_cnn\",\n        description=ENCODER_METADATA[\"StackedParallelCNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate applied to the embedding.\")\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\"\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    num_stacked_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"If stacked_layers is null, this is the number of elements in the stack of parallel convolutional \"\n        \"layers. \",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"num_stacked_layers\"],\n    )\n\n    stacked_layers: list[dict] = schema_utils.DictList(\n        default=None,\n        description=\"a nested list of lists of dictionaries containing the parameters of the stack of parallel \"\n        \"convolutional layers. The length of the list determines the number of stacked parallel \"\n        \"convolutional layers, length of the sub-lists determines the number of parallel conv layers and \"\n        \"the content of each dictionary determines the parameters for a specific layer. \",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"stacked_layers\"],\n    )\n\n    num_filters: int = NumFiltersField()\n\n    filter_size: int = FilterSizeField()\n\n    pool_function: str = PoolFunctionField()\n\n    pool_size: int = PoolSizeField()\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If True the input sequence is expected to be made of integers and will be mapped into embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField()\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of parallel fully connected layers to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedParallelCNN\"][\"fc_layers\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"rnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass StackedRNNConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"StackedRNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"rnn\",\n        description=ENCODER_METADATA[\"StackedRNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate.\")\n\n    recurrent_dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout rate for the recurrent state\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"recurrent_dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(default=\"tanh\", description=\"The default activation function.\")\n\n    recurrent_activation: str = schema_utils.ActivationOptions(\n        default=\"sigmoid\",\n        description=\"The activation function to use in the recurrent step\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"recurrent_activation\"],\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    cell_type: str = schema_utils.StringOptions(\n        [\"rnn\", \"lstm\", \"gru\"],\n        default=\"rnn\",\n        description=\"The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference \"\n        \"about the differences between the cells please refer to \"\n        \"[torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"cell_type\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked recurrent layers.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"num_layers\"],\n    )\n\n    state_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The size of the state of the rnn.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"state_size\"],\n    )\n\n    bidirectional: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, two recurrent networks will perform encoding in the forward and backward direction and \"\n        \"their outputs will be concatenated.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"bidirectional\"],\n    )\n\n    unit_forget_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, add 1 to the bias of the forget gate at initialization\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"unit_forget_bias\"],\n    )\n\n    recurrent_initializer: str = schema_utils.InitializerOptions(\n        default=\"orthogonal\",\n        description=\"The initializer for recurrent matrix weights\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"recurrent_initializer\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If True the input sequence is expected to be made of integers and will be mapped into embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField(default=\"last\")\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedRNN\"][\"output_size\"],\n    )\n\n    norm: str = common_fields.NormField(description=\"The default norm that will be used for each layer.\")\n\n    norm_params: dict = common_fields.NormParamsField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField(description=\"Number of parallel fully connected layers to use.\")\n\n    fc_activation: str = schema_utils.ActivationOptions()\n\n    fc_dropout: float = common_fields.DropoutField()\n\n    fc_layers: list[dict] = common_fields.FCLayersField()\n\n\n@DeveloperAPI\n@register_encoder_config(\"cnnrnn\", [AUDIO, SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass StackedCNNRNNConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"StackedCNNRNN\"\n\n    type: str = schema_utils.ProtectedString(\n        \"cnnrnn\",\n        description=ENCODER_METADATA[\"StackedCNNRNN\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(description=\"Dropout rate.\")\n\n    recurrent_dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout rate for the recurrent state\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"recurrent_dropout\"],\n    )\n\n    conv_dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout rate for the convolutional layers\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"conv_dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        default=\"tanh\", description=\"The default activation function to use.\"\n    )\n\n    recurrent_activation: str = schema_utils.ActivationOptions(\n        default=\"sigmoid\",\n        description=\"The activation function to use in the recurrent step\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"recurrent_activation\"],\n    )\n\n    conv_activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each convolutional layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"conv_activation\"],\n    )\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    cell_type: str = schema_utils.StringOptions(\n        [\"rnn\", \"lstm\", \"gru\"],\n        default=\"rnn\",\n        description=\"The type of recurrent cell to use. Available values are: `rnn`, `lstm`, `gru`. For reference \"\n        \"about the differences between the cells please refer to \"\n        \"[torch.nn Recurrent Layers](https://pytorch.org/docs/stable/nn.html#recurrent-layers).\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"cell_type\"],\n    )\n\n    num_conv_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=NUM_CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"num_conv_layers\"],\n    )\n\n    conv_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for conv_layers\n        default=None,\n        description=CONV_LAYERS_DESCRIPTION,\n        parameter_metadata=ENCODER_METADATA[\"conv_params\"][\"conv_layers\"],\n    )\n\n    num_filters: int = NumFiltersField()\n\n    filter_size: int = FilterSizeField(default=5)\n\n    pool_function: str = PoolFunctionField()\n\n    pool_size: int = PoolSizeField(default=2)\n\n    strides: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Stride length of the convolution.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"strides\"],\n    )\n\n    padding: str = schema_utils.StringOptions(\n        [\"valid\", \"same\"],\n        default=\"same\",\n        description=\"Padding to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"padding\"],\n    )\n\n    dilation_rate: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Dilation rate to use for dilated convolution.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"dilation_rate\"],\n    )\n\n    pool_strides: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Factor to scale down.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"pool_strides\"],\n    )\n\n    pool_padding: str = schema_utils.StringOptions(\n        [\"valid\", \"same\"],\n        default=\"same\",\n        description=\"Padding to use.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"pool_padding\"],\n    )\n\n    num_rec_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of stacked recurrent layers.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"num_rec_layers\"],\n    )\n\n    state_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The size of the state of the rnn.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"state_size\"],\n    )\n\n    bidirectional: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, two recurrent networks will perform encoding in the forward and backward direction and \"\n        \"their outputs will be concatenated.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"bidirectional\"],\n    )\n\n    unit_forget_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, add 1 to the bias of the forget gate at initialization\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"unit_forget_bias\"],\n    )\n\n    recurrent_initializer: str = schema_utils.InitializerOptions(\n        default=\"orthogonal\",\n        description=\"The initializer for recurrent matrix weights\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"recurrent_initializer\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If True the input sequence is expected to be made of integers and will be mapped into embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField(default=\"last\")\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedCNNRNN\"][\"output_size\"],\n    )\n\n    norm: str = common_fields.NormField(description=\"The default norm that will be used for each layer.\")\n\n    norm_params: dict = common_fields.NormParamsField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField(description=\"Number of parallel fully connected layers to use.\")\n\n    fc_activation: str = schema_utils.ActivationOptions()\n\n    fc_dropout: float = common_fields.DropoutField()\n\n    fc_layers: list[dict] = common_fields.FCLayersField()\n\n\n@DeveloperAPI\n@register_encoder_config(\"transformer\", [SEQUENCE, TEXT, TIMESERIES])\n@ludwig_dataclass\nclass StackedTransformerConfig(SequenceEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"StackedTransformer\"\n\n    type: str = schema_utils.ProtectedString(\n        \"transformer\",\n        description=ENCODER_METADATA[\"StackedTransformer\"][\"type\"].long_description,\n    )\n\n    dropout: float = common_fields.DropoutField(default=0.1, description=\"The dropout rate for the transformer block.\")\n\n    max_sequence_length: int = common_fields.MaxSequenceLengthField()\n\n    representation: str = common_fields.RepresentationField()\n\n    vocab: list = common_fields.VocabField()\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of transformer layers.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"num_layers\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The size of the hidden representation within the transformer block. It is usually the same as \"\n        \"the embedding_size, but if the two values are different, a projection layer will be added before \"\n        \"the first transformer block.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"hidden_size\"],\n    )\n\n    num_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of attention heads in each transformer block.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"num_heads\"],\n    )\n\n    transformer_output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Size of the fully connected layer after self attention in the transformer block. This is usually \"\n        \"the same as hidden_size and embedding_size.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"transformer_output_size\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = common_fields.BiasInitializerField()\n\n    weights_initializer: str = common_fields.WeightsInitializerField()\n\n    should_embed: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If True the input sequence is expected to be made of integers and will be mapped into embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"should_embed\"],\n    )\n\n    embedding_size: int = common_fields.EmbeddingSizeField()\n\n    embeddings_on_cpu: bool = common_fields.EmbeddingsOnCPUField()\n\n    embeddings_trainable: bool = common_fields.EmbeddingsTrainableField()\n\n    pretrained_embeddings: str = common_fields.PretrainedEmbeddingsField()\n\n    reduce_output: str = common_fields.ReduceOutputField(default=\"last\")\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The default output_size that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"StackedTransformer\"][\"output_size\"],\n    )\n\n    norm: str = common_fields.NormField(description=\"The default norm that will be used for each layer.\")\n\n    norm_params: dict = common_fields.NormParamsField()\n\n    num_fc_layers: int = common_fields.NumFCLayersField(description=\"Number of parallel fully connected layers to use.\")\n\n    fc_activation: str = schema_utils.ActivationOptions()\n\n    fc_dropout: float = common_fields.DropoutField()\n\n    fc_layers: list[dict] = common_fields.FCLayersField()\n"
  },
  {
    "path": "ludwig/schema/encoders/set_encoders.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import SET\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_encoder_config(\"embed\", SET)\n@ludwig_dataclass\nclass SetSparseEncoderConfig(BaseEncoderConfig):\n    @staticmethod\n    def module_name():\n        return \"SetSparseEncoder\"\n\n    type: str = schema_utils.ProtectedString(\n        \"embed\",\n        description=ENCODER_METADATA[\"SetSparseEncoder\"][\"type\"].long_description,\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Dropout probability for the embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"dropout\"],\n    )\n\n    activation: str = schema_utils.ActivationOptions(\n        description=\"The default activation function that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"activation\"],\n    )\n\n    representation: str = schema_utils.StringOptions(\n        [\"dense\", \"sparse\"],\n        default=\"dense\",\n        description=\"The representation of the embedding. Either dense or sparse.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"representation\"],\n    )\n\n    vocab: list[str] = schema_utils.List(\n        default=None,\n        description=\"Vocabulary of the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"vocab\"],\n    )\n\n    use_bias: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether the layer uses a bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"use_bias\"],\n    )\n\n    bias_initializer: str = schema_utils.InitializerOptions(\n        default=\"zeros\",\n        description=\"Initializer to use for the bias vector.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"bias_initializer\"],\n    )\n\n    weights_initializer: str = schema_utils.InitializerOptions(\n        description=\"Initializer to use for the weights matrix.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"weights_initializer\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=50,\n        description=\"The maximum embedding size, the actual size will be min(vocabulary_size, embedding_size) for \"\n        \"dense representations and exactly vocabulary_size for the sparse encoding, where vocabulary_size \"\n        \"is the number of different strings appearing in the training set in the input column (plus 1 for \"\n        \"the unknown token placeholder <UNK>).\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"embedding_size\"],\n    )\n\n    embeddings_on_cpu: bool = schema_utils.Boolean(\n        default=False,\n        description=\"By default embedding matrices are stored on GPU memory if a GPU is used, as it allows for faster \"\n        \"access, but in some cases the embedding matrix may be too large. This parameter forces the \"\n        \"placement of the embedding matrix in regular memory and the CPU is used for embedding lookup, \"\n        \"slightly slowing down the process as a result of data transfer between CPU and GPU memory.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"embeddings_on_cpu\"],\n    )\n\n    embeddings_trainable: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true embeddings are trained during the training process, if false embeddings are fixed. It \"\n        \"may be useful when loading pretrained embeddings for avoiding finetuning them. This parameter \"\n        \"has effect only when representation is dense as sparse one-hot encodings are not trainable.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"embeddings_trainable\"],\n    )\n\n    pretrained_embeddings: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"By default dense embeddings are initialized randomly, but this parameter allows to specify a \"\n        \"path to a file containing embeddings in the GloVe format. When the file containing the \"\n        \"embeddings is loaded, only the embeddings with labels present in the vocabulary are kept, \"\n        \"the others are discarded. If the vocabulary contains strings that have no match in the \"\n        \"embeddings file, their embeddings are initialized with the average of all other embedding plus \"\n        \"some random noise to make them different from each other. This parameter has effect only if \"\n        \"representation is dense.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"pretrained_embeddings\"],\n    )\n\n    output_size: int = schema_utils.PositiveInteger(\n        default=10,\n        description=\"If output_size is not already specified in fc_layers this is the default output_size that will \"\n        \"be used for each layer. It indicates the size of the output of a fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"output_size\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"batch\", \"layer\"],\n        default=None,\n        allow_none=True,\n        description=\"The default norm that will be used for each layer.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"norm\"],\n    )\n\n    norm_params: dict = schema_utils.Dict(\n        default=None,\n        description=\"Parameters used if norm is either `batch` or `layer`.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"norm_params\"],\n    )\n\n    num_fc_layers: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"This is the number of stacked fully connected layers that the input to the feature passes \"\n        \"through. Their output is projected in the feature's output space.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"num_fc_layers\"],\n    )\n\n    fc_layers: list[dict] = schema_utils.DictList(  # TODO (Connor): Add nesting logic for fc_layers\n        default=None,\n        description=\"List of dictionaries containing the parameters for each fully connected layer.\",\n        parameter_metadata=ENCODER_METADATA[\"SetSparseEncoder\"][\"fc_layers\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/text/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/schema/encoders/text/encoders.py",
    "content": "from collections.abc import Callable\nfrom typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, TEXT\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig\nfrom ludwig.schema.encoders.text.hf_model_params import DebertaModelParams\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.llms.base_model import BaseModelDataclassField\nfrom ludwig.schema.llms.model_parameters import ModelParametersConfig, ModelParametersConfigField\nfrom ludwig.schema.llms.peft import AdapterDataclassField, BaseAdapterConfig\nfrom ludwig.schema.llms.quantization import QuantizationConfig, QuantizationConfigField\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY, ParameterMetadata\nfrom ludwig.schema.utils import ludwig_dataclass\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig\n\n\nclass HFEncoderConfig(SequenceEncoderConfig):\n    trainable: bool\n    use_pretrained: bool\n    pretrained_model_name_or_path: str\n    reduce_output: str\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"TextPreprocessingConfig\"):\n        model_name = self.pretrained_model_name_or_path\n        if model_name is None and self.use_pretrained:\n            # no default model name, so model name is required by the subclass\n            raise ValueError(\n                f\"Missing required parameter for `{self.type}` encoder: `pretrained_model_name_or_path` when \"\n                \"`use_pretrained` is True.\"\n            )\n        preprocessing.tokenizer = \"hf_tokenizer\"\n        preprocessing.pretrained_model_name_or_path = model_name\n        if not self.can_cache_embeddings():\n            preprocessing.cache_encoder_embeddings = False\n\n    def is_pretrained(self) -> bool:\n        return self.use_pretrained\n\n    def can_cache_embeddings(self) -> bool:\n        \"\"\"Returns true if the encoder's output embeddings will not change during training.\"\"\"\n        return not self.trainable and self.reduce_output != \"attention\"\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass HFEncoderImplConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the base HF encoder implmenetation.\"\"\"\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"HFEncoder\"][\"use_pretrained\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"HFEncoder\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n    )\n\n    # Internal params set based on preprocessing metadata\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        description=\"\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n            \"True for trained models to prevent loading pretrained encoder weights from model hub.\"\n        ),\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"albert\", TEXT)\n@ludwig_dataclass\nclass ALBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an ALBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"ALBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"albert\",\n        description=ENCODER_METADATA[\"ALBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"albert-base-v2\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30000,\n        description=\"Vocabulary size of the ALBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"vocab_size\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Dimensionality of vocabulary embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"embedding_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_hidden_layers\"],\n    )\n\n    num_hidden_groups: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Number of groups for the hidden layers, parameters in the same group are shared.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_hidden_groups\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"The dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer \"\n        \"encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"intermediate_size\"],\n    )\n\n    inner_group_num: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of inner repetition of attention and ffn.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"inner_group_num\"],\n    )\n\n    hidden_act: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu_new\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling AlbertModel or TFAlbertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"layer_norm_eps\"],\n    )\n\n    classifier_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for attached classifiers.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"classifier_dropout_prob\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"position_embedding_type\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=3,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n# TODO: uncomment when sentencepiece doesn't cause segfaults: https://github.com/ludwig-ai/ludwig/issues/2983\n@DeveloperAPI\n# @register_encoder_config(\"mt5\", TEXT)\n@ludwig_dataclass\nclass MT5Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an MT5 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"MT5\"\n\n    type: str = schema_utils.ProtectedString(\n        \"mt5\",\n        description=ENCODER_METADATA[\"MT5\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"google/mt5-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=250112,\n        description=\"Vocabulary size of the T5 model. Defines the number of different tokens that can be represented \"\n        \"by the inputs_ids passed when calling T5Model or TFT5Model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Size of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_model\"],\n    )\n\n    d_kv: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // \"\n        \"num_heads.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_kv\"],\n    )\n\n    d_ff: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Size of the intermediate feed forward layer in each T5Block.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_ff\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_layers\"],\n    )\n\n    num_decoder_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not \"\n        \"set.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_decoder_layers\"],\n    )\n\n    num_heads: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_heads\"],\n    )\n\n    relative_attention_num_buckets: int = schema_utils.PositiveInteger(\n        default=32,\n        description=\"The number of buckets to use for each attention layer.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"relative_attention_num_buckets\"],\n    )\n\n    dropout_rate: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The ratio for all dropout layers.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"dropout_rate\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-06,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_factor: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"A factor for initializing all weight matrices (should be kept to 1, used internally for \"\n        \"initialization testing)\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"initializer_factor\"],\n    )\n\n    feed_forward_proj: str = schema_utils.StringOptions(\n        [\"relu\", \"gated-gelu\"],\n        default=\"gated-gelu\",\n        description=\"Type of feed forward layer to be used. \",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"feed_forward_proj\"],\n    )\n\n    is_encoder_decoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"is_encoder_decoder\"],\n    )\n\n    use_cache: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"use_cache\"],\n    )\n\n    tokenizer_class: str = schema_utils.String(\n        default=\"T5Tokenizer\",\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"tokenizer_class\"],\n    )\n\n    tie_word_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether the model's input and output word embeddings should be tied.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"tie_word_embeddings\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pad_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"eos_token_id\"],\n    )\n\n    decoder_start_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"If an encoder-decoder model starts decoding with a different token than _bos_, the id of that \"\n        \"token.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"decoder_start_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"xlmroberta\", TEXT)\n@ludwig_dataclass\nclass XLMRoBERTaConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLMRoBERTa encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLMRoBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlmroberta\",\n        description=ENCODER_METADATA[\"XLMRoBERTa\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlm-roberta-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Vocabulary size of the XLMRoBERTa model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"vocab_size\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"eos_token_id\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=514,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed in.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"type_vocab_size\"],\n    )\n\n    add_pooling_layer: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to add a pooling layer to the encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"add_pooling_layer\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"bert\", TEXT)\n@ludwig_dataclass\nclass BERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an BERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"BERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"bert\",\n        description=ENCODER_METADATA[\"BERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"bert-base-uncased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the BERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"vocab_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"layer_norm_eps\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pad_token_id\"],\n    )\n\n    gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use gradient checkpointing.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"gradient_checkpointing\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"deberta\", TEXT)\n@ludwig_dataclass\nclass DebertaV2Config(HFEncoderImplConfig, DebertaModelParams):\n    \"\"\"This dataclass configures the schema used for a DeBERTa-v2 / v3 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"DeBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"deberta\",\n        description=ENCODER_METADATA[\"DeBERTa\"][\"type\"].long_description,\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"sileod/deberta-v3-base-tasksource-nli\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DeBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.StringOptions(\n        [\"cls_pooled\", \"last\", \"sum\", \"mean\", \"max\", \"concat\", \"attention\"],\n        default=\"sum\",\n        allow_none=True,\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n    )\n\n\n# TODO: uncomment once we figure out host memory issue: https://github.com/ludwig-ai/ludwig/issues/3107\n@DeveloperAPI\n# @register_encoder_config(\"xlm\", TEXT)\n@ludwig_dataclass\nclass XLMConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLM encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLM\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlm\",\n        description=ENCODER_METADATA[\"XLM\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlm-mlm-en-2048\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30145,\n        description=\"Vocabulary size of the BERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling XLMModel or TFXLMModel.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"vocab_size\"],\n    )\n\n    emb_dim: int = schema_utils.PositiveInteger(\n        default=2048,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"emb_dim\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_heads\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"dropout\"],\n    )\n\n    attention_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for the attention mechanism.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"attention_dropout\"],\n    )\n\n    gelu_activation: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use gelu for the activations instead of relu.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"gelu_activation\"],\n    )\n\n    sinusoidal_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"sinusoidal_embeddings\"],\n    )\n\n    causal: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should behave in a causal manner. Causal models use a triangular \"\n        \"attention mask in order to only attend to the left-side context instead if a bidirectional \"\n        \"context.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"causal\"],\n    )\n\n    asm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the \"\n        \"prediction layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"asm\"],\n    )\n\n    n_langs: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of languages the model handles. Set to 1 for monolingual models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_langs\"],\n    )\n\n    use_lang_emb: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use language embeddings. Some models use additional language embeddings, \"\n        \"see the multilingual models page for information on how to use them.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"use_lang_emb\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"max_position_embeddings\"],\n    )\n\n    embed_init_std: float = schema_utils.NonNegativeFloat(\n        default=2048**-0.5,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing the embedding \"\n        \"matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"embed_init_std\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"layer_norm_eps\"],\n    )\n\n    init_std: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices \"\n        \"except the embedding matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"init_std\"],\n    )\n\n    bos_index: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The index of the beginning of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"bos_index\"],\n    )\n\n    eos_index: int = schema_utils.NonNegativeInteger(\n        default=1,\n        description=\"The index of the end of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"eos_index\"],\n    )\n\n    pad_index: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"The index of the padding token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pad_index\"],\n    )\n\n    unk_index: int = schema_utils.NonNegativeInteger(\n        default=3,\n        description=\"The index of the unknown token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"unk_index\"],\n    )\n\n    mask_index: int = schema_utils.NonNegativeInteger(\n        default=5,\n        description=\"The index of the masking token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"mask_index\"],\n    )\n\n    is_encoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the initialized model should be a transformer encoder or decoder as seen in \"\n        \"Vaswani et al.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"is_encoder\"],\n    )\n\n    start_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"start_n_top\"],\n    )\n\n    end_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"end_n_top\"],\n    )\n\n    mask_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"Model agnostic parameter to identify masked tokens when generating text in an MLM context.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"mask_token_id\"],\n    )\n\n    lang_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the language used by the model. This parameter is used when generating text in a given \"\n        \"language.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"lang_id\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"bos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"gpt\", TEXT)\n@ludwig_dataclass\nclass GPTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an GPT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"GPT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"gpt\",\n        description=ENCODER_METADATA[\"GPT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"max_sequence_length\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"reduce_output\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"openai-gpt\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the GPT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling OpenAIGPTModel or TFOpenAIGPTModel.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=40478,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_embd\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_head\"],\n    )\n\n    afn: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\"],  # gelu_new results in a KeyError.\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"afn\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"initializer_range\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"gpt2\", TEXT)\n@ludwig_dataclass\nclass GPT2Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an GPT2 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"GPT2\"\n\n    type: str = schema_utils.ProtectedString(\n        \"gpt2\",\n        description=ENCODER_METADATA[\"GPT2\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"gpt2\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=50257,\n        description=\"Vocabulary size of the GPT-2 model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling GPT2Model or TFGPT2Model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_embd\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_head\"],\n    )\n\n    n_inner: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Dimensionality of the inner feed-forward layers. None will set it to 4 times n_embd\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_inner\"],\n    )\n\n    activation_function: str = schema_utils.StringOptions(\n        [\"relu\", \"silu\", \"gelu\", \"tanh\", \"gelu_new\"],\n        default=\"gelu_new\",\n        description=\"Activation function, to be selected in the list ['relu', 'silu', 'gelu', 'tanh', 'gelu_new'].\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"activation_function\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"initializer_range\"],\n    )\n\n    scale_attn_weights: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Scale attention weights by dividing by sqrt(hidden_size).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"scale_attn_weights\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"roberta\", TEXT)\n@ludwig_dataclass\nclass RoBERTaConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an RoBERTa encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"RoBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"roberta\",\n        description=ENCODER_METADATA[\"RoBERTa\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"roberta-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Vocabulary size of the RoBERTa model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"vocab_size\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"transformer_xl\", TEXT)\n@ludwig_dataclass\nclass TransformerXLConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an TransformerXL encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"TransformerXL\"\n\n    type: str = schema_utils.ProtectedString(\n        \"transformer_xl\",\n        description=ENCODER_METADATA[\"TransformerXL\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"transfo-xl-wt103\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=267735,\n        description=\"Vocabulary size of the TransfoXL model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling TransfoXLModel or TFTransfoXLModel.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"vocab_size\"],\n    )\n\n    cutoffs: list[int] = schema_utils.List(\n        int,\n        default=[20000, 40000, 200000],\n        description=\"Cutoffs for the adaptive softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"cutoffs\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the model’s hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_model\"],\n    )\n\n    d_embed: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_embed\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"n_head\"],\n    )\n\n    d_head: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Dimensionality of the model’s heads.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_head\"],\n    )\n\n    d_inner: int = schema_utils.PositiveInteger(\n        default=4096,\n        description=\" Inner dimension in FF\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_inner\"],\n    )\n\n    div_val: int = schema_utils.PositiveInteger(\n        default=4,\n        description=\"Divident value for adapative input and softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"div_val\"],\n    )\n\n    pre_lnorm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to apply LayerNorm to the input instead of the output in the blocks.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pre_lnorm\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=18,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"n_layer\"],\n    )\n\n    mem_len: int = schema_utils.PositiveInteger(\n        default=1600,\n        description=\"Length of the retained previous heads.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"mem_len\"],\n    )\n\n    clamp_len: int = schema_utils.PositiveInteger(\n        default=1000,\n        description=\"Use the same pos embeddings after clamp_len.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"clamp_len\"],\n    )\n\n    same_length: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use the same attn length for all tokens\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"same_length\"],\n    )\n\n    proj_share_all_but_first: bool = schema_utils.Boolean(\n        default=True,\n        description=\"True to share all but first projs, False not to share.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"proj_share_all_but_first\"],\n    )\n\n    attn_type: int = schema_utils.IntegerRange(\n        default=0,\n        min=0,\n        max=3,\n        description=\"Attention type. 0 for Transformer-XL, 1 for Shaw et al, 2 for Vaswani et al, 3 for Al Rfou et al.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"attn_type\"],\n    )\n\n    sample_softmax: int = schema_utils.Integer(\n        default=-1,\n        description=\"Number of samples in the sampled softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"sample_softmax\"],\n    )\n\n    adaptive: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use adaptive softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"adaptive\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"dropout\"],\n    )\n\n    dropatt: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"dropatt\"],\n    )\n\n    untie_r: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether ot not to untie relative position biases.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"untie_r\"],\n    )\n\n    init: str = schema_utils.String(\n        default=\"normal\",\n        description=\"Parameter initializer to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init\"],\n    )\n\n    init_range: float = schema_utils.NonNegativeFloat(\n        default=0.01,\n        description=\"Parameters initialized by U(-init_range, init_range).\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init_range\"],\n    )\n\n    proj_init_std: float = schema_utils.NonNegativeFloat(\n        default=0.01,\n        description=\"Parameters initialized by N(0, init_std)\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"proj_init_std\"],\n    )\n\n    init_std: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"Parameters initialized by N(0, init_std)\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init_std\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"layer_norm_epsilon\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"xlnet\", TEXT)\n@ludwig_dataclass\nclass XLNetConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLNet encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLNet\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlnet\",\n        description=ENCODER_METADATA[\"XLNet\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlnet-base-cased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32000,\n        description=\"Vocabulary size of the XLNet model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling XLNetModel or TFXLNetModel.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"d_model\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"n_head\"],\n    )\n\n    d_inner: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"d_inner\"],\n    )\n\n    ff_activation: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler. If string, \"\n        \"'gelu', 'relu', 'silu' and 'gelu_new' are supported.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"ff_activation\"],\n    )\n\n    untie_r: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to untie relative position biases\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"untie_r\"],\n    )\n\n    attn_type: str = schema_utils.StringOptions(\n        [\"bi\"],\n        default=\"bi\",\n        description=\"The attention type used by the model. Currently only 'bi' is supported.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"attn_type\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"layer_norm_eps\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"dropout\"],\n    )\n\n    mem_len: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of tokens to cache. The key/value pairs that have already been pre-computed in a \"\n        \"previous forward pass won’t be re-computed. \",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"mem_len\"],\n    )\n\n    reuse_len: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of tokens in the current batch to be cached and reused in the future.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"reuse_len\"],\n    )\n\n    use_mems_eval: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the model should make use of the recurrent memory mechanism in evaluation mode.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_mems_eval\"],\n    )\n\n    use_mems_train: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should make use of the recurrent memory mechanism in train mode.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_mems_train\"],\n    )\n\n    bi_data: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use bidirectional input pipeline. Usually set to True during pretraining and \"\n        \"False during finetuning.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"bi_data\"],\n    )\n\n    clamp_len: int = schema_utils.Integer(\n        default=-1,\n        description=\"Clamp all relative distances larger than clamp_len. Setting this attribute to -1 means no \"\n        \"clamping.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"clamp_len\"],\n    )\n\n    same_length: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use the same attention length for each token.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"same_length\"],\n    )\n\n    summary_type: str = schema_utils.StringOptions(\n        [\"last\", \"first\", \"mean\", \"cls_index\", \"attn\"],\n        default=\"last\",\n        description=\"Argument used when doing sequence summary. Used in the sequence classification and multiple \"\n        \"choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_type\"],\n    )\n\n    summary_use_proj: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_use_proj\"],\n    )\n\n    summary_activation: str = schema_utils.String(\n        default=\"tanh\",\n        description=\"Argument used when doing sequence summary. Used in the sequence classification and multiple \"\n        \"choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_activation\"],\n    )\n\n    summary_last_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"Used in the sequence classification and multiple choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_last_dropout\"],\n    )\n\n    start_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"start_n_top\"],\n    )\n\n    end_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\" Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"end_n_top\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=5,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"distilbert\", TEXT)\n@ludwig_dataclass\nclass DistilBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an DistilBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"DistilBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"distilbert\",\n        description=ENCODER_METADATA[\"DistilBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"distilbert-base-uncased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the DistilBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling DistilBertModel or TFDistilBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"vocab_size\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"dropout\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"max_position_embeddings\"],\n    )\n\n    sinusoidal_pos_embds: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use sinusoidal positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"sinusoidal_pos_embds\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"n_heads\"],\n    )\n\n    dim: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\" Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"dim\"],\n    )\n\n    hidden_dim: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"The size of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"hidden_dim\"],\n    )\n\n    attention_dropout: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"attention_dropout\"],\n    )\n\n    activation: str | Callable = schema_utils.StringOptions(  # TODO: Add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler. If string, \"\n        \"'gelu', 'relu', 'silu' and 'gelu_new' are supported.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"activation\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"initializer_range\"],\n    )\n\n    qa_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probabilities used in the question answering model DistilBertForQuestionAnswering.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"qa_dropout\"],\n    )\n\n    seq_classif_dropout: float = schema_utils.FloatRange(\n        default=0.2,\n        min=0,\n        max=1,\n        description=\"The dropout probabilities used in the sequence classification and the multiple choice model \"\n        \"DistilBertForSequenceClassification.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"seq_classif_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n# TODO: uncomment when CTRL bug (https://github.com/ludwig-ai/ludwig/issues/2977) has been fixed to add back in\n@DeveloperAPI\n# @register_encoder_config(\"ctrl\", TEXT)\n@ludwig_dataclass\nclass CTRLConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an CTRL encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"CTRL\"\n\n    type: str = schema_utils.ProtectedString(\n        \"ctrl\",\n        description=ENCODER_METADATA[\"CTRL\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"ctrl\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=246534,\n        description=\"Vocabulary size of the CTRL model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling CTRLModel or TFCTRLModel.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=1280,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_embd\"],\n    )\n\n    dff: int = schema_utils.PositiveInteger(\n        default=8192,\n        description=\"Dimensionality of the inner dimension of the feed forward networks (FFN).\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"dff\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=48,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_head\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\" The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-6,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"initializer_range\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"camembert\", TEXT)\n@ludwig_dataclass\nclass CamemBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an CamemBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"CamemBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"camembert\",\n        description=ENCODER_METADATA[\"CamemBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"use_pretrained\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"camembert-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32005,\n        description=\"Vocabulary size of the CamemBERT model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"vocab_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=514,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-05,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"layer_norm_eps\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pad_token_id\"],\n    )\n\n    gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use gradient checkpointing.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"gradient_checkpointing\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"t5\", TEXT)\n@ludwig_dataclass\nclass T5Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an T5 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"T5\"\n\n    type: str = schema_utils.ProtectedString(\n        \"t5\",\n        description=ENCODER_METADATA[\"T5\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"t5-small\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32128,\n        description=\"Vocabulary size of the T5 model. Defines the number of different tokens that can be represented \"\n        \"by the inputs_ids passed when calling T5Model or TFT5Model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Size of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_model\"],\n    )\n\n    d_kv: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // \"\n        \"num_heads.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_kv\"],\n    )\n\n    d_ff: int = schema_utils.PositiveInteger(\n        default=2048,\n        description=\"Size of the intermediate feed forward layer in each T5Block.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_ff\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_layers\"],\n    )\n\n    num_decoder_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not \"\n        \"set.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_decoder_layers\"],\n    )\n\n    num_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_heads\"],\n    )\n\n    relative_attention_num_buckets: int = schema_utils.PositiveInteger(\n        default=32,\n        description=\"The number of buckets to use for each attention layer.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"relative_attention_num_buckets\"],\n    )\n\n    dropout_rate: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The ratio for all dropout layers.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"dropout_rate\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-6,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"layer_norm_eps\"],\n    )\n\n    initializer_factor: float = schema_utils.NonNegativeFloat(\n        default=1,\n        description=\"A factor for initializing all weight matrices (should be kept to 1, used internally for \"\n        \"initialization testing).\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"initializer_factor\"],\n    )\n\n    feed_forward_proj: str = schema_utils.StringOptions(\n        [\"relu\", \"gated-gelu\"],\n        default=\"relu\",\n        description=\"Type of feed forward layer to be used. Should be one of 'relu' or 'gated-gelu'. T5v1.1 uses the \"\n        \"'gated-gelu' feed forward projection. Original T5 uses 'relu'.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"feed_forward_proj\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"flaubert\", TEXT)\n@ludwig_dataclass\nclass FlauBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an FlauBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"FlauBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"flaubert\",\n        description=ENCODER_METADATA[\"FlauBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"flaubert/flaubert_small_cased\",\n        description=\"Name of path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30145,\n        description=\"Vocabulary size of the FlauBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling FlaubertModel or TFFlaubertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"vocab_size\"],\n    )\n\n    pre_norm: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to apply the layer normalization before or after the feed forward layer following the \"\n        \"attention in each layer (Vaswani et al., Tensor2Tensor for Neural Machine Translation. 2018)\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pre_norm\"],\n    )\n\n    layerdrop: float = schema_utils.FloatRange(\n        default=0.2,\n        min=0,\n        max=1,\n        description=\"Probability to drop layers during training (Fan et al., Reducing Transformer Depth on Demand \"\n        \"with Structured Dropout. ICLR 2020)\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"layerdrop\"],\n    )\n\n    emb_dim: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"emb_dim\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_heads\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"dropout\"],\n    )\n\n    attention_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for the attention mechanism\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"attention_dropout\"],\n    )\n\n    gelu_activation: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use a gelu activation instead of relu.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"gelu_activation\"],\n    )\n\n    sinusoidal_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"sinusoidal_embeddings\"],\n    )\n\n    causal: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should behave in a causal manner. Causal models use a triangular \"\n        \"attention mask in order to only attend to the left-side context instead if a bidirectional \"\n        \"context.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"causal\"],\n    )\n\n    asm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the \"\n        \"prediction layer.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"asm\"],\n    )\n\n    n_langs: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of languages the model handles. Set to 1 for monolingual models.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_langs\"],\n    )\n\n    use_lang_emb: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use language embeddings. Some models use additional language embeddings, \"\n        \"see the multilingual models page for information on how to use them.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"use_lang_emb\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"max_position_embeddings\"],\n    )\n\n    embed_init_std: float = schema_utils.NonNegativeFloat(\n        default=2048**-0.5,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing the embedding \"\n        \"matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"embed_init_std\"],\n    )\n\n    init_std: int = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices \"\n        \"except the embedding matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"init_std\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-06,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"layer_norm_eps\"],\n    )\n\n    bos_index: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The index of the beginning of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"bos_index\"],\n    )\n\n    eos_index: int = schema_utils.NonNegativeInteger(\n        default=1,\n        description=\"The index of the end of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"eos_index\"],\n    )\n\n    pad_index: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"The index of the padding token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pad_index\"],\n    )\n\n    unk_index: int = schema_utils.NonNegativeInteger(\n        default=3,\n        description=\"The index of the unknown token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"unk_index\"],\n    )\n\n    mask_index: int = schema_utils.NonNegativeInteger(\n        default=5,\n        description=\"The index of the masking token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"mask_index\"],\n    )\n\n    is_encoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the initialized model should be a transformer encoder or decoder as seen in \"\n        \"Vaswani et al.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"is_encoder\"],\n    )\n\n    mask_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"Model agnostic parameter to identify masked tokens when generating text in an MLM context.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"mask_token_id\"],\n    )\n\n    lang_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the language used by the model. This parameter is used when generating text in a given \"\n        \"language.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"lang_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"electra\", TEXT)\n@ludwig_dataclass\nclass ELECTRAConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an ELECTRA encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"ELECTRA\"\n\n    type: str = schema_utils.ProtectedString(\n        \"electra\",\n        description=ENCODER_METADATA[\"ELECTRA\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"google/electra-small-discriminator\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the ELECTRA model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling ElectraModel or TFElectraModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"vocab_size\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"embedding_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=4,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the “intermediate” (i.e., feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling ElectraModel or TFElectraModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"layer_norm_eps\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"longformer\", TEXT)\n@ludwig_dataclass\nclass LongformerConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for a Longformer encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"Longformer\"\n\n    type: str = schema_utils.ProtectedString(\n        \"longformer\",\n        description=ENCODER_METADATA[\"Longformer\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"use_pretrained\"],\n    )\n\n    attention_window: list[int] | int = schema_utils.OneOfOptionsField(\n        default=512,\n        allow_none=False,\n        description=\"Size of an attention window around each token. If an int, use the same size for all layers. To \"\n        \"specify a different window size for each layer, use a List[int] where len(attention_window) == \"\n        \"num_hidden_layers.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=False, description=\"\", default=512),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"attention_window\"],\n    )\n\n    sep_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"ID of the separator token, which is used when building a sequence from multiple sequences\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"sep_token_id\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"allenai/longformer-base-4096\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ParameterMetadata(internal_only=True),\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=50265,\n        description=\"Vocabulary size of the Longformer model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"vocab_size\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=4098,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed when calling LongformerEncoder\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"type_vocab_size\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"auto_transformer\", TEXT)\n@ludwig_dataclass\nclass AutoTransformerConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an AutoTransformer encoder.\"\"\"\n\n    def __post_init__(self):\n        if self.pretrained_model_name_or_path is None:\n            raise ConfigValidationError(\n                \"`pretrained_model_name_or_path` must be specified for encoder: `auto_transformer`.\"\n            )\n\n    @staticmethod\n    def module_name():\n        return \"AutoTransformer\"\n\n    @property\n    def use_pretrained(self) -> bool:\n        # Always set this to True since we always want to use the pretrained weights\n        # We don't currently support training from scratch for AutoTransformers\n        return True\n\n    type: str = schema_utils.ProtectedString(\n        \"auto_transformer\",\n        description=ENCODER_METADATA[\"AutoTransformer\"][\"type\"].long_description,\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"pretrained_model_name_or_path\"],\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"max_sequence_length\"],\n    )\n\n    reduce_output: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Vocabulary size of the AutoTransformer model. If None, the vocab size will be inferred \"\n            \"from the given pretrained model\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"vocab_size\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"tf_idf\", TEXT, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass TfIdfEncoderConfig(SequenceEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"tf_idf\")\n\n    max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n\n    str2idf: dict[str, int] = schema_utils.Dict(parameter_metadata=INTERNAL_ONLY)\n\n    vocab: list = schema_utils.List(default=None, parameter_metadata=INTERNAL_ONLY)\n\n    vocab_size: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"TextPreprocessingConfig\"):\n        preprocessing.compute_idf = True\n\n    def can_cache_embeddings(self) -> bool:\n        return True\n\n\n@DeveloperAPI\n@register_encoder_config(\"llm\", TEXT, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass LLMEncoderConfig(SequenceEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"llm\")\n    base_model: str = BaseModelDataclassField()\n    max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n    quantization: QuantizationConfig | None = QuantizationConfigField().get_default_field()\n    model_parameters: ModelParametersConfig | None = ModelParametersConfigField().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/encoders/text/hf_model_params.py",
    "content": "from ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\"\"\"\nNOTE TO DEVELOPERS: the implementation of the schema classes below must match the parameters of the HF PretrainedConfig\nclass exactly. This is because we convert this object into the matching HF PretrainedConfig object before passing it to\nthe model. Additionally, for loading and saving pretrained models, we take the config from the existing model and load\nit into this config before saving. As such, if any params needed by the pretrained model are missing, we will not be\nable to load checkpoints correctly.\n\nA common mistake is to look at the PretrainedConfig __init__ method params and ignore any additional **kwargs. In some\ncases, these kwargs are used to set additional params on the config object. For example, the DebertaConfig class has\n`position_buckets` as a kwarg param, but it nonetheless requires this to construct the model architecture.\n\nTo debug issues with missing parameters, try printing out the `model.config` of the pretrained transformer and check\nfor any params it includes that are not present in your schema config.\n\"\"\"\n\n\n@ludwig_dataclass\nclass DebertaModelParams(schema_utils.BaseMarshmallowConfig):\n    @classmethod\n    def get_hf_config_param_names(cls) -> set[str]:\n        return DebertaModelParams.get_valid_field_names()\n\n    # Model architecture params for training from scratch\n    # TODO(travis): conditionally disable setting these when `use_pretrained=True`.\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        description=\"\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=1536,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=24,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=24,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=6144,\n        description=\"Dimensionality of the 'intermediate' (often named feed-forward) layer in the Transformer encoder.\",\n    )\n\n    hidden_act: str = schema_utils.StringOptions(\n        options=[\"gelu\", \"relu\", \"silu\", \"tanh\", \"gelu_fast\", \"mish\", \"linear\", \"sigmoid\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n    )\n\n    hidden_dropout_prob: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the attention probabilities.\",\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=(\n            \"The maximum sequence length that this model might ever be used with. Typically set this to something \"\n            \"large just in case (e.g., 512 or 1024 or 2048).\"\n        ),\n    )\n\n    type_vocab_size: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=(\"The vocabulary size of the `token_type_ids`.\"),\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=(\n            \"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\"\n        ),\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-7,\n        description=\"The epsilon used by the layer normalization layers.\",\n    )\n\n    relative_attention: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether use relative position encoding.\",\n    )\n\n    max_relative_positions: int = schema_utils.Integer(\n        default=-1,\n        description=(\n            \"The range of relative positions `[-max_position_embeddings, max_position_embeddings]`. Use the same \"\n            \"value as `max_position_embeddings`.\"\n        ),\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The value used to pad input_ids.\",\n    )\n\n    position_biased_input: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether add absolute position embedding to content embedding.\",\n    )\n\n    pos_att_type: list[str] = schema_utils.List(\n        default=[\"p2c\", \"c2p\"],\n        description=(\n            \"The type of relative position attention, it can be a combination of `['p2c', 'c2p']`, e.g. `['p2c']`, \"\n            \"`['p2c', 'c2p']`, `['p2c', 'c2p']`.\"\n        ),\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n    )\n\n    pooler_hidden_size: int = schema_utils.PositiveInteger(\n        default=1536,\n        description=\"The hidden size of the pooler layers.\",\n    )\n\n    pooler_dropout: float = schema_utils.NonNegativeFloat(\n        default=0,\n        description=\"The dropout ratio for the pooler layers.\",\n    )\n\n    pooler_hidden_act: str = schema_utils.StringOptions(\n        options=[\"gelu\", \"relu\", \"silu\", \"tanh\", \"gelu_fast\", \"mish\", \"linear\", \"sigmoid\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The activation function (function or string) in the pooler.\",\n    )\n\n    position_buckets: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The number of buckets to use for each attention layer.\",\n    )\n\n    share_att_key: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to share attention key across layers.\",\n    )\n\n    norm_rel_ebd: str = schema_utils.StringOptions(\n        options=[\"layer_norm\", \"none\"],\n        default=\"layer_norm\",\n        description=\"The normalization method for relative embeddings.\",\n    )\n"
  },
  {
    "path": "ludwig/schema/encoders/text_encoders.py",
    "content": "from collections.abc import Callable\nfrom typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, TEXT\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.sequence_encoders import SequenceEncoderConfig\nfrom ludwig.schema.encoders.text.hf_model_params import DebertaModelParams\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.llms.base_model import BaseModelDataclassField\nfrom ludwig.schema.llms.model_parameters import ModelParametersConfig, ModelParametersConfigField\nfrom ludwig.schema.llms.peft import AdapterDataclassField, BaseAdapterConfig\nfrom ludwig.schema.llms.quantization import QuantizationConfig, QuantizationConfigField\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY, ParameterMetadata\nfrom ludwig.schema.utils import ludwig_dataclass\n\nif TYPE_CHECKING:\n    from ludwig.schema.features.preprocessing.text import TextPreprocessingConfig\n\n\nclass HFEncoderConfig(SequenceEncoderConfig):\n    trainable: bool\n    use_pretrained: bool\n    pretrained_model_name_or_path: str\n    reduce_output: str\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"TextPreprocessingConfig\"):\n        model_name = self.pretrained_model_name_or_path\n        if model_name is None and self.use_pretrained:\n            # no default model name, so model name is required by the subclass\n            raise ValueError(\n                f\"Missing required parameter for `{self.type}` encoder: `pretrained_model_name_or_path` when \"\n                \"`use_pretrained` is True.\"\n            )\n        preprocessing.tokenizer = \"hf_tokenizer\"\n        preprocessing.pretrained_model_name_or_path = model_name\n        if not self.can_cache_embeddings():\n            preprocessing.cache_encoder_embeddings = False\n\n    def is_pretrained(self) -> bool:\n        return self.use_pretrained\n\n    def can_cache_embeddings(self) -> bool:\n        \"\"\"Returns true if the encoder's output embeddings will not change during training.\"\"\"\n        return not self.trainable and self.reduce_output != \"attention\"\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass HFEncoderImplConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the base HF encoder implmenetation.\"\"\"\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"HFEncoder\"][\"use_pretrained\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"HFEncoder\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n    )\n\n    # Internal params set based on preprocessing metadata\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        description=\"\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n            \"True for trained models to prevent loading pretrained encoder weights from model hub.\"\n        ),\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"albert\", TEXT)\n@ludwig_dataclass\nclass ALBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an ALBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"ALBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"albert\",\n        description=ENCODER_METADATA[\"ALBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"albert-base-v2\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30000,\n        description=\"Vocabulary size of the ALBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"vocab_size\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Dimensionality of vocabulary embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"embedding_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_hidden_layers\"],\n    )\n\n    num_hidden_groups: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Number of groups for the hidden layers, parameters in the same group are shared.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_hidden_groups\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"The dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer \"\n        \"encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"intermediate_size\"],\n    )\n\n    inner_group_num: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of inner repetition of attention and ffn.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"inner_group_num\"],\n    )\n\n    hidden_act: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu_new\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling AlbertModel or TFAlbertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"layer_norm_eps\"],\n    )\n\n    classifier_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for attached classifiers.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"classifier_dropout_prob\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"position_embedding_type\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=3,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ALBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n# TODO: uncomment when sentencepiece doesn't cause segfaults: https://github.com/ludwig-ai/ludwig/issues/2983\n@DeveloperAPI\n# @register_encoder_config(\"mt5\", TEXT)\n@ludwig_dataclass\nclass MT5Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an MT5 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"MT5\"\n\n    type: str = schema_utils.ProtectedString(\n        \"mt5\",\n        description=ENCODER_METADATA[\"MT5\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"google/mt5-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=250112,\n        description=\"Vocabulary size of the T5 model. Defines the number of different tokens that can be represented \"\n        \"by the inputs_ids passed when calling T5Model or TFT5Model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Size of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_model\"],\n    )\n\n    d_kv: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // \"\n        \"num_heads.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_kv\"],\n    )\n\n    d_ff: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Size of the intermediate feed forward layer in each T5Block.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"d_ff\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_layers\"],\n    )\n\n    num_decoder_layers: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not \"\n        \"set.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_decoder_layers\"],\n    )\n\n    num_heads: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"num_heads\"],\n    )\n\n    relative_attention_num_buckets: int = schema_utils.PositiveInteger(\n        default=32,\n        description=\"The number of buckets to use for each attention layer.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"relative_attention_num_buckets\"],\n    )\n\n    dropout_rate: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The ratio for all dropout layers.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"dropout_rate\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-06,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_factor: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"A factor for initializing all weight matrices (should be kept to 1, used internally for \"\n        \"initialization testing)\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"initializer_factor\"],\n    )\n\n    feed_forward_proj: str = schema_utils.StringOptions(\n        [\"relu\", \"gated-gelu\"],\n        default=\"gated-gelu\",\n        description=\"Type of feed forward layer to be used. \",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"feed_forward_proj\"],\n    )\n\n    is_encoder_decoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"is_encoder_decoder\"],\n    )\n\n    use_cache: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"use_cache\"],\n    )\n\n    tokenizer_class: str = schema_utils.String(\n        default=\"T5Tokenizer\",\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"tokenizer_class\"],\n    )\n\n    tie_word_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether the model's input and output word embeddings should be tied.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"tie_word_embeddings\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pad_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"eos_token_id\"],\n    )\n\n    decoder_start_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"If an encoder-decoder model starts decoding with a different token than _bos_, the id of that \"\n        \"token.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"decoder_start_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"MT5\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"xlmroberta\", TEXT)\n@ludwig_dataclass\nclass XLMRoBERTaConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLMRoBERTa encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLMRoBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlmroberta\",\n        description=ENCODER_METADATA[\"XLMRoBERTa\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlm-roberta-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Vocabulary size of the XLMRoBERTa model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"vocab_size\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"eos_token_id\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=514,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed in.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"type_vocab_size\"],\n    )\n\n    add_pooling_layer: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to add a pooling layer to the encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"add_pooling_layer\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLMRoBERTa\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"bert\", TEXT)\n@ludwig_dataclass\nclass BERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an BERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"BERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"bert\",\n        description=ENCODER_METADATA[\"BERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"bert-base-uncased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the BERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"vocab_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"layer_norm_eps\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pad_token_id\"],\n    )\n\n    gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use gradient checkpointing.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"gradient_checkpointing\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"BERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"deberta\", TEXT)\n@ludwig_dataclass\nclass DebertaV2Config(HFEncoderImplConfig, DebertaModelParams):\n    \"\"\"This dataclass configures the schema used for a DeBERTa-v2 / v3 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"DeBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"deberta\",\n        description=ENCODER_METADATA[\"DeBERTa\"][\"type\"].long_description,\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"tasksource/deberta-base-long-nli\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DeBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.StringOptions(\n        [\"cls_pooled\", \"last\", \"sum\", \"mean\", \"max\", \"concat\", \"attention\"],\n        default=\"sum\",\n        allow_none=True,\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n    )\n\n\n# TODO: uncomment once we figure out host memory issue: https://github.com/ludwig-ai/ludwig/issues/3107\n@DeveloperAPI\n# @register_encoder_config(\"xlm\", TEXT)\n@ludwig_dataclass\nclass XLMConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLM encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLM\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlm\",\n        description=ENCODER_METADATA[\"XLM\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlm-mlm-en-2048\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"reduce_output\"],\n    )\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30145,\n        description=\"Vocabulary size of the BERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling XLMModel or TFXLMModel.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"vocab_size\"],\n    )\n\n    emb_dim: int = schema_utils.PositiveInteger(\n        default=2048,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"emb_dim\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_heads\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"dropout\"],\n    )\n\n    attention_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for the attention mechanism.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"attention_dropout\"],\n    )\n\n    gelu_activation: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use gelu for the activations instead of relu.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"gelu_activation\"],\n    )\n\n    sinusoidal_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"sinusoidal_embeddings\"],\n    )\n\n    causal: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should behave in a causal manner. Causal models use a triangular \"\n        \"attention mask in order to only attend to the left-side context instead if a bidirectional \"\n        \"context.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"causal\"],\n    )\n\n    asm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the \"\n        \"prediction layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"asm\"],\n    )\n\n    n_langs: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of languages the model handles. Set to 1 for monolingual models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"n_langs\"],\n    )\n\n    use_lang_emb: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use language embeddings. Some models use additional language embeddings, \"\n        \"see the multilingual models page for information on how to use them.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"use_lang_emb\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"max_position_embeddings\"],\n    )\n\n    embed_init_std: float = schema_utils.NonNegativeFloat(\n        default=2048**-0.5,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing the embedding \"\n        \"matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"embed_init_std\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"layer_norm_eps\"],\n    )\n\n    init_std: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices \"\n        \"except the embedding matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"init_std\"],\n    )\n\n    bos_index: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The index of the beginning of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"bos_index\"],\n    )\n\n    eos_index: int = schema_utils.NonNegativeInteger(\n        default=1,\n        description=\"The index of the end of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"eos_index\"],\n    )\n\n    pad_index: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"The index of the padding token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pad_index\"],\n    )\n\n    unk_index: int = schema_utils.NonNegativeInteger(\n        default=3,\n        description=\"The index of the unknown token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"unk_index\"],\n    )\n\n    mask_index: int = schema_utils.NonNegativeInteger(\n        default=5,\n        description=\"The index of the masking token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"mask_index\"],\n    )\n\n    is_encoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the initialized model should be a transformer encoder or decoder as seen in \"\n        \"Vaswani et al.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"is_encoder\"],\n    )\n\n    start_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"start_n_top\"],\n    )\n\n    end_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"end_n_top\"],\n    )\n\n    mask_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"Model agnostic parameter to identify masked tokens when generating text in an MLM context.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"mask_token_id\"],\n    )\n\n    lang_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the language used by the model. This parameter is used when generating text in a given \"\n        \"language.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"lang_id\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"bos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLM\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"gpt\", TEXT)\n@ludwig_dataclass\nclass GPTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an GPT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"GPT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"gpt\",\n        description=ENCODER_METADATA[\"GPT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"max_sequence_length\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"reduce_output\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"openai-gpt\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the GPT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling OpenAIGPTModel or TFOpenAIGPTModel.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=40478,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_embd\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"n_head\"],\n    )\n\n    afn: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\"],  # gelu_new results in a KeyError.\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"afn\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"initializer_range\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"gpt2\", TEXT)\n@ludwig_dataclass\nclass GPT2Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an GPT2 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"GPT2\"\n\n    type: str = schema_utils.ProtectedString(\n        \"gpt2\",\n        description=ENCODER_METADATA[\"GPT2\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"gpt2\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=50257,\n        description=\"Vocabulary size of the GPT-2 model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling GPT2Model or TFGPT2Model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_embd\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_head\"],\n    )\n\n    n_inner: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Dimensionality of the inner feed-forward layers. None will set it to 4 times n_embd\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"n_inner\"],\n    )\n\n    activation_function: str = schema_utils.StringOptions(\n        [\"relu\", \"silu\", \"gelu\", \"tanh\", \"gelu_new\"],\n        default=\"gelu_new\",\n        description=\"Activation function, to be selected in the list ['relu', 'silu', 'gelu', 'tanh', 'gelu_new'].\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"activation_function\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"initializer_range\"],\n    )\n\n    scale_attn_weights: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Scale attention weights by dividing by sqrt(hidden_size).\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"scale_attn_weights\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"GPT2\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"roberta\", TEXT)\n@ludwig_dataclass\nclass RoBERTaConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an RoBERTa encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"RoBERTa\"\n\n    type: str = schema_utils.ProtectedString(\n        \"roberta\",\n        description=ENCODER_METADATA[\"RoBERTa\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"roberta-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Vocabulary size of the RoBERTa model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"vocab_size\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"RoBERTa\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"transformer_xl\", TEXT)\n@ludwig_dataclass\nclass TransformerXLConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an TransformerXL encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"TransformerXL\"\n\n    type: str = schema_utils.ProtectedString(\n        \"transformer_xl\",\n        description=ENCODER_METADATA[\"TransformerXL\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"transfo-xl-wt103\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=267735,\n        description=\"Vocabulary size of the TransfoXL model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling TransfoXLModel or TFTransfoXLModel.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"vocab_size\"],\n    )\n\n    cutoffs: list[int] = schema_utils.List(\n        int,\n        default=[20000, 40000, 200000],\n        description=\"Cutoffs for the adaptive softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"cutoffs\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the model’s hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_model\"],\n    )\n\n    d_embed: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the embeddings\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_embed\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"n_head\"],\n    )\n\n    d_head: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Dimensionality of the model’s heads.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_head\"],\n    )\n\n    d_inner: int = schema_utils.PositiveInteger(\n        default=4096,\n        description=\" Inner dimension in FF\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"d_inner\"],\n    )\n\n    div_val: int = schema_utils.PositiveInteger(\n        default=4,\n        description=\"Divident value for adapative input and softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"div_val\"],\n    )\n\n    pre_lnorm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to apply LayerNorm to the input instead of the output in the blocks.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pre_lnorm\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=18,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"n_layer\"],\n    )\n\n    mem_len: int = schema_utils.PositiveInteger(\n        default=1600,\n        description=\"Length of the retained previous heads.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"mem_len\"],\n    )\n\n    clamp_len: int = schema_utils.PositiveInteger(\n        default=1000,\n        description=\"Use the same pos embeddings after clamp_len.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"clamp_len\"],\n    )\n\n    same_length: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use the same attn length for all tokens\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"same_length\"],\n    )\n\n    proj_share_all_but_first: bool = schema_utils.Boolean(\n        default=True,\n        description=\"True to share all but first projs, False not to share.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"proj_share_all_but_first\"],\n    )\n\n    attn_type: int = schema_utils.IntegerRange(\n        default=0,\n        min=0,\n        max=3,\n        description=\"Attention type. 0 for Transformer-XL, 1 for Shaw et al, 2 for Vaswani et al, 3 for Al Rfou et al.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"attn_type\"],\n    )\n\n    sample_softmax: int = schema_utils.Integer(\n        default=-1,\n        description=\"Number of samples in the sampled softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"sample_softmax\"],\n    )\n\n    adaptive: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use adaptive softmax.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"adaptive\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"dropout\"],\n    )\n\n    dropatt: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"dropatt\"],\n    )\n\n    untie_r: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether ot not to untie relative position biases.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"untie_r\"],\n    )\n\n    init: str = schema_utils.String(\n        default=\"normal\",\n        description=\"Parameter initializer to use.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init\"],\n    )\n\n    init_range: float = schema_utils.NonNegativeFloat(\n        default=0.01,\n        description=\"Parameters initialized by U(-init_range, init_range).\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init_range\"],\n    )\n\n    proj_init_std: float = schema_utils.NonNegativeFloat(\n        default=0.01,\n        description=\"Parameters initialized by N(0, init_std)\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"proj_init_std\"],\n    )\n\n    init_std: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"Parameters initialized by N(0, init_std)\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"init_std\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-5,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"layer_norm_epsilon\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"TransformerXL\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"xlnet\", TEXT)\n@ludwig_dataclass\nclass XLNetConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an XLNet encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"XLNet\"\n\n    type: str = schema_utils.ProtectedString(\n        \"xlnet\",\n        description=ENCODER_METADATA[\"XLNet\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"xlnet-base-cased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32000,\n        description=\"Vocabulary size of the XLNet model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling XLNetModel or TFXLNetModel.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"d_model\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"n_head\"],\n    )\n\n    d_inner: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"d_inner\"],\n    )\n\n    ff_activation: str = schema_utils.StringOptions(\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler. If string, \"\n        \"'gelu', 'relu', 'silu' and 'gelu_new' are supported.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"ff_activation\"],\n    )\n\n    untie_r: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to untie relative position biases\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"untie_r\"],\n    )\n\n    attn_type: str = schema_utils.StringOptions(\n        [\"bi\"],\n        default=\"bi\",\n        description=\"The attention type used by the model. Currently only 'bi' is supported.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"attn_type\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"layer_norm_eps\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"dropout\"],\n    )\n\n    mem_len: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of tokens to cache. The key/value pairs that have already been pre-computed in a \"\n        \"previous forward pass won’t be re-computed. \",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"mem_len\"],\n    )\n\n    reuse_len: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of tokens in the current batch to be cached and reused in the future.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"reuse_len\"],\n    )\n\n    use_mems_eval: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the model should make use of the recurrent memory mechanism in evaluation mode.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_mems_eval\"],\n    )\n\n    use_mems_train: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should make use of the recurrent memory mechanism in train mode.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"use_mems_train\"],\n    )\n\n    bi_data: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use bidirectional input pipeline. Usually set to True during pretraining and \"\n        \"False during finetuning.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"bi_data\"],\n    )\n\n    clamp_len: int = schema_utils.Integer(\n        default=-1,\n        description=\"Clamp all relative distances larger than clamp_len. Setting this attribute to -1 means no \"\n        \"clamping.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"clamp_len\"],\n    )\n\n    same_length: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use the same attention length for each token.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"same_length\"],\n    )\n\n    summary_type: str = schema_utils.StringOptions(\n        [\"last\", \"first\", \"mean\", \"cls_index\", \"attn\"],\n        default=\"last\",\n        description=\"Argument used when doing sequence summary. Used in the sequence classification and multiple \"\n        \"choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_type\"],\n    )\n\n    summary_use_proj: bool = schema_utils.Boolean(\n        default=True,\n        description=\"\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_use_proj\"],\n    )\n\n    summary_activation: str = schema_utils.String(\n        default=\"tanh\",\n        description=\"Argument used when doing sequence summary. Used in the sequence classification and multiple \"\n        \"choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_activation\"],\n    )\n\n    summary_last_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        description=\"Used in the sequence classification and multiple choice models.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"summary_last_dropout\"],\n    )\n\n    start_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\"Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"start_n_top\"],\n    )\n\n    end_n_top: int = schema_utils.PositiveInteger(\n        default=5,\n        description=\" Used in the SQuAD evaluation script.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"end_n_top\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=5,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pad_token_id\"],\n    )\n\n    bos_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The beginning of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"bos_token_id\"],\n    )\n\n    eos_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"The end of sequence token ID.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"eos_token_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"XLNet\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"distilbert\", TEXT)\n@ludwig_dataclass\nclass DistilBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an DistilBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"DistilBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"distilbert\",\n        description=ENCODER_METADATA[\"DistilBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"distilbert-base-uncased\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the DistilBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling DistilBertModel or TFDistilBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"vocab_size\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"dropout\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"max_position_embeddings\"],\n    )\n\n    sinusoidal_pos_embds: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use sinusoidal positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"sinusoidal_pos_embds\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"n_heads\"],\n    )\n\n    dim: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\" Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"dim\"],\n    )\n\n    hidden_dim: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"The size of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"hidden_dim\"],\n    )\n\n    attention_dropout: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"attention_dropout\"],\n    )\n\n    activation: str | Callable = schema_utils.StringOptions(  # TODO: Add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler. If string, \"\n        \"'gelu', 'relu', 'silu' and 'gelu_new' are supported.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"activation\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"initializer_range\"],\n    )\n\n    qa_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probabilities used in the question answering model DistilBertForQuestionAnswering.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"qa_dropout\"],\n    )\n\n    seq_classif_dropout: float = schema_utils.FloatRange(\n        default=0.2,\n        min=0,\n        max=1,\n        description=\"The dropout probabilities used in the sequence classification and the multiple choice model \"\n        \"DistilBertForSequenceClassification.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"seq_classif_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"DistilBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n# TODO: uncomment when CTRL bug (https://github.com/ludwig-ai/ludwig/issues/2977) has been fixed to add back in\n@DeveloperAPI\n# @register_encoder_config(\"ctrl\", TEXT)\n@ludwig_dataclass\nclass CTRLConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an CTRL encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"CTRL\"\n\n    type: str = schema_utils.ProtectedString(\n        \"ctrl\",\n        description=ENCODER_METADATA[\"CTRL\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"ctrl\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=246534,\n        description=\"Vocabulary size of the CTRL model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling CTRLModel or TFCTRLModel.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"vocab_size\"],\n    )\n\n    n_positions: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_positions\"],\n    )\n\n    n_ctx: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Dimensionality of the causal mask (usually same as n_positions)\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_ctx\"],\n    )\n\n    n_embd: int = schema_utils.PositiveInteger(\n        default=1280,\n        description=\"Dimensionality of the embeddings and hidden states.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_embd\"],\n    )\n\n    dff: int = schema_utils.PositiveInteger(\n        default=8192,\n        description=\"Dimensionality of the inner dimension of the feed forward networks (FFN).\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"dff\"],\n    )\n\n    n_layer: int = schema_utils.PositiveInteger(\n        default=48,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_layer\"],\n    )\n\n    n_head: int = schema_utils.PositiveInteger(\n        default=16,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"n_head\"],\n    )\n\n    resid_pdrop: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\" The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"resid_pdrop\"],\n    )\n\n    embd_pdrop: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"embd_pdrop\"],\n    )\n\n    attn_pdrop: float = schema_utils.NonNegativeFloat(\n        default=0.1,\n        description=\"The dropout ratio for the attention.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"attn_pdrop\"],\n    )\n\n    layer_norm_epsilon: float = schema_utils.NonNegativeFloat(\n        default=1e-6,\n        description=\"The epsilon to use in the layer normalization layers\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"layer_norm_epsilon\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"initializer_range\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CTRL\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"camembert\", TEXT)\n@ludwig_dataclass\nclass CamemBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an CamemBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"CamemBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"camembert\",\n        description=ENCODER_METADATA[\"CamemBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"use_pretrained\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"camembert-base\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32005,\n        description=\"Vocabulary size of the CamemBERT model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"vocab_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=768,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=3072,\n        description=\"Dimensionality of the “intermediate” (often named feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=514,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed when calling BertModel or TFBertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-05,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"layer_norm_eps\"],\n    )\n\n    pad_token_id: int = schema_utils.Integer(\n        default=1,\n        description=\"The ID of the token to use as padding.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pad_token_id\"],\n    )\n\n    gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use gradient checkpointing.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"gradient_checkpointing\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"CamemBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"t5\", TEXT)\n@ludwig_dataclass\nclass T5Config(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an T5 encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"T5\"\n\n    type: str = schema_utils.ProtectedString(\n        \"t5\",\n        description=ENCODER_METADATA[\"T5\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"t5-small\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=32128,\n        description=\"Vocabulary size of the T5 model. Defines the number of different tokens that can be represented \"\n        \"by the inputs_ids passed when calling T5Model or TFT5Model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"vocab_size\"],\n    )\n\n    d_model: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Size of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_model\"],\n    )\n\n    d_kv: int = schema_utils.PositiveInteger(\n        default=64,\n        description=\"Size of the key, query, value projections per attention head. d_kv has to be equal to d_model // \"\n        \"num_heads.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_kv\"],\n    )\n\n    d_ff: int = schema_utils.PositiveInteger(\n        default=2048,\n        description=\"Size of the intermediate feed forward layer in each T5Block.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"d_ff\"],\n    )\n\n    num_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_layers\"],\n    )\n\n    num_decoder_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer decoder. Will use the same value as num_layers if not \"\n        \"set.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_decoder_layers\"],\n    )\n\n    num_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"num_heads\"],\n    )\n\n    relative_attention_num_buckets: int = schema_utils.PositiveInteger(\n        default=32,\n        description=\"The number of buckets to use for each attention layer.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"relative_attention_num_buckets\"],\n    )\n\n    dropout_rate: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The ratio for all dropout layers.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"dropout_rate\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-6,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"layer_norm_eps\"],\n    )\n\n    initializer_factor: float = schema_utils.NonNegativeFloat(\n        default=1,\n        description=\"A factor for initializing all weight matrices (should be kept to 1, used internally for \"\n        \"initialization testing).\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"initializer_factor\"],\n    )\n\n    feed_forward_proj: str = schema_utils.StringOptions(\n        [\"relu\", \"gated-gelu\"],\n        default=\"relu\",\n        description=\"Type of feed forward layer to be used. Should be one of 'relu' or 'gated-gelu'. T5v1.1 uses the \"\n        \"'gated-gelu' feed forward projection. Original T5 uses 'relu'.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"feed_forward_proj\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"T5\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"flaubert\", TEXT)\n@ludwig_dataclass\nclass FlauBERTConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an FlauBERT encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"FlauBERT\"\n\n    type: str = schema_utils.ProtectedString(\n        \"flaubert\",\n        description=ENCODER_METADATA[\"FlauBERT\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"flaubert/flaubert_small_cased\",\n        description=\"Name of path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30145,\n        description=\"Vocabulary size of the FlauBERT model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling FlaubertModel or TFFlaubertModel.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"vocab_size\"],\n    )\n\n    pre_norm: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to apply the layer normalization before or after the feed forward layer following the \"\n        \"attention in each layer (Vaswani et al., Tensor2Tensor for Neural Machine Translation. 2018)\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pre_norm\"],\n    )\n\n    layerdrop: float = schema_utils.FloatRange(\n        default=0.2,\n        min=0,\n        max=1,\n        description=\"Probability to drop layers during training (Fan et al., Reducing Transformer Depth on Demand \"\n        \"with Structured Dropout. ICLR 2020)\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"layerdrop\"],\n    )\n\n    emb_dim: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"emb_dim\"],\n    )\n\n    n_layers: int = schema_utils.PositiveInteger(\n        default=6,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_layers\"],\n    )\n\n    n_heads: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_heads\"],\n    )\n\n    dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"dropout\"],\n    )\n\n    attention_dropout: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for the attention mechanism\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"attention_dropout\"],\n    )\n\n    gelu_activation: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use a gelu activation instead of relu.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"gelu_activation\"],\n    )\n\n    sinusoidal_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use sinusoidal positional embeddings instead of absolute positional embeddings.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"sinusoidal_embeddings\"],\n    )\n\n    causal: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not the model should behave in a causal manner. Causal models use a triangular \"\n        \"attention mask in order to only attend to the left-side context instead if a bidirectional \"\n        \"context.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"causal\"],\n    )\n\n    asm: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether or not to use an adaptive log softmax projection layer instead of a linear layer for the \"\n        \"prediction layer.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"asm\"],\n    )\n\n    n_langs: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The number of languages the model handles. Set to 1 for monolingual models.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"n_langs\"],\n    )\n\n    use_lang_emb: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use language embeddings. Some models use additional language embeddings, \"\n        \"see the multilingual models page for information on how to use them.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"use_lang_emb\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"max_position_embeddings\"],\n    )\n\n    embed_init_std: float = schema_utils.NonNegativeFloat(\n        default=2048**-0.5,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing the embedding \"\n        \"matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"embed_init_std\"],\n    )\n\n    init_std: int = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices \"\n        \"except the embedding matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"init_std\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-06,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"layer_norm_eps\"],\n    )\n\n    bos_index: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The index of the beginning of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"bos_index\"],\n    )\n\n    eos_index: int = schema_utils.NonNegativeInteger(\n        default=1,\n        description=\"The index of the end of sentence token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"eos_index\"],\n    )\n\n    pad_index: int = schema_utils.NonNegativeInteger(\n        default=2,\n        description=\"The index of the padding token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pad_index\"],\n    )\n\n    unk_index: int = schema_utils.NonNegativeInteger(\n        default=3,\n        description=\"The index of the unknown token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"unk_index\"],\n    )\n\n    mask_index: int = schema_utils.NonNegativeInteger(\n        default=5,\n        description=\"The index of the masking token in the vocabulary.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"mask_index\"],\n    )\n\n    is_encoder: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the initialized model should be a transformer encoder or decoder as seen in \"\n        \"Vaswani et al.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"is_encoder\"],\n    )\n\n    mask_token_id: int = schema_utils.Integer(\n        default=0,\n        description=\"Model agnostic parameter to identify masked tokens when generating text in an MLM context.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"mask_token_id\"],\n    )\n\n    lang_id: int = schema_utils.Integer(\n        default=0,\n        description=\"The ID of the language used by the model. This parameter is used when generating text in a given \"\n        \"language.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"lang_id\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"FlauBERT\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"electra\", TEXT)\n@ludwig_dataclass\nclass ELECTRAConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an ELECTRA encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"ELECTRA\"\n\n    type: str = schema_utils.ProtectedString(\n        \"electra\",\n        description=ENCODER_METADATA[\"ELECTRA\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"use_pretrained\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"google/electra-small-discriminator\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"saved_weights_in_checkpoint\"],\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=30522,\n        description=\"Vocabulary size of the ELECTRA model. Defines the number of different tokens that can be \"\n        \"represented by the inputs_ids passed when calling ElectraModel or TFElectraModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"vocab_size\"],\n    )\n\n    embedding_size: int = schema_utils.PositiveInteger(\n        default=128,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"embedding_size\"],\n    )\n\n    hidden_size: int = schema_utils.PositiveInteger(\n        default=256,\n        description=\"Dimensionality of the encoder layers and the pooler layer.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_size\"],\n    )\n\n    num_hidden_layers: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Number of hidden layers in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"num_hidden_layers\"],\n    )\n\n    num_attention_heads: int = schema_utils.PositiveInteger(\n        default=4,\n        description=\"Number of attention heads for each attention layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"num_attention_heads\"],\n    )\n\n    intermediate_size: int = schema_utils.PositiveInteger(\n        default=1024,\n        description=\"Dimensionality of the “intermediate” (i.e., feed-forward) layer in the Transformer encoder.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"intermediate_size\"],\n    )\n\n    hidden_act: str | Callable = schema_utils.StringOptions(  # TODO: add support for callable\n        [\"gelu\", \"relu\", \"silu\", \"gelu_new\"],\n        default=\"gelu\",\n        description=\"The non-linear activation function (function or string) in the encoder and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_act\"],\n    )\n\n    hidden_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"hidden_dropout_prob\"],\n    )\n\n    attention_probs_dropout_prob: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the attention probabilities.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"attention_probs_dropout_prob\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=512,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"The vocabulary size of the token_type_ids passed when calling ElectraModel or TFElectraModel.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"type_vocab_size\"],\n    )\n\n    initializer_range: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"The standard deviation of the truncated_normal_initializer for initializing all weight matrices.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"initializer_range\"],\n    )\n\n    layer_norm_eps: float = schema_utils.NonNegativeFloat(\n        default=1e-12,\n        description=\"The epsilon used by the layer normalization layers.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"layer_norm_eps\"],\n    )\n\n    position_embedding_type: str = schema_utils.StringOptions(\n        [\"absolute\", \"relative_key\", \"relative_key_query\"],\n        default=\"absolute\",\n        description=\"Type of position embedding.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"position_embedding_type\"],\n    )\n\n    classifier_dropout: float = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=0,\n        max=1,\n        description=\"The dropout ratio for the classification head.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"classifier_dropout\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"ELECTRA\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"longformer\", TEXT)\n@ludwig_dataclass\nclass LongformerConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for a Longformer encoder.\"\"\"\n\n    @staticmethod\n    def module_name():\n        return \"Longformer\"\n\n    type: str = schema_utils.ProtectedString(\n        \"longformer\",\n        description=ENCODER_METADATA[\"Longformer\"][\"type\"].long_description,\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"max_sequence_length\"],\n    )\n\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. If false, the model will train from \"\n        \"scratch which is very computationally expensive.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"use_pretrained\"],\n    )\n\n    attention_window: list[int] | int = schema_utils.OneOfOptionsField(\n        default=512,\n        allow_none=False,\n        description=\"Size of an attention window around each token. If an int, use the same size for all layers. To \"\n        \"specify a different window size for each layer, use a List[int] where len(attention_window) == \"\n        \"num_hidden_layers.\",\n        field_options=[\n            schema_utils.PositiveInteger(allow_none=False, description=\"\", default=512),\n            schema_utils.List(list_type=int, allow_none=False),\n        ],\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"attention_window\"],\n    )\n\n    sep_token_id: int = schema_utils.Integer(\n        default=2,\n        description=\"ID of the separator token, which is used when building a sequence from multiple sequences\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"sep_token_id\"],\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=\"allenai/longformer-base-4096\",\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"pretrained_model_name_or_path\"],\n    )\n\n    saved_weights_in_checkpoint: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Are the pretrained encoder weights saved in this model's checkpoint? Automatically set to\"\n        \"True for trained models to prevent loading pretrained encoder weights from model hub.\",\n        parameter_metadata=ParameterMetadata(internal_only=True),\n    )\n\n    reduce_output: str = schema_utils.String(\n        default=\"cls_pooled\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=50265,\n        description=\"Vocabulary size of the Longformer model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"vocab_size\"],\n    )\n\n    max_position_embeddings: int = schema_utils.PositiveInteger(\n        default=4098,\n        description=\"The maximum sequence length that this model might ever be used with. Typically set this to \"\n        \"something large just in case (e.g., 512 or 1024 or 2048).\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"max_position_embeddings\"],\n    )\n\n    type_vocab_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"The vocabulary size of the token_type_ids passed when calling LongformerEncoder\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"type_vocab_size\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"Longformer\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"auto_transformer\", TEXT)\n@ludwig_dataclass\nclass AutoTransformerConfig(HFEncoderConfig):\n    \"\"\"This dataclass configures the schema used for an AutoTransformer encoder.\"\"\"\n\n    def __post_init__(self):\n        # Always force use_pretrained=True — we don't support training from scratch for AutoTransformers\n        self.use_pretrained = True\n        if self.pretrained_model_name_or_path is None:\n            raise ConfigValidationError(\n                \"`pretrained_model_name_or_path` must be specified for encoder: `auto_transformer`.\"\n            )\n\n    @staticmethod\n    def module_name():\n        return \"AutoTransformer\"\n\n    # Always True — we don't support training from scratch for AutoTransformers\n    use_pretrained: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to use the pretrained weights for the model. Always True for AutoTransformers.\",\n    )\n\n    type: str = schema_utils.ProtectedString(\n        \"auto_transformer\",\n        description=ENCODER_METADATA[\"AutoTransformer\"][\"type\"].long_description,\n    )\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Name or path of the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"pretrained_model_name_or_path\"],\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Maximum length of the input sequence.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"max_sequence_length\"],\n    )\n\n    reduce_output: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"The method used to reduce a sequence of tensors down to a single tensor.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"reduce_output\"],\n    )\n\n    trainable: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to finetune the model on your dataset.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"trainable\"],\n    )\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n\n    vocab: list = schema_utils.List(\n        default=None,\n        description=\"Vocabulary for the encoder\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"vocab\"],\n    )\n\n    vocab_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Vocabulary size of the AutoTransformer model. If None, the vocab size will be inferred \"\n            \"from the given pretrained model\"\n        ),\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"vocab_size\"],\n    )\n\n    pretrained_kwargs: dict = schema_utils.Dict(\n        default=None,\n        description=\"Additional kwargs to pass to the pretrained model.\",\n        parameter_metadata=ENCODER_METADATA[\"AutoTransformer\"][\"pretrained_kwargs\"],\n    )\n\n\n@DeveloperAPI\n@register_encoder_config(\"tf_idf\", TEXT, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass TfIdfEncoderConfig(SequenceEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"tf_idf\")\n\n    max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n\n    str2idf: dict[str, int] = schema_utils.Dict(parameter_metadata=INTERNAL_ONLY)\n\n    vocab: list = schema_utils.List(default=None, parameter_metadata=INTERNAL_ONLY)\n\n    vocab_size: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n\n    def set_fixed_preprocessing_params(self, model_type: str, preprocessing: \"TextPreprocessingConfig\"):\n        preprocessing.compute_idf = True\n\n    def can_cache_embeddings(self) -> bool:\n        return True\n\n\n@DeveloperAPI\n@register_encoder_config(\"llm\", TEXT, model_types=[MODEL_ECD])\n@ludwig_dataclass\nclass LLMEncoderConfig(SequenceEncoderConfig):\n    type: str = schema_utils.ProtectedString(\"llm\")\n    base_model: str = BaseModelDataclassField()\n    max_sequence_length: int = schema_utils.Integer(default=None, allow_none=True, parameter_metadata=INTERNAL_ONLY)\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n    quantization: QuantizationConfig | None = QuantizationConfigField().get_default_field()\n    model_parameters: ModelParametersConfig | None = ModelParametersConfigField().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/encoders/utils.py",
    "content": "from dataclasses import Field\nfrom typing import Any, TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, TYPE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import ENCODER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json\nfrom ludwig.utils.registry import Registry\n\nif TYPE_CHECKING:\n    from ludwig.schema.encoders.base import BaseEncoderConfig\n\n\nencoder_config_registry = Registry()\n\n\n@DeveloperAPI\ndef register_encoder_config(name: str, features: str | list[str], model_types: list[str] | None = None):\n    if model_types is None:\n        model_types = [MODEL_ECD]\n\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for model_type in model_types:\n            for feature in features:\n                key = (model_type, feature)\n                feature_registry = encoder_config_registry.get(key, {})\n                feature_registry[name] = cls\n                encoder_config_registry[key] = feature_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_encoder_cls(model_type: str, feature: str, name: str):\n    return encoder_config_registry[(model_type, feature)][name]\n\n\n@DeveloperAPI\ndef get_encoder_classes(model_type: str, feature: str) -> dict[str, type[\"BaseEncoderConfig\"]]:\n    return encoder_config_registry[(model_type, feature)]\n\n\n@DeveloperAPI\ndef get_encoder_descriptions(model_type: str, feature_type: str) -> dict[str, Any]:\n    \"\"\"This function returns a dictionary of encoder descriptions available at the type selection.\n\n    The process works as follows - 1) Get a dictionary of valid encoders from the encoder config registry,\n    but inverse the key/value pairs since we need to index `valid_encoders` later with an altered version\n    of the encoder config class name. 2) Loop through Encoder Metadata entries, if a metadata entry has an\n    encoder name that matches a valid encoder, add the description metadata to the output dictionary.\n\n    Args:\n        model_type (str): The model type to get encoder descriptions for\n        feature_type (str): The feature type to get encoder descriptions for\n    Returns:\n         dict: A dictionary mapping encoder registered names to their respective description metadata.\n    \"\"\"\n    output = {}\n    valid_encoders = {\n        cls.module_name() if hasattr(cls, \"module_name\") else None: registered_name\n        for registered_name, cls in get_encoder_classes(model_type, feature_type).items()\n    }\n\n    for k, v in ENCODER_METADATA.items():\n        if k in valid_encoders.keys():\n            output[valid_encoders[k]] = convert_metadata_to_json(v[TYPE])\n\n    return output\n\n\n@DeveloperAPI\ndef get_encoder_conds(encoder_classes: dict[str, type[\"BaseEncoderConfig\"]]) -> list[dict[str, Any]]:\n    \"\"\"Returns a JSON schema of conditionals to validate against encoder types for specific feature types.\"\"\"\n    conds = []\n    for encoder_type, encoder_cls in encoder_classes.items():\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(encoder_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        encoder_cond = schema_utils.create_cond(\n            {\"type\": encoder_type},\n            other_props,\n        )\n        conds.append(encoder_cond)\n    return conds\n\n\n@DeveloperAPI\ndef EncoderDataclassField(\n    model_type: str, feature_type: str, default: str, description: str = \"\", blocklist: list[str] = []\n) -> Field:\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify an encoder config.\n\n    Returns: Initialized dataclass field that converts an untyped dict with params to an encoder config.\n    \"\"\"\n    encoder_registry = get_encoder_classes(model_type, feature_type)\n\n    class EncoderSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(\n                registry=encoder_registry, default_value=default, description=description, allow_str_value=True\n            )\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return encoder_registry[key]\n\n        def _jsonschema_type_mapping(self):\n            # NOTE: Edit carefully if necessary! We want these enums to remain in a consistent order, so do not use sets\n            # or other unordered data structures to chaperone the registry keys around.\n            #\n            # Also, note the placement inside this function - since this is a list, it will not update with any late\n            # additions to the registry (e.g. in our tests)!\n            enum = [e for e in encoder_registry.keys() if e not in blocklist]\n\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": enum,\n                        \"enumDescriptions\": get_encoder_descriptions(model_type, feature_type),\n                        \"default\": default,\n                    },\n                },\n                \"title\": \"encoder_options\",\n                \"allOf\": get_encoder_conds(encoder_registry),\n            }\n\n    return EncoderSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/export_schema.py",
    "content": "\"\"\"Export Ludwig config JSON schema.\n\nUsage:\n    python -m ludwig.schema.export_schema [--model-type ecd|llm|combined] [--output FILE]\n    ludwig export_schema [--model-type ecd|llm|combined] [--output FILE]\n\nGenerates a JSON Schema (Draft 7) for Ludwig config validation.\n\"\"\"\n\nimport argparse\nimport json\n\nfrom ludwig.config_validation.validation import get_schema\nfrom ludwig.constants import MODEL_ECD, MODEL_LLM\nfrom ludwig.globals import LUDWIG_VERSION\n\nSCHEMA_BASE_URL = \"https://ludwig-ai.github.io/schema\"\n\n\ndef _strip_parameter_metadata(obj):\n    \"\"\"Recursively remove ``parameter_metadata`` keys from a schema dict.\n\n    The Ludwig schema generator attaches ``parameter_metadata`` objects to\n    every field (UI display hints, suggested values, etc.).  These are useful\n    internally but add significant bloat to the published JSON Schema and are\n    not relevant for validation or IDE auto-complete.\n    \"\"\"\n    if isinstance(obj, dict):\n        return {k: _strip_parameter_metadata(v) for k, v in obj.items() if k != \"parameter_metadata\"}\n    if isinstance(obj, list):\n        return [_strip_parameter_metadata(item) for item in obj]\n    return obj\n\n\ndef export_schema(model_type: str = MODEL_ECD, *, strip_metadata: bool = True) -> dict:\n    \"\"\"Export the full Ludwig config JSON schema for a given model type.\"\"\"\n    schema = get_schema(model_type)\n    schema[\"$schema\"] = \"http://json-schema.org/draft-07/schema#\"\n    schema[\"$id\"] = f\"{SCHEMA_BASE_URL}/ludwig-config-{model_type}.json\"\n    schema[\"title\"] = f\"Ludwig {model_type.upper()} Configuration\"\n    schema[\"description\"] = f\"Configuration schema for Ludwig {model_type.upper()} models (v{LUDWIG_VERSION})\"\n    if strip_metadata:\n        schema = _strip_parameter_metadata(schema)\n    return schema\n\n\ndef export_combined_schema(*, strip_metadata: bool = True) -> dict:\n    \"\"\"Export a combined schema that covers both ECD and LLM model types.\"\"\"\n    ecd_schema = get_schema(MODEL_ECD)\n    llm_schema = get_schema(MODEL_LLM)\n\n    # Merge properties from both schemas\n    all_properties = {}\n    all_properties.update(ecd_schema.get(\"properties\", {}))\n    all_properties.update(llm_schema.get(\"properties\", {}))\n\n    combined = {\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n        \"$id\": f\"{SCHEMA_BASE_URL}/ludwig-config.json\",\n        \"title\": \"Ludwig Configuration\",\n        \"description\": f\"Configuration schema for Ludwig models (v{LUDWIG_VERSION})\",\n        \"type\": \"object\",\n        \"properties\": all_properties,\n        \"required\": [\"input_features\", \"output_features\"],\n        \"additionalProperties\": True,\n    }\n    if strip_metadata:\n        combined = _strip_parameter_metadata(combined)\n    return combined\n\n\ndef main(sys_argv=None):\n    parser = argparse.ArgumentParser(description=\"Export Ludwig config JSON schema\")\n    parser.add_argument(\n        \"--model-type\",\n        choices=[MODEL_ECD, MODEL_LLM, \"combined\"],\n        default=\"combined\",\n        help=\"Model type to export schema for (default: combined)\",\n    )\n    parser.add_argument(\"--output\", \"-o\", type=str, default=None, help=\"Output file (default: stdout)\")\n    parser.add_argument(\n        \"--full\",\n        action=\"store_true\",\n        help=\"Include parameter_metadata in the output (default: stripped)\",\n    )\n    args = parser.parse_args(sys_argv)\n\n    strip_metadata = not args.full\n\n    if args.model_type == \"combined\":\n        schema = export_combined_schema(strip_metadata=strip_metadata)\n    else:\n        schema = export_schema(args.model_type, strip_metadata=strip_metadata)\n\n    output = json.dumps(schema, indent=2, sort_keys=False)\n\n    if args.output:\n        with open(args.output, \"w\") as f:\n            f.write(output)\n            f.write(\"\\n\")\n    else:\n        print(output)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "ludwig/schema/features/__init__.py",
    "content": "import ludwig.schema.features.audio_feature\nimport ludwig.schema.features.bag_feature\nimport ludwig.schema.features.binary_feature\nimport ludwig.schema.features.category_feature\nimport ludwig.schema.features.date_feature\nimport ludwig.schema.features.h3_feature\nimport ludwig.schema.features.image_feature\nimport ludwig.schema.features.number_feature\nimport ludwig.schema.features.sequence_feature\nimport ludwig.schema.features.set_feature\nimport ludwig.schema.features.text_feature\nimport ludwig.schema.features.timeseries_feature\nimport ludwig.schema.features.vector_feature  # noqa\n"
  },
  {
    "path": "ludwig/schema/features/audio_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUDIO, MODEL_ECD\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(AUDIO)\n@input_mixin_registry.register(AUDIO)\n@ludwig_dataclass\nclass AudioInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"AudioInputFeatureConfigMixin is a dataclass that configures the parameters used in both the audio input\n    feature and the audio global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=AUDIO)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=AUDIO,\n        default=\"parallel_cnn\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(AUDIO)\n@ludwig_dataclass\nclass AudioInputFeatureConfig(AudioInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"AudioInputFeatureConfig is a dataclass that configures the parameters used for an audio input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(AUDIO)\n"
  },
  {
    "path": "ludwig/schema/features/augmentation/__init__.py",
    "content": "# Register all augmentation schemas\nimport ludwig.schema.features.augmentation.image  # noqa: F401\n"
  },
  {
    "path": "ludwig/schema/features/augmentation/base.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseAugmentationConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base class for augmentation.\"\"\"\n\n    type: str\n"
  },
  {
    "path": "ludwig/schema/features/augmentation/image.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUGMENTATION, IMAGE, TYPE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.augmentation.base import BaseAugmentationConfig\nfrom ludwig.schema.features.augmentation.utils import register_augmentation_config\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"auto_augmentation\", features=IMAGE)\n@ludwig_dataclass\nclass AutoAugmentationConfig(BaseAugmentationConfig):\n    \"\"\"Automatic augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"auto_augmentation\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n    method: str = schema_utils.String(\n        default=\"trivial_augment\",\n        description=\"Specifies the method for applying automatic data augmentation\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"auto_augmentation_method\"],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_horizontal_flip\", features=IMAGE)\n@ludwig_dataclass\nclass RandomHorizontalFlipConfig(BaseAugmentationConfig):\n    \"\"\"Random horizontal flip augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_horizontal_flip\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_vertical_flip\", features=IMAGE)\n@ludwig_dataclass\nclass RandomVerticalFlipConfig(BaseAugmentationConfig):\n    \"\"\"Random vertical flip augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_vertical_flip\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_rotate\", features=IMAGE)\n@ludwig_dataclass\nclass RandomRotateConfig(BaseAugmentationConfig):\n    \"\"\"Random rotation augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_rotate\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"type\"],\n    )\n    degree: int = schema_utils.Integer(\n        default=15,\n        description=\"Range of angle for random rotation, i.e.,  [-degree, +degree].\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"rotation_degree\"],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_blur\", features=IMAGE)\n@ludwig_dataclass\nclass RandomBlurConfig(BaseAugmentationConfig):\n    \"\"\"Random blur augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_blur\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n    kernel_size: int = schema_utils.Integer(\n        default=3,\n        description=\"Kernel size for random blur.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"kernel_size\"],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_brightness\", features=IMAGE)\n@ludwig_dataclass\nclass RandomBrightnessConfig(BaseAugmentationConfig):\n    \"\"\"Random brightness augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_brightness\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n\n    min: float = schema_utils.FloatRange(\n        default=0.5,\n        description=\"Minimum factor for random brightness.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"min_brightness\"],\n    )\n\n    max: float = schema_utils.FloatRange(\n        default=2.0,\n        description=\"Maximum factor for random brightness.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"max_brightness\"],\n    )\n\n\n@DeveloperAPI\n@register_augmentation_config(name=\"random_contrast\", features=IMAGE)\n@ludwig_dataclass\nclass RandomContrastConfig(BaseAugmentationConfig):\n    \"\"\"Random Contrast augmentation operation.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random_contrast\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][TYPE],\n    )\n\n    min: float = schema_utils.FloatRange(\n        default=0.5,\n        description=\"Minimum factor for random contrast.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"min_contrast\"],\n    )\n\n    max: float = schema_utils.FloatRange(\n        default=2.0,\n        description=\"Maximum factor for random contrast.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][AUGMENTATION][\"max_contrast\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/augmentation/utils.py",
    "content": "import copy\nfrom dataclasses import field\nfrom typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import TYPE\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.augmentation.base import BaseAugmentationConfig\nfrom ludwig.utils.registry import Registry\n\n_augmentation_config_registry = Registry()\n\n\n@DeveloperAPI\ndef get_augmentation_config_registry() -> Registry:\n    return _augmentation_config_registry\n\n\n@DeveloperAPI\ndef register_augmentation_config(name: str, features: str | list[str]):\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for feature in features:\n            augmentation_registry = get_augmentation_config_registry().get(feature, {})\n            augmentation_registry[name] = cls\n            get_augmentation_config_registry()[feature] = augmentation_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_augmentation_cls(feature: str, name: str):\n    return get_augmentation_config_registry()[feature][name]\n\n\n@DeveloperAPI\ndef get_augmentation_classes(feature: str):\n    return get_augmentation_config_registry()[feature]\n\n\n@DeveloperAPI\ndef AugmentationDataclassField(\n    feature_type: str,\n    default: str | BaseAugmentationConfig = False,\n    default_augmentations: list[BaseAugmentationConfig] | None = None,\n    description: str = \"\",\n):\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify an augmentation\n    config.\n\n    Args:\n        default: The default augmentation config to use.\n        default_augmentations: The default list of augmentations to use when param value is set to `True`.\n        description: The description of the augmentation config.\n\n    Returns: Initialized dataclass field that converts a list with params to an augmentation config.\n    \"\"\"\n\n    default_augmentations = default_augmentations or []\n    default_augmentations = [a.to_dict() for a in default_augmentations]\n\n    if isinstance(default, bool):\n        default = default_augmentations if default else []\n\n    class AugmentationContainerMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field that deserializes a list for a valid augmentation config from the augmentation_registry and\n        creates a corresponding JSON schema for external usage.\"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if isinstance(value, bool):\n                value = default_augmentations if value else []\n\n            if not isinstance(value, list):\n                raise ConfigValidationError(f\"Augmentation config must be a list, found: {type(value)}\")\n\n            augmentation_classes = get_augmentation_classes(feature_type)\n            augmentation_list = []\n            for augmentation in value:\n                augmentation_op = augmentation[TYPE]\n                if augmentation_op in augmentation_classes:\n                    augmentation_cls = augmentation_classes[augmentation_op]\n                    pre = augmentation_cls()\n                    try:\n                        augmentation_list.append(pre.Schema().load(augmentation))\n                    except (TypeError, ConfigValidationError) as error:\n                        raise ConfigValidationError(\n                            f\"Invalid augmentation params: {value}, see `{pre}` definition. Error: {error}\"\n                        )\n                else:\n                    raise ConfigValidationError(\n                        f\"Invalid augmentation type: '{augmentation_op}', \"\n                        f\"expected one of: {list(augmentation_classes.keys())}\"\n                    )\n            return augmentation_list\n\n        def _jsonschema_type_mapping(self):\n            return get_augmentation_list_jsonschema(feature_type, default)\n\n    try:\n        assert isinstance(default, list), \"Augmentation config must be a list.\"\n        load_augmentation_list = []\n        dump_augmentation_list = []\n        for augmentation in default:\n            augmentation_op = augmentation[TYPE]\n            augmentation_cls = get_augmentation_cls(feature_type, augmentation_op)\n            pre = augmentation_cls()\n            try:\n                load_augmentation_list.append(pre.Schema().load(augmentation))\n                dump_augmentation_list.append(pre.Schema().dump(augmentation))\n            except (TypeError, ConfigValidationError) as error:\n                raise ConfigValidationError(\n                    f\"Invalid augmentation params: {default}, see `{pre}` definition. Error: {error}\"\n                )\n\n        load_default = lambda: copy.deepcopy(load_augmentation_list)\n        dump_default = dump_augmentation_list\n\n        return field(\n            metadata={\n                \"marshmallow_field\": AugmentationContainerMarshmallowField(\n                    allow_none=False,\n                    dump_default=dump_default,\n                    load_default=load_default,\n                )\n            },\n            default_factory=load_default,\n        )\n    except Exception as e:\n        raise ConfigValidationError(f\"Unsupported augmentation type. See augmentation_registry. \" f\"Details: {e}\")\n\n\n@DeveloperAPI\ndef get_augmentation_list_jsonschema(feature_type: str, default: list[dict[str, Any]]):\n    \"\"\"This function returns a JSON augmentation schema.\n\n    Returns: JSON Schema\n    \"\"\"\n    augmentation_types = sorted(list(get_augmentation_config_registry()[feature_type].keys()))\n    schema = {\n        \"oneOf\": [\n            {\n                \"type\": \"array\",\n                \"items\": {\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"type\": {\n                            \"type\": \"string\",\n                            \"enum\": augmentation_types,\n                            \"title\": \"type\",\n                            \"description\": \"Type of augmentation to apply.\",\n                        },\n                    },\n                    \"additionalProperties\": True,\n                    \"allOf\": get_augmentation_list_conds(feature_type),\n                    \"required\": [\"type\"],\n                },\n                \"title\": \"array_option\",\n            },\n            {\"type\": \"boolean\", \"description\": \"Apply standard augmentation pipeline.\", \"title\": \"boolean_option\"},\n        ],\n        \"title\": \"augmentation\",\n    }\n\n    return schema\n\n\n@DeveloperAPI\ndef get_augmentation_list_conds(feature_type: str):\n    \"\"\"This function returns a list of if-then JSON clauses for each augmentation type along with their properties\n    and constraints.\n\n    Returns: List of JSON clauses\n    \"\"\"\n    conds = []\n    for augmentation_op in get_augmentation_classes(feature_type):\n        schema_cls = get_augmentation_cls(feature_type, augmentation_op)\n        augmentation_schema = schema_utils.unload_jsonschema_from_marshmallow_class(schema_cls)\n        augmentation_props = augmentation_schema[\"properties\"]\n        schema_utils.remove_duplicate_fields(augmentation_props)\n        augmentation_cond = schema_utils.create_cond({\"type\": augmentation_op}, augmentation_props)\n        conds.append(augmentation_cond)\n    return conds\n"
  },
  {
    "path": "ludwig/schema/features/bag_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BAG, MODEL_ECD\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(BAG)\n@input_mixin_registry.register(BAG)\n@ludwig_dataclass\nclass BagInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"BagInputFeatureConfigMixin is a dataclass that configures the parameters used in both the bag input feature\n    and the bag global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=BAG)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=BAG,\n        default=\"embed\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(BAG)\n@ludwig_dataclass\nclass BagInputFeatureConfig(BagInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"BagInputFeatureConfig is a dataclass that configures the parameters used for a bag input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(BAG)\n"
  },
  {
    "path": "ludwig/schema/features/base.py",
    "content": "import logging\nfrom collections.abc import Iterable\nfrom dataclasses import field\nfrom typing import Any, Generic, TypeVar\n\nfrom rich.console import Console\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    DATE,\n    H3,\n    IMAGE,\n    MODEL_ECD,\n    MODEL_LLM,\n    NUMBER,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    VECTOR,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.utils import (\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    get_input_feature_jsonschema,\n    get_output_feature_jsonschema,\n    llm_input_config_registry,\n    llm_output_config_registry,\n)\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY, ParameterMetadata\nfrom ludwig.schema.utils import ludwig_dataclass\n\nlogger = logging.getLogger(__name__)\n_error_console = Console(stderr=True, style=\"bold red\")\n_info_console = Console(stderr=True, style=\"bold green\")\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseFeatureConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base class for feature configs.\"\"\"\n\n    def __post_init__(self):\n        # TODO(travis): this should be done through marshmallow dataclass' `required` field param,\n        # but requires a refactor`\n        if self.name is None:\n            raise ConfigValidationError(\"All features must have a name.\")\n        if self.type is None:\n            raise ConfigValidationError(f\"Feature {self.name} must have a type.\")\n\n    active: bool = True\n\n    name: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Name of the feature.\",\n    )\n\n    type: str = schema_utils.StringOptions(\n        default=None,\n        allow_none=True,\n        options=[AUDIO, BAG, BINARY, CATEGORY, DATE, H3, IMAGE, NUMBER, SEQUENCE, SET, TEXT, TIMESERIES, VECTOR],\n        description=\"Type of the feature.\",\n    )\n\n    column: str = schema_utils.String(\n        allow_none=True,\n        default=None,\n        description=\"The column name of this feature. Defaults to name if not specified.\",\n    )\n\n    proc_column: str = schema_utils.String(\n        allow_none=True,\n        default=None,\n        description=\"The name of the preprocessed column name of this feature. Internal only.\",\n        parameter_metadata=ParameterMetadata(internal_only=True),\n    )\n\n    def enable(self):\n        \"\"\"This function allows the user to specify which features from a dataset should be included during model\n        training. This is the equivalent to toggling on a feature in the model creation UI.\n\n        Returns:\n            None\n        \"\"\"\n        if self.active:\n            _error_console.print(\"This feature is already enabled!\")\n        else:\n            self.active = True\n            _info_console.print(f\"{self.name} feature enabled!\\n\")\n            logger.info(self.__repr__())\n\n    def disable(self):\n        \"\"\"This function allows the user to specify which features from a dataset should not be included during\n        model training. This is the equivalent to toggling off a feature in the model creation UI.\n\n        Returns:\n            None\n        \"\"\"\n        if not self.active:\n            _error_console.print(\"This feature is already disabled!\")\n        else:\n            self.active = False\n            _info_console.print(f\"{self.name} feature disabled!\\n\")\n            logger.info(self.__repr__())\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseInputFeatureConfig(BaseFeatureConfig):\n    \"\"\"Base input feature config class.\"\"\"\n\n    tied: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Name of input feature to tie the weights of the encoder with.  It needs to be the name of a \"\n        \"feature of the same type and with the same encoder parameters. If text or sequence features are tied, \"\n        \"consider setting the `sequence_length` parameter in `preprocessing` to ensure that the tied features have \"\n        \"equal sized outputs. This is necessary when using the `sequence` combiner.\",\n    )\n\n    def has_augmentation(self) -> bool:\n        return False\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ECDInputFeatureConfig(BaseFeatureConfig):\n    pass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseOutputFeatureConfig(BaseFeatureConfig):\n    \"\"\"Base output feature config class.\"\"\"\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n    )\n\n    default_validation_metric: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Internal only use parameter: default validation metric for output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list[str] = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n    )\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n    )\n\n    input_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=ParameterMetadata(internal_only=True),\n    )\n\n    num_classes: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Size of the input to the decoder.\",\n        parameter_metadata=ParameterMetadata(internal_only=True),\n    )\n\n\nT = TypeVar(\"T\", bound=BaseFeatureConfig)\n\n\nclass FeatureCollection(Generic[T], schema_utils.ListSerializable):\n    def __init__(self, features: list[T]):\n        self._features = features\n        self._name_to_feature = {f.name: f for f in features}\n        for k, v in self._name_to_feature.items():\n            setattr(self, k, v)\n\n    def to_list(self) -> list[dict[str, Any]]:\n        out_list = []\n        for feature in self._features:\n            out_list.append(feature.to_dict())\n        return out_list\n\n    def items(self) -> Iterable[tuple[str, T]]:\n        return self._name_to_feature.items()\n\n    def __iter__(self):\n        return iter(self._features)\n\n    def __len__(self):\n        return len(self._features)\n\n    def __getitem__(self, i) -> T:\n        if isinstance(i, str):\n            return self._name_to_feature[i]\n        else:\n            return self._features[i]\n\n\nclass FeatureList(schema_utils.LudwigSchemaField):\n    \"\"\"A schema field that deserializes a list of dicts into a FeatureCollection.\n\n    Each item is resolved via the inner TypeSelection's resolve() method.\n    \"\"\"\n\n    def __init__(\n        self,\n        inner: schema_utils.TypeSelection,\n        min_length: int | None = None,\n        max_length: int | None = None,\n        equal: int | None = None,\n        metadata: dict | None = None,\n    ):\n        self.inner = inner\n        self.min_length = min_length\n        self.max_length = max_length\n        self.equal = equal\n        self.metadata = metadata or {}\n\n    def _deserialize(self, value, attr, data, **kwargs) -> FeatureCollection:\n        if not isinstance(value, list):\n            raise ConfigValidationError(f\"Expected a list of features for '{attr}', got {type(value).__name__}\")\n\n        # Validate length constraints\n        n = len(value)\n        if self.equal is not None and n != self.equal:\n            raise ConfigValidationError(f\"Expected exactly {self.equal} feature(s) for '{attr}', got {n}\")\n        if self.min_length is not None and n < self.min_length:\n            raise ConfigValidationError(f\"Expected at least {self.min_length} feature(s) for '{attr}', got {n}\")\n        if self.max_length is not None and n > self.max_length:\n            raise ConfigValidationError(f\"Expected at most {self.max_length} feature(s) for '{attr}', got {n}\")\n\n        feature_list = [self.inner.resolve(item) for item in value]\n        return FeatureCollection(feature_list)\n\n    def _jsonschema_type_mapping(self):\n        inner_schema = self.inner._jsonschema_type_mapping() or {}\n        result = {\"type\": \"array\", \"items\": inner_schema}\n        if self.min_length is not None:\n            result[\"minItems\"] = self.min_length\n        if self.max_length is not None:\n            result[\"maxItems\"] = self.max_length\n        if self.equal is not None:\n            result[\"minItems\"] = self.equal\n            result[\"maxItems\"] = self.equal\n        return result\n\n\nclass FeaturesTypeSelection(schema_utils.TypeSelection):\n    def __init__(\n        self,\n        *args,\n        min_length: int | None = 1,\n        max_length: int | None = None,\n        supplementary_metadata=None,\n        **kwargs,\n    ):\n        super().__init__(*args, **kwargs)\n        self.min_length = min_length\n        self.max_length = max_length\n        self.supplementary_metadata = {} if supplementary_metadata is None else supplementary_metadata\n\n    def get_list_field(self):\n        min_length = self.min_length\n        max_length = self.max_length\n        equal = None\n        if min_length == max_length:\n            min_length = None\n            max_length = None\n            equal = self.max_length\n\n        return field(\n            metadata={\n                \"marshmallow_field\": FeatureList(\n                    self,\n                    min_length=min_length,\n                    max_length=max_length,\n                    equal=equal,\n                    metadata=self.supplementary_metadata,\n                )\n            },\n        )\n\n\nclass ECDInputFeatureSelection(FeaturesTypeSelection):\n    def __init__(self):\n        super().__init__(\n            registry=ecd_input_config_registry,\n            description=\"Type of the input feature\",\n            supplementary_metadata={\"uniqueItemProperties\": [\"name\"]},\n        )\n\n    def _jsonschema_type_mapping(self):\n        return get_input_feature_jsonschema(MODEL_ECD)\n\n\nclass LLMInputFeatureSelection(FeaturesTypeSelection):\n    def __init__(self):\n        super().__init__(registry=llm_input_config_registry, description=\"Type of the input feature\")\n\n    def _jsonschema_type_mapping(self):\n        return get_input_feature_jsonschema(MODEL_LLM)\n\n\nclass ECDOutputFeatureSelection(FeaturesTypeSelection):\n    def __init__(self):\n        super().__init__(registry=ecd_output_config_registry, description=\"Type of the output feature\")\n\n    def _jsonschema_type_mapping(self):\n        return get_output_feature_jsonschema(MODEL_ECD)\n\n\nclass LLMOutputFeatureSelection(FeaturesTypeSelection):\n    def __init__(self):\n        # TODO(Arnav): Remove the hard check on max_length once we support multiple output features.\n        super().__init__(max_length=1, registry=llm_output_config_registry, description=\"Type of the output feature\")\n\n    def _jsonschema_type_mapping(self):\n        return get_output_feature_jsonschema(MODEL_LLM)\n"
  },
  {
    "path": "ludwig/schema/features/binary_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, BINARY_WEIGHTED_CROSS_ENTROPY, MODEL_ECD, ROC_AUC\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(BINARY)\n@ludwig_dataclass\nclass BinaryInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"BinaryInputFeatureConfigMixin is a dataclass that configures the parameters used in both the binary input\n    feature and the binary global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=BINARY)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BinaryInputFeatureConfig(BinaryInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"BinaryInputFeatureConfig is a dataclass that configures the parameters used for a binary input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(BINARY)\n\n    encoder: BaseEncoderConfig = None\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(BINARY)\n@ludwig_dataclass\nclass ECDBinaryInputFeatureConfig(BinaryInputFeatureConfig):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=BINARY,\n        default=\"passthrough\",\n    )\n\n\n@DeveloperAPI\n@output_mixin_registry.register(BINARY)\n@ludwig_dataclass\nclass BinaryOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"BinaryOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the binary output\n    feature and the binary global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = None\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=BINARY,\n        default=BINARY_WEIGHTED_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BinaryOutputFeatureConfig(BinaryOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"BinaryOutputFeatureConfig is a dataclass that configures the parameters used for a binary output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(BINARY)\n\n    calibration: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Calibrate the model's output probabilities using temperature scaling.\",\n        parameter_metadata=FEATURE_METADATA[BINARY][\"calibration\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [ROC_AUC],\n        default=ROC_AUC,\n        description=\"Internal only use parameter: default validation metric for binary output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[BINARY][\"dependencies\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"binary_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[BINARY][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[BINARY][\"reduce_input\"],\n    )\n\n    threshold: float = schema_utils.FloatRange(\n        default=0.5,\n        min=0,\n        max=1,\n        description=\"The threshold used to convert output probabilities to predictions. Predicted probabilities greater\"\n        \"than or equal to threshold are mapped to True.\",\n        parameter_metadata=FEATURE_METADATA[BINARY][\"threshold\"],\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(BINARY)\n@ludwig_dataclass\nclass ECDBinaryOutputFeatureConfig(BinaryOutputFeatureConfig):\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=BINARY,\n        default=\"regressor\",\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(BINARY)\n@ludwig_dataclass\nclass BinaryDefaultsConfig(BinaryInputFeatureConfigMixin, BinaryOutputFeatureConfigMixin):\n    # NOTE(travis): defaults use ECD input feature as it contains all the encoders\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=BINARY,\n        default=\"passthrough\",\n    )\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=BINARY,\n        default=\"regressor\",\n    )\n"
  },
  {
    "path": "ludwig/schema/features/category_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ACCURACY, CATEGORY, CATEGORY_DISTRIBUTION, MODEL_ECD, MODEL_LLM, SOFTMAX_CROSS_ENTROPY\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    llm_output_config_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(CATEGORY)\n@ludwig_dataclass\nclass CategoryInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"CategoryInputFeatureConfigMixin is a dataclass that configures the parameters used in both the category\n    input feature and the category global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=CATEGORY)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass CategoryInputFeatureConfig(CategoryInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"CategoryInputFeatureConfig is a dataclass that configures the parameters used for a category input\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(CATEGORY)\n\n    encoder: BaseEncoderConfig = None\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(CATEGORY)\n@ludwig_dataclass\nclass ECDCategoryInputFeatureConfig(CategoryInputFeatureConfig):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=CATEGORY,\n        default=\"dense\",\n    )\n\n\n@DeveloperAPI\n@output_mixin_registry.register(CATEGORY)\n@ludwig_dataclass\nclass CategoryOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"CategoryOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the category\n    output feature and the category global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = None\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=CATEGORY,\n        default=SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass CategoryOutputFeatureConfig(CategoryOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"CategoryOutputFeatureConfig is a dataclass that configures the parameters used for a category output\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(CATEGORY)\n\n    calibration: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Calibrate the model's output probabilities using temperature scaling.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][\"calibration\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [ACCURACY],\n        default=ACCURACY,\n        description=\"Internal only use parameter: default validation metric for category output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][\"dependencies\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"category_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][\"reduce_input\"],\n    )\n\n    top_k: int = schema_utils.PositiveInteger(\n        default=3,\n        description=\"Determines the parameter k, the number of categories to consider when computing the top_k \"\n        \"measure. It computes accuracy but considering as a match if the true category appears in the \"\n        \"first k predicted categories ranked by decoder's confidence.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][\"top_k\"],\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(CATEGORY)\n@ludwig_dataclass\nclass ECDCategoryOutputFeatureConfig(CategoryOutputFeatureConfig):\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=CATEGORY,\n        default=\"classifier\",\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(CATEGORY_DISTRIBUTION)\n@ludwig_dataclass\nclass CategoryDistributionOutputFeatureConfig(CategoryOutputFeatureConfig):\n    \"\"\"CategoryDistributionOutputFeatureConfig is a dataclass that configures the parameters used for a\n    category_distribution output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(CATEGORY_DISTRIBUTION)\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=CATEGORY,\n        default=\"classifier\",\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"category_distribution_output\")\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(CATEGORY)\n@ludwig_dataclass\nclass CategoryDefaultsConfig(CategoryInputFeatureConfigMixin, CategoryOutputFeatureConfigMixin):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=CATEGORY,\n        default=\"dense\",\n    )\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=CATEGORY,\n        default=\"classifier\",\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(CATEGORY_DISTRIBUTION)\n@ludwig_dataclass\nclass CategoryDistributionDefaultsConfig(CategoryOutputFeatureConfigMixin):\n    pass\n\n\n@DeveloperAPI\n@llm_output_config_registry.register(CATEGORY)\n@ludwig_dataclass\nclass LLMCategoryOutputFeatureConfig(CategoryOutputFeatureConfig):\n    \"\"\"LLMCategoryOutputFeatureConfig is a dataclass that configures the parameters used for a category output\n    feature when using the Ludwig Light Model.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"category_llm\")\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_LLM,\n        feature_type=CATEGORY,\n        default=\"category_extractor\",\n    )\n"
  },
  {
    "path": "ludwig/schema/features/date_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DATE, MODEL_ECD\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(DATE)\n@input_mixin_registry.register(DATE)\n@ludwig_dataclass\nclass DateInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"DateInputFeatureConfigMixin is a dataclass that configures the parameters used in both the date input\n    feature and the date global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=DATE)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=DATE,\n        default=\"embed\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(DATE)\n@ludwig_dataclass\nclass DateInputFeatureConfig(DateInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"DateInputFeature is a dataclass that configures the parameters used for a date input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(DATE)\n"
  },
  {
    "path": "ludwig/schema/features/h3_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import H3, MODEL_ECD\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import ecd_defaults_config_registry, ecd_input_config_registry, input_mixin_registry\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(H3)\n@input_mixin_registry.register(H3)\n@ludwig_dataclass\nclass H3InputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"H3InputFeatureConfigMixin is a dataclass that configures the parameters used in both the h3 input feature\n    and the h3 global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=H3)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=H3,\n        default=\"embed\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(H3)\n@ludwig_dataclass\nclass H3InputFeatureConfig(H3InputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"H3InputFeatureConfig is a dataclass that configures the parameters used for an h3 input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(H3)\n"
  },
  {
    "path": "ludwig/schema/features/image_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import IMAGE, LOSS, MODEL_ECD, SOFTMAX_CROSS_ENTROPY\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.augmentation.base import BaseAugmentationConfig\nfrom ludwig.schema.features.augmentation.image import RandomHorizontalFlipConfig, RandomRotateConfig\nfrom ludwig.schema.features.augmentation.utils import AugmentationDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n# Augmentation operations when augmentation is set to True\nAUGMENTATION_DEFAULT_OPERATIONS = [\n    RandomHorizontalFlipConfig(),\n    RandomRotateConfig(),\n]\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(IMAGE)\n@input_mixin_registry.register(IMAGE)\n@ludwig_dataclass\nclass ImageInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"ImageInputFeatureConfigMixin is a dataclass that configures the parameters used in both the image input\n    feature and the image global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=IMAGE)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=IMAGE,\n        default=\"stacked_cnn\",\n    )\n\n    augmentation: list[BaseAugmentationConfig] = AugmentationDataclassField(\n        feature_type=IMAGE,\n        default=False,\n        default_augmentations=AUGMENTATION_DEFAULT_OPERATIONS,\n        description=\"Augmentation operation configuration.\",\n    )\n\n    def has_augmentation(self) -> bool:\n        # Check for None, False, and []\n        return bool(self.augmentation)\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(IMAGE)\n@ludwig_dataclass\nclass ImageInputFeatureConfig(ImageInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"ImageInputFeatureConfig is a dataclass that configures the parameters used for an image input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(IMAGE)\n\n\n@DeveloperAPI\n@output_mixin_registry.register(IMAGE)\n@ludwig_dataclass\nclass ImageOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"ImageOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the image output\n    feature and the image global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=IMAGE,\n        default=\"unet\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=IMAGE,\n        default=SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(IMAGE)\n@ludwig_dataclass\nclass ImageOutputFeatureConfig(ImageOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"ImageOutputFeatureConfig is a dataclass that configures the parameters used for an image output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(IMAGE)\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][\"dependencies\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [LOSS],\n        default=LOSS,\n        description=\"Internal only use parameter: default validation metric for image output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"image_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][\"reduce_input\"],\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(IMAGE)\n@ludwig_dataclass\nclass ImageDefaultsConfig(ImageInputFeatureConfigMixin, ImageOutputFeatureConfigMixin):\n    pass\n"
  },
  {
    "path": "ludwig/schema/features/loss/__init__.py",
    "content": "from ludwig.schema.features.loss.loss import get_loss_classes, get_loss_cls, get_loss_schema_registry  # noqa\n"
  },
  {
    "path": "ludwig/schema/features/loss/loss.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    BINARY,\n    BINARY_WEIGHTED_CROSS_ENTROPY,\n    CATEGORY,\n    CORN,\n    HUBER,\n    IMAGE,\n    MEAN_ABSOLUTE_ERROR,\n    MEAN_ABSOLUTE_PERCENTAGE_ERROR,\n    MEAN_SQUARED_ERROR,\n    NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY,\n    NUMBER,\n    ROOT_MEAN_SQUARED_ERROR,\n    ROOT_MEAN_SQUARED_PERCENTAGE_ERROR,\n    SEQUENCE,\n    SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n    SET,\n    SIGMOID_CROSS_ENTROPY,\n    SOFTMAX_CROSS_ENTROPY,\n    TEXT,\n    TIMESERIES,\n    VECTOR,\n)\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LOSS_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.registry import Registry\n\nROBUST_LAMBDA_DESCRIPTION = (\n    \"Replaces the loss with `(1 - robust_lambda) * loss + robust_lambda / c` where `c` is the number of \"\n    \"classes. Useful in case of noisy labels.\"\n)\n\nCONFIDENCE_PENALTY_DESCRIPTION = (\n    \"Penalizes overconfident predictions (low entropy) by adding an additional term \"\n    \"that penalizes too confident predictions by adding a `a * (max_entropy - entropy) / max_entropy` \"\n    \"term to the loss, where a is the value of this parameter. Useful in case of noisy labels.\"\n)\n\nCLASS_WEIGHTS_DESCRIPTION = (\n    \"Weights to apply to each class in the loss. If not specified, all classes are weighted equally. \"\n    \"The value can be a vector of weights, one for each class, that is multiplied to the \"\n    \"loss of the datapoints that have that class as ground truth. It is an alternative to oversampling in \"\n    \"case of unbalanced class distribution. The ordering of the vector follows the category to integer ID \"\n    \"mapping in the JSON metadata file (the `<UNK>` class needs to be included too). Alternatively, the value \"\n    \"can be a dictionary with class strings as keys and weights as values, like \"\n    \"`{class_a: 0.5, class_b: 0.7, ...}`.\"\n)\n\nCLASS_SIMILARITIES_DESCRIPTION = (\n    \"If not `null` it is a `c x c` matrix in the form of a list of lists that contains the mutual similarity of \"\n    \"classes. It is used if `class_similarities_temperature` is greater than 0. The ordering of the vector follows \"\n    \"the category to integer ID mapping in the JSON metadata file (the `<UNK>` class needs to be included too).\"\n)\n\nCLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION = (\n    \"The temperature parameter of the softmax that is performed on each row of `class_similarities`. The output of \"\n    \"that softmax is used to determine the supervision vector to provide instead of the one hot vector that would be \"\n    \"provided otherwise for each datapoint. The intuition behind it is that errors between similar classes are more \"\n    \"tolerable than errors between really different classes.\"\n)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseLossConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base class for feature configs.\"\"\"\n\n    type: str\n\n    weight: float = 1.0\n\n    @classmethod\n    def name(cls) -> str:\n        return \"[undefined]\"\n\n\n_loss_registry = Registry[type[BaseLossConfig]]()\n_loss_feature_registry = Registry[dict[str, type[BaseLossConfig]]]()\n\n\n@DeveloperAPI\ndef get_loss_schema_registry() -> Registry[type[BaseLossConfig]]:\n    return _loss_registry\n\n\n@DeveloperAPI\ndef get_loss_cls(feature: str, name: str) -> type[BaseLossConfig]:\n    return _loss_feature_registry[feature][name]\n\n\n@DeveloperAPI\ndef get_loss_classes(feature: str) -> dict[str, type[BaseLossConfig]]:\n    return _loss_feature_registry[feature]\n\n\ndef register_loss(features: str | list[str]):\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls: type[BaseLossConfig]):\n        _loss_registry[cls.type] = cls\n        for feature in features:\n            feature_registry = _loss_feature_registry.get(feature, {})\n            feature_registry[cls.type] = cls\n            _loss_feature_registry[feature] = feature_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\n@register_loss([NUMBER, TIMESERIES, VECTOR])\n@ludwig_dataclass\nclass MSELossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        MEAN_SQUARED_ERROR,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"MSELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Mean Squared Error (MSE)\"\n\n\n@DeveloperAPI\n@register_loss([NUMBER, TIMESERIES, VECTOR])\n@ludwig_dataclass\nclass MAELossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        MEAN_ABSOLUTE_ERROR,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"MAELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Mean Absolute Error (MAE)\"\n\n\n@DeveloperAPI\n@register_loss([NUMBER, TIMESERIES, VECTOR])\n@ludwig_dataclass\nclass MAPELossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        MEAN_ABSOLUTE_PERCENTAGE_ERROR,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"MAELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Mean Absolute Percentage Error (MAPE)\"\n\n\n@DeveloperAPI\n@register_loss([NUMBER])\n@ludwig_dataclass\nclass RMSELossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        ROOT_MEAN_SQUARED_ERROR,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"RMSELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Root Mean Squared Error (RMSE)\"\n\n\n@DeveloperAPI\n@register_loss([NUMBER])\n@ludwig_dataclass\nclass RMSPELossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        ROOT_MEAN_SQUARED_PERCENTAGE_ERROR,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"RMSPELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Root Mean Squared Percentage Error (RMSPE)\"\n\n\n@DeveloperAPI\n@register_loss([BINARY])\n@ludwig_dataclass\nclass BWCEWLossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        BINARY_WEIGHTED_CROSS_ENTROPY,\n        description=\"Type of loss.\",\n    )\n\n    positive_class_weight: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"Weight of the positive class.\",\n        parameter_metadata=LOSS_METADATA[\"BWCEWLoss\"][\"positive_class_weight\"],\n    )\n\n    robust_lambda: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=ROBUST_LAMBDA_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"BWCEWLoss\"][\"robust_lambda\"],\n    )\n\n    confidence_penalty: float = schema_utils.NonNegativeFloat(\n        default=0,\n        description=CONFIDENCE_PENALTY_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"BWCEWLoss\"][\"confidence_penalty\"],\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"BWCEWLoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Binary Weighted Cross Entropy (BWCE)\"\n\n\n@DeveloperAPI\n@register_loss([CATEGORY, VECTOR, IMAGE])\n@ludwig_dataclass\nclass SoftmaxCrossEntropyLossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        SOFTMAX_CROSS_ENTROPY,\n        description=\"Type of loss.\",\n    )\n\n    class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField(\n        default=None,\n        description=CLASS_WEIGHTS_DESCRIPTION,\n        field_options=[\n            schema_utils.Dict(default=None, allow_none=True),\n            schema_utils.List(list_type=float, allow_none=False),\n        ],\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"class_weights\"],\n    )\n\n    robust_lambda: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=ROBUST_LAMBDA_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"robust_lambda\"],\n    )\n\n    confidence_penalty: float = schema_utils.NonNegativeFloat(\n        default=0,\n        description=CONFIDENCE_PENALTY_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"confidence_penalty\"],\n    )\n\n    class_similarities: list = schema_utils.List(\n        list,\n        default=None,\n        description=CLASS_SIMILARITIES_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"class_similarities\"],\n    )\n\n    class_similarities_temperature: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=CLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"class_similarities_temperature\"],\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"SoftmaxCrossEntropyLoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Softmax Cross Entropy\"\n\n\n@DeveloperAPI\n@register_loss([SEQUENCE, TEXT])\n@ludwig_dataclass\nclass SequenceSoftmaxCrossEntropyLossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n        description=\"Type of loss.\",\n    )\n\n    class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField(\n        default=None,\n        description=CLASS_WEIGHTS_DESCRIPTION,\n        field_options=[\n            schema_utils.Dict(default=None, allow_none=True),\n            schema_utils.List(list_type=float, allow_none=False),\n        ],\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"class_weights\"],\n    )\n\n    robust_lambda: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=ROBUST_LAMBDA_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"robust_lambda\"],\n    )\n\n    confidence_penalty: float = schema_utils.NonNegativeFloat(\n        default=0,\n        description=CONFIDENCE_PENALTY_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"confidence_penalty\"],\n    )\n\n    class_similarities: list = schema_utils.List(\n        list,\n        default=None,\n        description=CLASS_SIMILARITIES_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"class_similarities\"],\n    )\n\n    class_similarities_temperature: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=CLASS_SIMILARITIES_TEMPERATURE_DESCRIPTION,\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"class_similarities_temperature\"],\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"weight\"],\n    )\n\n    unique: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, the loss is only computed for unique elements in the sequence.\",\n        parameter_metadata=LOSS_METADATA[\"SequenceSoftmaxCrossEntropyLoss\"][\"unique\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Sequence Softmax Cross Entropy\"\n\n\n@DeveloperAPI\n@register_loss([SEQUENCE, TEXT])\n@ludwig_dataclass\nclass NextTokenSoftmaxCrossEntropyLossConfig(SequenceSoftmaxCrossEntropyLossConfig):\n    type: str = schema_utils.ProtectedString(\n        NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY,\n        description=\"Type of loss.\",\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Next Token Softmax Cross Entropy\"\n\n\n@DeveloperAPI\n@register_loss([SET])\n@ludwig_dataclass\nclass SigmoidCrossEntropyLossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        SIGMOID_CROSS_ENTROPY,\n        description=\"Type of loss.\",\n    )\n\n    class_weights: list[float] | dict | None = schema_utils.OneOfOptionsField(\n        default=None,\n        description=CLASS_WEIGHTS_DESCRIPTION,\n        field_options=[\n            schema_utils.Dict(default=None, allow_none=True),\n            schema_utils.List(list_type=float, allow_none=False),\n        ],\n        parameter_metadata=LOSS_METADATA[\"SigmoidCrossEntropyLoss\"][\"class_weights\"],\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"SigmoidCrossEntropyLoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Sigmoid Cross Entropy\"\n\n\n@DeveloperAPI\n@register_loss([NUMBER, TIMESERIES, VECTOR])\n@ludwig_dataclass\nclass HuberLossConfig(BaseLossConfig):\n    type: str = schema_utils.ProtectedString(\n        HUBER,\n        description=(\n            \"Loss that combines advantages of both `mean_absolute_error` (MAE) and `mean_squared_error` (MSE). The \"\n            \"delta-scaled L1 region makes the loss less sensitive to outliers than MSE, while the L2 region provides \"\n            \"smoothness over MAE near 0. See [Huber loss](https://en.wikipedia.org/wiki/Huber_loss) for more details.\"\n        ),\n    )\n\n    delta: float = schema_utils.FloatRange(\n        default=1.0,\n        min=0,\n        min_inclusive=False,\n        description=\"Threshold at which to change between delta-scaled L1 and L2 loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"MSELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Huber Loss\"\n\n\n@DeveloperAPI\n@register_loss([CATEGORY])\n@ludwig_dataclass\nclass CORNLossConfig(BaseLossConfig):\n    \"\"\"Conditional Ordinal Regression for Neural networks, used for ordered cateogry values.\n\n    Source:\n    Xintong Shi, Wenzhi Cao, and Sebastian Raschka (2021).\n    Deep Neural Networks for Rank-Consistent Ordinal Regression Based On Conditional Probabilities.\n    Arxiv preprint; https://arxiv.org/abs/2111.08851\n    \"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        CORN,\n        description=\"Type of loss.\",\n    )\n\n    weight: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"Weight of the loss.\",\n        parameter_metadata=LOSS_METADATA[\"MSELoss\"][\"weight\"],\n    )\n\n    @classmethod\n    def name(self) -> str:\n        return \"Conditional Ordinal Regression (CORN)\"\n\n    @property\n    def class_weights(self) -> int:\n        return 1.0\n\n    @property\n    def class_similarities_temperature(self) -> int:\n        return 0\n"
  },
  {
    "path": "ludwig/schema/features/loss/utils.py",
    "content": "from dataclasses import Field\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.loss import get_loss_classes, get_loss_cls\n\n\n@DeveloperAPI\ndef get_loss_conds(feature_type: str):\n    \"\"\"Returns a JSON schema of conditionals to validate against loss types for specific feature types.\"\"\"\n    conds = []\n    for loss in get_loss_classes(feature_type):\n        loss_cls = get_loss_cls(feature_type, loss)\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(loss_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        loss_cond = schema_utils.create_cond(\n            {\"type\": loss},\n            other_props,\n        )\n        conds.append(loss_cond)\n    return conds\n\n\n@DeveloperAPI\ndef LossDataclassField(feature_type: str, default: str) -> Field:\n    loss_registry = get_loss_classes(feature_type)\n\n    class LossSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(registry=loss_registry, default_value=default)\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return get_loss_cls(feature_type, key)\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\"type\": \"string\", \"enum\": list(loss_registry.keys()), \"default\": default},\n                },\n                \"title\": \"loss_options\",\n                \"allOf\": get_loss_conds(feature_type),\n            }\n\n    return LossSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/features/number_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MEAN_SQUARED_ERROR, MODEL_ECD, NUMBER\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(NUMBER)\n@ludwig_dataclass\nclass NumberInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"NumberInputFeatureConfigMixin is a dataclass that configures the parameters used in both the number input\n    feature and the number global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=NUMBER)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass NumberInputFeatureConfig(NumberInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"NumberInputFeatureConfig is a dataclass that configures the parameters used for a number input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(NUMBER)\n\n    encoder: BaseEncoderConfig = None\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(NUMBER)\n@ludwig_dataclass\nclass ECDNumberInputFeatureConfig(NumberInputFeatureConfig):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=NUMBER,\n        default=\"passthrough\",\n    )\n\n\n@DeveloperAPI\n@output_mixin_registry.register(NUMBER)\n@ludwig_dataclass\nclass NumberOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"NumberOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the number output\n    feature and the number global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = None\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=NUMBER,\n        default=MEAN_SQUARED_ERROR,\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass NumberOutputFeatureConfig(NumberOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"NumberOutputFeatureConfig is a dataclass that configures the parameters used for a category output\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(NUMBER)\n\n    clip: list[int] | tuple[int] = schema_utils.FloatRangeTupleDataclassField(\n        n=2,\n        default=None,\n        allow_none=True,\n        min=0,\n        max=999999999,\n        description=\"Clip the predicted output to the specified range.\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][\"clip\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [MEAN_SQUARED_ERROR],\n        default=MEAN_SQUARED_ERROR,\n        description=\"Internal only use parameter: default validation metric for number output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][\"dependencies\"],\n    )\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][\"reduce_input\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"number_output\")\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(NUMBER)\n@ludwig_dataclass\nclass ECDNumberOutputFeatureConfig(NumberOutputFeatureConfig):\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=NUMBER,\n        default=\"regressor\",\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(NUMBER)\n@ludwig_dataclass\nclass NumberDefaultsConfig(NumberInputFeatureConfigMixin, NumberOutputFeatureConfigMixin):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=NUMBER,\n        default=\"passthrough\",\n    )\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=NUMBER,\n        default=\"regressor\",\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/__init__.py",
    "content": "# Register all preprocessors\nfrom ludwig.schema.features.preprocessing import (  # noqa\n    audio,\n    bag,\n    binary,\n    category,\n    date,\n    h3,\n    image,\n    number,\n    sequence,\n    set,\n    text,\n    timeseries,\n    vector,\n)\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/audio.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUDIO, BFILL, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(AUDIO)\n@ludwig_dataclass\nclass AudioPreprocessingConfig(BasePreprocessingConfig):\n    audio_file_length_limit_in_s: int = schema_utils.NonNegativeFloat(\n        default=7.5,\n        allow_none=False,\n        description=\"Float value that defines the maximum limit of the audio file in seconds. All files longer than \"\n        \"this limit are cut off. All files shorter than this limit are padded with padding_value\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"audio_file_length_limit_in_s\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=BFILL,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in an audio column\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    in_memory: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Defines whether the audio dataset will reside in memory during the training process or will be \"\n        \"dynamically fetched from disk (useful for large datasets). In the latter case a training batch \"\n        \"of input audio will be fetched from disk each training iteration.\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"in_memory\"],\n    )\n\n    padding_value: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        allow_none=False,\n        description=\"Float value that is used for padding.\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"padding_value\"],\n    )\n\n    norm: str = schema_utils.StringOptions(\n        [\"per_file\"],\n        default=None,\n        allow_none=True,\n        description=\"Normalization strategy for the audio files. If None, no normalization is performed. If \"\n        \"per_file, z-norm is applied on a 'per file' level\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"norm\"],\n    )\n\n    type: str = schema_utils.StringOptions(\n        [\"fbank\", \"group_delay\", \"raw\", \"stft\", \"stft_phase\"],\n        default=\"fbank\",\n        description=\"Defines the type of audio feature to be used.\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"type\"],\n    )\n\n    window_length_in_s: float = schema_utils.NonNegativeFloat(\n        default=0.04,\n        description=\"Defines the window length used for the short time Fourier transformation. This is only needed if \"\n        \"the audio_feature_type is 'raw'.\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"window_length_in_s\"],\n    )\n\n    window_shift_in_s: float = schema_utils.NonNegativeFloat(\n        default=0.02,\n        description=\"Defines the window shift used for the short time Fourier transformation (also called \"\n        \"hop_length). This is only needed if the audio_feature_type is 'raw'. \",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"window_shift_in_s\"],\n    )\n\n    num_fft_points: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"Defines the number of fft points used for the short time Fourier transformation\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"num_fft_points\"],\n    )\n\n    window_type: str = schema_utils.StringOptions(\n        [\"bartlett\", \"blackman\", \"hamming\", \"hann\"],\n        default=\"hamming\",\n        description=\"Defines the type window the signal is weighted before the short time Fourier transformation.\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"window_type\"],\n    )\n\n    num_filter_bands: int = schema_utils.PositiveInteger(\n        default=80,\n        description=\"Defines the number of filters used in the filterbank. Only needed if audio_feature_type \"\n        \"is 'fbank'\",\n        parameter_metadata=FEATURE_METADATA[AUDIO][PREPROCESSING][\"num_filter_bands\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/bag.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BAG, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\nfrom ludwig.utils.tokenizers import tokenizer_registry\n\n\n@DeveloperAPI\n@register_preprocessor(BAG)\n@ludwig_dataclass\nclass BagPreprocessingConfig(BasePreprocessingConfig):\n    tokenizer: str = schema_utils.StringOptions(\n        tokenizer_registry.keys(),\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to transform the raw text content of the dataset column to a set of elements. The \"\n        \"default value space splits the string on spaces. Common options include: underscore (splits on \"\n        \"underscore), comma (splits on comma), json (decodes the string into a set or a list through a \"\n        \"JSON parser).\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"tokenizer\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a set column\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. If the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[BAG][PREPROCESSING][\"most_common\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/base.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\n\n\n@DeveloperAPI\nclass BasePreprocessingConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Base class for input feature preprocessing. Not meant to be used directly.\n\n    The dataclass format prevents arbitrary properties from being set. Consequently, in child classes, all properties\n    from the corresponding input feature class are copied over: check each class to check which attributes are different\n    from the preprocessing of each feature.\n    \"\"\"\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/binary.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    BFILL,\n    BINARY,\n    DROP_ROW,\n    FFILL,\n    FILL_WITH_FALSE,\n    FILL_WITH_MODE,\n    FILL_WITH_TRUE,\n    PREPROCESSING,\n)\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\n\n\n@DeveloperAPI\n@register_preprocessor(BINARY)\n@ludwig_dataclass\nclass BinaryPreprocessingConfig(BasePreprocessingConfig):\n    \"\"\"BinaryPreprocessingConfig is a dataclass that configures the parameters used for a binary input feature.\"\"\"\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        [FILL_WITH_MODE, BFILL, FFILL, DROP_ROW, FILL_WITH_FALSE, FILL_WITH_TRUE],\n        default=FILL_WITH_FALSE,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a binary column\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fallback_true_label: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The label to interpret as 1 (True) when the binary feature doesn't have a \"\n        \"conventional boolean value\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"fallback_true_label\"],\n    )\n\n    fill_value: int | float | str = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        field_options=[\n            schema_utils.FloatRange(default=None, allow_none=True, min=0, max=1, description=\"\"),\n            schema_utils.StringOptions(options=strings_utils.all_bool_strs(), default=\"Y\", allow_none=False),\n            schema_utils.Boolean(default=True, description=\"\"),\n        ],\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: int | float | str = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        field_options=[\n            schema_utils.FloatRange(default=1.0, allow_none=False, min=0, max=1, description=\"\"),\n            schema_utils.StringOptions(options=strings_utils.all_bool_strs(), default=\"Y\", allow_none=False),\n            schema_utils.Boolean(default=True, description=\"\"),\n        ],\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"binary_output\")\n@ludwig_dataclass\nclass BinaryOutputPreprocessingConfig(BinaryPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        [FILL_WITH_MODE, BFILL, FFILL, DROP_ROW, FILL_WITH_FALSE, FILL_WITH_TRUE],\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a binary output feature\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fallback_true_label: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The label to interpret as 1 (True) when the binary feature doesn't have a \"\n        \"conventional boolean value\",\n        parameter_metadata=FEATURE_METADATA[BINARY][PREPROCESSING][\"fallback_true_label\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/category.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CATEGORY, DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\n\n\n@DeveloperAPI\n@register_preprocessor(CATEGORY)\n@ludwig_dataclass\nclass CategoryPreprocessingConfig(BasePreprocessingConfig):\n    \"\"\"CategoryPreprocessingConfig is a dataclass that configures the parameters used for a category input\n    feature.\"\"\"\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a category column\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=(\n            \"The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`\"\n        ),\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether the string has to be lowercased before being handled by the tokenizer.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. if the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"most_common\"],\n    )\n\n    cache_encoder_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"For fixed encoders, compute encoder embeddings in preprocessing to avoid this step at train time. \"\n            \"Can speed up the time taken per step during training, but will invalidate the preprocessed data \"\n            \"if the encoder type is changed.\"\n        ),\n        parameter_metadata=PREPROCESSING_METADATA[\"cache_encoder_embeddings\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"category_output\")\n@ludwig_dataclass\nclass CategoryOutputPreprocessingConfig(CategoryPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a category output feature\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether the string has to be lowercased before being handled by the tokenizer.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. if the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"most_common\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"category_distribution_output\")\n@ludwig_dataclass\nclass CategoryDistributionOutputPreprocessingConfig(BasePreprocessingConfig):\n    def __post_init__(self):\n        if self.vocab is None:\n            raise ConfigValidationError(\"`vocab` must be specified for `category_distribution` output feature.\")\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a category output feature\",\n        parameter_metadata=FEATURE_METADATA[CATEGORY][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    vocab: list[str] = schema_utils.List(default=None)\n\n\n@DeveloperAPI\n@register_preprocessor(\"category_llm\")\n@ludwig_dataclass\nclass LLMCategoryOutputPreprocessingConfig(CategoryOutputPreprocessingConfig):\n    def __post_init__(self):\n        if self.vocab is None:\n            raise ConfigValidationError(\"`vocab` must be specified for `category_llm` output feature.\")\n        if self.fallback_label is None:\n            raise ConfigValidationError(\"`fallback_label` must be specified for `category_llm` output feature.\")\n\n    vocab: list[str] = schema_utils.List(\n        default=None,\n        allow_none=False,\n        description=\"The list of labels that the model can predict.\",\n    )\n\n    fallback_label: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        description=\"The label to use when the model doesn't match any of the labels in the `labels` list.\",\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/date.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BFILL, DATE, DROP_ROW, FFILL, FILL_WITH_CONST, PREPROCESSING\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(DATE)\n@ludwig_dataclass\nclass DatePreprocessingConfig(BasePreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        [FILL_WITH_CONST, BFILL, FFILL, DROP_ROW],\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a date column\",\n        parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    datetime_format: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"This parameter can either be a datetime format string, or null, in which case the datetime \"\n        \"format will be inferred automatically.\",\n        parameter_metadata=FEATURE_METADATA[DATE][PREPROCESSING][\"datetime_format\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/h3.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import FILL_WITH_CONST, H3, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(H3)\n@ludwig_dataclass\nclass H3PreprocessingConfig(BasePreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in an h3 column\",\n        parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: int = schema_utils.PositiveInteger(\n        default=576495936675512319,\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: int = schema_utils.PositiveInteger(\n        default=576495936675512319,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[H3][PREPROCESSING][\"computed_fill_value\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/image.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BFILL, DROP_ROW, IMAGE, IMAGENET1K, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(IMAGE)\n@ludwig_dataclass\nclass ImagePreprocessingConfig(BasePreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=BFILL,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in an image column\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. If the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    height: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The image height in pixels. If this parameter is set, images will be resized to the specified \"\n        \"height using the resize_method parameter. If None, images will be resized to the size of the \"\n        \"first image in the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"height\"],\n    )\n\n    width: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The image width in pixels. If this parameter is set, images will be resized to the specified \"\n        \"width using the resize_method parameter. If None, images will be resized to the size of the \"\n        \"first image in the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"width\"],\n    )\n\n    num_channels: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channels in the images. If specified, images will be read in the mode specified by the \"\n        \"number of channels. If not specified, the number of channels will be inferred from the image \"\n        \"format of the first valid image in the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"num_channels\"],\n    )\n\n    resize_method: str = schema_utils.StringOptions(\n        [\"crop_or_pad\", \"interpolate\"],\n        default=\"interpolate\",\n        allow_none=False,\n        description=\"The method to use for resizing images.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"resize_method\"],\n    )\n\n    infer_image_num_channels: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, then the number of channels in the dataset is inferred from a sample of the first image \"\n        \"in the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_num_channels\"],\n    )\n\n    infer_image_dimensions: bool = schema_utils.Boolean(\n        default=True,\n        description=\"If true, then the height and width of images in the dataset will be inferred from a sample of \"\n        \"the first image in the dataset. Each image that doesn't conform to these dimensions will be \"\n        \"resized according to resize_method. If set to false, then the height and width of images in the \"\n        \"dataset will be specified by the user.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_dimensions\"],\n    )\n\n    infer_image_max_height: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=False,\n        description=\"If infer_image_dimensions is set, this is used as the maximum height of the images in \"\n        \"the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_max_height\"],\n    )\n\n    infer_image_max_width: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=False,\n        description=\"If infer_image_dimensions is set, this is used as the maximum width of the images in \"\n        \"the dataset.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_max_width\"],\n    )\n\n    infer_image_sample_size: int = schema_utils.PositiveInteger(\n        default=100,\n        allow_none=False,\n        description=\"The sample size used for inferring dimensions of images in infer_image_dimensions.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_sample_size\"],\n    )\n\n    standardize_image: str | None = schema_utils.StringOptions(\n        [IMAGENET1K],\n        default=None,\n        allow_none=True,\n        description=\"Standardize image by per channel mean centering and standard deviation scaling .\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"standardize_image\"],\n    )\n\n    in_memory: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Defines whether image dataset will reside in memory during the training process or will be \"\n        \"dynamically fetched from disk (useful for large datasets). In the latter case a training batch \"\n        \"of input images will be fetched from disk each training iteration.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"in_memory\"],\n    )\n\n    num_processes: int = schema_utils.PositiveInteger(\n        default=1,\n        allow_none=False,\n        description=\"Specifies the number of processes to run for preprocessing images.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"num_processes\"],\n    )\n\n    requires_equal_dimensions: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, then width and height must be equal.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"requires_equal_dimensions\"],\n    )\n\n    num_classes: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of channel classes in the images. If specified, this value will be validated \"\n        \"against the inferred number of classes. Use 2 to convert grayscale images to binary images.\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"num_classes\"],\n    )\n\n    infer_image_num_classes: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, then the number of channel classes in the dataset will be inferred from a sample of \"\n        \"the first image in the dataset. Each unique channel value will be mapped to a class and preprocessing will \"\n        \"create a masked image based on the channel classes. \",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"infer_image_num_classes\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"image_output\")\n@ludwig_dataclass\nclass ImageOutputPreprocessingConfig(ImagePreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in an image column\",\n        parameter_metadata=FEATURE_METADATA[IMAGE][PREPROCESSING][\"missing_value_strategy\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/number.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    DROP_ROW,\n    FILL_WITH_CONST,\n    FILL_WITH_MEAN,\n    MISSING_VALUE_STRATEGY_OPTIONS,\n    NUMBER,\n    PREPROCESSING,\n)\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(NUMBER)\n@ludwig_dataclass\nclass NumberPreprocessingConfig(BasePreprocessingConfig):\n    \"\"\"NumberPreprocessingConfig is a dataclass that configures the parameters used for a number input feature.\"\"\"\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN],\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a number column\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: float = schema_utils.FloatRange(\n        default=0.0,\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: float = schema_utils.FloatRange(\n        default=0.0,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    normalization: str = schema_utils.StringOptions(\n        [\"zscore\", \"minmax\", \"log1p\", \"iq\"],\n        default=\"zscore\",\n        allow_none=True,\n        description=(\n            \"Normalization strategy to use for this number feature. If the value is `null` no normalization is \"\n            \"performed.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"normalization\"],\n    )\n\n    outlier_strategy: str | None = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN, None],\n        default=None,\n        allow_none=True,\n        description=(\n            \"Determines how outliers will be handled in the dataset. In most cases, replacing outliers with the \"\n            \"column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough \"\n            \"to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers \"\n            \"is to leave them in the data, which is the behavior when this parameter is left as `null`.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"outlier_strategy\"],\n    )\n\n    outlier_threshold: float | None = schema_utils.FloatRange(\n        default=3.0,\n        allow_none=False,\n        min=0.0,\n        description=(\n            \"Standard deviations from the mean past which a value is considered an outlier. The 3-sigma \"\n            \"rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2 \"\n            \"standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations \"\n            \"of the mean (see: [68–95–99.7 rule](https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule)). \"\n            \"As such anything farther away than that is highly likely to be an outlier, and may distort the learning \"\n            \"process by disproportionately affecting the model.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"outlier_threshold\"],\n    )\n\n    computed_outlier_fill_value: float = schema_utils.FloatRange(\n        default=0.0,\n        allow_none=False,\n        description=\"The internally computed fill value to replace outliers with in case the \"\n        \"outlier_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"computed_outlier_fill_value\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"number_output\")\n@ludwig_dataclass\nclass NumberOutputPreprocessingConfig(NumberPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS + [FILL_WITH_MEAN],\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a number output feature\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    normalization: str = schema_utils.StringOptions(\n        [\"zscore\", \"minmax\", \"log1p\", \"iq\"],\n        default=None,\n        allow_none=True,\n        description=\"Normalization strategy to use for this number feature.\",\n        parameter_metadata=FEATURE_METADATA[NUMBER][PREPROCESSING][\"normalization\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/sequence.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, SEQUENCE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\n\n\n@DeveloperAPI\n@register_preprocessor(SEQUENCE)\n@ludwig_dataclass\nclass SequencePreprocessingConfig(BasePreprocessingConfig):\n    tokenizer: str = schema_utils.String(\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to map from the raw string content of the dataset column to a sequence of elements.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"tokenizer\"],\n    )\n\n    vocab_file: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the \"\n        \"first string until \\t or \\n is considered a word.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"vocab_file\"],\n    )\n\n    sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The desired length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated and sequences shorter than this value will be padded. If None, sequence length will be \"\n        \"inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"sequence_length\"],\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"max_sequence_length\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=20000,\n        allow_none=False,\n        description=\"The maximum number of most common tokens in the vocabulary. If the data contains more than this \"\n        \"amount, the most infrequent symbols will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"most_common\"],\n    )\n\n    padding_symbol: str = schema_utils.String(\n        default=strings_utils.PADDING_SYMBOL,\n        allow_none=False,\n        description=\"The string used as a padding symbol. This special token is mapped to the integer ID 0 in the \"\n        \"vocabulary.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"padding_symbol\"],\n    )\n\n    unknown_symbol: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The string used as an unknown placeholder. This special token is mapped to the integer ID 1 in \"\n        \"the vocabulary.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"unknown_symbol\"],\n    )\n\n    padding: str = schema_utils.StringOptions(\n        [\"left\", \"right\"],\n        default=\"right\",\n        allow_none=False,\n        description=\"The direction of the padding.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"padding\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"lowercase\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a text column\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    ngram_size: int = schema_utils.PositiveInteger(\n        default=2,\n        allow_none=False,\n        description=\"The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"ngram_size\"],\n    )\n\n    cache_encoder_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Compute encoder embeddings in preprocessing, speeding up training time considerably.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"cache_encoder_embeddings\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"sequence_output\")\n@ludwig_dataclass\nclass SequenceOutputPreprocessingConfig(SequencePreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a sequence output feature\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The desired length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated and sequences shorter than this value will be padded. If None, sequence length will be \"\n        \"inferred from the training dataset.\",\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"max_sequence_length\"],\n    )\n\n    tokenizer: str = schema_utils.String(\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to map from the raw string content of the dataset column to a sequence of elements.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"tokenizer\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=20000,\n        allow_none=False,\n        description=\"The maximum number of most common tokens in the vocabulary. If the data contains more than this \"\n        \"amount, the most infrequent symbols will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"most_common\"],\n    )\n\n    ngram_size: int = schema_utils.PositiveInteger(\n        default=2,\n        allow_none=False,\n        description=\"The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][PREPROCESSING][\"ngram_size\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/set.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, SET\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\n\n\n@DeveloperAPI\n@register_preprocessor(SET)\n@ludwig_dataclass\nclass SetPreprocessingConfig(BasePreprocessingConfig):\n    tokenizer: str = schema_utils.String(\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to transform the raw text content of the dataset column to a set of elements. The \"\n        \"default value space splits the string on spaces. Common options include: underscore (splits on \"\n        \"underscore), comma (splits on comma), json (decodes the string into a set or a list through a \"\n        \"JSON parser).\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"tokenizer\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a set column\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. If the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"most_common\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"set_output\")\n@ludwig_dataclass\nclass SetOutputPreprocessingConfig(SetPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a set output feature\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    tokenizer: str = schema_utils.String(\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to transform the raw text content of the dataset column to a set of elements. The \"\n        \"default value space splits the string on spaces. Common options include: underscore (splits on \"\n        \"underscore), comma (splits on comma), json (decodes the string into a set or a list through a \"\n        \"JSON parser).\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"tokenizer\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=True,\n        description=\"The maximum number of most common tokens to be considered. If the data contains more than this \"\n        \"amount, the most infrequent tokens will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[SET][PREPROCESSING][\"most_common\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/text.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, TEXT\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.llms.prompt import PromptConfig, PromptConfigField\nfrom ludwig.schema.metadata import FEATURE_METADATA, PREPROCESSING_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils import strings_utils\nfrom ludwig.utils.tokenizers import tokenizer_registry\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseTextPreprocessingConfig(BasePreprocessingConfig):\n    \"\"\"TextPreprocessingConfig is a dataclass that configures the parameters used for a text input feature.\"\"\"\n\n    pretrained_model_name_or_path: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"This can be either the name of a pretrained HuggingFace model or a path where it was downloaded.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"pretrained_model_name_or_path\"],\n    )\n\n    tokenizer: str = schema_utils.StringOptions(\n        tokenizer_registry.keys(),\n        default=\"space_punct\",\n        allow_none=False,\n        description=\"Defines how to map from the raw string content of the dataset column to a sequence of elements.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"tokenizer\"],\n    )\n\n    vocab_file: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Filepath string to a UTF-8 encoded file containing the sequence's vocabulary. On each line the \"\n        \"first string until `\\\\t` or `\\\\n` is considered a word.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"vocab_file\"],\n    )\n\n    sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The desired length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated and sequences shorter than this value will be padded. If None, sequence length will be \"\n        \"inferred from the training dataset.\",\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"max_sequence_length\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=20000,\n        allow_none=False,\n        description=\"The maximum number of most common tokens in the vocabulary. If the data contains more than this \"\n        \"amount, the most infrequent symbols will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"most_common\"],\n    )\n\n    padding_symbol: str = schema_utils.String(\n        default=strings_utils.PADDING_SYMBOL,\n        allow_none=False,\n        description=\"The string used as the padding symbol for sequence features. Ignored for features using \"\n        \"huggingface encoders, which have their own vocabulary.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"padding_symbol\"],\n    )\n\n    unknown_symbol: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The string used as the unknown symbol for sequence features. Ignored for features using \"\n        \"huggingface encoders, which have their own vocabulary.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"unknown_symbol\"],\n    )\n\n    padding: str = schema_utils.StringOptions(\n        [\"left\", \"right\"],\n        default=\"right\",\n        allow_none=False,\n        description=\"The direction of the padding.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"padding\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"lowercase\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a text column.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=(\n            \"The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=strings_utils.UNKNOWN_SYMBOL,\n        allow_none=False,\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"`missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n    ngram_size: int = schema_utils.PositiveInteger(\n        default=2,\n        allow_none=False,\n        description=\"The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"ngram_size\"],\n    )\n\n    cache_encoder_embeddings: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"For pretrained encoders, compute encoder embeddings in preprocessing, \"\n            \"speeding up training time considerably. Only supported when `encoder.trainable=false`.\"\n        ),\n        parameter_metadata=PREPROCESSING_METADATA[\"cache_encoder_embeddings\"],\n    )\n\n    compute_idf: bool = schema_utils.Boolean(\n        default=False,\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(TEXT)\n@ludwig_dataclass\nclass TextPreprocessingConfig(BaseTextPreprocessingConfig):\n    \"\"\"TextPreprocessingConfig is a dataclass that configures the parameters used for a text input feature.\"\"\"\n\n    prompt: PromptConfig = PromptConfigField().get_default_field()\n\n\n@DeveloperAPI\n@register_preprocessor(\"text_llm_input\")\n@ludwig_dataclass\nclass LLMTextInputPreprocessingConfig(BaseTextPreprocessingConfig):\n    \"\"\"LLMs require the prompt to be provided at the top-level, not preprocessing.\"\"\"\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"max_sequence_length\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"text_output\")\n@ludwig_dataclass\nclass TextOutputPreprocessingConfig(BaseTextPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a text output feature.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The desired length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated and sequences shorter than this value will be padded. If None, sequence length will be \"\n        \"inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"sequence_length\"],\n    )\n\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"max_sequence_length\"],\n    )\n\n    tokenizer: str = schema_utils.StringOptions(\n        tokenizer_registry.keys(),\n        default=\"space_punct\",\n        allow_none=False,\n        description=\"Defines how to map from the raw string content of the dataset column to a sequence of elements.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"tokenizer\"],\n    )\n\n    lowercase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If true, converts the string to lowercase before tokenizing.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"lowercase\"],\n    )\n\n    most_common: int = schema_utils.PositiveInteger(\n        default=20000,\n        allow_none=False,\n        description=\"The maximum number of most common tokens in the vocabulary. If the data contains more than this \"\n        \"amount, the most infrequent symbols will be treated as unknown.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"most_common\"],\n    )\n\n    ngram_size: int = schema_utils.PositiveInteger(\n        default=2,\n        allow_none=False,\n        description=\"The size of the ngram when using the `ngram` tokenizer (e.g, 2 = bigram, 3 = trigram, etc.).\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"ngram_size\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"text_llm_output\")\n@ludwig_dataclass\nclass LLMTextOutputPreprocessingConfig(TextOutputPreprocessingConfig):\n    max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The maximum length (number of tokens) of the sequence. Sequences that are longer than this value \"\n        \"will be truncated. Useful as a stopgap measure if `sequence_length` is set to `None`. If `None`, max sequence \"\n        \"length will be inferred from the training dataset.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][PREPROCESSING][\"max_sequence_length\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/timeseries.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, TIMESERIES\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.tokenizers import tokenizer_registry\n\n\n@ludwig_dataclass\nclass BaseTimeseriesPreprocessingConfig(BasePreprocessingConfig):\n    tokenizer: str = schema_utils.StringOptions(\n        tokenizer_registry.keys(),\n        default=\"space\",\n        allow_none=False,\n        description=\"Defines how to map from the raw string content of the dataset column to a sequence of elements.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"tokenizer\"],\n    )\n\n    timeseries_length_limit: int = schema_utils.PositiveInteger(\n        default=256,\n        allow_none=False,\n        description=\"Defines the maximum length of the timeseries. All timeseries longer than this limit are cut off.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"timeseries_length_limit\"],\n    )\n\n    padding_value: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        allow_none=False,\n        description=\"Float value that is used for padding and replacing missing values within a row.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"padding_value\"],\n    )\n\n    padding: str = schema_utils.StringOptions(\n        [\"left\", \"right\"],\n        default=\"right\",\n        allow_none=False,\n        description=\"The direction of the padding.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"padding\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=(\n            \"What strategy to follow when there's a missing value in a column. Currently applies only to a row missing \"\n            \"in its entirety, not invididual elements within the row. For now, `NaN` values within a row are filled \"\n            \"using the `padding_value`.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        description=(\n            \"The value to replace missing values with in case the `missing_value_strategy` is `fill_with_const`.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        description=(\n            \"The internally computed fill value to replace missing values with in case the \"\n            \"`missing_value_strategy` is `fill_with_mode` or `fill_with_mean`.\"\n        ),\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesPreprocessingConfig(BaseTimeseriesPreprocessingConfig):\n    window_size: int = schema_utils.NonNegativeInteger(\n        default=0,\n        allow_none=False,\n        description=(\n            \"Optional lookback window size used to convert a column-major dataset (one observation per row) \"\n            \"into a row-major dataset (each row has a timeseries window of observations). Starting from a given \"\n            \"observation, a sliding window is taken going `window_size - 1` rows back to form the timeseries input \"\n            \"feature. If this value is left as 0, then it is assumed that the dataset has been provided in row-major \"\n            \"format (i.e., it has already been preprocessed such that each row is a timeseries window).\"\n        ),\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when a row of data is missing.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"timeseries_output\")\n@ludwig_dataclass\nclass TimeseriesOutputPreprocessingConfig(BaseTimeseriesPreprocessingConfig):\n    horizon: int = schema_utils.NonNegativeInteger(\n        default=0,\n        allow_none=False,\n        description=(\n            \"Optional forecasting horizon used to convert a column-major dataset (one observation per row) \"\n            \"into a row-major dataset (each row has a timeseries window of observations). Starting from a given \"\n            \"observation, a sliding window is token going `horizon` rows forward in time, excluding the observation \"\n            \"in the current row. If this value is left as 0, then it is assumed that the dataset has been provided in \"\n            \"row-major format (i.e., it has already been preprocessed such that each row is a timeseries window).\"\n        ),\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when a row of data is missing.\",\n        parameter_metadata=FEATURE_METADATA[TIMESERIES][PREPROCESSING][\"missing_value_strategy\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/utils.py",
    "content": "from dataclasses import field\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.utils.registry import Registry\n\npreprocessing_registry = Registry()\n\n\n@DeveloperAPI\ndef register_preprocessor(name: str):\n    def wrap(preprocessing_config: BasePreprocessingConfig):\n        preprocessing_registry[name] = preprocessing_config\n        return preprocessing_config\n\n    return wrap\n\n\n@DeveloperAPI\ndef PreprocessingDataclassField(feature_type: str):\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify a preprocessing\n    config.\n\n    Returns: Initialized dataclass field that converts an untyped dict with params to a preprocessing config.\n    \"\"\"\n\n    class PreprocessingMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field that deserializes a dict for a valid preprocessing config from the preprocessing_registry\n        and creates a corresponding JSON schema for external usage.\"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return None\n            if isinstance(value, dict):\n                if feature_type in preprocessing_registry:\n                    pre = preprocessing_registry[feature_type]\n                    try:\n                        return pre.Schema().load(value)\n                    except (TypeError, ConfigValidationError) as error:\n                        raise ConfigValidationError(\n                            f\"Invalid preprocessing params: {value}, see `{pre}` definition. Error: {error}\"\n                        )\n                raise ConfigValidationError(\n                    f\"Invalid params for preprocessor: {value}, expect dict with at least a valid `type` attribute.\"\n                )\n            raise ConfigValidationError(\"Field should be None or dict\")\n\n        def _jsonschema_type_mapping(self):\n            preprocessor_cls = preprocessing_registry[feature_type]\n            props = schema_utils.unload_jsonschema_from_marshmallow_class(preprocessor_cls)[\"properties\"]\n            return {\n                \"type\": \"object\",\n                \"properties\": props,\n                \"title\": \"preprocessing_options\",\n                \"additionalProperties\": True,\n            }\n\n    try:\n        preprocessor = preprocessing_registry[feature_type]\n        load_default = lambda: preprocessor.Schema().load({})\n        dump_default = preprocessor.Schema().dump({})\n\n        return field(\n            metadata={\n                \"marshmallow_field\": PreprocessingMarshmallowField(\n                    allow_none=False,\n                    dump_default=dump_default,\n                    load_default=load_default,\n                )\n            },\n            default_factory=load_default,\n        )\n    except Exception as e:\n        raise ConfigValidationError(\n            f\"Unsupported preprocessing type: {feature_type}. See preprocessing_registry. \" f\"Details: {e}\"\n        )\n"
  },
  {
    "path": "ludwig/schema/features/preprocessing/vector.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DROP_ROW, FILL_WITH_CONST, MISSING_VALUE_STRATEGY_OPTIONS, PREPROCESSING, VECTOR\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import register_preprocessor\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_preprocessor(VECTOR)\n@ludwig_dataclass\nclass VectorPreprocessingConfig(BasePreprocessingConfig):\n    vector_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The size of the vector. If None, the vector size will be inferred from the data.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"vector_size\"],\n    )\n\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=FILL_WITH_CONST,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a vector column\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        pattern=r\"^([0-9]+(\\.[0-9]*)?\\s*)*$\",\n        description=\"The value to replace missing values with in case the missing_value_strategy is fill_with_const\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"fill_value\"],\n    )\n\n    computed_fill_value: str = schema_utils.String(\n        default=\"\",\n        allow_none=False,\n        pattern=r\"^([0-9]+(\\.[0-9]*)?\\s*)*$\",\n        description=\"The internally computed fill value to replace missing values with in case the \"\n        \"missing_value_strategy is fill_with_mode or fill_with_mean\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"computed_fill_value\"],\n    )\n\n\n@DeveloperAPI\n@register_preprocessor(\"vector_output\")\n@ludwig_dataclass\nclass VectorOutputPreprocessingConfig(VectorPreprocessingConfig):\n    missing_value_strategy: str = schema_utils.StringOptions(\n        MISSING_VALUE_STRATEGY_OPTIONS,\n        default=DROP_ROW,\n        allow_none=False,\n        description=\"What strategy to follow when there's a missing value in a vector output feature\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"missing_value_strategy\"],\n    )\n\n    vector_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The size of the vector. If None, the vector size will be inferred from the data.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][PREPROCESSING][\"vector_size\"],\n    )\n"
  },
  {
    "path": "ludwig/schema/features/sequence_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import LOSS, MODEL_ECD, SEQUENCE, SEQUENCE_SOFTMAX_CROSS_ENTROPY\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(SEQUENCE)\n@ludwig_dataclass\nclass SequenceInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"SequenceInputFeatureConfigMixin is a dataclass that configures the parameters used in both the sequence\n    input feature and the sequence global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=SEQUENCE)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=SEQUENCE,\n        default=\"embed\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(SEQUENCE)\n@ludwig_dataclass\nclass SequenceInputFeatureConfig(SequenceInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"SequenceInputFeatureConfig is a dataclass that configures the parameters used for a sequence input\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(SEQUENCE)\n\n\n@DeveloperAPI\n@output_mixin_registry.register(SEQUENCE)\n@ludwig_dataclass\nclass SequenceOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"SequenceOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the sequence\n    output feature and the sequence global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=SEQUENCE,\n        default=\"generator\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=SEQUENCE,\n        default=SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(SEQUENCE)\n@ludwig_dataclass\nclass SequenceOutputFeatureConfig(SequenceOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"SequenceOutputFeatureConfig is a dataclass that configures the parameters used for a sequence output\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(SEQUENCE)\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [LOSS],\n        default=LOSS,\n        description=\"Internal only use parameter: default validation metric for sequence output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][\"dependencies\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"sequence_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[SEQUENCE][\"reduce_input\"],\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(SEQUENCE)\n@ludwig_dataclass\nclass SequenceDefaultsConfig(SequenceInputFeatureConfigMixin, SequenceOutputFeatureConfigMixin):\n    pass\n"
  },
  {
    "path": "ludwig/schema/features/set_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import JACCARD, MODEL_ECD, SET, SIGMOID_CROSS_ENTROPY\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(SET)\n@ludwig_dataclass\nclass SetInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"SetInputFeatureConfigMixin is a dataclass that configures the parameters used in both the set input feature\n    and the set global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=SET)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=SET,\n        default=\"embed\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(SET)\n@ludwig_dataclass\nclass SetInputFeatureConfig(SetInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"SetInputFeatureConfig is a dataclass that configures the parameters used for a set input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(SET)\n\n\n@DeveloperAPI\n@output_mixin_registry.register(SET)\n@ludwig_dataclass\nclass SetOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"SetOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the set output\n    feature and the set global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=SET,\n        default=\"classifier\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=SET,\n        default=SIGMOID_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(SET)\n@ludwig_dataclass\nclass SetOutputFeatureConfig(SetOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"SetOutputFeatureConfig is a dataclass that configures the parameters used for a set output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(SET)\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [JACCARD],\n        default=JACCARD,\n        description=\"Internal only use parameter: default validation metric for set output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[SET][\"dependencies\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"set_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[SET][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[SET][\"reduce_input\"],\n    )\n\n    threshold: float = schema_utils.FloatRange(\n        default=0.5,\n        min=0,\n        max=1,\n        description=\"The threshold used to convert output probabilities to predictions. Tokens with predicted\"\n        \"probabilities greater than or equal to threshold are predicted to be in the output set (True).\",\n        parameter_metadata=FEATURE_METADATA[SET][\"threshold\"],\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(SET)\n@ludwig_dataclass\nclass SetDefaultsConfig(SetInputFeatureConfigMixin, SetOutputFeatureConfigMixin):\n    pass\n"
  },
  {
    "path": "ludwig/schema/features/text_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    LOSS,\n    MODEL_ECD,\n    MODEL_LLM,\n    NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY,\n    SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n    TEXT,\n)\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    llm_defaults_config_registry,\n    llm_input_config_registry,\n    llm_output_config_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(TEXT)\n@ludwig_dataclass\nclass TextInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"TextInputFeatureConfigMixin is a dataclass that configures the parameters used in both the text input\n    feature and the text global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=TEXT)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass TextInputFeatureConfig(TextInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"TextInputFeatureConfig is a dataclass that configures the parameters used for a text input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(TEXT)\n\n    encoder: BaseEncoderConfig = None\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(TEXT)\n@ludwig_dataclass\nclass ECDTextInputFeatureConfig(TextInputFeatureConfig):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=TEXT,\n        default=\"parallel_cnn\",\n    )\n\n\n@DeveloperAPI\n@llm_input_config_registry.register(TEXT)\n@ludwig_dataclass\nclass LLMTextInputFeatureConfig(TextInputFeatureConfig):\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"text_llm_input\")\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_LLM,\n        feature_type=TEXT,\n        default=\"passthrough\",\n    )\n\n\n@DeveloperAPI\n@output_mixin_registry.register(TEXT)\n@ludwig_dataclass\nclass TextOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"TextOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the text output\n    feature and the text global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = None\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=TEXT,\n        default=SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass TextOutputFeatureConfig(TextOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"TextOutputFeatureConfig is a dataclass that configures the parameters used for a text output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(TEXT)\n\n    class_similarities: list = schema_utils.List(\n        list,\n        default=None,\n        description=\"If not null this parameter is a c x c matrix in the form of a list of lists that contains the \"\n        \"mutual similarity of classes. It is used if `class_similarities_temperature` is greater than 0. \",\n        parameter_metadata=FEATURE_METADATA[TEXT][\"class_similarities\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [LOSS],\n        default=LOSS,\n        description=\"Internal only use parameter: default validation metric for binary output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][\"dependencies\"],\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"text_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[TEXT][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=\"sum\",\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[TEXT][\"reduce_input\"],\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(TEXT)\n@ludwig_dataclass\nclass ECDTextOutputFeatureConfig(TextOutputFeatureConfig):\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=TEXT,\n        default=\"generator\",\n    )\n\n\n@DeveloperAPI\n@llm_output_config_registry.register(TEXT)\n@ludwig_dataclass\nclass LLMTextOutputFeatureConfig(TextOutputFeatureConfig):\n    default_validation_metric: str = schema_utils.StringOptions(\n        [LOSS],\n        default=LOSS,\n        description=\"Internal only use parameter: default validation metric for text output feature for LLMs.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"text_llm_output\")\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_LLM,\n        feature_type=TEXT,\n        default=\"text_extractor\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=TEXT,\n        default=NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(TEXT)\n@ludwig_dataclass\nclass ECDTextDefaultsConfig(TextInputFeatureConfigMixin, TextOutputFeatureConfigMixin):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=TEXT,\n        default=\"parallel_cnn\",\n    )\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=TEXT,\n        default=\"generator\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=TEXT,\n        default=SEQUENCE_SOFTMAX_CROSS_ENTROPY,\n    )\n\n\n@DeveloperAPI\n@llm_defaults_config_registry.register(TEXT)\n@ludwig_dataclass\nclass LLMTextDefaultsConfig(TextInputFeatureConfigMixin, TextOutputFeatureConfigMixin):\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_LLM,\n        feature_type=TEXT,\n        default=\"passthrough\",\n    )\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_LLM,\n        feature_type=TEXT,\n        default=\"text_extractor\",\n    )\n\n    # TODO(Arnav): Refactor LossDataclassField to only accept loss types that are valid for the model\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=TEXT,\n        default=NEXT_TOKEN_SOFTMAX_CROSS_ENTROPY,\n    )\n"
  },
  {
    "path": "ludwig/schema/features/timeseries_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import HUBER, MEAN_SQUARED_ERROR, MODEL_ECD, TIMESERIES, VECTOR\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"TimeseriesInputFeatureConfigMixin is a dataclass that configures the parameters used in both the timeseries\n    input feature and the timeseries global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=TIMESERIES)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=TIMESERIES,\n        default=\"parallel_cnn\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesInputFeatureConfig(TimeseriesInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"TimeseriesInputFeatureConfig is a dataclass that configures the parameters used for a timeseries input\n    feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(TIMESERIES)\n\n\n@DeveloperAPI\n@output_mixin_registry.register(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"TimeseriesOutputFeatureConfigMixin configures the parameters used in both the timeseries output feature and\n    the timeseries global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=TIMESERIES,\n        default=\"projector\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=TIMESERIES,\n        default=HUBER,\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesOutputFeatureConfig(BaseOutputFeatureConfig, TimeseriesOutputFeatureConfigMixin):\n    \"\"\"TimeseriesOutputFeatureConfig configures the parameters used for a timeseries output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(TIMESERIES)\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"dependencies\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [MEAN_SQUARED_ERROR],\n        default=MEAN_SQUARED_ERROR,\n        description=\"Internal parameter.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"timeseries_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"reduce_input\"],\n    )\n\n    horizon: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Internal parameter. Obtained from preprocessing\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(TIMESERIES)\n@ludwig_dataclass\nclass TimeseriesDefaultsConfig(TimeseriesInputFeatureConfigMixin, TimeseriesOutputFeatureConfigMixin):\n    pass\n"
  },
  {
    "path": "ludwig/schema/features/utils.py",
    "content": "from collections import defaultdict\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MODEL_ECD, MODEL_LLM\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.utils.registry import Registry\n\ninput_config_registries = defaultdict(Registry)\noutput_config_registries = defaultdict(Registry)\n\necd_input_config_registry = input_config_registries[MODEL_ECD]\nllm_input_config_registry = input_config_registries[MODEL_LLM]\n\necd_output_config_registry = output_config_registries[MODEL_ECD]\nllm_output_config_registry = output_config_registries[MODEL_LLM]\n\ninput_mixin_registry = Registry()\noutput_mixin_registry = Registry()\n\"\"\"ECD models support the full range of feature parameters available in Ludwig, so any feature schema can be\nregistered into it.\n\nSee `BinaryDefaultsConfig` for an example.\n\"\"\"\necd_defaults_config_registry = Registry()\n\nllm_defaults_config_registry = Registry()\n\n\ndef input_config_registry(model_type: str) -> Registry:\n    return input_config_registries[model_type]\n\n\ndef output_config_registry(model_type: str) -> Registry:\n    return output_config_registries[model_type]\n\n\n@DeveloperAPI\ndef get_input_feature_cls(model_type: str, name: str):\n    # TODO(travis): not needed once we remove existing model config implementation\n    return input_config_registries[model_type][name]\n\n\n@DeveloperAPI\ndef get_output_feature_cls(model_type: str, name: str):\n    # TODO(ksbrar): What is this?\n    return output_config_registries[model_type][name]\n\n\n@DeveloperAPI\ndef get_input_feature_jsonschema(model_type: str):\n    \"\"\"This function returns a JSON schema structured to only requires a `type` key and then conditionally applies\n    a corresponding input feature's field constraints.\n\n    Returns: JSON Schema\n    \"\"\"\n    input_feature_types = sorted(list(input_config_registry(model_type).keys()))\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"type\": \"string\", \"title\": \"name\", \"description\": \"Name of the input feature.\"},\n            \"type\": {\n                \"type\": \"string\",\n                \"enum\": input_feature_types,\n                \"title\": \"type\",\n                \"description\": \"Type of the input feature\",\n            },\n            \"column\": {\"type\": \"string\", \"title\": \"column\", \"description\": \"Name of the column.\"},\n        },\n        \"additionalProperties\": True,\n        \"allOf\": get_input_feature_conds(model_type),\n        \"required\": [\"name\", \"type\"],\n        \"title\": \"input_feature\",\n    }\n\n    return schema\n\n\n@DeveloperAPI\ndef get_input_feature_conds(model_type: str):\n    \"\"\"This function returns a list of if-then JSON clauses for each input feature type along with their properties\n    and constraints.\n\n    Returns: List of JSON clauses\n    \"\"\"\n    input_feature_types = sorted(list(input_config_registry(model_type).keys()))\n    conds = []\n    for feature_type in input_feature_types:\n        schema_cls = get_input_feature_cls(model_type, feature_type)\n        feature_schema = schema_utils.unload_jsonschema_from_marshmallow_class(schema_cls)\n        feature_props = feature_schema[\"properties\"]\n        schema_utils.remove_duplicate_fields(feature_props)\n\n        feature_cond = schema_utils.create_cond({\"type\": feature_type}, feature_props)\n        conds.append(feature_cond)\n    return conds\n\n\n@DeveloperAPI\ndef get_output_feature_jsonschema(model_type: str):\n    \"\"\"This function returns a JSON schema structured to only requires a `type` key and then conditionally applies\n    a corresponding output feature's field constraints.\n\n    Returns: JSON Schema\n    \"\"\"\n    output_feature_types = sorted(list(output_config_registry(model_type).keys()))\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"type\": \"string\", \"title\": \"name\", \"description\": \"Name of the output feature.\"},\n            \"type\": {\n                \"type\": \"string\",\n                \"enum\": output_feature_types,\n                \"title\": \"type\",\n                \"description\": \"Type of the output feature\",\n            },\n            \"column\": {\"type\": \"string\", \"title\": \"column\", \"description\": \"Name of the column.\"},\n        },\n        \"additionalProperties\": True,\n        \"allOf\": get_output_feature_conds(model_type),\n        \"required\": [\"name\", \"type\"],\n        \"title\": \"output_feature\",\n    }\n\n    return schema\n\n\n@DeveloperAPI\ndef get_output_feature_conds(model_type: str):\n    \"\"\"This function returns a list of if-then JSON clauses for each output feature type along with their\n    properties and constraints.\n\n    Returns: List of JSON clauses\n    \"\"\"\n    output_feature_types = sorted(list(output_config_registry(model_type).keys()))\n    conds = []\n    for feature_type in output_feature_types:\n        schema_cls = get_output_feature_cls(model_type, feature_type)\n        feature_schema = schema_utils.unload_jsonschema_from_marshmallow_class(schema_cls)\n        feature_props = feature_schema[\"properties\"]\n        schema_utils.remove_duplicate_fields(feature_props)\n        feature_cond = schema_utils.create_cond({\"type\": feature_type}, feature_props)\n        conds.append(feature_cond)\n    return conds\n"
  },
  {
    "path": "ludwig/schema/features/vector_feature.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MEAN_SQUARED_ERROR, MODEL_ECD, VECTOR\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import DecoderDataclassField\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import EncoderDataclassField\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.utils import LossDataclassField\nfrom ludwig.schema.features.preprocessing.base import BasePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\nfrom ludwig.schema.features.utils import (\n    ecd_defaults_config_registry,\n    ecd_input_config_registry,\n    ecd_output_config_registry,\n    input_mixin_registry,\n    output_mixin_registry,\n)\nfrom ludwig.schema.metadata import FEATURE_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import INTERNAL_ONLY\nfrom ludwig.schema.utils import BaseMarshmallowConfig, ludwig_dataclass\n\n\n@DeveloperAPI\n@input_mixin_registry.register(VECTOR)\n@ludwig_dataclass\nclass VectorInputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"VectorInputFeatureConfigMixin is a dataclass that configures the parameters used in both the vector input\n    feature and the vector global defaults section of the Ludwig Config.\"\"\"\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=VECTOR)\n\n    encoder: BaseEncoderConfig = EncoderDataclassField(\n        MODEL_ECD,\n        feature_type=VECTOR,\n        default=\"dense\",\n    )\n\n\n@DeveloperAPI\n@ecd_input_config_registry.register(VECTOR)\n@ludwig_dataclass\nclass VectorInputFeatureConfig(VectorInputFeatureConfigMixin, BaseInputFeatureConfig):\n    \"\"\"VectorInputFeatureConfig is a dataclass that configures the parameters used for a vector input feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(VECTOR)\n\n\n@DeveloperAPI\n@output_mixin_registry.register(VECTOR)\n@ludwig_dataclass\nclass VectorOutputFeatureConfigMixin(BaseMarshmallowConfig):\n    \"\"\"VectorOutputFeatureConfigMixin is a dataclass that configures the parameters used in both the vector output\n    feature and the vector global defaults section of the Ludwig Config.\"\"\"\n\n    decoder: BaseDecoderConfig = DecoderDataclassField(\n        MODEL_ECD,\n        feature_type=VECTOR,\n        default=\"projector\",\n    )\n\n    loss: BaseLossConfig = LossDataclassField(\n        feature_type=VECTOR,\n        default=MEAN_SQUARED_ERROR,\n    )\n\n\n@DeveloperAPI\n@ecd_output_config_registry.register(VECTOR)\n@ludwig_dataclass\nclass VectorOutputFeatureConfig(VectorOutputFeatureConfigMixin, BaseOutputFeatureConfig):\n    \"\"\"VectorOutputFeatureConfig is a dataclass that configures the parameters used for a vector output feature.\"\"\"\n\n    type: str = schema_utils.ProtectedString(VECTOR)\n\n    dependencies: list = schema_utils.List(\n        default=[],\n        description=\"List of input features that this feature depends on.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"dependencies\"],\n    )\n\n    default_validation_metric: str = schema_utils.StringOptions(\n        [MEAN_SQUARED_ERROR],\n        default=MEAN_SQUARED_ERROR,\n        description=\"Internal only use parameter: default validation metric for binary output feature.\",\n        parameter_metadata=INTERNAL_ONLY,\n    )\n\n    preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=\"vector_output\")\n\n    reduce_dependencies: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce the dependencies of the output feature.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"reduce_dependencies\"],\n    )\n\n    reduce_input: str = schema_utils.ReductionOptions(\n        default=None,\n        description=\"How to reduce an input that is not a vector, but a matrix or a higher order tensor, on the first \"\n        \"dimension (second if you count the batch dimension)\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"reduce_input\"],\n    )\n\n    softmax: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Determines whether to apply a softmax at the end of the decoder. This is useful for predicting a \"\n        \"vector of values that sum up to 1 and can be interpreted as probabilities.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"softmax\"],\n    )\n\n    vector_size: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The size of the vector. If None, the vector size will be inferred from the data.\",\n        parameter_metadata=FEATURE_METADATA[VECTOR][\"vector_size\"],\n    )\n\n\n@DeveloperAPI\n@ecd_defaults_config_registry.register(VECTOR)\n@ludwig_dataclass\nclass VectorDefaultsConfig(VectorInputFeatureConfigMixin, VectorOutputFeatureConfigMixin):\n    pass\n"
  },
  {
    "path": "ludwig/schema/hyperopt/__init__.py",
    "content": "from abc import ABC\n\nimport ludwig.schema.hyperopt.parameter  # noqa: F401\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import LOSS, TEST, TRAIN, VALIDATION\nfrom ludwig.modules import metric_modules  # noqa: Needed to ensure that the metric registry is populated.\nfrom ludwig.modules.metric_registry import get_metric_registry\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.hyperopt.executor import ExecutorConfig, ExecutorDataclassField\nfrom ludwig.schema.hyperopt.search_algorithm import BaseSearchAlgorithmConfig, SearchAlgorithmDataclassField\nfrom ludwig.schema.utils import ludwig_dataclass as dataclass\n\n\n@DeveloperAPI\n@dataclass\nclass HyperoptConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Basic hyperopt settings.\"\"\"\n\n    output_feature: str = schema_utils.String(  # TODO: make more restrictive\n        default=\"combined\",\n        description=(\n            \"The name of the output feature that we want to optimize the metric or loss of. Available values \"\n            \"are `combined` or the name of any output feature provided in the configuration. `combined` is a special \"\n            \"output feature that allows to optimize for the aggregated loss and metrics of all output features.\"\n        ),\n    )\n\n    goal: str = schema_utils.StringOptions(\n        options=[\"minimize\", \"maximize\"],\n        default=\"minimize\",\n        allow_none=False,\n        description=(\n            \"Indicates if to minimize or maximize a metric or a loss of any of the output features on any of the \"\n            \"dataset splits. Available values are: minimize (default) or maximize.\"\n        ),\n    )\n\n    metric: str = schema_utils.StringOptions(\n        options=get_metric_registry().keys(),\n        default=LOSS,\n        allow_none=False,\n        description=(\n            \"The metric that we want to optimize for. The default one is loss, but depending on the type of the \"\n            \"feature defined in output_feature, different metrics and losses are available. Check the metrics section \"\n            \"of the specific output feature type to figure out what metrics are available to use.\"\n        ),\n    )\n\n    split: str = schema_utils.StringOptions(\n        options=[TRAIN, VALIDATION, TEST],\n        default=VALIDATION,\n        allow_none=False,\n        description=(\n            \"The split of data that we want to compute our metric on. By default it is the validation split, but \"\n            \"you have the flexibility to specify also train or test splits.\"\n        ),\n    )\n\n    eval_split: str = schema_utils.StringOptions(\n        options=[TRAIN, VALIDATION, TEST],\n        default=VALIDATION,\n        allow_none=False,\n        description=(\n            \"The split of data that we want to run evaluation on. By default it is the validation split, but \"\n            \"you have the flexibility to specify also train or test splits.\"\n        ),\n    )\n\n    search_alg: BaseSearchAlgorithmConfig = SearchAlgorithmDataclassField(\n        description=(\n            \"Specifies the algorithm to sample the defined parameters space. Candidate algorithms are those \"\n            \"found in [Ray Tune's Search Algorithms](https://docs.ray.io/en/latest/tune/api/suggestion.html).\"\n        )\n    )\n\n    executor: ExecutorConfig = ExecutorDataclassField(\n        description=(\n            \"specifies how to execute the hyperparameter optimization. The execution could happen locally in a serial \"\n            \"manner or in parallel across multiple workers and with GPUs as well if available. The executor section \"\n            \"includes specification for work scheduling and the number of samples to generate.\"\n        )\n    )\n\n    parameters: dict = schema_utils.Dict(\n        allow_none=False,\n        description=(\n            \"This section consists of a set of hyperparameters to optimize. They are provided as keys (the names of \"\n            \"the parameters) and values associated with them (that define the search space). The values vary depending \"\n            \"on the type of the hyperparameter. Syntax for this section is based on [Ray Tune's Search Space \"\n            \"parameters](https://docs.ray.io/en/latest/tune/api/search_space.html).\"\n        ),\n    )\n\n\n@DeveloperAPI\ndef get_hyperopt_jsonschema():\n    props = schema_utils.unload_jsonschema_from_marshmallow_class(HyperoptConfig)[\"properties\"]\n\n    return {\n        \"type\": [\"object\", \"null\"],\n        \"properties\": props,\n        \"title\": \"hyperopt_options\",\n        \"description\": \"Settings for hyperopt\",\n    }\n\n\n@DeveloperAPI\nclass HyperoptField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(HyperoptConfig, default_missing=True)\n\n    def _jsonschema_type_mapping(self):\n        return get_hyperopt_jsonschema()\n"
  },
  {
    "path": "ludwig/schema/hyperopt/executor.py",
    "content": "from dataclasses import field\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import RAY\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.hyperopt.scheduler import BaseSchedulerConfig, SchedulerDataclassField\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ExecutorConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Basic executor settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(RAY)\n\n    num_samples: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=(\n            \"This parameter, along with the `space` specifications in the `parameters` section, controls how many \"\n            \"trials are generated.\"\n        ),\n    )\n\n    time_budget_s: int = schema_utils.PositiveInteger(\n        default=3600, allow_none=True, description=\"The number of seconds for the entire hyperopt run.\"\n    )\n\n    trial_driver_resources: dict[str, float] = schema_utils.Dict(\n        default=None,\n        description=(\n            \"The resources reserved by each trial driver. This differs from cpu_resources_per_trial and \"\n            \"gpu_resources_per_trial because these resources are reserved for the driver, not its subsequent \"\n            \"workers. Only used when the trials themselves are on the Ray backend. Defaults to 1 CPU.\"\n        ),\n    )\n\n    cpu_resources_per_trial: int = schema_utils.PositiveInteger(\n        default=1, description=\"The number of CPU cores allocated to each trial\"\n    )\n\n    gpu_resources_per_trial: int = schema_utils.NonNegativeInteger(\n        default=0, description=\"The number of GPU devices allocated to each trial\"\n    )\n\n    kubernetes_namespace: str | None = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"When running on Kubernetes, provide the namespace of the Ray cluster to sync results between \"\n            \"pods. See the Ray docs for more info.\"\n        ),\n    )\n\n    max_concurrent_trials: str | int | None = schema_utils.OneOfOptionsField(\n        default=\"auto\",\n        allow_none=True,\n        description=(\"The maximum number of trials to train concurrently. Defaults to auto if not specified.\"),\n        field_options=[\n            schema_utils.PositiveInteger(\n                default=1, allow_none=False, description=\"Manually set a number of concurrent trials.\"\n            ),\n            schema_utils.StringOptions(\n                options=[\"auto\"],\n                default=\"auto\",\n                allow_none=False,\n                description=\"Automatically set number of concurrent trials.\",\n            ),\n        ],\n    )\n\n    scheduler: BaseSchedulerConfig = SchedulerDataclassField(description=\"\")\n\n\n@DeveloperAPI\ndef ExecutorDataclassField(description: str, default: dict = {}):\n    class ExecutorMarshmallowField(schema_utils.LudwigSchemaField):\n        def _deserialize(self, value, attr, data, **kwargs):\n            if isinstance(value, dict):\n                try:\n                    return ExecutorConfig.Schema().load(value)\n                except (TypeError, ConfigValidationError):\n                    raise ConfigValidationError(f\"Invalid params for executor: {value}, see ExecutorConfig class.\")\n            raise ConfigValidationError(\"Field should be dict\")\n\n        def _jsonschema_type_mapping(self):\n            return {\n                **schema_utils.unload_jsonschema_from_marshmallow_class(ExecutorConfig),\n                \"title\": \"executor\",\n                \"description\": description,\n            }\n\n    if not isinstance(default, dict):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n\n    load_default = lambda: ExecutorConfig.Schema().load(default)\n    dump_default = ExecutorConfig.Schema().dump(default)\n\n    return field(\n        metadata={\n            \"marshmallow_field\": ExecutorMarshmallowField(\n                allow_none=False,\n                load_default=load_default,\n                dump_default=dump_default,\n                metadata={\"description\": description, \"parameter_metadata\": None},\n            )\n        },\n        default_factory=load_default,\n    )\n"
  },
  {
    "path": "ludwig/schema/hyperopt/parameter.py",
    "content": "from pydantic.fields import FieldInfo\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.hyperopt.utils import register_parameter_config\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\ndef quantization_number_field(dtype: type[float] | type[int] = float, default=None) -> FieldInfo:\n    description = (\n        \"Quantization number. Output values will be rounded to the nearest increment of `q` in range.\"\n        \"Quantization makes the upper bound inclusive.\"\n    )\n    if dtype is int:\n        field = schema_utils.Integer(default=default, allow_none=True, description=description)\n    else:\n        field = schema_utils.FloatRange(default=default, allow_none=True, description=description)\n\n    return field\n\n\ndef log_base_field(default: float = 10) -> FieldInfo:\n    return schema_utils.FloatRange(default=default, description=\"Logarithmic base.\")\n\n\n@DeveloperAPI\n@register_parameter_config(\"choice\")\n@ludwig_dataclass\nclass ChoiceParameterConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Config for a randomly sampled categorical search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"choice\")\n\n    categories: list = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The list of values to use in creating the categorical space. The type of each value of the list is \"\n            \"general, i.e., they could be strings, integers, floats and anything else, even entire dictionaries.\"\n        ),\n        field_options=[\n            schema_utils.List(list_type=float, allow_none=False, description=\"The list of floats to randomly sample.\"),\n            schema_utils.List(list_type=int, allow_none=False, description=\"The list of integers to randomly sample.\"),\n            schema_utils.List(list_type=str, allow_none=False, description=\"The list of strings to randomly sample.\"),\n            schema_utils.List(\n                list_type=list,\n                inner_type=dict,\n                allow_none=False,\n                description=\"The list of lists of configs to randomly sample.\",\n            ),\n            schema_utils.DictList(allow_none=False, description=\"A list of nested config parameters to sample.\"),\n        ],\n    )\n\n\n@DeveloperAPI\n@register_parameter_config(\"grid_search\")\n@ludwig_dataclass\nclass GridSearchParameterConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Config for a grid search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"grid_search\")\n\n    values: list = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The list of values to use in creating the grid search space. The type of each value of the list is \"\n            \"general, i.e., they could be strings, integers, floats and anything else, even entire dictionaries.\"\n        ),\n        field_options=[\n            schema_utils.List(list_type=float, allow_none=False, description=\"The list of floats to randomly sample.\"),\n            schema_utils.List(list_type=int, allow_none=False, description=\"The list of integers to randomly sample.\"),\n            schema_utils.List(list_type=str, allow_none=False, description=\"The list of strings to randomly sample.\"),\n        ],\n    )\n\n\n@DeveloperAPI\n@register_parameter_config(\"uniform\")\n@ludwig_dataclass\nclass UniformParameterConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Config for a real-valued uniform search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"uniform\")\n\n    lower: float = schema_utils.FloatRange(default=None, description=\"The minimum value the parameter can have.\")\n\n    upper: float = schema_utils.FloatRange(default=None, description=\"The maximum value the parameter can have.\")\n\n\n@DeveloperAPI\n@register_parameter_config(\"quniform\")\n@ludwig_dataclass\nclass QUniformParameterConfig(UniformParameterConfig):\n    \"\"\"Config for a real-valued uniform search space with quantization.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"quniform\")\n\n    q: float = quantization_number_field()\n\n\n@DeveloperAPI\n@register_parameter_config(\"loguniform\")\n@ludwig_dataclass\nclass LogUniformParameterConfig(UniformParameterConfig):\n    \"\"\"Config for a log-scaled real-valued uniform numeric search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"loguniform\")\n\n    base: float = log_base_field()\n\n\n@DeveloperAPI\n@register_parameter_config(\"qloguniform\")\n@ludwig_dataclass\nclass QLogUniformParameterConfig(UniformParameterConfig):\n    \"\"\"Config for a log-scaled real-valued uniform search space with quantization.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"qloguniform\")\n\n    q: float = quantization_number_field()\n\n    base: float = log_base_field()\n\n\n@DeveloperAPI\n@register_parameter_config(\"randn\")\n@ludwig_dataclass\nclass RandnParameterConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Config for a Gaussian search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"randn\")\n\n    mean: float = schema_utils.FloatRange(default=0.0, description=\"Mean of the  normal distribution.\")\n\n    sd: float = schema_utils.FloatRange(default=1.0, description=\"Standard deviation of the normal distribution.\")\n\n\n@DeveloperAPI\n@register_parameter_config(\"qrandn\")\n@ludwig_dataclass\nclass QRandnParameterConfig(RandnParameterConfig):\n    \"\"\"Config for a Gaussian search space with quantization.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"qrandn\")\n\n    q: float = quantization_number_field()\n\n\n@DeveloperAPI\n@register_parameter_config(\"randint\")\n@ludwig_dataclass\nclass RandintParameterConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Config for an integer-valued uniform search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"randint\")\n\n    lower: int = schema_utils.Integer(default=None, description=\"The minimum value the parameter can have.\")\n\n    upper: int = schema_utils.Integer(default=None, description=\"The maximum value the parameter can have.\")\n\n\n@DeveloperAPI\n@register_parameter_config(\"qrandint\")\n@ludwig_dataclass\nclass QRandintParameterConfig(RandintParameterConfig):\n    \"\"\"Config for an integer-valued uniform search space with quantization.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"qrandint\")\n\n    q: int = quantization_number_field(dtype=int)\n\n\n@DeveloperAPI\n@register_parameter_config(\"lograndint\")\n@ludwig_dataclass\nclass LogRandintParameterConfig(RandintParameterConfig):\n    \"\"\"Config for an log-scaled integer-valued search space.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"lograndint\")\n\n    base: float = log_base_field()\n\n\n@DeveloperAPI\n@register_parameter_config(\"qlograndint\")\n@ludwig_dataclass\nclass QLogRandintParameterConfig(RandintParameterConfig):\n    \"\"\"Config for an log-scaled integer-valued search space with quantization.\"\"\"\n\n    space: str = schema_utils.ProtectedString(\"qlograndint\")\n\n    q: int = quantization_number_field(dtype=int)\n\n    base: float = log_base_field()\n"
  },
  {
    "path": "ludwig/schema/hyperopt/scheduler.py",
    "content": "from abc import ABC\nfrom collections.abc import Callable\nfrom dataclasses import field\nfrom importlib import import_module\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.hyperopt import utils as hyperopt_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n# ----------------------------------------------------------------------------------------------------------------------\n# To prevent direct dependency on ray import, the following static key stores are duplicated:\n\n# from ray.tune.schedulers import SCHEDULER_IMPORT\n# https://github.com/ray-project/ray/blob/137a1b12c3b31a3622fa5f721a05a64e9b559b05/python/ray/tune/schedulers/__init__.py#L28\n\n# from ray.tune.result import DEFAULT_RESULT_KEYS\n# Taken from https://github.com/ray-project/ray/blob/137a1b12c3b31a3622fa5f721a05a64e9b559b05/python/ray/tune/result.py\nTRAINING_ITERATION = \"training_iteration\"\nTIME_TOTAL_S = \"time_total_s\"\nTIMESTEPS_TOTAL = \"timesteps_total\"\nMEAN_ACCURACY = \"mean_accuracy\"\nMEAN_LOSS = \"mean_loss\"\nDEFAULT_RESULT_KEYS = (TRAINING_ITERATION, TIME_TOTAL_S, TIMESTEPS_TOTAL, MEAN_ACCURACY, MEAN_LOSS)\n\n# from ray.tune.result import DEFAULT_METRIC\nRAY_TUNE_DESULT_DEFAULT_METRIC = \"_metric\"\n# ----------------------------------------------------------------------------------------------------------------------\n\n\n# Field aliases to cut down on code reuse:\n@DeveloperAPI\ndef metric_alias(default=None):\n    return schema_utils.StringOptions(\n        options=list(DEFAULT_RESULT_KEYS) + [RAY_TUNE_DESULT_DEFAULT_METRIC],\n        default=default,\n        allow_none=default is None,\n        description=(\n            \"The training result objective value attribute. Stopping procedures will use this attribute. If None but a \"\n            \"mode was passed, the ray.tune.result.DEFAULT_METRIC will be used per default.\"\n        ),\n    )\n\n\n@DeveloperAPI\ndef time_attr_alias(default=TRAINING_ITERATION):\n    return schema_utils.StringOptions(\n        options=list(DEFAULT_RESULT_KEYS),\n        default=default,\n        allow_none=False,\n        description=(\n            \"A training result attr to use for comparing time. Note that you can pass in something non-temporal such as\"\n            \" training_iteration as a measure of progress, the only requirement is that the attribute should increase \"\n            \"monotonically.\"\n        ),\n    )\n\n\n@DeveloperAPI\ndef max_t_alias(default=100):\n    return schema_utils.PositiveInteger(\n        default=default,\n        description=(\n            \"max time units per trial. Trials will be stopped after max_t time units (determined by time_attr) have \"\n            \"passed.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseSchedulerConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Base class for schedulers.\n\n    Not meant to be used directly.\n    \"\"\"\n\n    type: str\n    \"\"\"Name corresponding to a scheduler in `ludwig.schema.hyperopt.scheduler.scheduler_registry`.\n\n    Technically mutable, but attempting to load a derived scheduler with `type` set to a mismatched value will result in\n    a `ValidationError`.\n    \"\"\"\n\n    time_attr: str = time_attr_alias()\n\n    metric: str | None = metric_alias()\n\n    mode: str | None = schema_utils.StringOptions(\n        options=[\"min\", \"max\"],\n        default=None,\n        allow_none=True,\n        description=(\n            \"One of {min, max}. Determines whether objective is minimizing or maximizing the metric attribute.\"\n        ),\n    )\n\n    def dependencies_installed(self):\n        \"\"\"Some search algorithms require additional packages to be installed, check that they are available.\"\"\"\n        missing_packages = []\n        missing_installs = []\n        for package_name, install_name in hyperopt_utils.get_scheduler_dependencies(self.type):\n            try:\n                import_module(package_name)\n            except ImportError:\n                missing_packages.append(package_name)\n                missing_installs.append(install_name)\n\n        if missing_packages:\n            missing_packages = \", \".join(missing_packages)\n            missing_installs = \" \".join(missing_installs)\n            raise ImportError(\n                f\"Some packages needed to use hyperopt scheduler {self.type} are not installed: \"\n                f\"{missing_packages}. To add these dependencies, run `pip install {missing_installs}`. For more \"\n                \"details, please refer to Ray Tune documentation for this scheduler.\"\n            )\n        return True\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseHyperbandSchedulerConfig(BaseSchedulerConfig):\n    max_t: int = max_t_alias()\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"async_hyperband\")\n@hyperopt_utils.register_scheduler_config(\"asynchyperband\")\n@hyperopt_utils.register_scheduler_config(\"asha\")\n@ludwig_dataclass\nclass AsyncHyperbandSchedulerConfig(BaseHyperbandSchedulerConfig):\n    \"\"\"Asynchronous hyperband (ASHA) scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"async_hyperband\")\n\n    max_t: int = max_t_alias()\n\n    grace_period: int = schema_utils.PositiveInteger(\n        default=1,\n        description=(\n            \"Only stop trials at least this old in time. The units are the same as the attribute named by `time_attr`.\"\n        ),\n    )\n\n    reduction_factor: int = schema_utils.NonNegativeFloat(\n        default=4, description=(\"Used to set halving rate and amount. This is simply a unit-less scalar.\")\n    )\n\n    brackets: int = schema_utils.PositiveInteger(\n        default=1,\n        description=(\n            \"Number of brackets. Each bracket has a different halving rate, specified by the reduction factor.\"\n        ),\n    )\n\n    stop_last_trials: bool = schema_utils.Boolean(\n        default=True, description=\"Whether to terminate the trials after reaching `max_t`.\"\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"hyperband\")\n@ludwig_dataclass\nclass HyperbandSchedulerConfig(BaseHyperbandSchedulerConfig):\n    \"\"\"Standard hyperband scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"hyperband\")\n\n    max_t: int = max_t_alias(default=81)\n\n    reduction_factor: int = schema_utils.NonNegativeFloat(\n        default=3, description=(\"Used to set halving rate and amount. This is simply a unit-less scalar.\")\n    )\n\n    stop_last_trials: bool = schema_utils.Boolean(\n        default=True, description=(\"Whether to terminate the trials after reaching max_t. Defaults to True.\")\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"median_stopping_rule\")\n@hyperopt_utils.register_scheduler_config(\"medianstoppingrule\")\n@ludwig_dataclass\nclass MedianStoppingRuleSchedulerConfig(BaseSchedulerConfig):\n    \"\"\"Median Stopping Rule scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"median_stopping_rule\")\n\n    time_attr: str = time_attr_alias(TIME_TOTAL_S)\n\n    grace_period: float = schema_utils.NonNegativeFloat(\n        default=60.0,\n        description=(\n            \"Only stop trials at least this old in time. The mean will only be computed from this time onwards. The \"\n            \"units are the same as the attribute named by `time_attr`.\"\n        ),\n    )\n\n    min_samples_required: int = schema_utils.PositiveInteger(\n        default=3, description=(\"Minimum number of trials to compute median over.\")\n    )\n\n    min_time_slice: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=(\n            \"Each trial runs at least this long before yielding (assuming it isn't stopped). Note: trials ONLY yield \"\n            \"if there are not enough samples to evaluate performance for the current result AND there are other \"\n            \"trials waiting to run. The units are the same as the attribute named by `time_attr`.\"\n        ),\n    )\n\n    hard_stop: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"If False, pauses trials instead of stopping them. When all other trials are complete, paused trials will \"\n            \"be resumed and allowed to run FIFO.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"pbt\")\n@ludwig_dataclass\nclass PopulationBasedTrainingSchedulerConfig(BaseSchedulerConfig):\n    \"\"\"Population Based Training scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"pbt\")\n\n    time_attr: str = time_attr_alias(TIME_TOTAL_S)\n\n    perturbation_interval: float = schema_utils.NonNegativeFloat(\n        default=60.0,\n        description=(\n            \"Models will be considered for perturbation at this interval of `time_attr`. Note that perturbation incurs \"\n            \"checkpoint overhead, so you shouldn't set this to be too frequent.\"\n        ),\n    )\n\n    burn_in_period: float = schema_utils.NonNegativeFloat(\n        default=60.0,\n        description=(\n            \"Models will not be considered for perturbation before this interval of time_attr has passed. This \"\n            \"guarantees that models are trained for at least a certain amount of time or timesteps before being \"\n            \"perturbed.\"\n        ),\n    )\n\n    hyperparam_mutations: dict | None = schema_utils.Dict(\n        default=None,\n        description=(\n            \"Hyperparams to mutate. The format is as follows: for each key, either a list, function, or a tune search \"\n            \"space object (`tune.loguniform`, tune.uniform, etc.) can be provided. A list specifies an allowed set of \"\n            \"categorical values. A function or tune search space object specifies the distribution of a continuous \"\n            \"parameter. You must use `tune.choice`, `tune.uniform`, `tune.loguniform`, etc.. Arbitrary \"\n            \"`tune.sample_from` objects are not supported. A key can also hold a dict for nested hyperparameters. You \"\n            \"must specify at least one of `hyperparam_mutations` or `custom_explore_fn`. Tune will sample the search \"\n            \"space provided by `hyperparam_mutations` for the initial hyperparameter values if the corresponding \"\n            \"hyperparameters are not present in a trial's initial config.\"\n        ),\n    )\n\n    quantile_fraction: float = schema_utils.FloatRange(\n        default=0.25,\n        allow_none=False,\n        min=0,\n        max=0.5,\n        description=(\n            \"Parameters are transferred from the top `quantile_fraction` fraction of trials to the bottom \"\n            \"`quantile_fraction` fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing \"\n            \"no exploitation at all.\"\n        ),\n    )\n\n    resample_probability: float = schema_utils.NonNegativeFloat(\n        default=0.25,\n        description=(\n            \"The probability of resampling from the original distribution when applying `hyperparam_mutations`. If \"\n            \"not resampled, the value will be perturbed by a factor chosen from `perturbation_factors` if continuous, \"\n            \"or changed to an adjacent value if discrete.\"\n        ),\n    )\n\n    perturbation_factors: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n        default=(1.2, 0.8),\n        allow_none=False,\n        max=None,\n        description=(\"Scaling factors to choose between when mutating a continuous hyperparameter.\"),\n    )\n\n    # TODO: Add schema support for Callable\n    custom_explore_fn: str | Callable = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"You can also specify a custom exploration function. This function is invoked as `f(config)` after \"\n            \"built-in perturbations from `hyperparam_mutations` are applied, and should return config updated as \"\n            \"needed. You must specify at least one of `hyperparam_mutations` or `custom_explore_fn`.\"\n        ),\n    )\n\n    log_config: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"Whether to log the ray config of each model to `local_dir` at each exploit. Allows config schedule to be \"\n            \"reconstructed.\"\n        ),\n    )\n\n    require_attrs: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"Whether to require `time_attr` and metric to appear in result for every iteration. If True, error will \"\n            \"be raised if these values are not present in trial result.\"\n        ),\n    )\n\n    synch: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"If False, will use asynchronous implementation of PBT. Trial perturbations occur every \"\n            \"`perturbation_interval` for each trial independently. If True, will use synchronous implementation of \"\n            \"PBT. Perturbations will occur only after all trials are synced at the same `time_attr` every \"\n            \"`perturbation_interval`. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"pbt_replay\")\n@ludwig_dataclass\nclass PopulationBasedTrainingReplaySchedulerConfig(BaseSchedulerConfig):\n    \"\"\"Population Based Training Replay scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"pbt_replay\")\n\n    # TODO: This should technically be a required paremeter. Do we need to add support for required params?\n    policy_file: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The PBT policy file. Usually this is stored in `~/ray_results/experiment_name/pbt_policy_xxx.txt` where \"\n            \"`xxx` is the trial ID.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"pb2\", dependencies=[(\"sklearn\", \"scikit-learn\"), (\"GPy\", \"GPy\")])\n@ludwig_dataclass\nclass PopulationBasedBanditsSchedulerConfig(BaseSchedulerConfig):\n    \"\"\"Population Based Bandits (PB2) scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"pb2\")\n\n    time_attr: str = time_attr_alias(TIME_TOTAL_S)\n\n    perturbation_interval: float = schema_utils.NonNegativeFloat(\n        default=60.0,\n        description=(\n            \"Models will be considered for perturbation at this interval of `time_attr`. Note that perturbation \"\n            \"incurs checkpoint overhead, so you shouldn't set this to be too frequent.\"\n        ),\n    )\n\n    hyperparam_bounds: dict | None = schema_utils.Dict(\n        default=None,\n        description=(\n            \"Hyperparameters to mutate. The format is as follows: for each key, enter a list of the form [min, max] \"\n            \"representing the minimum and maximum possible hyperparameter values.\"\n        ),\n    )\n\n    quantile_fraction: float = schema_utils.FloatRange(\n        default=0.25,\n        allow_none=False,\n        min=0,\n        max=0.5,\n        description=(\n            \"Parameters are transferred from the top `quantile_fraction` fraction of trials to the bottom \"\n            \"`quantile_fraction` fraction. Needs to be between 0 and 0.5. Setting it to 0 essentially implies doing \"\n            \"no exploitation at all.\"\n        ),\n    )\n\n    log_config: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"Whether to log the ray config of each model to `local_dir` at each exploit. Allows config schedule to be \"\n            \"reconstructed.\"\n        ),\n    )\n\n    require_attrs: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"Whether to require `time_attr` and metric to appear in result for every iteration. If True, error will \"\n            \"be raised if these values are not present in trial result.\"\n        ),\n    )\n\n    synch: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"If False, will use asynchronous implementation of PBT. Trial perturbations occur every \"\n            \"`perturbation_interval` for each trial independently. If True, will use synchronous implementation of \"\n            \"PBT. Perturbations will occur only after all trials are synced at the same `time_attr` every \"\n            \"`perturbation_interval`. Defaults to False. See Appendix A.1 here https://arxiv.org/pdf/1711.09846.pdf.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"hb_bohb\")\n@ludwig_dataclass\nclass BOHBSchedulerConfig(BaseHyperbandSchedulerConfig):\n    \"\"\"Hyperband for BOHB (hb_bohb) scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"hb_bohb\")\n\n    max_t: int = max_t_alias(default=81)\n\n    reduction_factor: int = schema_utils.NonNegativeFloat(\n        default=3, description=(\"Used to set halving rate and amount. This is simply a unit-less scalar.\")\n    )\n\n    stop_last_trials: bool = schema_utils.Boolean(\n        default=True, description=(\"Whether to terminate the trials after reaching `max_t`. Defaults to True.\")\n    )\n\n\n# TODO: Double-check support for this\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"fifo\")\n@ludwig_dataclass\nclass FIFOSchedulerConfig(BaseSchedulerConfig):\n    \"\"\"FIFO trial scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"fifo\")\n\n\n# TODO: Double-check support for this as well as whether Callable args work properly\n@DeveloperAPI\n@hyperopt_utils.register_scheduler_config(\"resource_changing\")\n@ludwig_dataclass\nclass ResourceChangingSchedulerConfig(BaseSchedulerConfig):\n    \"\"\"Resource changing scheduler settings.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"resource_changing\")\n\n    base_scheduler: str | None | Callable = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\"The scheduler to provide decisions about trials. If None, a default FIFOScheduler will be used.\"),\n    )\n\n    resources_allocation_function: str | Callable = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The callable used to change live trial resource requiements during tuning. This callable will be called on\"\n            \" each trial as it finishes one step of training. The callable must take four arguments: `TrialRunner`, \"\n            \"current `Trial`, current result `dict` and the `ResourceChangingScheduler` calling it. The callable must \"\n            \"return a `PlacementGroupFactory`, `Resources`, `dict` or None (signifying no need for an update). If \"\n            \"`resources_allocation_function` is None, no resource requirements will be changed at any time. By \"\n            \" default, `DistributeResources` will be used, distributing available CPUs and GPUs over all running \"\n            \"trials in a robust way, without any prioritization.\"\n        ),\n    )\n\n\n@DeveloperAPI\ndef get_scheduler_conds():\n    \"\"\"Returns a JSON schema of conditionals to validate against scheduler types defined in\n    `ludwig.schema.hyperopt.scheduler_registry`.\"\"\"\n    conds = []\n    for scheduler_config in hyperopt_utils.scheduler_config_registry:\n        scheduler_cls = hyperopt_utils.scheduler_config_registry[scheduler_config]\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(scheduler_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        preproc_cond = schema_utils.create_cond(\n            {\"type\": scheduler_config},\n            other_props,\n        )\n        conds.append(preproc_cond)\n    return conds\n\n\n@DeveloperAPI\ndef SchedulerDataclassField(default={\"type\": \"fifo\"}, description=\"Hyperopt scheduler settings.\"):\n    \"\"\"Custom dataclass field that when used inside of a dataclass will allow any scheduler in\n    `ludwig.schema.hyperopt.scheduler.scheduler_registry`. Sets default scheduler to 'fifo'.\n\n    :param default: Dict specifying a scheduler with a `type` field and its associated parameters. Will attempt to use\n           `type` to load scheduler from registry with given params. (default: {\"type\": \"fifo\"}).\n    :return: Initialized dataclass field that converts untyped dicts with params to scheduler dataclass instances.\n    \"\"\"\n\n    class SchedulerMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field that deserializes a dict to a valid scheduler from\n        `ludwig.schema.hyperopt.scheduler_registry` and creates a corresponding `oneOf` JSON schema for external\n        usage.\"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return None\n            if isinstance(value, dict):\n                if \"type\" in value and value[\"type\"] in hyperopt_utils.scheduler_config_registry:\n                    scheduler_config_cls = hyperopt_utils.scheduler_config_registry[value[\"type\"].lower()]\n                    try:\n                        return scheduler_config_cls.Schema().load(value)\n                    except (TypeError, ConfigValidationError) as e:\n                        raise ConfigValidationError(\n                            f\"Invalid params for scheduler: {value}, see `{opt}` definition. Error: {e}\"\n                        )\n                raise ConfigValidationError(\n                    f\"Invalid params for scheduler: {value}, expect dict with at least a valid `type` attribute.\"\n                )\n            raise ConfigValidationError(\"Field should be None or dict\")\n\n        def _jsonschema_type_mapping(self):\n            # Note that this uses the same conditional pattern as combiners:\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": list(hyperopt_utils.scheduler_config_registry.keys()),\n                        \"default\": default[\"type\"],\n                        \"description\": \"The type of scheduler to use during hyperopt\",\n                    },\n                },\n                \"title\": \"scheduler_options\",\n                \"allOf\": get_scheduler_conds(),\n                \"required\": [\"type\"],\n                \"description\": description,\n            }\n\n    if (\n        not isinstance(default, dict)\n        or \"type\" not in default\n        or default[\"type\"] not in hyperopt_utils.scheduler_config_registry\n    ):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n    try:\n        opt = hyperopt_utils.scheduler_config_registry[default[\"type\"].lower()]\n        load_default = lambda: opt.Schema().load(default)\n        dump_default = opt.Schema().dump(default)\n\n        return field(\n            metadata={\n                \"marshmallow_field\": SchedulerMarshmallowField(\n                    allow_none=False,\n                    dump_default=dump_default,\n                    load_default=load_default,\n                    metadata={\"description\": description},\n                )\n            },\n            default_factory=load_default,\n        )\n    except Exception as e:\n        raise ConfigValidationError(\n            f\"Unsupported scheduler type: {default['type']}. See scheduler_config_registry. Details: {e}\"\n        )\n"
  },
  {
    "path": "ludwig/schema/hyperopt/search_algorithm.py",
    "content": "from dataclasses import field\nfrom importlib.util import find_spec\nfrom typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.hyperopt import utils as hyperopt_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\ndef points_to_evaluate_field(description: str | None = None):\n    return schema_utils.DictList(\n        description=description\n        or (\n            \"Initial parameter suggestions to be run first. This is for when you already have some good parameters \"\n            \"you want to run first to help the algorithm make better suggestions for future parameters. Needs to be \"\n            \"a list of dicts containing the configurations.\"\n        ),\n    )\n\n\ndef evaluated_rewards_field(description: str | None = None):\n    return schema_utils.List(\n        description=description\n        or (\n            \"If you have previously evaluated the parameters passed in as points_to_evaluate you can avoid re-running \"\n            \"those trials by passing in the reward attributes as a list so the optimiser can be told the results \"\n            \"without needing to re-compute the trial. Must be the same length as `points_to_evaluate`.\"\n        )\n    )\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseSearchAlgorithmConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Basic search algorithm settings.\"\"\"\n\n    type: str = schema_utils.String(default=\"variant_generator\", description=\"The search algorithm to use.\")\n\n    def set_random_state(self, ludwig_random_state: int) -> None:\n        \"\"\"Overwrite the config random state.\n\n        Search algorithms refer to random state by different names, however we want to overwrite unset random states\n        with the Ludwig random state. This method uses a registry of random state field names to provide a single\n        interface across all search algorithms.\n        \"\"\"\n        rs_field = hyperopt_utils.get_search_algorithm_random_state_field(self.type)\n        if rs_field is not None and self.__getattribute__(rs_field) is None:\n            self.__setattr__(rs_field, ludwig_random_state)\n\n    def dependencies_installed(self) -> bool:\n        \"\"\"Some search algorithms require additional packages to be installed, check that they are available.\"\"\"\n        missing_packages = []\n        missing_installs = []\n        for package_name, install_name in hyperopt_utils.get_search_algorithm_dependencies(self.type):\n            if find_spec(package_name) is None:\n                missing_packages.append(package_name)\n                missing_installs.append(install_name)\n\n        if missing_packages:\n            missing_packages = \", \".join(missing_packages)\n            missing_installs = \" \".join(missing_installs)\n            raise ImportError(\n                f\"Some packages needed to use hyperopt search algorithm {self.type} are not installed: \"\n                f\"{missing_packages}. To add these dependencies, run `pip install {missing_installs}`. For more \"\n                \"details, please refer to Ray Tune documentation for this search algorithm.\"\n            )\n        return True\n\n\n@DeveloperAPI\ndef SearchAlgorithmDataclassField(description: str = \"\", default: dict = {\"type\": \"variant_generator\"}):\n    class SearchAlgorithmMarshmallowField(schema_utils.LudwigSchemaField):\n        def _deserialize(self, value, attr, data, **kwargs):\n            if isinstance(value, dict):\n                try:\n                    return BaseSearchAlgorithmConfig.Schema().load(value)\n                except (TypeError, ConfigValidationError):\n                    raise ConfigValidationError(\n                        f\"Invalid params for scheduler: {value}, see SearchAlgorithmConfig class.\"\n                    )\n            raise ConfigValidationError(\"Field should be dict\")\n\n        def _jsonschema_type_mapping(self):\n            return {\n                # **schema_utils.unload_jsonschema_from_marshmallow_class(BaseSearchAlgorithmConfig),\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": list(hyperopt_utils.search_algorithm_config_registry.keys()),\n                        \"default\": default[\"type\"],\n                        \"description\": \"The type of scheduler to use during hyperopt\",\n                    },\n                },\n                \"title\": \"search_algorithm_options\",\n                \"required\": [\"type\"],\n                \"description\": description,\n            }\n\n    if not isinstance(default, dict):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n\n    load_default = lambda: BaseSearchAlgorithmConfig.Schema().load(default)\n    dump_default = BaseSearchAlgorithmConfig.Schema().dump(default)\n\n    return field(\n        metadata={\n            \"marshmallow_field\": SearchAlgorithmMarshmallowField(\n                allow_none=False,\n                load_default=load_default,\n                dump_default=dump_default,\n                metadata={\"description\": description, \"parameter_metadata\": None},\n            )\n        },\n        default_factory=load_default,\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"random\", random_state_field=\"random_state\")\n@hyperopt_utils.register_search_algorithm_config(\"variant_generator\", random_state_field=\"random_state\")\n@ludwig_dataclass\nclass BasicVariantSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.StringOptions(options=[\"random\", \"variant_generator\"], default=\"random\", allow_none=False)\n\n    points_to_evaluate: list[dict] | None = schema_utils.DictList(\n        description=(\n            \"Initial parameter suggestions to be run first. This is for when you already have some good parameters \"\n            \"you want to run first to help the algorithm make better suggestions for future parameters. Needs to be \"\n            \"a list of dicts containing the configurations.\"\n        )\n    )\n\n    max_concurrent: int = schema_utils.NonNegativeInteger(\n        default=0, description=\"Maximum number of concurrently running trials. If 0 (default), no maximum is enforced.\"\n    )\n\n    constant_grid_search: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"If this is set to True, Ray Tune will first try to sample random values and keep them constant over grid \"\n            \"search parameters. If this is set to False (default), Ray Tune will sample new random parameters in each \"\n            \"grid search condition.\"\n        ),\n    )\n\n    random_state: int = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Seed or numpy random generator to use for reproducible results. If None (default), will use the global \"\n            \"numpy random generator (np.random). Please note that full reproducibility cannot be guaranteed in a \"\n            \"distributed environment.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"ax\", dependencies=[(\"ax\", \"ax-platform\"), (\"sqlalchemy\", \"sqlalchemy\")]\n)\n@ludwig_dataclass\nclass AxSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"ax\")\n\n    space: list[dict] | None = schema_utils.DictList(\n        description=(\n            r\"Parameters in the experiment search space. Required elements in the dictionaries are: \\“name\\” (name of \"\n            r\"this parameter, string), \\“type\\” (type of the parameter: \\“range\\”, \\“fixed\\”, or \\“choice\\”, string), \"\n            r\"\\“bounds\\” for range parameters (list of two values, lower bound first), \\“values\\” for choice \"\n            r\"parameters (list of values), and \\“value\\” for fixed parameters (single value).\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    parameter_constraints: list | None = schema_utils.List(\n        description=r\"Parameter constraints, such as \\“x3 >= x4\\” or \\“x3 + x4 >= 2\\”.\"\n    )\n\n    outcome_constraints: list | None = schema_utils.List(\n        description=r\"Outcome constraints of form \\“metric_name >= bound\\”, like \\“m1 <= 3.\\”\"\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"bayesopt\", random_state_field=\"random_state\", dependencies=[(\"bayes_opt\", \"bayesian-optimization\")]\n)\n@ludwig_dataclass\nclass BayesOptSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"bayesopt\")\n\n    space: dict | None = schema_utils.Dict(\n        description=(\n            \"Continuous search space. Parameters will be sampled from this space which will be used to run trials\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    utility_kwargs: dict | None = schema_utils.Dict(\n        description=(\n            \"Parameters to define the utility function. The default value is a dictionary with three keys: \"\n            \"- kind: ucb (Upper Confidence Bound) - kappa: 2.576 - xi: 0.0\"\n        )\n    )\n\n    random_state: int = schema_utils.Integer(default=None, allow_none=True, description=\"Used to initialize BayesOpt.\")\n\n    random_search_steps: int = schema_utils.Integer(\n        default=10,\n        description=(\n            \"Number of initial random searches. This is necessary to avoid initial local overfitting of \"\n            \"the Bayesian process.\"\n        ),\n    )\n\n    verbose: int = schema_utils.IntegerOptions(\n        options=[0, 1, 2], default=0, description=\"The level of verbosity. `0` is least verbose, `2` is most verbose.\"\n    )\n\n    patience: int = schema_utils.NonNegativeInteger(\n        default=5, description=\"Number of epochs to wait for a change in the top models.\"\n    )\n\n    skip_duplicate: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"If False, the optimizer will allow duplicate points to be registered. This behavior may be desired in \"\n            \"high noise situations where repeatedly probing the same point will give different answers. In other \"\n            \"situations, the acquisition may occasionaly generate a duplicate point.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"blendsearch\", dependencies=[(\"flaml\", \"flaml[blendsearch]\")])\n@ludwig_dataclass\nclass BlendsearchSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"blendsearch\")\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"bohb\", random_state_field=\"seed\", dependencies=[(\"hpbandster\", \"hpbandster\"), (\"ConfigSpace\", \"ConfigSpace\")]\n)\n@ludwig_dataclass\nclass BOHBSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"bohb\")\n\n    space: dict | None = schema_utils.Dict(\n        description=(\n            \"Continuous ConfigSpace search space. Parameters will be sampled from this space which will be used \"\n            \"to run trials.\"\n        )\n    )\n\n    bohb_config: dict | None = schema_utils.Dict(description=\"configuration for HpBandSter BOHB algorithm\")\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    seed: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Optional random seed to initialize the random number generator. Setting this should lead to identical \"\n            \"initial configurations at each run.\"\n        ),\n    )\n\n    max_concurrent: int = schema_utils.Integer(\n        default=0,\n        description=(\n            \"Number of maximum concurrent trials. If this Searcher is used in a `ConcurrencyLimiter`, the \"\n            \"`max_concurrent` value passed to it will override the value passed here. Set to <= 0 for no limit on \"\n            \"concurrency.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"cfo\", dependencies=[(\"flaml\", \"flaml\")])\n@ludwig_dataclass\nclass CFOSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"cfo\")\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"dragonfly\", random_state_field=\"random_state_seed\", dependencies=[(\"dragonfly\", \"dragonfly-opt\")]\n)\n@ludwig_dataclass\nclass DragonflySAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"dragonfly\")\n\n    optimizer: str | None = schema_utils.StringOptions(\n        options=[\"random\", \"bandit\", \"genetic\"],\n        default=None,\n        allow_none=True,\n        description=(\n            \"Optimizer provided from dragonfly. Choose an optimiser that extends `BlackboxOptimiser`. If this is a \"\n            \"string, `domain` must be set and `optimizer` must be one of [random, bandit, genetic].\"\n        ),\n    )\n\n    domain: str | None = schema_utils.StringOptions(\n        options=[\"cartesian\", \"euclidean\"],\n        default=None,\n        allow_none=True,\n        description=(\n            \"Optional domain. Should only be set if you don't pass an optimizer as the `optimizer` argument. If set, \"\n            \"has to be one of `[cartesian, euclidean]`.\"\n        ),\n    )\n\n    space: list[dict] | None = schema_utils.DictList(\n        description=(\n            \"Search space. Should only be set if you don't pass an optimizer as the `optimizer` argument. Defines the \"\n            \"search space and requires a `domain` to be set. Can be automatically converted from the `param_space` \"\n            \"dict passed to `tune.Tuner()`.\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    evaluated_rewards: list | None = evaluated_rewards_field()\n\n    random_state_seed: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Seed for reproducible results. Defaults to None. Please note that setting this to a value will change \"\n            \"global random state for `numpy` on initalization and loading from checkpoint.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"hebo\", random_state_field=\"random_state_seed\", dependencies=[(\"hebo\", \"HEBO\")]\n)\n@ludwig_dataclass\nclass HEBOSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"hebo\")\n\n    space: list[dict] | None = schema_utils.DictList(\n        description=\"A dict mapping parameter names to Tune search spaces or a HEBO DesignSpace object.\"\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    evaluated_rewards: list | None = evaluated_rewards_field()\n\n    random_state_seed: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Seed for reproducible results. Defaults to None. Please note that setting this to a value will change \"\n            \"global random state for `numpy` on initalization and loading from checkpoint.\"\n        ),\n    )\n\n    max_concurrent: int = schema_utils.NonNegativeInteger(\n        default=8,\n        description=(\n            \"Number of maximum concurrent trials. If this Searcher is used in a `ConcurrencyLimiter`, the \"\n            \"`max_concurrent` value passed to it will override the value passed here.\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"hyperopt\", random_state_field=\"random_state_seed\", dependencies=[(\"hyperopt\", \"hyperopt\")]\n)\n@ludwig_dataclass\nclass HyperoptSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"hyperopt\")\n\n    space: list[dict] | None = schema_utils.DictList(\n        description=(\n            \"HyperOpt configuration. Parameters will be sampled from this configuration and will be used to override \"\n            \"parameters generated in the variant generation process.\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    n_initial_points: int = schema_utils.PositiveInteger(\n        default=20,\n        description=(\n            \"The number of random evaluations of the objective function before starting to approximate it with tree \"\n            \"parzen estimators. Defaults to 20.\"\n        ),\n    )\n\n    random_state_seed: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\"Seed for reproducible results. Defaults to None.\"),\n    )\n\n    gamma: float = schema_utils.FloatRange(\n        min=0.0,\n        max=1.0,\n        default=0.25,\n        description=(\n            \"The split to use in TPE. TPE models two splits of the evaluated hyperparameters: the top performing \"\n            \"`gamma` percent, and the remaining examples. For more details, see [Making a Science of Model Search: \"\n            \"Hyperparameter Optimization in Hundreds of Dimensions for Vision Architectures.]\"\n            \"(http://proceedings.mlr.press/v28/bergstra13.pdf).\"\n        ),\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"nevergrad\", dependencies=[(\"nevergrad\", \"nevergrad\")])\n@ludwig_dataclass\nclass NevergradSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"nevergrad\")\n\n    # TODO: Add a registry mapping string names to nevergrad optimizers\n    # optimizer: Optional[str] = None\n\n    # TODO: Add schemas for nevergrad optimizer kwargs\n    optimizer_kwargs: dict | None = schema_utils.Dict(description=\"Kwargs passed in when instantiating the optimizer.\")\n\n    space: list[dict] | None = schema_utils.DictList(\n        description=(\n            \"Nevergrad parametrization to be passed to optimizer on instantiation, or list of parameter names if you \"\n            \"passed an optimizer object.\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\n    \"optuna\", random_state_field=\"seed\", dependencies=[(\"optuna\", \"optuna\")]\n)\n@ludwig_dataclass\nclass OptunaSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"optuna\")\n\n    space: dict | None = schema_utils.Dict(\n        description=(\n            \"Hyperparameter search space definition for Optuna's sampler. This can be either a dict with parameter \"\n            \"names as keys and optuna.distributions as values, or a Callable - in which case, it should be a \"\n            \"define-by-run function using optuna.trial to obtain the hyperparameter values. The function should \"\n            \"return either a dict of constant values with names as keys, or None. For more information, see \"\n            \"[the Optuna docs]\"\n            \"(https://optuna.readthedocs.io/en/stable/tutorial/10_key_features/002_configurations.html).\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    # TODO: Add a registry of Optuna samplers schemas\n    # sampler = None\n\n    seed: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Seed to initialize sampler with. This parameter is only used when `sampler=None`. In all other cases, \"\n            \"the sampler you pass should be initialized with the seed already.\"\n        ),\n    )\n\n    evaluated_rewards: list | None = evaluated_rewards_field()\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"skopt\", dependencies=[(\"skopt\", \"scikit-optimize\")])\nclass SkoptSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"skopt\")\n\n    optimizer: Any | None = None\n\n    space: dict | None = schema_utils.Dict(\n        description=(\n            \"A dict mapping parameter names to valid parameters, i.e. tuples for numerical parameters and lists \"\n            \"for categorical parameters. If you passed an optimizer instance as the optimizer argument, this should \"\n            \"be a list of parameter names instead.\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    evaluated_rewards: list | None = evaluated_rewards_field(\n        description=(\n            \"If you have previously evaluated the parameters passed in as points_to_evaluate you can avoid \"\n            \"re-running those trials by passing in the reward attributes as a list so the optimiser can be told the \"\n            \"results without needing to re-compute the trial. Must be the same length as points_to_evaluate. (See \"\n            \"tune/examples/skopt_example.py)\"\n        )\n    )\n\n    convert_to_python: bool = schema_utils.Boolean(\n        default=True,\n        description=\"SkOpt outputs numpy primitives (e.g. `np.int64`) instead of Python types. If this setting is set \"\n        \"to `True`, the values will be converted to Python primitives.\",\n    )\n\n\n@DeveloperAPI\n@hyperopt_utils.register_search_algorithm_config(\"zoopt\", dependencies=[(\"zoopt\", \"zoopt\")])\n@ludwig_dataclass\nclass ZooptSAConfig(BaseSearchAlgorithmConfig):\n    type: str = schema_utils.ProtectedString(\"zoopt\")\n\n    algo: str = schema_utils.ProtectedString(\n        pstring=\"asracos\",\n        description=\"To specify an algorithm in zoopt you want to use. Only support ASRacos currently.\",\n    )\n\n    budget: int | None = schema_utils.PositiveInteger(\n        default=None, allow_none=True, description=\"Optional. Number of samples.\"\n    )\n\n    dim_dict: dict | None = schema_utils.Dict(\n        description=(\n            \"Dimension dictionary. For continuous dimensions: (continuous, search_range, precision); For discrete \"\n            \"dimensions: (discrete, search_range, has_order); For grid dimensions: (grid, grid_list). More details \"\n            \"can be found in zoopt package.\"\n        )\n    )\n\n    points_to_evaluate: list[dict] | None = points_to_evaluate_field()\n\n    parallel_num: int = schema_utils.PositiveInteger(\n        default=1,\n        description=(\n            \"How many workers to parallel. Note that initial phase may start less workers than this number. More \"\n            \"details can be found in zoopt package.\"\n        ),\n    )\n"
  },
  {
    "path": "ludwig/schema/hyperopt/utils.py",
    "content": "from collections.abc import Callable\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.registry import Registry\n\nparameter_config_registry = Registry()\nscheduler_config_registry = Registry()\nscheduler_dependencies_registry = Registry()\nsearch_algorithm_config_registry = Registry()\nsearch_algorithm_dependencies_registry = Registry()\nsearch_algorithm_random_state_field_registry = Registry()\n\n\n@DeveloperAPI\ndef get_parameter_cls(name: str) -> type[\"BaseParameterConfig\"]:  # noqa: F821\n    \"\"\"Get a registered hyperopt parameter config class by name.\n\n    Args:\n        name: the name of a parameter config class registered in `ludwig.schema.hyperopt.parameter`\n\n    Returns:\n        A parameter config class from `ludwig.schema.hyperopt.parameter`\n    \"\"\"\n    return parameter_config_registry[name]\n\n\n@DeveloperAPI\ndef get_scheduler_cls(name: str) -> type[\"BaseSchedulerConfig\"]:  # noqa: F821\n    \"\"\"Get a registered hyperopt scheduler config class by name.\n\n    Args:\n        name: the name of a scheduler config class registered in `ludwig.schema.hyperopt.scheduler`\n\n    Returns:\n        A scheduler config class from `ludwig.schema.hyperopt.scheduler`\n    \"\"\"\n    return search_algorithm_config_registry[name]\n\n\n@DeveloperAPI\ndef get_scheduler_dependencies(name: str) -> list[str]:\n    \"\"\"Get the list of dependencies for a registered hyperopt scheduler.\n\n    Args:\n        name: the name of a scheduler config class registered in `ludwig.schema.hyperopt.scheduler`\n\n    Returns:\n        The list of imports needed to use the scheduler\n    \"\"\"\n    return scheduler_dependencies_registry[name]\n\n\n@DeveloperAPI\ndef get_search_algorithm_cls(name: str) -> type[\"BaseSearchAlgorithmConfig\"]:  # noqa: F821\n    \"\"\"Get a registered hyperopt search algorithm config class by name.\n\n    Args:\n        name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm`\n\n    Returns:\n        A scheduler config class from `ludwig.schema.hyperopt.search_algorithm`\n    \"\"\"\n    return search_algorithm_config_registry[name]\n\n\n@DeveloperAPI\ndef get_search_algorithm_dependencies(name: str) -> list[str]:\n    \"\"\"Get the list of dependencies for a registered hyperopt search algorithm.\n\n    Args:\n        name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm`\n\n    Returns:\n        The list of imports needed to use the search algorithm\n    \"\"\"\n    return search_algorithm_dependencies_registry[name]\n\n\n@DeveloperAPI\ndef get_search_algorithm_random_state_field(name: str):\n    \"\"\"Get the field name of the random state for a registered hyperopt search algorithm.\n\n    Args:\n        name: the name of a search algorithm config class registered in `ludwig.schema.hyperopt.search_algorithm`\n\n    Returns:\n        The name of the random state field in the config\n    \"\"\"\n    return search_algorithm_random_state_field_registry[name]\n\n\n@DeveloperAPI\ndef register_parameter_config(name: str) -> Callable:\n    \"\"\"Register a parameter config class by name.\n\n    Args:\n        name: the name to register the parameter class under, does not need to correspond to the value of `space`\n\n    Returns:\n        Wrapper function to decorate a `BaseParameterConfig` subclass\n    \"\"\"\n\n    def wrap(cls: type[\"BaseParameterConfig\"]) -> type[\"BaseParameterConfig\"]:  # noqa: F821\n        \"\"\"Add a parameter config class to the registry.\n\n        Args:\n            cls: a subclass of `BaseParameterConfig`\n\n        Returns:\n            `cls` unaltered\n        \"\"\"\n        parameter_config_registry[name] = cls\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef register_scheduler_config(name: str, dependencies: list[tuple[str]] | None = None):\n    \"\"\"Register a scheduler config class by name.\n\n    Args:\n        name: the name to scheduler the parameter class under, does not need to correspond to the value of `type`\n        dependencies: the list of scheduler dependency package name/install name pairs, e.g.\n                      `(\"sklearn\", \"scikit-learn\")`\n\n    Returns:\n        Wrapper function to decorate a `BaseSchedulerConfig` subclass\n    \"\"\"\n\n    def wrap(scheduler_config: type[\"BaseSchedulerConfig\"]) -> type[\"BaseSchedulerConfig\"]:  # noqa: F821\n        \"\"\"Add a parameter config class to the registry.\n\n        Args:\n            cls: a subclass of `BaseParameterConfig`\n\n        Returns:\n            `cls` unaltered\n        \"\"\"\n        scheduler_config_registry[name] = scheduler_config\n        scheduler_dependencies_registry[name] = dependencies if dependencies is not None else []\n        return scheduler_config\n\n    return wrap\n\n\n# TODO: create a search alg metadata class to register in place of individual metadata args\n@DeveloperAPI\ndef register_search_algorithm_config(\n    name: str, random_state_field: str | None = None, dependencies: list[tuple[str, str]] | None = None\n) -> Callable:\n    \"\"\"Register a search algorithm config class by name.\n\n    Args:\n        name: the name to register the search algorithm class under, does not need to correspond to the value of `type`\n        random_state_field: the name of the random state in this search algorithm\n        dependencies: the list of search algorithm dependency package name/install name pairs, e.g.\n                      `(\"sklearn\", \"scikit-learn\")`\n\n    Returns:\n        Wrapper function to decorate a `BaseSearchAlgorithmConfig` subclass\n    \"\"\"\n\n    def wrap(cls: type[\"BaseSearchAlgorithmConfig\"]) -> type[\"BaseSearchAlgorithmConfig\"]:  # noqa: F821\n        search_algorithm_config_registry[name] = cls\n        search_algorithm_dependencies_registry[name] = dependencies if dependencies is not None else []\n        search_algorithm_random_state_field_registry[name] = random_state_field\n        return cls\n\n    return wrap\n"
  },
  {
    "path": "ludwig/schema/jsonschema.py",
    "content": "\"\"\"JSON Schema generation for Ludwig config classes.\n\nUses pydantic's model_json_schema() under the hood, replacing the previous marshmallow-based converter.\n\"\"\"\n\n\ndef marshmallow_schema_to_jsonschema_dict(schema_instance):\n    \"\"\"Backward-compatible JSON schema generation.\n\n    Previously converted marshmallow schemas. Now uses pydantic's model_json_schema().\n    The schema_instance can be either:\n    - A pydantic model class (BaseMarshmallowConfig subclass)\n    - A _SchemaAdapter instance\n    - Legacy: called with a marshmallow Schema instance (raises helpful error)\n    \"\"\"\n    from ludwig.schema.utils import _SchemaAdapter, BaseMarshmallowConfig\n\n    # Handle _SchemaAdapter\n    if isinstance(schema_instance, _SchemaAdapter):\n        cls = schema_instance._cls\n    elif isinstance(schema_instance, type) and issubclass(schema_instance, BaseMarshmallowConfig):\n        cls = schema_instance\n    elif isinstance(schema_instance, BaseMarshmallowConfig):\n        cls = type(schema_instance)\n    else:\n        raise TypeError(\n            f\"Expected a Ludwig config class or schema adapter, got {type(schema_instance)}. \"\n            \"Marshmallow schemas are no longer supported. Use pydantic BaseModel subclasses.\"\n        )\n\n    schema_dict = cls.model_json_schema()\n    name = cls.__name__\n\n    # Wrap in definitions format for backward compat\n    return {\n        \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n        \"definitions\": {name: schema_dict},\n        \"$ref\": f\"#/definitions/{name}\",\n    }\n"
  },
  {
    "path": "ludwig/schema/llms/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/schema/llms/base_model.py",
    "content": "import logging\nimport os\nfrom dataclasses import field\n\nfrom transformers import AutoConfig\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BASE_MODEL\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LLM_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json\n\nlogger = logging.getLogger(__name__)\n\n# Maps a preset LLM name to the full slash-delimited HF path. If the user chooses a preset LLM, the preset LLM name is\n# replaced with the full slash-delimited HF path using this map, after JSON validation but before config object\n# initialization.\nMODEL_PRESETS = {\n    # Bloom\n    \"bloomz-3b\": \"bigscience/bloomz-3b\",\n    \"bloomz-7b1\": \"bigscience/bloomz-7b1\",\n    # CodeLlama\n    \"codellama-7b\": \"codellama/CodeLlama-7b-hf\",\n    \"codellama-13b\": \"codellama/CodeLlama-13b-hf\",\n    \"codellama-34b\": \"codellama/CodeLlama-34b-hf\",\n    \"codellama-7b-instruct\": \"codellama/CodeLlama-7b-instruct-hf\",\n    \"codellama-13b-instruct\": \"codellama/CodeLlama-13b-instruct-hf\",\n    \"codellama-34b-instruct\": \"codellama/CodeLlama-34b-instruct-hf\",\n    # GPT Neo and GPT J\n    \"gpt-neo-2.7B\": \"EleutherAI/gpt-neo-2.7B\",\n    \"gpt-j-6b\": \"EleutherAI/gpt-j-6b\",\n    # LLama-2\n    \"llama-2-7b\": \"meta-llama/Llama-2-7b-hf\",\n    \"llama-2-13b\": \"meta-llama/Llama-2-13b-hf\",\n    \"llama-2-70b\": \"meta-llama/Llama-2-70b-hf\",\n    \"llama-2-7b-chat\": \"meta-llama/Llama-2-7b-chat-hf\",\n    \"llama-2-13b-chat\": \"meta-llama/Llama-2-13b-chat-hf\",\n    \"llama-2-70b-chat\": \"meta-llama/Llama-2-70b-chat-hf\",\n    # Mistral\n    \"mistral-7b\": \"mistralai/Mistral-7B-v0.1\",\n    \"mistral-7b-instruct\": \"mistralai/Mistral-7B-Instruct-v0.1\",\n    # Mixtral\n    \"mixtral-8x7b\": \"mistralai/Mixtral-8x7B-v0.1\",\n    \"mixtral-8x7b-instruct\": \"mistralai/Mixtral-8x7B-Instruct-v0.1\",\n    # OPT\n    \"opt-350m\": \"facebook/opt-350m\",\n    \"opt-1.3b\": \"facebook/opt-1.3b\",\n    \"opt-6.7b\": \"facebook/opt-6.7b\",\n    # Pythia\n    \"pythia-2.8b\": \"EleutherAI/pythia-2.8b\",\n    \"pythia-12b\": \"EleutherAI/pythia-12b\",\n    # Vicuna\n    \"vicuna-7b\": \"lmsys/vicuna-7b-v1.3\",\n    \"vicuna-13b\": \"lmsys/vicuna-13b-v1.3\",\n    # Zephyr\n    \"zephyr-7b-alpha\": \"HuggingFaceH4/zephyr-7b-alpha\",\n    \"zephyr-7b-beta\": \"HuggingFaceH4/zephyr-7b-beta\",\n    # Phi\n    \"phi-1\": \"microsoft/phi-1\",\n    \"phi-1_5\": \"microsoft/phi-1_5\",\n    \"phi-2\": \"microsoft/phi-2\",\n}\n\n\n@DeveloperAPI\ndef BaseModelDataclassField():\n    description = (\n        \"Base pretrained model to use. This can be one of the presets defined by Ludwig, a fully qualified \"\n        \"name of a pretrained model from the HuggingFace Hub, or a path to a directory containing a \"\n        \"pretrained model.\"\n    )\n\n    def validate(model_name: str):\n        \"\"\"Validates and upgrades the given model name to its full path, if applicable.\n\n        If the name exists in `MODEL_PRESETS`, returns the corresponding value from the dict; otherwise checks if the\n        given name (which should be a full path) exists locally or in the transformers library.\n        \"\"\"\n        if isinstance(model_name, str):\n            if model_name in MODEL_PRESETS:\n                return MODEL_PRESETS[model_name]\n            if os.path.isdir(model_name):\n                return model_name\n            try:\n                AutoConfig.from_pretrained(model_name, trust_remote_code=True)\n                return model_name\n            except OSError:\n                raise ConfigValidationError(\n                    f\"Specified base model `{model_name}` could not be loaded. If this is a private repository, make \"\n                    f\"sure to set HUGGING_FACE_HUB_TOKEN in your environment. Check that {model_name} is a valid \"\n                    \"pretrained CausalLM listed on huggingface or a valid local directory containing the weights for a \"\n                    \"pretrained CausalLM from huggingface. See: \"\n                    \"https://huggingface.co/models?pipeline_tag=text-generation&sort=downloads for a full list.\"\n                )\n        raise ConfigValidationError(\n            f\"`base_model` should be a string, instead given: {model_name}. This can be a preset or any pretrained \"\n            \"CausalLM on huggingface. See: https://huggingface.co/models?pipeline_tag=text-generation&sort=downloads\"\n        )\n\n    class BaseModelField(schema_utils.LudwigSchemaField):\n        def _serialize(self, value, attr, obj, **kwargs):\n            if isinstance(value, str):\n                return value\n            raise ConfigValidationError(f\"Value to serialize is not a string: {value}\")\n\n        def _deserialize(self, value, attr, obj, **kwargs):\n            return validate(value)\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"anyOf\": [\n                    {\n                        \"type\": \"string\",\n                        \"enum\": list(MODEL_PRESETS.keys()),\n                        \"description\": (\n                            \"Pick from a set of popular LLMs of different sizes across a variety of architecture types.\"\n                        ),\n                        \"title\": \"preset\",\n                        \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[BASE_MODEL][\"_anyOf\"][\"preset\"]),\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Enter the full path to a huggingface LLM.\",\n                        \"title\": \"custom\",\n                        \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[BASE_MODEL][\"_anyOf\"][\"custom\"]),\n                    },\n                ],\n                \"description\": description,\n                \"title\": \"base_model_options\",\n                \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[BASE_MODEL][\"_meta\"]),\n            }\n\n    return field(\n        metadata={\n            \"marshmallow_field\": BaseModelField(\n                required=True,\n                allow_none=False,\n                validate=validate,\n                metadata={  # TODO: extra metadata dict probably unnecessary, but currently a widespread pattern\n                    \"description\": description,\n                    \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[BASE_MODEL][\"_meta\"]),\n                },\n            ),\n        },\n        # TODO: This is an unfortunate side-effect of dataclass init order - you cannot have non-default fields follow\n        # default fields, so we have to give `base_model` a fake default of `None`.\n        default=None,\n    )\n"
  },
  {
    "path": "ludwig/schema/llms/generation.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LLM_METADATA\n\n\n@DeveloperAPI\n@schema_utils.ludwig_dataclass\nclass LLMGenerationConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Parameters for LLM Generation Config.\n\n    Should match the parameters in\n    https://huggingface.co/docs/transformers/v4.28.0/en/main_classes/text_generation#transformers.GenerationConfig\n    \"\"\"\n\n    # Parameters that control the length of the output\n\n    max_new_tokens: int | None = schema_utils.PositiveInteger(\n        default=32,\n        allow_none=True,\n        description=\"The maximum number of new tokens to generate, ignoring the number of tokens in the input prompt. \"\n        \"If not set, this is dynamically determined by Ludwig based on either the `max_sequence_length` of the ouput \"\n        \"feature, the global_max_sequence_length specified in preprocessing (if specified), or the \"\n        \"maximum context length supported by the model (in the order specified).\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"max_new_tokens\"],\n    )\n\n    min_new_tokens: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"The minimum number of new tokens to generate, ignoring the number of tokens in the input prompt.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"min_new_tokens\"],\n    )\n\n    max_length: int = schema_utils.PositiveInteger(\n        default=32,\n        allow_none=True,\n        description=\"The maximum length the generated tokens can have. Corresponds to the length of the input prompt \"\n        \"+ max_new_tokens. Its effect is overridden by max_new_tokens, if also set.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"max_length\"],\n    )\n\n    min_length: int = schema_utils.NonNegativeInteger(\n        default=0,\n        allow_none=True,\n        description=\"The minimum length of the sequence to be generated. Corresponds to the length of the \"\n        \"input prompt + min_new_tokens. Its effect is overridden by min_new_tokens, if also set.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"min_length\"],\n    )\n\n    early_stopping: bool | str | None = schema_utils.Boolean(\n        default=False,\n        description=\"Controls the stopping condition for beam-based methods, like beam-search. It accepts the following\"\n        \" values: True, where the generation stops as soon as there are num_beams complete candidates; False, where an \"\n        \"heuristic is applied and the generation stops when is it very unlikely to find better candidates; `never`, \"\n        \"where the beam search procedure only stops when there cannot be better candidates (canonical beam search \"\n        \"algorithm)\",\n    )\n\n    max_time: float | None = schema_utils.FloatRange(\n        default=None,\n        min=None,\n        max=None,\n        allow_none=True,\n        description=\"The maximum amount of time you allow the computation to run for in seconds. generation will still\"\n        \" finish the current pass after allocated time has been passed. \",\n    )\n\n    # Parameters that control the generation strategy used\n\n    do_sample: bool | None = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not to use sampling ; use greedy decoding otherwise.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"do_sample\"],\n    )\n\n    num_beams: int | None = schema_utils.PositiveInteger(\n        default=1,\n        allow_none=True,\n        description=\"Number of beams for beam search. 1 means no beam search and is the default value.\"\n        \" The beam search strategy generates the translation word by word from left-to-right while keeping a fixed\"\n        \" number (beam) of active candidates at each time step during token generation. By increasing the beam size,\"\n        \" the translation performance can increase at the expense of significantly reducing the decoder speed.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"num_beams\"],\n    )\n\n    num_beam_groups: int | None = schema_utils.PositiveInteger(\n        default=1,\n        allow_none=True,\n        description=\"Number of groups to divide num_beams into in order to ensure diversity among different groups of \"\n        \"beams. 1 means no group beam search.\",\n    )\n\n    penalty_alpha: float | None = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"The values balance the model confidence and the degeneration penalty in contrastive \"\n        \" search decoding.\",\n    )\n\n    use_cache: bool | None = schema_utils.Boolean(\n        default=True,\n        description=\"Whether or not the model should use the past last key/values attentions (if applicable to the \"\n        \"model) to speed up decoding.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"use_cache\"],\n    )\n\n    prompt_lookup_num_tokens: int | None = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of tokens to consider as a candidate from the prompt for prompt lookup decoding, \"\n        \" an alternate way of performing assisted generation. If set to 0, the prompt lookup decoding is not used.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"prompt_lookup_num_tokens\"],\n    )\n\n    # Parameters for manipulation of the model output logits\n\n    temperature: float | None = schema_utils.NonNegativeFloat(\n        default=0.1,\n        allow_none=True,\n        description=\"Temperature is used to control the randomness of predictions.\"\n        \" A high temperature value (closer to 1) makes the output more diverse and random, while a lower temperature\"\n        \" (closer to 0) makes the model's responses more deterministic and focused on the most likely outcome.\"\n        \" In other words, temperature adjusts the probability distribution from which the model picks the next token.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"temperature\"],\n    )\n\n    top_k: int | None = schema_utils.PositiveInteger(\n        default=50,\n        allow_none=True,\n        description=\"The number of highest probability vocabulary tokens to keep for top-k-filtering.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"top_k\"],\n    )\n\n    top_p: float | None = schema_utils.FloatRange(\n        default=1.0,\n        min=0.0,\n        max=1.0,\n        allow_none=True,\n        description=\"If set to float < 1, only the most probable tokens with probabilities that add up to \"\n        \"top_p or higher are kept for generation.\",\n        parameter_metadata=LLM_METADATA[\"generation\"][\"top_p\"],\n    )\n\n    typical_p: float | None = schema_utils.FloatRange(\n        default=1.0,\n        min=0.0,\n        max=1.0,\n        allow_none=True,\n        description=\"Local typicality measures how similar the conditional probability of predicting a target token \"\n        \"next is to the expected conditional probability of predicting a random token next, given the partial text \"\n        \"already generated. If set to float < 1, the smallest set of the most locally typical tokens with \"\n        \"probabilities that add up to typical_p or higher are kept for generation.\",\n    )\n\n    epsilon_cutoff: float | None = schema_utils.FloatRange(\n        default=0.0,\n        min=0.0,\n        max=1.0,\n        allow_none=True,\n        description=\"If set to float strictly between 0 and 1, only tokens with a conditional probability greater \"\n        \"than epsilon_cutoff will be sampled. In the paper, suggested values range from 3e-4 to 9e-4, depending on the\"\n        \" size of the model.\",\n    )\n\n    eta_cutoff: float | None = schema_utils.FloatRange(\n        default=0.0,\n        min=0.0,\n        max=1.0,\n        allow_none=True,\n        description=\"Eta sampling is a hybrid of locally typical sampling and epsilon sampling. If set to float \"\n        \"strictly between 0 and 1, a token is only considered if it is greater than either eta_cutoff or \"\n        \"sqrt(eta_cutoff) * exp(-entropy(softmax(next_token_logits))). The latter term is intuitively the expected next\"\n        \" token probability, scaled by sqrt(eta_cutoff). In the paper, suggested values range from 3e-4 to 2e-3, \"\n        \"depending on the size of the model.\",\n    )\n\n    diversity_penalty: float | None = schema_utils.NonNegativeFloat(\n        default=0.0,\n        allow_none=True,\n        description=\"The value used to control the diversity of the generated text. The higher the value, the more \"\n        \"diverse the text will be. If set to 0, no diversity is enforced.\"\n        \"This value is subtracted from a beam(s) score if it generates a token same as any beam from other group at a\"\n        \"particular time. Note that diversity_penalty is only effective if group beam search is enabled.\",\n    )\n\n    repetition_penalty: float | None = schema_utils.NonNegativeFloat(\n        default=1.0,\n        allow_none=True,\n        description=\"The parameter for repetition penalty. 1.0 means no penalty. \"\n        \"See [this paper](https://arxiv.org/pdf/1909.05858.pdf) for more details.\",\n    )\n\n    encoder_repetition_penalty: float | None = schema_utils.NonNegativeFloat(\n        default=1.0,\n        allow_none=True,\n        description=\"The paramater for encoder_repetition_penalty. An exponential penalty on sequences that are not\"\n        \" in the original input. 1.0 means no penalty.\",\n    )\n\n    length_penalty: float | None = schema_utils.Float(\n        default=1.0,\n        allow_none=True,\n        description=\"Exponential penalty to the length that is used with beam-based generation. It is applied as an \"\n        \"exponent to the sequence length, which in turn is used to divide the score of the sequence. Since the score is\"\n        \" the log likelihood of the sequence (i.e. negative), length_penalty > 0.0 promotes longer sequences, while \"\n        \"length_penalty < 0.0 encourages shorter sequences.\",\n    )\n\n    no_repeat_ngram_size: int | None = schema_utils.NonNegativeInteger(\n        default=0,\n        allow_none=True,\n        description=\"If set to int > 0, all ngrams of that size can only occur once.\",\n    )\n\n    bad_words_ids: list[list[int]] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=\"List of token ids that are not allowed to be generated. In order to get the tokens of the words \"\n        \"that should not appear in the generated text, use tokenizer(bad_word, add_prefix_space=True).input_ids.\",\n    )\n\n    force_words_ids: list[list[int]] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=\"List of token ids that are forced to be generated by the model. In order to get the tokens of the\"\n        \" words that should appear in the generated text, use tokenizer(force_word, add_prefix_space=True).input_ids.\",\n    )\n\n    renormalize_logits: bool | None = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to renormalize the logits after temperature and top_k/top_p filtering.\",\n    )\n\n    # TODO(This needs to be defined based on the Constraint class)\n    # constraints:\n\n    forced_bos_token_id: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"The id of the token to force as the first generated token after the decoder_start_token_id.\"\n        \"Useful for multilingual models like mBART where the first generated token needs to be the target language\"\n        \"token.\",\n    )\n\n    forced_eos_token_id: int | list[int] | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"The id of the token to force as the last generated token when max_length is reached. Optionally, \"\n        \"use a list to set multiple end-of-sequence tokens.\",\n    )\n\n    remove_invalid_values: bool | None = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to remove possible nan and inf outputs of the model to prevent the generation method to \"\n        \"crash. Note that using remove_invalid_values can slow down generation.\",\n    )\n\n    exponential_decay_length_penalty: tuple[int, float] | None = schema_utils.FloatRange(\n        default=None,\n        min=0.0,\n        max=1.0,\n        allow_none=True,\n        description=\"This Tuple adds an exponentially increasing length penalty, after a certain amount of tokens have \"\n        \"been generated. The tuple shall consist of: (start_index, decay_factor) where start_index indicates where \"\n        \"penalty starts and decay_factor represents the factor of exponential decay\",\n    )\n\n    suppress_tokens: list[int] | None = schema_utils.List(\n        list_type=int,\n        default=None,\n        allow_none=True,\n        description=\"A list of tokens that will be suppressed at generation. The SupressTokens logit processor will set\"\n        \" their log probs to -inf so that they are not sampled.\",\n    )\n\n    begin_suppress_tokens: list[int] | None = schema_utils.List(\n        list_type=int,\n        default=None,\n        allow_none=True,\n        description=\"A list of tokens that will be suppressed at the beginning of the generation. The \"\n        \"SupressBeginTokens logit processor will set their log probs to -inf so that they are not sampled.\",\n    )\n\n    forced_decoder_ids: list[list[int]] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=\"A list of forced decoder ids. The ForcedDecoderIds logit processor will set the log probs of all \"\n        \"tokens that are not in the list to -inf so that they are not sampled.\",\n    )\n\n    sequence_bias: dict[tuple[int], float] | None = schema_utils.Dict(\n        default=None,\n        allow_none=True,\n        description=\"A dictionary of token ids to bias the generation towards. The SequenceBias logit processor will \"\n        \"add the bias to the log probs of the tokens in the dictionary. Positive biases increase the odds of the \"\n        \"sequence being selected, while negative biases do the opposite. \",\n    )\n\n    guidance_scale: float | None = schema_utils.FloatRange(\n        default=None,\n        min=0.0,\n        allow_none=True,\n        description=\"The guidance scale for classifier free guidance (CFG). CFG is enabled by setting guidance_scale >\"\n        \" 1. Higher guidance scale encourages the model to generate samples that are more closely linked to the input\"\n        \" prompt, usually at the expense of poorer quality.\",\n    )\n\n    # Special tokens that can be used at generation time\n\n    pad_token_id: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"The id of the padding token. If not set, the padding token id of the tokenizer is used.\",\n    )\n\n    bos_token_id: int | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"The id of the beginning of sentence token. If not set, the bos token id of the tokenizer is used.\",\n    )\n\n    eos_token_id: int | list[int] | None = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"The id of the end of sentence token. If not set, the eos token id of the tokenizer is used.\",\n    )\n\n\n@DeveloperAPI\nclass LLMGenerationConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(LLMGenerationConfig)\n\n    def _jsonschema_type_mapping(self):\n        return schema_utils.unload_jsonschema_from_marshmallow_class(LLMGenerationConfig)\n"
  },
  {
    "path": "ludwig/schema/llms/model_parameters.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass RoPEScalingConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Dynamic RoPE-scaling (rotary position embeddings) to extend the context length of LLM like LLaMA, GPT-NeoX,\n    or Falcon.\n\n    This parameter is a dictionary containing the scaling configuration for the RoPE embeddings. Currently supports\n    three scaling strategies: linear and dynamic. Their scaling factor must be an float greater than 1. The expected\n    format is {'rope_type': strategy name, 'factor': scaling factor}\n    \"\"\"\n\n    def __post_init__(self):\n        # Both parameters must be set, or none.\n        if not self.rope_type:\n            raise ConfigValidationError(\n                f\"`rope_scaling`'s `rope_type` field must be one of ['linear', 'dynamic'], got {self.rope_type}\"\n            )\n\n        if not self.factor:\n            raise ConfigValidationError(\n                f\"When using `rope_scaling`, `factor` must be specified and be > 1. Got {self.factor}.\"\n            )\n\n    rope_type: str | None = schema_utils.StringOptions(\n        options=[\"linear\", \"dynamic\"],\n        default=None,\n        allow_none=True,\n        description=\"Currently supports two strategies: linear and dynamic scaling.\",\n    )\n\n    factor: float | None = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        min=1.0,\n        min_inclusive=False,\n        description=\"The scaling factor for RoPE embeddings.\",\n    )\n\n\n@DeveloperAPI\nclass RoPEScalingConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(RoPEScalingConfig, default_missing=True)\n\n    def _jsonschema_type_mapping(self):\n        return schema_utils.unload_jsonschema_from_marshmallow_class(RoPEScalingConfig, title=\"rope_scaling\")\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ModelParametersConfig(schema_utils.BaseMarshmallowConfig):\n    rope_scaling: RoPEScalingConfig = RoPEScalingConfigField().get_default_field()\n\n    neftune_noise_alpha: int | None = schema_utils.IntegerRange(\n        default=0,\n        min=0,\n        allow_none=True,\n        description=\"The alpha parameter for the embedding noise, which controls the amount of noise added to the \"\n        \"embeddings. The higher the value, the more noise is added. This is based on the paper NEFTune: Noisy \"\n        \"Embeddings Improve Instruction Finetuning. Paper: https://arxiv.org/pdf/2310.05914.pdf. Default: 0.\"\n        \"Suggested values: 5, 10\",\n    )\n\n    def to_dict(self):\n        config = {}\n        if self.rope_scaling:\n            config[\"rope_scaling\"] = self.rope_scaling.to_dict()\n        if self.neftune_noise_alpha:\n            config[\"neftune_noise_alpha\"] = self.neftune_noise_alpha\n        return config\n\n\n@DeveloperAPI\nclass ModelParametersConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(ModelParametersConfig, default_missing=True)\n\n    def _jsonschema_type_mapping(self):\n        return {\n            \"oneOf\": [\n                {\"type\": \"null\", \"title\": \"disabled\", \"description\": \"Skip configurable model parameters.\"},\n                {\n                    **schema_utils.unload_jsonschema_from_marshmallow_class(ModelParametersConfig),\n                    \"title\": \"enabled\",\n                    \"description\": \"Set model parameters options.\",\n                },\n            ],\n            \"title\": \"Model Parameters\",\n            \"description\": \"Configurable model parameters for LLMs.\",\n        }\n"
  },
  {
    "path": "ludwig/schema/llms/peft.py",
    "content": "from abc import ABC, abstractmethod\nfrom typing import TYPE_CHECKING\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LLM_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.registry import Registry\n\nif TYPE_CHECKING:\n    from peft import PeftConfig\n\n\nadapter_registry = Registry()\n\n\n@DeveloperAPI\ndef register_adapter(name: str):\n    def wrap(config: BaseAdapterConfig):\n        adapter_registry[name] = config\n        return config\n\n    return wrap\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass LoraPostprocessorConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"This Dataclass is a schema for the nested postprocessing config under adapter of type \"lora\".\"\"\"\n\n    merge_adapter_into_base_model: bool = schema_utils.Boolean(\n        default=False,\n        description=\"\"\"Instructs whether or not the fine-tuned LoRA weights are to be merged into the base LLM model so\nthat the complete fine-tuned model is available to be used and/or persisted, and then reused upon loading as a single\nmodel (rather than having to load base and fine-tuned models separately).\"\"\",\n    )\n    progressbar: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Instructs whether or not to show a progress bar indicating the unload and merge process.\",\n    )\n\n\n@DeveloperAPI\nclass LoraPostprocessorConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(LoraPostprocessorConfig)\n\n    def _jsonschema_type_mapping(self):\n        return schema_utils.unload_jsonschema_from_marshmallow_class(LoraPostprocessorConfig, title=\"LoraPostprocessor\")\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseAdapterConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    type: str\n\n    pretrained_adapter_weights: str | None = schema_utils.String(\n        default=None, description=\"Path to pretrained weights.\", allow_none=True\n    )\n\n    postprocessor: LoraPostprocessorConfig = LoraPostprocessorConfigField().get_default_field()\n\n    @abstractmethod\n    def to_config(self, **kwargs) -> \"PeftConfig\":\n        pass\n\n\n@DeveloperAPI\n@register_adapter(name=\"lora\")\n@ludwig_dataclass\nclass LoraConfig(BaseAdapterConfig):\n    def __post_init__(self):\n        if self.alpha is None:\n            self.alpha = self.r * 2\n\n    type: str = schema_utils.ProtectedString(\n        \"lora\",\n        description=LLM_METADATA[\"adapter\"][\"lora\"][\"type\"].long_description,\n    )\n\n    r: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Lora attention dimension.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"r\"],\n    )\n\n    alpha: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The alpha parameter for Lora scaling. Defaults to `2 * r`.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"alpha\"],\n    )\n\n    dropout: float = schema_utils.NonNegativeFloat(\n        default=0.05,\n        description=\"The dropout probability for Lora layers.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"dropout\"],\n    )\n\n    # TODO(travis): figure out why calling this `bias` doesn't work\n    bias_type: str = schema_utils.StringOptions(\n        options=[\"none\", \"all\", \"lora_only\"],\n        default=\"none\",\n        description=\"Bias type for Lora.\",\n    )\n\n    target_modules: list[str] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=(\n            \"List of module names or regex expression of the module names to replace with LoRA. \"\n            \"For example, ['q', 'v'] or '.*decoder.*(SelfAttention|EncDecAttention).*(q|v)$'. \"\n            \"Defaults to targeting the query and value matrices of all self-attention and encoder-decoder attention \"\n            \"layers.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"target_modules\"],\n    )\n\n    use_rslora: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"When set to True, uses Rank-Stabilized LoRA which sets the adapter scaling factor to \"\n            \"lora_alpha/math.sqrt(r), since it was proven to work better. Otherwise, it will use the original \"\n            \"default value of lora_alpha/r. Paper: https://arxiv.org/abs/2312.03732.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"use_rslora\"],\n    )\n\n    use_dora: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Enable 'Weight-Decomposed Low-Rank Adaptation' (DoRA). This technique decomposes the updates of the \"\n            \"weights into two parts, magnitude and direction. Direction is handled by normal LoRA, whereas the \"\n            \"magnitude is handled by a separate learnable parameter. This can improve the performance of LoRA, \"\n            \"especially at low ranks. Right now, DoRA only supports non-quantized linear layers. DoRA introduces a \"\n            \"bigger overhead than pure LoRA, so it is recommended to merge weights for inference. For more \"\n            \"information, see https://arxiv.org/abs/2402.09353\"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"lora\"][\"use_dora\"],\n    )\n\n    def to_config(self, task_type: str = None, **kwargs) -> \"PeftConfig\":\n        from peft import LoraConfig as _LoraConfig\n\n        return _LoraConfig(\n            r=self.r,\n            lora_alpha=self.alpha,\n            lora_dropout=self.dropout,\n            bias=self.bias_type,\n            target_modules=self.target_modules,\n            task_type=task_type,\n            use_rslora=self.use_rslora,\n            use_dora=self.use_dora,\n        )\n\n    @classmethod\n    def name(cls) -> str:\n        return \"LoRA\"\n\n    @classmethod\n    def description(cls) -> str:\n        return LLM_METADATA[\"adapter\"][\"lora\"][\"type\"].long_description\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BasePromptLearningConfig(BaseAdapterConfig):\n    \"\"\"Config for prompt learning adapters. Not meant to be used directly.\n\n    Adapted from https://github.com/huggingface/peft/blob/main/src/peft/utils/config.py (PromptLearningConfig)\n    \"\"\"\n\n    num_virtual_tokens: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Number of virtual tokens to add to the prompt. Virtual tokens are used to control the behavior of \"\n        \" the model during inference. \",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"prompt_learning\"][\"num_virtual_tokens\"],\n    )\n\n    token_dim: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The hidden embedding dimension of the base transformer model.\",\n    )\n\n    num_transformer_submodules: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of transformer submodules in the base transformer model.\",\n    )\n\n    num_attention_heads: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of attention heads in the base transformer model.\",\n    )\n\n    num_layers: int | None = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of layers in the base transformer model.\",\n    )\n\n\n# TODO(travis): fix text generation when using prompt tuning:\n#     RuntimeError: shape '[-1, 17]' is invalid for input of size 9\n# @DeveloperAPI\n# @register_adapter(\"prompt_tuning\")\n# @ludwig_dataclass\n# class PromptTuningConfig(BasePromptLearningConfig):\n#     \"\"\"Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/prompt_tuning.py.\"\"\"\n\n#     def __post_init__(self):\n#         if self.prompt_tuning_init == \"TEXT\" and not self.prompt_tuning_init_text:\n#             raise ConfigValidationError(\n#                 \"Must provide `prompt_tuning_init_text` when `prompt_tuning_init` is set to `TEXT`.\"\n#             )\n\n\"\"\"#     type: str = schema_utils.ProtectedString(\"prompt_tuning\")\"\"\"  # Quotes allow mypy to run without syntax errors.\n\n#     prompt_tuning_init: str = schema_utils.StringOptions(\n#         [\"RANDOM\", \"TEXT\"],\n#         default=\"RANDOM\",\n#         description=\"The type of initialization to use for the prompt embedding. \",\n#         parameter_metadata=LLM_METADATA[\"adapter\"][\"prompt_tuning\"][\"prompt_tuning_init\"],\n#     )\n\n#     prompt_tuning_init_text: str = schema_utils.String(\n#         default=\"\",\n#         description=\"The text to use to initialize the prompt embedding.\",\n#         parameter_metadata=LLM_METADATA[\"adapter\"][\"prompt_tuning\"][\"prompt_tuning_init_text\"],\n#     )\n\n#     def to_config(self, **kwargs) -> \"PeftConfig\":\n#         from peft import PromptTuningConfig as _PromptTuningConfig\n\n#         return _PromptTuningConfig(\n#             num_virtual_tokens=self.num_virtual_tokens,\n#             token_dim=self.token_dim,\n#             num_transformer_submodules=self.num_transformer_submodules,\n#             num_attention_heads=self.num_attention_heads,\n#             num_layers=self.num_layers,\n#             prompt_tuning_init=self.prompt_tuning_init,\n#             prompt_tuning_init_text=self.prompt_tuning_init_text,\n#             **kwargs\n#         )\n\n\n# TODO(travis): fix prefix tuning and p-tuning to work with DDP\n# @DeveloperAPI\n# @register_adapter(\"prefix_tuning\")\n# @schema_utils.ludwig_dataclass\n# class PrefixTuningConfig(BasePromptLearningConfig):\n#     \"\"\"Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/prefix_tuning.py.\"\"\"\n\n\"\"\"#     type: str = schema_utils.ProtectedString(\"prefix_tuning\")\"\"\"  # Quotes allow mypy to run without syntax errors.\n\n#     encoder_hidden_size: Optional[int] = schema_utils.Integer(\n#         default=None,\n#         allow_none=True,\n#         description=\"The hidden embedding dimension of the prompt encoder.\",\n#     )\n\n#     prefix_projection: bool = schema_utils.Boolean(\n#         default=False,\n#         description=\"Whether to use a projection layer in the prompt encoder to project the prefix tokens\",\n#     )\n\n#     def to_config(self, task_type: str = None, **kwargs) -> \"PeftConfig\":\n#         from peft import PrefixTuningConfig as _PrefixTuningConfig\n\n#         return _PrefixTuningConfig(\n#             num_virtual_tokens=self.num_virtual_tokens,\n#             token_dim=self.token_dim,\n#             num_transformer_submodules=self.num_transformer_submodules,\n#             num_attention_heads=self.num_attention_heads,\n#             num_layers=self.num_layers,\n#             encoder_hidden_size=self.encoder_hidden_size,\n#             prefix_projection=self.prefix_projection,\n#             task_type=task_type,\n#         )\n\n\n# @DeveloperAPI\n# @register_adapter(\"p_tuning\")\n# @ludwig_dataclass\n# class PTuningConfig(BasePromptLearningConfig):\n\"\"\"#     type: str = schema_utils.ProtectedString(\"p_tuning\")\"\"\"  # Quotes allow mypy to run without syntax errors.\n\n#     encoder_reparameterization_type: str = schema_utils.StringOptions(\n#         [\"MLP\", \"LSTM\"],\n#         default=\"MLP\",\n#         allow_none=False,\n#         description=\"The type of reparameterization to use for the prompt encoder.\",\n#     )\n\n#     encoder_hidden_size: Optional[int] = schema_utils.PositiveInteger(\n#         default=None,\n#         allow_none=True,\n#         description=\"The hidden embedding dimension of the prompt encoder.\",\n#     )\n\n#     encoder_num_layers: Optional[int] = schema_utils.PositiveInteger(\n#         default=2,\n#         allow_none=True,\n#         description=\"The number of layers in the prompt encoder.\",\n#     )\n\n#     encoder_dropout: Optional[float] = schema_utils.FloatRange(\n#         default=0.0,\n#         min=0.0,\n#         max=1.0,\n#         description=\"The dropout probability for the prompt encoder.\",\n#     )\n\n#     def to_config(self, task_type: str = None, **kwargs) -> \"PeftConfig\":\n#         from peft import PromptEncoderConfig as _PromptEncoderConfig\n\n#         return _PromptEncoderConfig(\n#             num_virtual_tokens=self.num_virtual_tokens,\n#             token_dim=self.token_dim,\n#             num_transformer_submodules=self.num_transformer_submodules,\n#             num_attention_heads=self.num_attention_heads,\n#             num_layers=self.num_layers,\n#             encoder_reparameterization_type=self.encoder_reparameterization_type,\n#             encoder_hidden_size=self.encoder_hidden_size,\n#             encoder_num_layers=self.encoder_num_layers,\n#             encoder_dropout=self.encoder_dropout,\n#             task_type=task_type,\n#         )\n\n\n@DeveloperAPI\n@register_adapter(\"adalora\")\n@ludwig_dataclass\nclass AdaloraConfig(LoraConfig):\n    \"\"\"Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/adalora.py.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"adalora\",\n        description=LLM_METADATA[\"adapter\"][\"adalora\"][\"type\"].long_description,\n    )\n\n    target_r: int = schema_utils.PositiveInteger(\n        default=8,\n        description=\"Target Lora Matrix Dimension. The target average rank of incremental matrix.\",\n    )\n\n    init_r: int = schema_utils.PositiveInteger(\n        default=12,\n        description=\"Initial Lora Matrix Dimension. The initial rank for each incremental matrix.\",\n    )\n\n    tinit: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The steps of initial fine-tuning warmup.\",\n    )\n\n    tfinal: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The steps of final fine-tuning warmup.\",\n    )\n\n    delta_t: int = schema_utils.NonNegativeInteger(\n        default=1,\n        description=\"The time internval between two budget allocations. The step interval of rank allocation.\",\n    )\n\n    beta1: float = schema_utils.FloatRange(\n        default=0.85,\n        min=0.0,\n        max=1.0,\n        description=\"The hyperparameter of EMA for sensitivity smoothing.\",\n    )\n\n    beta2: float = schema_utils.FloatRange(\n        default=0.85,\n        min=0.0,\n        max=1.0,\n        description=\" The hyperparameter of EMA for undertainty quantification.\",\n    )\n\n    orth_reg_weight: float = schema_utils.FloatRange(\n        default=0.5,\n        min=0.0,\n        max=1.0,\n        description=\"The coefficient of orthogonality regularization.\",\n    )\n\n    total_step: int = schema_utils.PositiveInteger(\n        default=10000,\n        allow_none=False,\n        description=\"The total training steps for AdaLoRA rank allocation scheduling. \"\n        \"Must be a positive integer (required by peft >= 0.14).\",\n    )\n\n    rank_pattern: dict | None = schema_utils.Dict(\n        default=None,\n        allow_none=True,\n        description=\"The allocated rank for each weight matrix by RankAllocator.\",\n    )\n\n    def to_config(self, **kwargs) -> \"PeftConfig\":\n        from peft import AdaLoraConfig as _AdaLoraConfig\n\n        return _AdaLoraConfig(\n            r=self.r,\n            lora_alpha=self.alpha,\n            lora_dropout=self.dropout,\n            bias=self.bias_type,\n            target_r=self.target_r,\n            init_r=self.init_r,\n            tinit=self.tinit,\n            tfinal=self.tfinal,\n            deltaT=self.delta_t,\n            beta1=self.beta1,\n            beta2=self.beta2,\n            orth_reg_weight=self.orth_reg_weight,\n            total_step=self.total_step,\n            rank_pattern=self.rank_pattern,\n        )\n\n    @classmethod\n    def name(cls) -> str:\n        return \"AdaLoRA\"\n\n    @classmethod\n    def description(cls) -> str:\n        return LLM_METADATA[\"adapter\"][\"adalora\"][\"type\"].long_description\n\n\n@DeveloperAPI\n# TODO: <Alex>02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix\n# \"TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')\"\n# (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938).\n# </Alex>\n# @register_adapter(\"adaption_prompt\")\n@ludwig_dataclass\nclass AdaptionPromptConfig(BaseAdapterConfig):\n    \"\"\"Adapted from https://github.com/huggingface/peft/blob/main/src/peft/tuners/adaption_prompt/config.py.\"\"\"\n\n    def __post_init__(self):\n        if not self.adapter_len:\n            raise ConfigValidationError(\n                \"`adapter_len` must be set to a value greater than 0 when finetuning is enabled and the adapter\"\n                \"type is `adaption_prompt`. This is the length of the adaption prompt to insert.\"\n            )\n\n        if not self.adapter_layers:\n            raise ConfigValidationError(\n                \"`adapter_layers` must be set to a value greater than 0 when finetuning is enabled and the adapter\"\n                \"type is `adaption_prompt`. This is the number of adapter layers to insert.\"\n            )\n\n    type: str = schema_utils.ProtectedString(\n        \"adaption_prompt\",\n        description=LLM_METADATA[\"adapter\"][\"adaption_prompt\"][\"type\"].long_description,\n    )\n\n    adapter_len: int = schema_utils.PositiveInteger(\n        default=4,\n        description=\"Number of adapter tokens to insert.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"adaption_prompt\"][\"adapter_len\"],\n    )\n\n    adapter_layers: int = schema_utils.PositiveInteger(\n        default=1,\n        allow_none=False,\n        description=\"Number of adapter layers to insert (from the top).\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"adaption_prompt\"][\"adapter_layers\"],\n    )\n\n    def to_config(self, task_type: str = None, **kwargs) -> \"PeftConfig\":\n        from peft import AdaptionPromptConfig as _AdaptionPromptConfig\n\n        return _AdaptionPromptConfig(\n            adapter_len=self.adapter_len,\n            adapter_layers=self.adapter_layers,\n            task_type=task_type,\n        )\n\n    @classmethod\n    def name(cls) -> str:\n        return \"Adaption Prompt\"\n\n    @classmethod\n    def description(cls) -> str:\n        return LLM_METADATA[\"adapter\"][\"adaption_prompt\"][\"type\"].long_description\n\n\n@DeveloperAPI\n@register_adapter(\"ia3\")\n@ludwig_dataclass\nclass IA3Config(BaseAdapterConfig):\n    type: str = schema_utils.ProtectedString(\n        \"ia3\",\n        description=LLM_METADATA[\"adapter\"][\"ia3\"][\"type\"].long_description,\n    )\n\n    target_modules: list[str] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=\"The names of the modules to apply (IA)^3 to.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"ia3\"][\"target_modules\"],\n    )\n\n    feedforward_modules: list[str] | None = schema_utils.List(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The names of the modules to be treated as feedforward modules, as in the original paper. These modules \"\n            \"will have (IA)^3 vectors multiplied to the input, instead of the output. feedforward_modules must be a \"\n            \"name or a subset of names present in target_modules.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"ia3\"][\"feedforward_modules\"],\n    )\n\n    fan_in_fan_out: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Set this to True if the layer to replace stores weight like (fan_in, fan_out). For example, gpt-2 uses \"\n            \"Conv1D which stores weights like (fan_in, fan_out) and hence this should be set to True. \"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"ia3\"][\"fan_in_fan_out\"],\n    )\n\n    modules_to_save: list[str] | None = schema_utils.List(\n        list_type=str,\n        default=None,\n        allow_none=True,\n        description=(\n            \"List of modules apart from (IA)^3 layers to be set as trainable and saved in the final checkpoint.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"ia3\"][\"modules_to_save\"],\n    )\n\n    init_ia3_weights: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to initialize the vectors in the (IA)^3 layers, defaults to True.\",\n        parameter_metadata=LLM_METADATA[\"adapter\"][\"ia3\"][\"init_ia3_weights\"],\n    )\n\n    def to_config(self, task_type: str = None, **kwargs) -> \"PeftConfig\":\n        from peft import IA3Config as _IA3Config\n\n        return _IA3Config(\n            target_modules=self.target_modules,\n            feedforward_modules=self.feedforward_modules,\n            fan_in_fan_out=self.fan_in_fan_out,\n            modules_to_save=self.modules_to_save,\n            init_ia3_weights=self.init_ia3_weights,\n            task_type=task_type,\n        )\n\n    @classmethod\n    def name(cls) -> str:\n        return \"IA3\"\n\n    @classmethod\n    def description(cls) -> str:\n        return LLM_METADATA[\"adapter\"][\"ia3\"][\"type\"].long_description\n\n\n@DeveloperAPI\ndef get_adapter_conds():\n    conds = []\n    for adapter_type, adapter_cls in adapter_registry.items():\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(adapter_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        preproc_cond = schema_utils.create_cond(\n            {\"type\": adapter_type},\n            other_props,\n        )\n        conds.append(preproc_cond)\n    return conds\n\n\n@DeveloperAPI\ndef AdapterDataclassField(default: str | None = None):\n    description = \"Whether to use parameter-efficient fine-tuning\"\n\n    class AdapterSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(\n                registry=adapter_registry,\n                default_value=default,\n                description=description,\n                parameter_metadata=None,\n                allow_str_value=True,\n                allow_none=True,\n            )\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return adapter_registry[key]\n\n        @staticmethod\n        def _jsonschema_type_mapping():\n            return {\n                \"oneOf\": [\n                    {\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"type\": {\n                                \"type\": \"string\",\n                                \"enum\": list(adapter_registry.keys()),\n                                \"description\": \"The type of PEFT adapter to use during fine-tuning\",\n                            },\n                        },\n                        \"title\": \"Perform parameter efficient fine-tuning\",\n                        \"allOf\": get_adapter_conds(),\n                        \"required\": [\"type\"],\n                        \"description\": \"The type of PEFT adapter to use during fine-tuning\",\n                        \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"adapter\"][\"_oneOf\"][\"allOf\"]),\n                    },\n                    {\n                        \"type\": \"null\",\n                        \"title\": \"adapter_null_option\",\n                        \"description\": \"Disable the adapter.\",\n                        \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"adapter\"][\"_oneOf\"][\"none\"]),\n                    },\n                ],\n                \"title\": \"adapter_options\",\n                \"description\": \"Whether to use parameter-efficient fine-tuning\",\n                \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"adapter\"][\"_meta\"]),\n                \"default\": default,\n            }\n\n    return AdapterSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/llms/prompt.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import SEMANTIC\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LLM_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass RetrievalConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"This Dataclass is a schema for the nested retrieval config under prompt.\"\"\"\n\n    def __post_init__(self):\n        # TODO: have a dynamically loaded schema based on the selection of the type param\n        # https://github.com/ludwig-ai/ludwig/pull/3351#discussion_r1181910954\n        # Ensure k is non-zero if we're using a retrieval strategy\n        if self.type is not None and self.k == 0:\n            self.k = 1\n\n        if self.type is None and self.k != 0:\n            raise ConfigValidationError(\"k must be 0 if retrieval type is None.\")\n        elif self.type is not None and self.k <= 0:\n            raise ConfigValidationError(\"k must be greater than 0 if retrieval type is not None.\")\n\n        if self.type is None and self.model_name is not None:\n            raise ConfigValidationError(\"model_name must be None if retrieval type is None.\")\n        elif self.type == SEMANTIC and self.model_name is None:\n            raise ConfigValidationError(f\"model_name must not be None if retrieval type is '{SEMANTIC}'.\")\n\n    type: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The type of retrieval to use for the prompt. If `None`, then no retrieval is used, and the task \"\n            \"is framed as a zero-shot learning problem. If not `None` (e.g. either 'random' or 'semantic'), then \"\n            \"samples are retrieved from an index of the training set and used to augment the input to the model \"\n            \"in a few-shot learning setting.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"retrieval\"][\"type\"],\n    )\n\n    index_name: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The name of the index to use for the prompt. Indices are stored in the ludwig cache by default.\",\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"retrieval\"][\"index_name\"],\n    )\n\n    model_name: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The model used to generate the embeddings used to retrieve samples to inject in the prompt.\",\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"retrieval\"][\"model_name\"],\n    )\n\n    k: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of samples to retrieve.\",\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"retrieval\"][\"k\"],\n    )\n\n\n@DeveloperAPI\nclass RetrievalConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(RetrievalConfig)\n\n    def _jsonschema_type_mapping(self):\n        return schema_utils.unload_jsonschema_from_marshmallow_class(RetrievalConfig, title=\"Retrieval\")\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass PromptConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"This Dataclass is a schema for the nested prompt config under preprocessing.\"\"\"\n\n    template: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"The template to use for the prompt. Must contain at least one of the columns from the input dataset \"\n            \"or `__sample__` as a variable surrounded in curly brackets {} to indicate where to insert the \"\n            \"current feature. Multiple columns can be inserted, e.g.: `The {color} {animal} jumped over \"\n            \"the {size} {object}`, where every term in curly brackets is a column in the dataset. If a `task` \"\n            \"is specified, then the template must also contain the `__task__` variable. If `retrieval` is specified, \"\n            \"then the template must also contain the `__context__` variable. If no template is provided, then a \"\n            \"default will be used based on the retrieval settings, and a task must be set in the config.\"\n        ),\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"template\"],\n    )\n\n    task: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The task to use for the prompt. Required if `template` is not set.\",\n        parameter_metadata=LLM_METADATA[\"prompt\"][\"task\"],\n    )\n\n    retrieval: RetrievalConfig = RetrievalConfigField().get_default_field()\n\n\n@DeveloperAPI\nclass PromptConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(PromptConfig)\n\n    def _jsonschema_type_mapping(self):\n        return schema_utils.unload_jsonschema_from_marshmallow_class(PromptConfig)\n"
  },
  {
    "path": "ludwig/schema/llms/quantization.py",
    "content": "import warnings\n\nfrom transformers import BitsAndBytesConfig\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import LLM_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json\nfrom ludwig.schema.utils import ludwig_dataclass\n\nwarnings.filterwarnings(\n    action=\"ignore\",\n    category=UserWarning,\n    module=\"bitsandbytes.cuda_setup.main\",\n)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass QuantizationConfig(schema_utils.BaseMarshmallowConfig):\n    bits: int = schema_utils.IntegerOptions(\n        options=[4, 8],\n        default=4,\n        description=\"The quantization level to apply to weights on load.\",\n        parameter_metadata=LLM_METADATA[\"quantization\"][\"bits\"],\n    )\n\n    llm_int8_threshold: float = schema_utils.NonNegativeFloat(\n        default=6.0,\n        description=(\n            \"This corresponds to the outlier threshold for outlier detection as described in `LLM.int8() : 8-bit \"\n            \"Matrix Multiplication for Transformers at Scale` paper: https://arxiv.org/abs/2208.07339. Any hidden \"\n            \"states value that is above this threshold will be considered an outlier and the operation on those \"\n            \"values will be done in fp16. Values are usually normally distributed, that is, most values are in the \"\n            \"range [-3.5, 3.5], but there are some exceptional systematic outliers that are very differently \"\n            \"distributed for large models. These outliers are often in the interval [-60, -6] or [6, 60]. Int8 \"\n            \"quantization works well for values of magnitude ~5, but beyond that, there is a significant performance \"\n            \"penalty. A good default threshold is 6, but a lower threshold might be needed for more unstable models \"\n            \"(small models, fine-tuning).\"\n        ),\n    )\n\n    llm_int8_has_fp16_weight: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"This flag runs LLM.int8() with 16-bit main weights. This is useful for fine-tuning as the weights do \"\n            \"not have to be converted back and forth for the backward pass.\"\n        ),\n    )\n\n    bnb_4bit_compute_dtype: str = schema_utils.StringOptions(\n        options=[\"float32\", \"float16\", \"bfloat16\"],\n        default=\"float16\",\n        description=(\n            \"This sets the computational type which might be different than the input type. For example, inputs \"\n            \"might be fp32, but computation can be set to bf16 for speedups.\"\n        ),\n    )\n\n    bnb_4bit_use_double_quant: bool = schema_utils.Boolean(\n        default=True,\n        description=(\n            \"This flag is used for nested quantization where the quantization constants from the first quantization \"\n            \"are quantized again.\"\n        ),\n    )\n\n    bnb_4bit_quant_type: str = schema_utils.StringOptions(\n        options=[\"fp4\", \"nf4\"],\n        default=\"nf4\",\n        description=\"This sets the quantization data type in the bnb.nn.Linear4Bit layers.\",\n    )\n\n    def to_bitsandbytes(self) -> BitsAndBytesConfig:\n        return BitsAndBytesConfig(\n            load_in_4bit=self.bits == 4,\n            load_in_8bit=self.bits == 8,\n            llm_int8_threshold=self.llm_int8_threshold,\n            llm_int8_has_fp16_weight=self.llm_int8_has_fp16_weight,\n            bnb_4bit_compute_dtype=self.bnb_4bit_compute_dtype,\n            bnb_4bit_use_double_quant=self.bnb_4bit_use_double_quant,\n            bnb_4bit_quant_type=self.bnb_4bit_quant_type,\n        )\n\n\n@DeveloperAPI\nclass QuantizationConfigField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(QuantizationConfig, default_missing=True)\n\n    def _jsonschema_type_mapping(self):\n        return {\n            \"oneOf\": [\n                {\n                    \"type\": \"null\",\n                    \"title\": \"disabled\",\n                    \"description\": \"Disable quantization.\",\n                    \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"quantization\"][\"_oneOf\"][\"none\"]),\n                },\n                {\n                    **schema_utils.unload_jsonschema_from_marshmallow_class(QuantizationConfig),\n                    \"title\": \"enabled\",\n                    \"description\": \"Set quantization options.\",\n                    \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"quantization\"][\"_oneOf\"][\"object\"]),\n                },\n            ],\n            \"title\": \"quantization\",\n            \"description\": \"Set quantization options.\",\n            \"parameter_metadata\": convert_metadata_to_json(LLM_METADATA[\"quantization\"][\"_meta\"]),\n        }\n"
  },
  {
    "path": "ludwig/schema/lr_scheduler.py",
    "content": "from abc import ABC\nfrom dataclasses import field\n\nimport ludwig.schema.utils as schema_utils\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import LOSS, MODEL_ECD, TRAINING\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.metadata import TRAINER_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass LRSchedulerConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Configuration for learning rate scheduler parameters.\"\"\"\n\n    decay: str = schema_utils.StringOptions(\n        options=[\"linear\", \"exponential\", \"cosine\"],\n        default=None,\n        allow_none=True,\n        description=\"Turn on decay of the learning rate.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"decay\"],\n    )\n\n    decay_rate: float = schema_utils.FloatRange(\n        default=0.96,\n        min=0,\n        max=1,\n        description=\"Decay per epoch (%): Factor to decrease the Learning rate.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"decay_rate\"],\n    )\n\n    decay_steps: int = schema_utils.PositiveInteger(\n        default=10000,\n        description=\"The number of steps to take in the exponential learning rate decay.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"decay_steps\"],\n    )\n\n    staircase: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Decays the learning rate at discrete intervals.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"staircase\"],\n    )\n\n    reduce_on_plateau: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=(\n            \"How many times to reduce the learning rate when the algorithm hits a plateau (i.e. the performance on the \"\n            \"training set does not improve)\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"reduce_on_plateau\"],\n    )\n\n    reduce_on_plateau_patience: int = schema_utils.NonNegativeInteger(\n        default=10,\n        description=(\n            \"How many evaluation steps have to pass before the learning rate reduces \" \"when `reduce_on_plateau > 0`.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"reduce_on_plateau_patience\"],\n    )\n\n    reduce_on_plateau_rate: float = schema_utils.FloatRange(\n        default=0.1,\n        min=0,\n        max=1,\n        description=\"Rate at which we reduce the learning rate when `reduce_on_plateau > 0`.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"reduce_on_plateau_rate\"],\n    )\n\n    warmup_evaluations: int = schema_utils.NonNegativeFloat(\n        default=0,\n        description=\"Number of evaluation steps to warmup the learning rate for.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"warmup_evaluations\"],\n    )\n\n    warmup_fraction: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Fraction of total training steps to warmup the learning rate for.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"warmup_fraction\"],\n    )\n\n    reduce_eval_metric: str = schema_utils.String(\n        default=LOSS,\n        allow_none=False,\n        description=(\n            \"Metric plateau used to trigger when we reduce the learning rate \" \"when `reduce_on_plateau > 0`.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"reduce_eval_metric\"],\n    )\n\n    reduce_eval_split: str = schema_utils.String(\n        default=TRAINING,\n        allow_none=False,\n        description=(\n            \"Which dataset split to listen on for reducing the learning rate \" \"when `reduce_on_plateau > 0`.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"reduce_eval_split\"],\n    )\n\n    # Parameters for CosineAnnealingWarmRestarts scheduler\n\n    t_0: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of steps before the first restart for cosine annealing decay. If not specified, it\"\n        \" will be set to `steps_per_checkpoint`.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"t_0\"],\n    )\n\n    t_mult: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Period multiplier after each restart for cosine annealing decay. Defaults to 1, i.e.,\"\n        \" restart every `t_0` steps. If set to a larger value, the period between restarts increases by that\"\n        \" 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.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"t_mult\"],\n    )\n\n    eta_min: float = schema_utils.FloatRange(\n        default=0,\n        min=0,\n        max=1,\n        description=\"Minimum learning rate allowed for cosine annealing decay. Default: 0.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scheduler\"][\"eta_min\"],\n    )\n\n\n# TODO(travis): too much boilerplate here, we should find a way to abstract all this and only require specifying the\n# minimal amount needed for the new config object.\n@DeveloperAPI\ndef LRSchedulerDataclassField(description: str, default: dict = None):\n    \"\"\"Returns custom dataclass field for `LRSchedulerConfig`. Allows `None` by default.\n\n    Args:\n        description: Description of the dataclass field\n        default: dict that specifies param values that will be loaded by its schema class (default: None).\n    \"\"\"\n    allow_none = True\n    default = default or {}\n\n    class LRSchedulerMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field class for learning rate scheduler.\n\n        Deserializes a dict to a valid instance of `LRSchedulerConfig` and creates a corresponding JSON schema for\n        external usage.\n        \"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return value\n            if isinstance(value, dict):\n                try:\n                    return LRSchedulerConfig.Schema().load(value)\n                except (TypeError, ConfigValidationError) as e:\n                    raise ConfigValidationError(\n                        f\"Invalid params for learning rate scheduler: {value}, see LRSchedulerConfig class. Error: {e}\"\n                    )\n            raise ConfigValidationError(\"Field should be None or dict\")\n\n        def _jsonschema_type_mapping(self):\n            return {\n                **schema_utils.unload_jsonschema_from_marshmallow_class(LRSchedulerConfig),\n                \"title\": \"learning_rate_scheduler_options\",\n                \"description\": description,\n            }\n\n    if not isinstance(default, dict):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n\n    load_default = lambda: LRSchedulerConfig.Schema().load(default)\n    dump_default = LRSchedulerConfig.Schema().dump(default)\n\n    return field(\n        metadata={\n            \"marshmallow_field\": LRSchedulerMarshmallowField(\n                allow_none=allow_none,\n                load_default=load_default,\n                dump_default=dump_default,\n                metadata={\n                    \"description\": description,\n                },\n            )\n        },\n        default_factory=load_default,\n    )\n"
  },
  {
    "path": "ludwig/schema/metadata/__init__.py",
    "content": "import os\nfrom typing import Any\n\nimport yaml\n\nfrom ludwig.schema.metadata.parameter_metadata import ParameterMetadata\n\n_PATH_HERE = os.path.abspath(os.path.dirname(__file__))\n_CONFIG_DIR = os.path.join(_PATH_HERE, \"configs\")\n\n\ndef _to_metadata(d: dict[str, Any]) -> ParameterMetadata | dict[str, Any]:\n    is_nested = False\n    for k, v in list(d.items()):\n        if isinstance(v, dict):\n            d[k] = _to_metadata(v)\n            is_nested = True\n\n    if is_nested:\n        return d\n\n    return ParameterMetadata.from_dict(d)\n\n\ndef _load(fname: str) -> dict[str, Any]:\n    with open(os.path.join(_CONFIG_DIR, fname)) as f:\n        return _to_metadata(yaml.safe_load(f))\n\n\nCOMMON_METADATA = _load(\"common.yaml\")\nCOMBINER_METADATA = _load(\"combiners.yaml\")\nDECODER_METADATA = _load(\"decoders.yaml\")\nENCODER_METADATA = _load(\"encoders.yaml\")\nFEATURE_METADATA = _load(\"features.yaml\")\nPREPROCESSING_METADATA = _load(\"preprocessing.yaml\")\nTRAINER_METADATA = _load(\"trainer.yaml\")\nOPTIMIZER_METADATA = _load(\"optimizers.yaml\")\nLOSS_METADATA = _load(\"loss.yaml\")\nLLM_METADATA = _load(\"llm.yaml\")\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/combiners.yaml",
    "content": "comparator:\n    type:\n        short_description: Used for recommendation problems, features associated with distinct entities, output depends on entity-level comparison.\n        long_description:\n            The comparator combiner compares the hidden representation of two entities defined by lists of\n            features. It assumes all outputs from encoders are tensors of size `b x h` where `b` is the batch\n            size and `h` is the hidden dimension, which can be different for each input. If the input tensors\n            have a different shape, it automatically flattens them. It then concatenates the representations\n            of each entity and projects them both to vectors of size `output_size`. Finally, it compares the\n            two entity representations by dot product, element-wise multiplication, absolute difference and\n            bilinear product. It returns the final `b x h` tensor where `h` is the size of the concatenation\n            of the four comparisons.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    entity_1:\n        literature_references:\n            - https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner\n        ui_display_name: Entity 1\n        expected_impact: 3\n    entity_2:\n        literature_references:\n            - https://ludwig.ai/0.6/configuration/combiner/#comparator-combiner\n        ui_display_name: Entity 2\n        expected_impact: 3\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        ui_display_name: null\n        expected_impact: 1\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 15 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: \"TRUE\"\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nconcat:\n    type:\n        short_description: Concatenates outputs of all encoders and passes concatenated representation through stack of fully connected layers.\n        long_description:\n            The concat combiner assumes all outputs from encoders are tensors of size `b x h` where `b` is\n            the batch size and `h` is the hidden dimension, which can differ for each input. It\n            concatenates along the `h` dimension, and then (optionally) passes the concatenated tensor\n            through a stack of fully connected layers. It returns the final `b x h` tensor where `h` is the\n            size of the last fully connected layer or the sum of the sizes of the `h` of all inputs in the\n            case there are no fully connected layers. If there is only a single input feature and no fully\n            connected layers, the output of the input feature encoder is passed through the combiner\n            unchanged.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    flatten_inputs:\n        ui_display_name: null\n        expected_impact: 1\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        ui_display_name: null\n        expected_impact: 1\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 16 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    residual:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: \"TRUE\"\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nproject_aggregate:\n    type:\n        short_description: Projects the encoder outputs to a common size then takes the average.\n        long_description:\n            The project aggregate combiner projects the input vectors to a common size\n            and then aggregates them by taking the average across all the vectors.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        ui_display_name: null\n        expected_impact: 1\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 17 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    projection_size:\n        ui_display_name: null\n        expected_impact: 1\n    residual:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: \"TRUE\"\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nsequence:\n    type:\n        short_description: Stacks a sequence concat combiner with a sequence encoder.\n        long_description:\n            The sequence combiner stacks a sequence concat combiner with a sequence encoder. All the\n            considerations about input tensor ranks described for the sequence concat combiner apply also in\n            this case, but the main difference is that this combiner uses the `b x s x h` output of the\n            sequence concat combiner, where `b` is the batch size, `s` is the sequence length and `h` is the\n            sum of the hidden dimensions of all input features, as input for any of the sequence encoders\n            described in the sequence features encoders section. All considerations on the shape of\n            the outputs for the sequence encoders also apply to the sequence combiner.\n    encoder:\n        ui_display_name: null\n        expected_impact: 3\n    main_sequence_feature:\n        ui_display_name: null\n        expected_impact: 3\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\nsequence_concat:\n    type:\n        short_description: Concatenates the outputs of multiple sequence features.\n        long_description:\n            The sequence_concat combiner assumes at least one output from the encoders is a tensor of size\n            `b x s x h` where `b` is the batch size, `s` is the length of the sequence and `h` is the hidden\n            dimension. A sequence-like (sequence, text or time series) input feature can be specified with\n            the `main_sequence_feature` parameter which takes the name of sequence-like input feature as its\n            value. If no `main_sequence_feature` is specified, the combiner will look through all the\n            features in the order they are defined in the configuration and will look for a feature with a\n            rank 3 tensor output (sequence, text or time series). If it cannot find one it will raise an\n            exception, otherwise the output of that feature will be used for concatenating the other features\n            along the sequence `s` dimension.\n\n            If there are other input features with a rank 3 output tensor, the combiner will concatenate\n            them alongside the s dimension, which means that all of them must have identical s dimension,\n            otherwise a dimension mismatch error will be returned thrown during training when a datapoint\n            with two sequential features of different lengths are provided.\n\n            Other features that have a b x h rank 2 tensor output will be replicated s times and\n            concatenated to the s dimension. The final output is a b x s x h' tensor where h' is the size of\n            the concatenation of the h dimensions of all input features.\n    main_sequence_feature:\n        ui_display_name: null\n        expected_impact: 3\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\ntabnet:\n    type:\n        short_description: Tabnet is specifically tailored for high performance on tabular data.\n        long_description:\n            The tabnet combiner implements the TabNet model, which uses attention and sparsity to achieve\n            high performance on tabular data. It assumes all outputs from encoders are tensors of size b x h\n            where b is the batch size and h is the hidden dimension, which can be different for each input.\n            If the input tensors have a different shape, it automatically flattens them. It returns the\n            final b x h' tensor where h' is the user-specified output size.\n        literature_references:\n            - https://arxiv.org/abs/1908.07442\n        compute_tier: 1\n    bn_epsilon:\n        default_value_reasoning:\n            Default value found in popular ML packages like Keras\n            and Tensorflow.\n        description_implications:\n            An epsilon is added to the denominator of the batch\n            normalization operation so that the function converges. Setting the epsilon\n            to 0 is inadvisable.\n        example_value:\n            - 1.0e-05\n        expected_impact: 1\n        literature_references:\n            - \"[Keras example](https://keras.io/api/layers/normalization_layers/batch_normalization/)\"\n        suggested_values: 1e-3-1e-9\n        suggested_values_reasoning: Common epsilon choices\n        ui_display_name: Batch Normalization Epsilon\n    bn_momentum:\n        description_implications:\n            \"Higher values result in faster updates, but more\n            sensitivity to noise in the dataset.  Lower values result in slower updates.\n\n\n            If momentum is set to 0, moving statistics will not be updated during\n            training. This is likely to cause variance between train and test performance,\n            and is not recommended.\"\n        example_value:\n            - 0.05\n        literature_references:\n            - \"TabNet Paper: https://arxiv.org/abs/1908.07442\"\n            - \"Torch Batch Norm: https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm1d.html\"\n        other_information:\n            \"`bn_momentum` is only used if `norm`: `batch`.  For other\n            values of `norm` it has no effect.\n\n\n            `bn_momentum` is different from optimizer momentum.  Batch norm moving\n            estimate statistics are updated according to the rule:\n\n            x_hat = (1 - momentum) * x_hat + momentum * x_t,\n\n            where x_hat is the estimated statistic and x_t is the new observed value.\"\n        suggested_values: 0.01-0.2\n        ui_display_name: Batch Norm Momentum\n        expected_impact: 1\n    bn_virtual_bs:\n        default_value_reasoning: Paper default.\n        description_implications:\n            Virtual Batch Normalization is a normalization method\n            that extends batch normalization. Regular batch normalization causes the\n            output of a neural network for an input example  to be highly dependent\n            on several other inputs  in the same minibatch. To avoid this problem\n            in virtual batch normalization (VBN), each example is normalized based\n            on the statistics collected on a reference batch of examples that are\n            chosen once and fixed at the start of training, and on itself. The reference\n            batch is normalized using only its own statistics. VBN is computationally\n            expensive because it requires running forward propagation on two minibatches\n            of data, so the authors use it only in the generator network. A higher\n            virtual batch size could improve normalization, but it also causes training\n            to run slower since each batch will be sampled multiple times.\n        expected_impact: 1\n        literature_references:\n            - https://paperswithcode.com/method/virtual-batch-normalization\n        ui_display_name: \"Ghost Normalization: Virtual batch size\"\n    dropout:\n        default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1908.07442).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    entmax_alpha:\n        ui_display_name: null\n        expected_impact: 1\n    entmax_mode:\n        ui_display_name: null\n        expected_impact: 1\n    num_shared_blocks:\n        ui_display_name: null\n        expected_impact: 1\n    num_steps:\n        ui_display_name: null\n        expected_impact: 1\n    num_total_blocks:\n        ui_display_name: null\n        expected_impact: 1\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 18 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    relaxation_factor:\n        ui_display_name: null\n        expected_impact: 1\n    size:\n        ui_display_name: null\n        expected_impact: 3\n    sparsity:\n        ui_display_name: null\n        expected_impact: 1\ntabtransformer:\n    type:\n        short_description: Projects and concatenates features, then passes them through a transformer.\n        long_description:\n            The tabtransformer combiner combines input features in the following sequence of operations.\n            Except for binary and number features, the combiner projects features to an embedding size.\n            These features are concatenated as if they were a sequence and passed through a transformer.\n            After the transformer, the number and binary features are concatenated (which are of size 1) and\n            then concatenated with the output of the transformer and is passed to a stack of fully connected\n            layers (from TabTransformer Tabular Data Modeling Using Contextual Embeddings). It assumes all\n            outputs from encoders are tensors of size `b x h` where `b` is the batch size and `h` is the\n            hidden dimension, which can be different for each input. If the input tensors have a different\n            shape, it automatically flattens them. It then projects each input tensor to the same hidden /\n            embedding size and encodes them with a stack of Transformer layers. Finally, the transformer\n            combiner applies a reduction to the outputs of the Transformer stack, followed by the above\n            concatenation and optional fully connected layers. The output is a `b x h` tensor where `h` is the\n            size of the last fully connected layer or the hidden / embedding size, or a `b x n x h` where `n`\n            is the number of input features and `h` is the hidden / embedding size if no reduction is applied.\n        literature_references:\n            - https://arxiv.org/abs/2012.06678\n        compute_tier: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1706.03762).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embed_input_feature_name:\n        default_value_reasoning:\n            Though the ideal embedding size depends on the task\n            and dataset, setting the feature embedding size equal to the hidden size\n            and adding feature embeddings to hidden representations ('add') is a good\n            starting point.\n        description_implications:\n            Input feature name embeddings have been shown to\n            improve performance of deep learning methods on tabular data. Feature\n            name embeddings play a similar role to positional embeddings in a language\n            model, allowing the network to learn conditional dependencies between\n            input features.\n        example_value:\n            - 64\n        literature_references:\n            - \"TabTransformer: Tabular Data Modeling Using Contextual Embeddings\"\n        other_information:\n            Must be an integer, 'add', or null. If an integer, specifies\n            the embedding size for input feature names. Input feature name embeddings\n            will be concatenated to hidden representations. Must be less than or equal\n            to hidden_size. If 'add', input feature names use embeddings the same\n            size as hidden_size, and are added (element-wise) to the hidden representations.\n            If null, input feature embeddings are not used.\n        related_parameters:\n            - hidden_size\n        ui_display_name: Embed Input Feature Name\n        expected_impact: 3\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    fc_residual:\n        ui_display_name: null\n        expected_impact: 1\n    hidden_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 2\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        ui_display_name: null\n        expected_impact: 1\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_heads:\n        default_value_reasoning:\n            \"The middle value explored in the original TabTransformer\n            paper. Source: https://arxiv.org/pdf/2012.06678.pdf\"\n        description_implications:\n            Increasing the number of attention heads can increase\n            model performance at the cost of additional compute and memory.\n        example_value:\n            - 8\n        expected_impact: 1\n        literature_references:\n            - https://arxiv.org/pdf/2012.06678.pdf\n        suggested_values: 16\n        suggested_values_reasoning:\n            If your model is underperforming, increasing the\n            number of attention heads can improve its ability to correlate items in\n            a sequence.\n        ui_display_name: Number of attention heads\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            \"The ideal number of transformer layers depends\n            on the length and complexity of input sequences, as well as the task.\n\n\n            For more complex tasks, and higher number of transformer layers may be\n            useful. However, too many layers will increase memory and slow training\n            while providing diminishing returns of model performance.\"\n        example_value:\n            - 1\n        expected_impact: 1\n        suggested_values: 1 - 12\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Transformer Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 19 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    transformer_output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 2\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 20 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Transformer Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: \"TRUE\"\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\ntransformer:\n    type:\n        short_description: The transformer combiner combines input features using a stack of Transformer blocks.\n        long_description:\n            The transformer combiner combines input features using a stack of Transformer blocks (from\n            Attention Is All You Need). It assumes all outputs from encoders are tensors of size `b x h`\n            where `b` is the batch size and `h` is the hidden dimension, which can be different for each\n            input. If the input tensors have a different shape, it automatically flattens them. It then\n            projects each input tensor to the same hidden / embedding size and encodes them with a stack of\n            Transformer layers. Finally, the transformer combiner applies a reduction to the outputs of the\n            Transformer stack, followed by optional fully connected layers. The output is a `b x h` tensor\n            where `h` is the size of the last fully connected layer or the hidden / embedding size, or a\n            `b x n x h` where `n` is the number of input features and `h` is the hidden / embedding size if\n            no reduction is applied.\n        literature_references:\n            - https://arxiv.org/abs/1706.03762\n        compute_tier: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1706.03762).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    fc_residual:\n        ui_display_name: null\n    hidden_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 2\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        ui_display_name: null\n        expected_impact: 1\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_heads:\n        ui_display_name: null\n        expected_impact: 1\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            \"The ideal number of transformer layers depends\n            on the length and complexity of input sequences, as well as the task.\n\n\n            For more complex tasks, and higher number of transformer layers may be\n            useful. However, too many layers will increase memory and slow training\n            while providing diminishing returns of model performance.\"\n        example_value:\n            - 1\n        expected_impact: 1\n        suggested_values: 1 - 12\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Transformer Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 21 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    transformer_output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 2\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 22 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Transformer Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: \"TRUE\"\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/common.yaml",
    "content": "activation:\n  default_value_reasoning: The Rectified Linear Units (ReLU) function is the\n    standard activation function used for adding non-linearity. It is simple,\n    fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n  description_implications: Changing the activation functions has an impact\n    on the computational load of the model and might require further hypterparameter\n    tuning\n  expected_impact: 2\n  suggested_values: relu\n  suggested_values_reasoning: ReLU will work well in the majority of the cases\n  ui_display_name: Activation\nbias_initializer:\n  default_value_reasoning: It is possible and common to initialize the biases\n    to be zero, since the asymmetry breaking is provided by the small random\n    numbers in the weights.\n  description_implications: It's rare to see any performance gains from choosing\n    a different bias initialization. Some practitioners like to use a small\n    constant value such as 0.01 for all biases to ensure that all ReLU units\n    are activated in the beginning and have some effect on the gradient. However,\n    it's still an open question as to whether this provides consistent improvement.\n  expected_impact: 1\n  literature_references:\n    - https://cs231n.github.io/neural-networks-2/\n  related_parameters:\n    - weights_initializer\n  suggested_values: zeros\n  suggested_values_reasoning: It is possible and common to initialize the biases\n    to be zero, since the asymmetry breaking is provided by the small random\n    numbers in the weights. For ReLU non-linearities, some people like to\n    use small constant value such as 0.01 for all biases because this ensures\n    that all ReLU units fire in the beginning and therefore obtain and propagate\n    some gradient. However, it is not clear if this provides a consistent\n    improvement (in fact some results seem to indicate that this performs\n    worse) and it is more common to simply use 0 bias initialization.\n  ui_display_name: Bias Initializer\ndropout:\n  default_value_reasoning: Dropout can cause training to become less stable.\n    Consider start with a dropout-free baseline, and add dropout gradually\n    in subsequent experiments.\n  description_implications: \"Dropout is a computationally cheap regularization\\\n    \\ method where during training, some neurons are randomly ignored or \\u201C\\\n    dropped out\\u201D. Increasing dropout has the effect of making the training\\\n    \\ process more noisy and lowering overall network capacity, but it can\\\n    \\ be an effective regularization method to reduce overfitting and improve\\\n    \\ generalization.\"\n  example_value:\n    - 0.2\n  expected_impact: 3\n  literature_references:\n    - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n  suggested_values: 0.05 - 0.8\n  suggested_values_reasoning: Tuning dropout is really something to be done\n    when all of the big choices about architecture have been settled. Consider\n    starting with 0.5 and adjusting the dropout depending on observed model\n    performance.\n  ui_display_name: Dropout\nfc_layers:\n  default_value_reasoning: By default the stack is built by using num_fc_layers,\n    output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n    activation, dropout. When a list of dictionaries is provided, the stack\n    is built following the parameters of each dict for building each layer.\n  description_implications: The more layers that are specified the deeper and\n    higher capacity the model will be. This makes it possible to potentially\n    achieve better performance when a big anough amount of data is provided,\n    but also makes the model more computationally expensive and potentially\n    more prone to overfitting.\n  example_value:\n    - dropout: 0.1\n      output_size: 128\n    - norm: layer\n      output_size: 64\n  expected_impact: 1\n  related_parameters:\n    - output_size\n    - use_bias\n    - weights_initializer\n    - bias_initializer\n    - norm\n    - norm_params\n    - activation\n    - dropout\n  suggested_values_reasoning: It is easier to define a stack of fully connected\n    layers by just specifying num_fc_layers, output_size and the other individual\n    parameters. It will create a stack of layers with identical properties.\n    Use this parameter only if you need a fine grained level of control of\n    each individual layer in the stack.\n  ui_display_name: Fully Connected Layers\nflatten_inputs:\n  ui_display_name: null\n  expected_impact: 1\nnorm:\n  default_value_reasoning: While batch normalization and layer normalization\n    usually lead to improvements, it can be useful to start with fewer bells\n    and whistles.\n  description_implications: Normalization helps stabilize the learning process\n    and can have a regularizing effect that can help with generalization.\n    It's often suggested that with normalization, you can use a higher learning\n    rate.\n  example_value:\n    - batch\n  expected_impact: 3\n  literature_references:\n    - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n  related_parameters:\n    - norm_params\n  suggested_values_reasoning: Normalization tries to solve \"internal covariate\n    shift\" that comes from the changing distributions of the inputs to layers\n    deep in the network when weights are updated. For example, batch normalization\n    standardizes the inputs to a layer for each mini-batch. Try out different\n    normalizations to see if that helps with training stability\n  ui_display_name: Normalization Type\nnorm_params:\n  ui_display_name: null\n  expected_impact: 1\nnum_fc_layers:\n  default_value_reasoning:\n    The encoder already has learnable parameters.Sometimes\n    the default is 1 for modules where the FC stack is used for shape management,\n    or the only source of learnable parameters.\n  description_implications: Increasing num_fc_layers will increase the capacity\n    of the model. The model will be slower to train, and there's a higher\n    risk of overfitting.\n  example_value:\n    - 1\n  expected_impact: 3\n  other_information:\n    Not all modules that have fc_layers also have an accompanying\n    num_fc_layers parameter. Where both are present, fc_layers takes precedent\n    over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n    layers that are configured by the defaults in FCStack.\n  related_parameters:\n    - fc_layers\n  suggested_values: 0-1\n  suggested_values_reasoning: The full model likely contains many learnable\n    parameters. Consider starting with very few, or without any additional\n    fully connected layers and add them if you observe evidence of limited\n    model capacity. Sometimes the default is 1 for modules where the FC stack\n    is used for shape management, or the only source of learnable parameters.\n  ui_display_name: Number of Fully Connected Layers\noutput_size:\n  default_value_reasoning: A modest value, not too small, not too large.\n  description_implications: If there are fully connected layers in this module,\n    increasing the output size of each fully connected layer will increase\n    the capacity of the model. However, the model may be slower to train,\n    and there's a higher risk of overfitting. If it seems like the model could\n    use even more capacity, consider increasing the number of fully connected\n    layers, or explore other architectures.\n  expected_impact: 3\n  other_information: If num_fc_layers=0 and fc_layers=None, and there are no\n    fully connected layers defined on the module, then this parameter may\n    have no effect on the module's final output shape.\n  related_parameters:\n    - num_fc_layers, fc_layers\n  suggested_values: 16 - 1024\n  suggested_values_reasoning: Increasing the output size increases the capacity\n    of the model. If this seems to have a positive effect, then it could be\n    worth increasing the number of layers, or trying a different architecture\n    with a larger capacity.\n  ui_display_name: Output Size\nresidual:\n  ui_display_name: null\n  expected_impact: 1\nuse_bias:\n  default_value_reasoning: \"Bias terms may improve model accuracy, and don't\n    have much impact in terms of memory or training speed. For most models\n    it is reasonable to use bias terms.\n\n\n    Batch Normalization, however, adds a trainable shift parameter which is\n    added to the activation. When Batch Normalization is used in a layer,\n    bias terms are redundant and may be removed.\"\n  description_implications: Bias terms may improve model accuracy, and don't\n    have much impact in terms of memory or training speed. For most models\n    it is reasonable to leave this parameter set to True.\n  example_value:\n    - true\n  expected_impact: 1\n  other_information: If fc_layers is not specified, or use_bias is not specified\n    for individual layers, the value of use_bias will be used as the default\n    for all layers.\n  related_parameters:\n    - bias_initializer, fc_layers\n  suggested_values: \"TRUE\"\n  ui_display_name: Use Bias\nweights_initializer:\n  default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n  description_implications: The method you choose to initialize layer weights\n    during training can have a big impact on performance as well as the reproducibility\n    of your final model between runs. As an example, if you were to randomly\n    initialize weights you would risk non-reproducibility (and possibly general\n    training performance), but sticking with constant values for initialization\n    might significantly increase the time needed for model convergence. Generally,\n    choosing one of the probabilistic approaches strikes a balance between\n    the two extremes, and the literature kicked off by the landmark [*Xavier\n    et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n    provides a few good options. See this nice discussion from [Weights and\n    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.)\n    for more information.\n  expected_impact: 1\n  literature_references:\n    - \"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    - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n  suggested_values: xavier_uniform\n  suggested_values_reasoning: Changing the weights initialization scheme is\n    something to consider if a model is having trouble with convergence, or\n    otherwise it is something to experiment with after other factors are considered.\n    The default choice (`xavier_uniform`) is a suitable starting point for\n    most tasks.\n  ui_display_name: Layer Weights Initializer\nembedding_initializer:\n  default_value_reasoning: According to https://arxiv.org/abs/1711.09160, choice\n    of embedding initialization is not important as long as the variance is\n    kept reasonably low.\n  description_implications:\n    According to https://arxiv.org/abs/1711.09160, choice\n    of embedding initialization is not important as long as the variance is\n    kept reasonably low.\n  example_value:\n    - kaiming\n  expected_impact: 1\n  literature_references:\n    - https://arxiv.org/abs/1711.09160\n  suggested_values: kaiming\n  suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326\n  ui_display_name: Embedding Initialization\nembedding_size:\n  default_value_reasoning: Not too big, not too small.\n  description_implications: 'An embedding is a relatively low-dimensional space\n    that is used to translate high-dimensional vectors like words, which can\n    have a large vocbulary size. Ideally, after an embedding is trained, it\n    captures some of the semantics of the input by placing semantically similar\n    inputs close together in the embedding space.\n\n\n    In most cases, the embedding size is chosen empirically, by trial and\n    error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n    to use the fourth root of the total number of unique categorical elements\n    while another is that the embedding dimension should be approximately\n    1.6 times the square root of the number of unique elements in the category,\n    and no less than 600.\"\n\n\n    Increasing the embedding size may cause the model to train more slowly,\n    but the higher dimensionality can also improve overall quality.'\n  expected_impact: 3\n  literature_references:\n    - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n  suggested_values: 1.6 * sqrt(vocab_size)\n  suggested_values_reasoning:\n    Rule of thumb suggested by a deep learning textbook.\n    Try models with smaller or larger embedding sizes to observe relative\n    impact.\n  ui_display_name: Embedding Size\nembeddings_on_cpu:\n  default_value_reasoning: By default embeddings matrices are stored on GPU\n    memory if a GPU is used, as it allows for faster access.\n  description_implications: By default embeddings matrices are stored on GPU\n    memory if a GPU is used, as it allows for faster access. However, in some\n    cases when the vocabulary size is very large, the full embedding matrix\n    may be really big and unwieldy to have in GPU memory. This parameter forces\n    the placement of the embedding matrix in regular memory and the CPU is\n    used to access them. This may slow down training due to additional data\n    transfer between CPU and GPU memory, but can lead to healthier GPU memory\n    resource usage.\n  expected_impact: 1\n  suggested_values:\n    - false\n  suggested_values_reasoning:\n    If GPU memory is not a constraint, having embeddings\n    stored and accessed within the GPU is faster.\n  ui_display_name: Embeddings on CPU\nembeddings_trainable:\n  default_value_reasoning:\n    If trained from scratch, embedding vectors are typically\n    learned alongside the rest of the model.\n  description_implications:\n    Typically this value is only set to False if pre-trained\n    embeddings are uploaded. Even then, it is reasonable to leave it as True\n    in order to fine-tune the embeddings.\n  expected_impact: 1\n  related_parameters:\n    - embedding_size, representation, pretrained_embeddings\n  ui_display_name: (under Embeddings header) Trainable?\npretrained_embeddings:\n  default_value_reasoning: Embeddings are commonly trained from scratch, or\n    incorporated as part of a pre-trained model package.\n  description_implications: If pretrained embeddings are specified, then the\n    model may have a head start in its representation of various input entities.\n  example_value:\n    - ~/Downloads/glove.6B.100d.txt\n  expected_impact: 0\n  related_parameters:\n    - embedding_size, embeddings_trainable\n  ui_display_name: Pretrained embeddings path\nmax_sequence_length:\n  default_value_reasoning: Sets the maximum sequence length of the expected\n    inputs, so input/output shapes are computed accurately.\n  internal_only: true\n  ui_display_name: null\nvocab:\n  default_value_reasoning: Computed and passed along internally according to\n    preprocessing settings.\n  example_value:\n    - a\n    - b\n    - c\n  internal_only: true\n  ui_display_name: Not Displayed\nvocab_size:\n  internal_only: true\n  ui_display_name: Not displayed\nrepresentation:\n  default_value_reasoning: Trainable, randomly initialized embedding vectors\n    often lead to more subtle representations of input entities than one-hot\n    vectors.\n  description_implications: If set to sparse, the representations for input\n    entities are fixed as one-hot vectors. This leads to less flexible representations\n    for input entities, but could lead to faster training since there are\n    less learnable parameters.\n  expected_impact: 1\n  other_information: \"\"\n  related_parameters:\n    - embedding_size, embeddings_trainable, pretrained_embeddings\n  ui_display_name: Representation approach\nreduce_output:\n  default_value_reasoning: Sums the tensors along the sequence dimension.\n  description_implications: \"\\\"last\\\", \\\"sum\\\", \\\"mean\\\", and \\\"max\\\" are the\\\n    \\ fastest and most memory-efficient operations\\u2013 they result in tensors\\\n    \\ that are the same-size as a single item in the input sequence. However,\\\n    \\ these are simple aggregation operations, therefore some information\\\n    \\ may be lost. \\n\\n\\\"concat\\\" concatenates each tensor together, creating\\\n    \\ a `(sequence length)*(tensor size)`-element tensor. \\\"concat\\\" preserves\\\n    \\ this information, but can be very memory-intensive and should only be\\\n    \\ applied if the sequence length and/or tensor size is small. \\n\\n\\\"attention\\\"\\\n    \\ takes a weighted sum of the items in the sequence, where the weights\\\n    \\ for each item in the sequence are determined by the model on-the-fly\\\n    \\ based on the features of the item itself. This is both slower and and\\\n    \\ more memory-intensive than the other operations; however, it can also\\\n    \\ provide a richer \\\"global\\\" representation of the sequence.\"\n  expected_impact: 1\n  related_parameters:\n    - max_sequence_length\n  suggested_values: '\"attention\". This and the default covers 95% of use cases.'\n  suggested_values_reasoning: If you would like better performance and are not\n    compute/memory-constrained, attention-based reduction can potentially\n    provide a richer global representation than the default, but note that attention\n    reduction does not work with `cache_encoder_embeddings=true`.\n  ui_display_name: Sequence Reducer\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/decoders.yaml",
    "content": "BaseDecoder:\n    type:\n        expected_impact: 1\n    fc_layers:\n        expected_impact: 1\n    num_fc_layers:\n        expected_impact: 3\n    fc_output_size:\n        expected_impact: 3\n    fc_use_bias:\n        expected_impact: 1\n    fc_weights_initializer:\n        expected_impact: 1\n    fc_bias_initializer:\n        expected_impact: 1\n    fc_norm:\n        expected_impact: 2\n    fc_norm_params:\n        expected_impact: 1\n    fc_activation:\n        expected_impact: 2\n    fc_dropout:\n        expected_impact: 3\nClassifier:\n    type:\n        short_description:\n            Projects combiner output to a vector the size of the number of available classes.\n        long_description:\n            The classifier decoder is a (potentially empty) stack of fully connected layers, followed by a\n            projection into a vector of size of the number of available classes, followed by a sigmoid.\n        expected_impact: 0\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    num_classes:\n        other_information: Internal Only\n        internal_only: true\n        ui_display_name: Not Displayed\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: true\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nProjector:\n    type:\n        short_description:\n            Projects combiner output into an output vector.\n        long_description:\n            The Projector decoder is a (potentially empty) stack of fully connected layers, followed by a\n            projection into a tensor of the vector size (optionally followed by a softmax in the case of\n            multi-class classification).\n        expected_impact: 0\n    activation:\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    clip:\n        ui_display_name: null\n        expected_impact: 1\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: true\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nRegressor:\n    type:\n        short_description:\n            Projects combiner output to a single number.\n        long_description:\n            The regressor decoder is a (potentially empty) stack of fully connected layers, followed by a\n            projection to a single number.\n        expected_impact: 0\n    activation:\n        ui_display_name: null\n        expected_impact: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values: true\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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            - \"Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nPassthroughDecoder:\n    type:\n        short_description:\n            Provides the raw input from the combiner.\n        long_description:\n            The passthrough decoder simply returns the raw output coming from the combiner.\n        expected_impact: 0\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\nSequenceGeneratorDecoder:\n    type:\n        short_description:\n            Generates a sequence by sampling from the model.\n        long_description:\n            The generator decoder is a (potentially empty) stack of fully connected layers, followed by an\n            RNN that generates outputs feeding on its own previous predictions and generates a tensor of\n            size `b x s' x c`, where `b` is the batch size, `s'` is the length of the generated sequence and\n            `c` is the number of classes, followed by a softmax_cross_entropy. During training teacher\n            forcing is adopted, meaning the list of targets is provided as both inputs and outputs (shifted\n            by 1), while at evaluation time greedy decoding (generating one token at a time and feeding it\n            as input for the next step) is performed by beam search, using a beam of 1 by default. In\n            general a generator expects a `b x h` shaped input tensor, where `h` is a hidden dimension. The\n            `h` vectors are (after an optional stack of fully connected layers) fed into the rnn generator.\n            One exception is when the generator uses attention, as in that case the expected size of the\n            input tensor is `b x s x h`, which is the output of a sequence, text or time series input\n            feature without reduced outputs or the output of a sequence-based combiner. If a `b x h` input\n            is provided to a generator decoder using an RNN with attention instead, an error will be raised\n            during model building.\n        expected_impact: 0\n    cell_type:\n        ui_display_name: null\n        expected_impact: 3\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    max_sequence_length:\n        expected_impact: 3\n        ui_display_name: null\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data and\n            task. For many data types, one layer is sufficient.\n        description_implications:\n            Increasing the number of layers may improve model\n            performance for longer sequences or more complex tasks.\n        example_value:\n            - 1\n        expected_impact: 3\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Recurrent Layers\n    reduce_input:\n        description_implications:\n            \"\\u201Clast\\u201D: Reduces tensor by taking the\\\n            \\ last non-zero element per sequence in the sequence dimension.\\n\\u201C\\\n            sum\\u201D: Reduces tensor by summing across the sequence dimension.\\n\\u201C\\\n            mean\\u201D: Reduces tensor by taking the mean of the sequence dimension.\\n\\\n            \\u201Cavg\\u201D: synonym for \\u201Cmean\\u201D.\\n\\u201Cmax\\u201D: Reduces\\\n            \\ tensor by taking the maximum value of the last dimension across the\\\n            \\ sequence dimension.\\n\\u201Cconcat\\u201D: Reduces tensor by concatenating\\\n            \\ the second and last dimension.\\n\\u201Cattention\\u201D: Reduces tensor\\\n            \\ by summing across the sequence dimension after applying feedforward\\\n            \\ attention.\\n\\u201Cnone\\u201D: no reduction.\"\n        expected_impact: 2\n        ui_display_name: Combiner Reduce Mode\n    vocab_size:\n        ui_display_name: Not displayed\nSequenceTaggerDecoder:\n    type:\n        short_description:\n            Used for classifying each element of an input sequence.\n        long_description:\n            The tagger decoder is a (potentially empty) stack of fully connected layers,\n            followed by a projection into a tensor of size `b x s x c`, where `b` is the batch size,\n            `s` is the length of the sequence and `c` is the number of classes, followed by a\n            `softmax_cross_entropy`.\n\n            This decoder requires its input to be shaped as `b x s x h`, where `h` is\n            a hidden dimension, which is the output of a sequence, text or time series input feature without\n            reduced outputs or the output of a sequence-based combiner. This can be done by ensuring that\n            at least one of the sequence, text or time series input feature's encoders has `reduce_output` set to\n            `None`. This will prevent a `b x h` input from being provided to this decoder and an error\n            from being raised during model building.\n\n            The tagger decoder also requires the `reduce_input` parameter of the output feature to be set to `None`.\n            If this is not set, Ludwig will automatically override the value by setting it to None and log a warning.\n        expected_impact: 0\n    attention_embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            Increasing the embedding size may cause the model\n            to train more slowly, but the higher dimensionality can also improve overall\n            quality.\n        expected_impact: 2\n        suggested_values: 128 - 2048\n        suggested_values_reasoning:\n            Try models with smaller or larger embedding sizes\n            to observe relative impact.\n        ui_display_name: Attention Embedding Size\n    attention_num_heads:\n        ui_display_name: null\n        expected_impact: 1\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    max_sequence_length:\n        expected_impact: 3\n        ui_display_name: null\n    use_attention:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab_size:\n        ui_display_name: Not displayed\n        internal_only: true\nUNetDecoder:\n    type:\n        short_description: The UNet decoder convolutional and up-conv layers\n        long_description:\n            Stacks of two 2D convolutional layers with optional normalization\n            and relu activation, preceeded by an up-conv layer in all but the\n            final level of the decoder.\n        compute_tier: 1\n    conv_norm:\n        expected_impact: 2\n        ui_display_name: Convolutional Normalization\n    height:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    input_size:\n        other_information: Internal Only\n        internal_only: true\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    num_channels:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    num_classes:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    width:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/encoders.yaml",
    "content": "BaseEncoder:\n    skip:\n        internal_only: true\n        other_information: Internal Only\n        ui_display_name: Not Displayed\nHFEncoder:\n    trainable:\n        default_value_reasoning:\n            Trainable is disabled by default to make the model useful for generating fast baselines, which can be\n            further sped up by setting `preprocessing.cache_encoder_embeddings`. In many cases strong performance\n            can be achieved without adjusting the weights of the pretrained model, but for best performance we\n            recommend setting this to true.\n        description_implications:\n            \"Ludwig currently supports two variations on fine-tuning, configured via the trainable encoder parameter:\n            (1) modifying the weights of the pretrained encoder to adapt them to the downstream task (trainable=true),\n            or (2) keeping the pretrained encoder weights fixed and training a stack of dense layers that sit\n            downstream as the combiner and decoder modules (trainable=false, default). This is sometimes distinguished\n            as transfer learning. Allowing the weights to be modified by setting trainable=true can significantly\n            improve performance on the downstream task, but will take significantly longer to train (due to the\n            additional backward passes over the pretrained model parameters). Additionally, more care needs to be\n            taken when selecting hyperparameters when trainable=true to prevent\n            [catastrophic forgettng](https://en.wikipedia.org/wiki/Catastrophic_interference), whereby the\n            model forgets all of the valuable information it learned during pretraining.\"\n        expected_impact: 3\n        literature_references:\n            - http://d2l.ai/chapter_computer-vision/fine-tuning.html\"\n        related_parameters:\n            - use_pretrained, pretrained_model, saved_weights_in_checkpoint\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Freezing the weights (i.e. `trainable = False`)\n            is only worth trying if you are loading in pretrained weights. In that\n            case, check to see if your model is overfitting. If so, freezing the weights\n            (and therefore reducing model complexity) may be beneficial.\n        ui_display_name: Trainable\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    reduce_output:\n        default_value_reasoning: Sums the tensors along the sequence dimension.\n        description_implications:\n            \"\\\"last\\\", \\\"sum\\\", \\\"mean\\\", and \\\"max\\\" are the\\\n            \\ fastest and most memory-efficient operations\\u2013 they result in tensors\\\n            \\ that are the same-size as a single item in the input sequence. However,\\\n            \\ these are simple aggregation operations, therefore some information\\\n            \\ may be lost. \\n\\n\\\"concat\\\" concatenates each tensor together, creating\\\n            \\ a `(sequence length)*(tensor size)`-element tensor. \\\"concat\\\" preserves\\\n            \\ this information, but can be very memory-intensive and should only be\\\n            \\ applied if the sequence length and/or tensor size is small. \\n\\n\\\"attention\\\"\\\n            \\ takes a weighted sum of the items in the sequence, where the weights\\\n            \\ for each item in the sequence are determined by the model on-the-fly\\\n            \\ based on the features of the item itself. This is both slower and and\\\n            \\ more memory-intensive than the other operations; however, it can also\\\n            \\ provide a richer \\\"global\\\" representation of the sequence.\"\n        expected_impact: 1\n        related_parameters:\n            - max_sequence_length\n        suggested_values: '\"attention\". This and the default covers 95% of use cases.'\n        suggested_values_reasoning:\n            If you would like better performance and are not\n            compute/memory-constrained, attention-based reduction can potentially\n            provide a richer global representation than the default.\n        ui_display_name: Sequence Reducer\nALBERT:\n    type:\n        short_description: Similar to BERT with lower memory footprint and faster training.\n        long_description:\n            The `albert` encoder loads a pretrained [ALBERT](https://arxiv.org/abs/1909.11942) (default `albert-base-v2`) model\n            using the Hugging Face transformers package. Albert is similar to BERT, with significantly lower memory usage and\n            somewhat faster training time:.\n        compute_tier: 2\n    attention_probs_dropout_prob:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, classifier_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_probs_dropout_prob\n    bos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Beginning-of-Sentence Token Id\n        expected_impact: 1\n    classifier_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, attention_probs_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: classifier_dropout_prob\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 1\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    eos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: End-of-Sentence Token Id\n        expected_impact: 1\n    hidden_act:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            Changing this activation function will only affect\n            the feed-forward layers of the transformer.\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - \"[Hugging face docs for ALBERT config](https://huggingface.co/docs/transformers/model_doc/albert#transformers.AlbertConfig.hidden_act)\\n\\\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)\"\n        suggested_values: gelu\n        suggested_values_reasoning: Taken from huggingface defaults.\n        ui_display_name: Hidden Layer Activation\n    hidden_dropout_prob:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"attention_probs_dropout_prob,\n\n              classifier_dropout_prob\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: hidden_dropout_prob\n    hidden_size:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 1\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    inner_group_num:\n        ui_display_name: null\n        expected_impact: 1\n    intermediate_size:\n        ui_display_name: null\n        expected_impact: 1\n    layer_norm_eps:\n        ui_display_name: null\n        expected_impact: 1\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 1\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_attention_heads:\n        ui_display_name: null\n        expected_impact: 1\n    num_hidden_groups:\n        ui_display_name: null\n        expected_impact: 1\n    num_hidden_layers:\n        ui_display_name: null\n        expected_impact: 1\n    pad_token_id:\n        ui_display_name: null\n        expected_impact: 1\n    position_embedding_type:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_kwargs:\n        default_value_reasoning: These arguments typically don't need to be specified.\n        expected_impact: 1\n        related_parameters:\n            - pretrained_model_name_or_path\n        suggested_values: Default\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        default_value_reasoning:\n            The default model is the canonical model for this\n            model architecture, and is therefore a good starting point for most use\n            cases.\n        description_implications:\n            \"There are two factors to consider when choosing\\\n            \\ a pre-trained model: (1) size, and (2) task similarity. \\n\\nThe larger\\\n            \\ the model, the more subtle its comprehension of inputs can become. However,\\\n            \\ larger models are also more compute and memory-intensive to train.\\n\\\n            \\nModels pretrained on highly-related source tasks are more likely to\\\n            \\ be successful on the target task. Consider searching the HuggingFace\\\n            \\ model repository for models trained on similar tasks.\"\n        expected_impact: 2\n        literature_references:\n            - https://arxiv.org/abs/1909.11942\n        related_parameters:\n            - use_pretrained, trainable, pretrained_kwargs\n        suggested_values: albert-large-v2, albert-base-chinese\n        suggested_values_reasoning:\n            \"If you would like better performance and are\n            not compute/memory-constrained, increasing model capacity can potentially\n            provide a richer representation than the default. The suggested value\n            upsizes the model while maintaining the same model architecture.\n\n\n            Language models trained on general corpora typically generalize well.\n            Consider deviating from the default only if the text in the dataset originates\n            from another domain (e.g. languages other than English).\"\n        ui_display_name: Pretrained model\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    type_vocab_size:\n        ui_display_name: null\n        expected_impact: 1\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nAutoTransformer:\n    type:\n        short_description: Automatically retrieves the architecture from the provided model name/path.\n        long_description:\n            The `auto_transformer` encoder automatically instantiates the model architecture for the specified\n            `pretrained_model_name_or_path`. Unlike the other HF encoders, `auto_transformer` does not provide a default value for\n            `pretrained_model_name_or_path`, this is its only mandatory parameter. See the Hugging Face\n            [AutoModels documentation](https://huggingface.co/docs/transformers/model_doc/auto) for more details.\n        literature_references:\n            - https://huggingface.co/docs/transformers/model_doc/auto\n        compute_tier: 2\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 3\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nBERT:\n    type:\n        short_description: Bidirectional transformer great for language modeling.\n        long_description:\n            The bert encoder loads a pretrained BERT (default bert-base-uncased) model using the Hugging\n            Face transformers package. BERT is a bidirectional transformer pretrained using a combination of\n            masked language modeling objective and next sentence prediction on a large corpus comprising the\n            Toronto Book Corpus and Wikipedia.\n        literature_references:\n            - https://arxiv.org/abs/1810.04805\n        compute_tier: 2\n    attention_probs_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, classifier_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_probs_dropout_prob\n    classifier_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, attention_probs_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: classifier_dropout\n    gradient_checkpointing:\n        ui_display_name: null\n        expected_impact: 1\n    hidden_act:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            Changing this activation function will only affect\n            the feed-forward layers of the transformer.\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - \"[Huggingface docs for BERT config](https://huggingface.co/docs/transformers/model_doc/bert#transformers.BertConfig.hidden_act)\\n\\\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)\"\n        suggested_values: gelu\n        suggested_values_reasoning: Taken from huggingface defaults.\n        ui_display_name: Hidden Layer Activation\n    hidden_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - attention_probs_dropout_prob, classifier_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: hidden_dropout_prob\n    hidden_size:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 1\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    intermediate_size:\n        ui_display_name: null\n        expected_impact: 1\n    layer_norm_eps:\n        ui_display_name: null\n        expected_impact: 1\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_attention_heads:\n        ui_display_name: null\n        expected_impact: 1\n    num_hidden_layers:\n        ui_display_name: null\n        expected_impact: 1\n    pad_token_id:\n        ui_display_name: null\n        expected_impact: 1\n    position_embedding_type:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_kwargs:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    type_vocab_size:\n        ui_display_name: null\n        expected_impact: 1\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nBagEmbedWeighted:\n    type:\n        short_description: Transforms feature to vector, maps to sparse or dense embeddings, then aggregates.\n        long_description:\n            The embed weighted encoder first transforms the element frequency vector to sparse integer\n            lists, which are then mapped to either dense or sparse embeddings (one-hot encodings). Lastly,\n            embeddings are aggregated as a weighted sum where each embedding is multiplied by its respective\n            element's frequency. Inputs are of size b while outputs are of size b x h where b is the batch\n            size and h is the dimensionality of the embeddings.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        default_value_reasoning:\n            If trained from scratch, embedding vectors are typically\n            learned alongside the rest of the model.\n        description_implications:\n            Typically this value is only set to False if pre-trained\n            embeddings are uploaded. Even then, it is reasonable to leave it as True\n            in order to fine-tune the embeddings.\n        expected_impact: 1\n        related_parameters:\n            - embedding_size, representation, pretrained_embeddings\n        ui_display_name: (under Embeddings header) Trainable?\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    force_embedding_size:\n        default_value_reasoning:\n            It is not often the case that the user has a strict\n            need for using an embedding size that should be larger than the vocabulary\n            size.\n        description_implications:\n            Should only be True if the user has a strict need\n            for using an embedding size that should be larger than the vocabulary\n            size. For example, there may be size requirements across multiple features\n            imposed by downstream modules like the ComparatorCombiner.\n        expected_impact: 1\n        related_parameters:\n            - embedding_size\n        suggested_values:\n            - false\n        suggested_values_reasoning: True for advanced usage only.\n        ui_display_name: Force Embedding Size\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pretrained_embeddings:\n        default_value_reasoning:\n            Embeddings are commonly trained from scratch, or\n            incorporated as part of a pre-trained model package.\n        description_implications:\n            If pretrained embeddings are specified, then the\n            model may have a head start in its representation of various input entities.\n        example_value:\n            - ~/Downloads/glove.6B.100d.txt\n        expected_impact: 0\n        related_parameters:\n            - embedding_size, embeddings_trainable\n        ui_display_name: Pretrained embeddings path\n    representation:\n        default_value_reasoning:\n            Trainable, randomly initialized embedding vectors\n            often lead to more subtle representations of input entities than one-hot\n            vectors.\n        description_implications:\n            If set to sparse, the representations for input\n            entities are fixed as one-hot vectors. This leads to less flexible representations\n            for input entities, but could lead to faster training since there are\n            less learnable parameters.\n        expected_impact: 1\n        other_information: \"\"\n        related_parameters:\n            - embedding_size, embeddings_trainable, pretrained_embeddings\n        ui_display_name: Representation approach\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from published [literature](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nCTRL:\n    type:\n        short_description: Language model trained to condition on control codes that govern style, content and task-specific behavior.\n        long_description:\n            The `ctrl` encoder loads a pretrained [CTRL](https://arxiv.org/abs/1909.05858) (default `ctrl`) model using the Hugging\n            Face transformers package. CTRL is a conditional transformer language model trained to condition on control codes that\n            govern style, content, and task-specific behavior.\n        literature_references:\n            - https://arxiv.org/abs/1909.05858\n        compute_tier: 2\n    attn_pdrop:\n        ui_display_name: null\n    dff:\n        ui_display_name: null\n    embd_pdrop:\n        ui_display_name: null\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    layer_norm_epsilon:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_ctx:\n        ui_display_name: null\n    n_embd:\n        ui_display_name: null\n    n_head:\n        ui_display_name: null\n    n_layer:\n        ui_display_name: null\n    n_positions:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    resid_pdrop:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nCamemBERT:\n    type:\n        short_description: Language model trained on large French text corpus.\n        long_description:\n            The `camembert` encoder loads a pretrained [CamemBERT](https://arxiv.org/abs/1911.03894)\n            (default `jplu/tf-camembert-base`) model using the Hugging Face transformers package. CamemBERT is pre-trained on a\n            large French language web-crawled text corpus.\n        literature_references:\n            - https://arxiv.org/abs/1911.03894\n        compute_tier: 2\n    attention_probs_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - classifier_dropout, hidden_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_probs_dropout_prob\n    classifier_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - attention_probs_dropout_prob, hidden_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: classifier_dropout\n    gradient_checkpointing:\n        ui_display_name: null\n    hidden_act:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            Changing this activation function will only affect\n            the feed-forward layers of the transformer.\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - \"[Relevant StackOverflow discussion](https://ai.stackexchange.com/questions/30341/why-does-a-transformer-not-use-an-activation-function-following-the-multi-head-a)\"\n        suggested_values: gelu\n        suggested_values_reasoning: Taken from huggingface defaults.\n        ui_display_name: Hidden Layer Activation\n    hidden_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"attention_probs_dropout_prob, \\nclassifier_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: hidden_dropout_prob\n    hidden_size:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 1\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    intermediate_size:\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_attention_heads:\n        ui_display_name: null\n    num_hidden_layers:\n        ui_display_name: null\n    pad_token_id:\n        ui_display_name: null\n    position_embedding_type:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    type_vocab_size:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nCategoricalEmbed:\n    type:\n        short_description: Maps the categorical feature to a dense embedding.\n        long_description:\n            The dense encoder maps to a dense embedding and is returned as outputs of size `b x h`,\n            where `b` is the batch size and `h` is the dimensionality of the embeddings.\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_initializer:\n        default_value_reasoning:\n            According to https://arxiv.org/abs/1711.09160, choice\n            of embedding initialization is not important as long as the variance is\n            kept reasonably low.\n        description_implications:\n            According to https://arxiv.org/abs/1711.09160, choice\n            of embedding initialization is not important as long as the variance is\n            kept reasonably low.\n        example_value:\n            - kaiming\n        expected_impact: 1\n        literature_references:\n            - https://arxiv.org/abs/1711.09160\n        suggested_values: kaiming\n        suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/326\n        ui_display_name: Embedding Initialization\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\nCategoricalSparse:\n    type:\n        short_description: Maps the categorical feature to a sparse embedding.\n        long_description:\n            The sparse encoder maps to a sparse embedding (one-hot encodings) and is returned as outputs of\n            size `b x h`, where `b` is the batch size and `h` is the dimensionality of the embeddings.\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_initializer:\n        default_value_reasoning:\n            According to https://arxiv.org/abs/1711.09160, choice\n            of embedding initialization is not important as long as the variance is\n            kept reasonably low.\n        description_implications:\n            According to https://arxiv.org/abs/1711.09160, choice\n            of embedding initialization is not important as long as the variance is\n            kept reasonably low.\n        example_value:\n            - kaiming\n        expected_impact: 1\n        literature_references:\n            - https://arxiv.org/abs/1711.09161\n        suggested_values: kaiming\n        suggested_values_reasoning: https://discuss.huggingface.co/t/state-of-the-art-technique-for-initializing-embedding-matrix/327\n        ui_display_name: Embedding Initialization\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\nDateEmbed:\n    type:\n        short_description: Embeds the date elements passes them through fully connected layers.\n        long_description:\n            The Embed encoder passes the year through a fully connected layer of one neuron and embeds all\n            other elements for the date, concatenates them and passes the concatenated representation\n            through fully connected layers.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nDateWave:\n    type:\n        short_description: Embeds the date elements by taking the cosine of their value before passing through fully connected layers.\n        long_description:\n            The Wave encoder passes the year through a fully connected layer of one neuron and represents\n            all other elements for the date by taking the cosine of their value with a different period (12\n            for months, 31 for days, etc.), concatenates them and passes the concatenated representation\n            through fully connected layers.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nDenseEncoder:\n    type:\n        short_description: Passes the raw numerical values through fully connected layers.\n        long_description:\n            The dense encoder passes the raw numerical values through fully connected layers. In this case\n            the inputs of size `b` are transformed to size `b x h`.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    input_size:\n        internal_only: true\n        other_information: Internal Only\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\n    fc_layers:\n        ui_display_name: null\n        expected_impact: 1\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            \"Increasing the number of layers may improve model\n            performance by allowing the model to synthesize learned features derived\n            from the original input. If the input is simple, ex. a category with a\n            few options, increasing the number of layers has no benefit. For more\n            complex inputs, additional layers add more 'processing power' to extract\n            useful information from the input.\n\n\n            However, more layers will increase training time and may reduce accuracy\n            due to overfitting.\"\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            If you have multiple input features, varying the number\n            of layers in the combiner or output feature decoder will have more impact.\n        related_parameters:\n            - layers\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    use_bias:\n        ui_display_name: null\n        expected_impact: 1\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nDistilBERT:\n    type:\n        short_description: A distilled version of BERT base that is 40% smaller and 60% faster with 95% of performance preserved.\n        long_description:\n            The `distilbert` encoder loads a pretrained [DistilBERT](https://medium.com/huggingface/distilbert-8cf3380435b5)\n            (default `distilbert-base-uncased`) model using the Hugging Face transformers package. DistilBERT is a small, fast, cheap and light\n            Transformer model trained by distilling BERT base. It has 40% less parameters than\n            bert-base-uncased, runs 60% faster while preserving over 95% of BERT’s performances as measured\n            on the GLUE language understanding benchmark.\n        compute_tier: 2\n    activation:\n        default_value_reasoning:\n            This is the default activation function used in the\n            Distillbert huggingface implementation\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    attention_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout, qa_dropout, seq_classif_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_dropout\n    dim:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"attention_dropout,\n\n              qa_dropout,\n\n              seq_classif_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout\n    hidden_dim:\n        ui_display_name: null\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_heads:\n        ui_display_name: null\n    n_layers:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    qa_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout, attention_dropout, seq_classif_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: qa_dropout\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    seq_classif_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"dropout,\n\n              attention_dropout,\n\n              qa_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: seq_classif_dropout\n    sinusoidal_pos_embds:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nELECTRA:\n    type:\n        short_description: Transformer encoder that can be used to encode a sequence of tokens with little compute\n        long_description:\n            The `electra`` encoder loads a pretrained [ELECTRA](https://openreview.net/pdf?id=r1xMH1BtvB) model using the Hugging Face transformers package.\n            ELECTRA is a new pretraining approach which trains two transformer models the generator and the\n            discriminator. The generator’s role is to replace tokens in a sequence, and is therefore trained\n            as a masked language model. The discriminator, which is the model we’re interested in, tries to\n            identify which tokens were replaced by the generator in the sequence.\n        literature_references:\n            - https://openreview.net/pdf?id=r1xMH1BtvB\n        compute_tier: 2\n    attention_probs_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, classifier_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_probs_dropout_prob\n    classifier_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - hidden_dropout_prob, attention_probs_dropout_prob\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: classifier_dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 1\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    hidden_act:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            Changing this activation function will only affect\n            the feed-forward layers of the transformer.\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - \"[Huggingface docs for ELECTRA config](https://huggingface.co/docs/transformers/model_doc/electra#transformers.ElectraConfig.hidden_act)\n\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)\"\n        suggested_values: gelu\n        suggested_values_reasoning: Taken from huggingface defaults.\n        ui_display_name: Hidden Layer Activation\n    hidden_dropout_prob:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"attention_probs_dropout_prob,\n\n              classifier_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: hidden_dropout_prob\n    hidden_size:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 1\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    intermediate_size:\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_attention_heads:\n        ui_display_name: null\n    num_hidden_layers:\n        ui_display_name: null\n    position_embedding_type:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    type_vocab_size:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nFlauBERT:\n    type:\n        short_description: Language model with BERT related architecture trained on large French text corpus.\n        long_description:\n            The `flaubert`` encoder loads a pretrained [FlauBERT](https://arxiv.org/abs/1912.05372) (default `jplu/tf-flaubert-base-uncased``) model\n            using the Hugging Face transformers package. FlauBERT has an architecture similar to BERT and is\n            pre-trained on a large French language corpus.\n        literature_references:\n            - https://arxiv.org/abs/1912.05372\n        compute_tier: 2\n    asm:\n        ui_display_name: null\n        expected_impact: 1\n    attention_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_dropout\n    bos_index:\n        ui_display_name: null\n        expected_impact: 1\n    causal:\n        ui_display_name: null\n        expected_impact: 1\n    dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - attention_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout\n    emb_dim:\n        ui_display_name: null\n        expected_impact: 1\n    embed_init_std:\n        ui_display_name: null\n        expected_impact: 1\n    eos_index:\n        ui_display_name: null\n        expected_impact: 1\n    gelu_activation:\n        ui_display_name: null\n        expected_impact: 1\n    init_std:\n        ui_display_name: null\n        expected_impact: 1\n    is_encoder:\n        ui_display_name: null\n        expected_impact: 1\n    lang_id:\n        ui_display_name: null\n        expected_impact: 1\n    layer_norm_eps:\n        ui_display_name: null\n        expected_impact: 1\n    layerdrop:\n        ui_display_name: null\n        expected_impact: 1\n    mask_index:\n        ui_display_name: null\n        expected_impact: 1\n    mask_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Mask Token ID\n        expected_impact: 1\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 1\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_heads:\n        ui_display_name: null\n        expected_impact: 1\n    n_langs:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        expected_impact: 1\n        ui_display_name: Number of Languages\n    n_layers:\n        ui_display_name: null\n        expected_impact: 1\n    pad_index:\n        ui_display_name: null\n        expected_impact: 1\n    pre_norm:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_kwargs:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    sinusoidal_embeddings:\n        ui_display_name: null\n        expected_impact: 1\n    trainable:\n        ui_display_name: null\n        expected_impact: 3\n    unk_index:\n        ui_display_name: null\n        expected_impact: 1\n    use_lang_emb:\n        ui_display_name: null\n        expected_impact: 1\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nGPT2:\n    type:\n        short_description: GPT-2 is a pre-trained language model used for NLP tasks like generation, summarization, and translation.\n        long_description: The `gpt2` encoder loads a pretrained\n            [GPT-2](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf)\n            (default `gpt2`) model using the Hugging Face transformers package. GPT-2 is a causal (unidirectional) transformer pretrained using language\n            modeling on a very large corpus of ~40 GB of text data.\n        literature_references:\n            - https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf\n        compute_tier: 3\n    activation_function:\n        ui_display_name: null\n    attn_pdrop:\n        ui_display_name: null\n    embd_pdrop:\n        ui_display_name: null\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    layer_norm_epsilon:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_ctx:\n        ui_display_name: null\n    n_embd:\n        ui_display_name: null\n    n_head:\n        ui_display_name: null\n    n_inner:\n        ui_display_name: null\n    n_layer:\n        ui_display_name: null\n    n_positions:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    resid_pdrop:\n        ui_display_name: null\n    scale_attn_weights:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nGPT:\n    type:\n        short_description: GPT is a pre-trained language model used for NLP tasks like generation, summarization, and translation.\n        long_description: The `gpt` encoder loads a pretrained\n            [GPT](https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf)\n            (default `openai-gpt`) model using the Hugging Face transformers package.\n            GPT is a causal (unidirectional) transformer pre-trained using language modeling on a large corpus with long range dependencies, the Toronto Book Corpus.\n        literature_references:\n            - https://s3-us-west-2.amazonaws.com/openai-assets/research-covers/language-unsupervised/language_understanding_paper.pdf\n        compute_tier: 2\n    afn:\n        ui_display_name: null\n    attn_pdrop:\n        ui_display_name: null\n    embd_pdrop:\n        ui_display_name: null\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    layer_norm_epsilon:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_ctx:\n        ui_display_name: null\n    n_embd:\n        ui_display_name: null\n    n_head:\n        ui_display_name: null\n    n_layer:\n        ui_display_name: null\n    n_positions:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    resid_pdrop:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nH3Embed:\n    type:\n        short_description: Encodes each H3 component with embeddings then takes a sum and passes them through fully connected layers.\n        long_description:\n            The Embed encoder encodes each component of the H3 representation (mode, edge, resolution,\n            base cell and children cells) with embeddings. Children cells with value 0 will be masked out.\n            After the embedding, all embeddings are summed and optionally passed through a stack of fully\n            connected layers.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    reduce_output:\n        default_value_reasoning: Sums the tensors along the sequence dimension.\n        description_implications:\n            \"\\\"last\\\", \\\"sum\\\", \\\"mean\\\", and \\\"max\\\" are the\\\n            \\ fastest and most memory-efficient operations\\u2013 they result in tensors\\\n            \\ that are the same-size as a single item in the input sequence. However,\\\n            \\ these are simple aggregation operations, therefore some information\\\n            \\ may be lost. \\n\\n\\\"concat\\\" concatenates each tensor together, creating\\\n            \\ a `(sequence length)*(tensor size)`-element tensor. \\\"concat\\\" preserves\\\n            \\ this information, but can be very memory-intensive and should only be\\\n            \\ applied if the sequence length and/or tensor size is small. \\n\\n\\\"attention\\\"\\\n            \\ takes a weighted sum of the items in the sequence, where the weights\\\n            \\ for each item in the sequence are determined by the model on-the-fly\\\n            \\ based on the features of the item itself. This is both slower and and\\\n            \\ more memory-intensive than the other operations; however, it can also\\\n            \\ provide a richer \\\"global\\\" representation of the sequence.\"\n        expected_impact: 1\n        related_parameters:\n            - max_sequence_length\n        suggested_values: '\"attention\". This and the default covers 95% of use cases.'\n        suggested_values_reasoning:\n            If you would like better performance and are not\n            compute/memory-constrained, attention-based reduction can potentially\n            provide a richer global representation than the default.\n        ui_display_name: Sequence Reducer\n    use_bias:\n        ui_display_name: null\n        expected_impact: 1\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nH3RNN:\n    type:\n        short_description: Encodes each H3 component with embeddings then passes them through an RNN encoder.\n        long_description:\n            The RNN encoder encodes each component of the H3 representation (mode, edge, resolution,\n            base cell and children cells) with embeddings. Children cells with value 0 will be masked out.\n            After the embedding, all embeddings are passed through an RNN encoder. The intuition behind this\n            is that, starting from the base cell, the sequence of children cells can be seen as a sequence\n            encoding the path in the tree of all H3 hexes.\n    activation:\n        ui_display_name: null\n        expected_impact: 1\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 2\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    bidirectional:\n        default_value_reasoning:\n            For short sequences, it is reasonable to use a vanilla\n            RNN.\n        description_implications:\n            Setting bidirectional to True may increase the compute\n            and memory requirements of the model, but may also increase model performance\n            on long sequences.\n        expected_impact: 0\n        literature_references:\n            - https://devopedia.org/bidirectional-rnn#:~:text=RNN%20has%20the%20limitation%20that,forward%20and%20reverse%20time%20order.\n        related_parameters:\n            - cell_type, activation, recurrent_activation, use_bias\n        suggested_values:\n            - true\n        suggested_values_reasoning:\n            \"RNNs can sometimes suffer from catastrophic forgetting\n            (source: https://en.wikipedia.org/wiki/Catastrophic_interference ) on\n            long sequences. Allowing the RNN to read from both the beginning and end\n            of the sequence can improve its representation at each timestep.\"\n        ui_display_name: Bidirectional\n    cell_type:\n        default_value_reasoning:\n            The LSTM cell has proven to be the most performant\n            of the three cells.\n        description_implications:\n            \"There are two reasons to consider other cell types:\n            (1) compute costs and (2) catastrophic forgetting (source: https://en.wikipedia.org/wiki/Catastrophic_interference\n            ). RNNs have marginally less compute costs, but are prone to catastrophic\n            forgetting.\"\n        expected_impact: 3\n        related_parameters:\n            - \"bidirectional\n\n              activation\n\n              recurrent_activation\n\n              use_bias\"\n        ui_display_name: Cell Type\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - recurrent_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    hidden_size:\n        default_value_reasoning:\n            H3 values numbers, so a small RNN dimensionality\n            is likely sufficient.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 2\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            Increasing the number of layers may improve model\n            performance for longer sequences or more complex tasks.\n        example_value:\n            - 1\n        expected_impact: 3\n        other_information:\n            If you have multiple input features, varying the number\n            of layers in the combiner or output feature decoder will have more impact.\n        related_parameters:\n            - layers\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Recurrent Layers\n    recurrent_activation:\n        default_value_reasoning: sigmoid' is commonly used\n        expected_impact: 1\n        other_information:\n            I don't think that this parameter is used anywhere in the\n            code base. It's being passed down but not used in the actual RNN forwarding\n            functions.\n        suggested_values: sigmoid, ReLu, tanh\n        ui_display_name: null\n    recurrent_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Recurrent Dropout\n    recurrent_initializer:\n        ui_display_name: null\n        expected_impact: 1\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    unit_forget_bias:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        ui_display_name: null\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nH3WeightedSum:\n    type:\n        short_description: Encodes each H3 component with embeddings then takes a weighted sum.\n        long_description:\n            The Weighted Sum encoder encodes each component of the H3 representation (mode, edge,\n            resolution, base cell and children cells) with embeddings. Children cells with value 0 will be\n            masked out. After the embedding, all embeddings are summed with a weighted sum (with learned\n            weights) and optionally passed through a stack of fully connected layers.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    should_softmax:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        ui_display_name: null\n        expected_impact: 1\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nLongformer:\n    type:\n        short_description: Transformer optimized for longer text inputs.\n        long_description:\n            The `longformer` encoder loads a pretrained [Longformer](https://arxiv.org/pdf/2004.05150.pdf)\n            (default `allenai/longformer-base-4096`) model using the Hugging Face transformers package. Longformer is a good choice\n            for longer text, as it supports sequences up to 4096 tokens long.\n        literature_references:\n            - https://arxiv.org/pdf/2004.05150.pdf\n        compute_tier: 2\n    attention_window:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_tokens:\n        ui_display_name: null\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            \"An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words or positions,\n            which can have a large vocbulary size. Ideally, after an embedding is\n            trained, it captures some of the semantics of the input by placing semantically\n            similar inputs close together in the embedding space.\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.\"\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    type_vocab_size:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    sep_token_id:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nMLPMixer:\n    type:\n        short_description: Image encoder which applies fully connected layers to different patches of the image.\n        long_description:\n            MLP-Mixer divides the image into equal-sized patches, applying fully connected layers to each\n            patch to compute per-patch representations (tokens) and combining the representations with\n            fully-connected mixer layers.\n        compute_tier: 1\n    avg_pool:\n        ui_display_name: null\n    channel_dim:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embed_size:\n        ui_display_name: null\n    height:\n        internal_only: true\n        ui_display_name: null\n    num_channels:\n        ui_display_name: null\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the size and\n            complexity of the input images. The default value is used in the paper\n            and tested on several image datasets.\n        description_implications:\n            Increasing the number of layers may improve model\n            performance for larger images or more complex image tasks.\n        example_value:\n            - 8\n        expected_impact: 3\n        literature_references:\n            - \"MLP-Mixer: An all-MLP Architecture for Vision\n\n              https://arxiv.org/abs/2105.01601\"\n        suggested_values: 4 - 32\n        suggested_values_reasoning:\n            Values from 8 - 32 are tested in the paper. It\n            is possible that fewer layers will be sufficient for some tasks.\n        ui_display_name: Number of Layers\n    patch_size:\n        default_value_reasoning: Taken from MLP-Mixer paper.\n        description_implications:\n            \"The implications of the image patch size for this\\\n            \\ layer depend on other factors, such as the true resolution of the incoming\\\n            \\ image dataset. If the patch size is kept consistent but a higher resolution\\\n            \\ image is used as input, then the resulting chunked sequence of tokens\\\n            \\ will be longer than it would have been if the input resolution was lower.\\\n            \\ \\n\\nThe original MLP-Mixer paper also notes that there is a tradeoff\\\n            \\ with respect to the projection units learned by a model. In their findings,\\\n            \\ a 32x32 patch size model learned very structured low frequency projection\\\n            \\ units, while the equivalent 16x16 model learned high frequencies and\\\n            \\ showed no clear structure.\"\n        expected_impact: 2\n        literature_references:\n            - \"[MLP Mixer paper](https://arxiv.org/pdf/2105.01601.pdf)\"\n        suggested_values:\n            - 16\n            - 32\n        suggested_values_reasoning:\n            16 and 32 are the values used in the original\n            MLP Mixer paper\n        ui_display_name: Patch Size\n    token_size:\n        ui_display_name: null\n    width:\n        internal_only: true\n        ui_display_name: null\nMT5:\n    type:\n        short_description: MT5 is a multilingual variant of T5 useful for multilingual NLP use cases.\n        long_description:\n            The `mt5` encoder loads a pretrained [MT5](https://arxiv.org/abs/2010.11934) (default `google/mt5-base`) model using the\n            Hugging Face transformers package. MT5 is a multilingual variant of T5 trained on a dataset of 101 languages.\n        compute_tier: 2\n    d_ff:\n        default_value_reasoning: Default value matches the pre-trained encoder.\n        description_implications:\n            If using a pre-trained encoder, this parameter will\n            be automatically derived from the pre-trained model.\n        expected_impact: 1\n        ui_display_name: Dimensionality of Feed-Forward Layer\n    d_kv:\n        ui_display_name: null\n    d_model:\n        ui_display_name: null\n    decoder_start_token_id:\n        ui_display_name: null\n    dropout_rate:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout_rate\n    eos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: End-of-Sentence Token Id\n    feed_forward_proj:\n        ui_display_name: null\n    initializer_factor:\n        ui_display_name: null\n    is_encoder_decoder:\n        ui_display_name: null\n    layer_norm_epsilon:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_decoder_layers:\n        ui_display_name: null\n    num_heads:\n        ui_display_name: null\n    num_layers:\n        default_value_reasoning:\n            The default value matches the number of layers in\n            the default pretrained encoder.\n        description_implications:\n            \"The ideal number of transformer layers depends\n            on the length and complexity of input sequences, as well as the task.\n\n\n            If using a pre-trained encoder, this parameter will be automatically derived\n            from the pre-trained model.\"\n        example_value:\n            - 8\n        expected_impact: 3\n        related_parameters:\n            - pretrained_model_or_path\n        suggested_values: 1 - 12\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Transformer Layers\n    pad_token_id:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    relative_attention_num_buckets:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    tie_word_embeddings:\n        default_value_reasoning:\n            Keeping the word embeddings separate ensures maximum\n            modeling flexibility.\n        description_implications:\n            The main tradeoff between True and False values\n            is in compute costs and model flexibility. If set to False, the model\n            will require more memory, but may be more flexible. If set to True, the\n            opposite is true.\n        example_value:\n            - true\n        expected_impact: 2\n        suggested_values:\n            - true\n        suggested_values_reasoning:\n            \"If set to True, then the word embeddings will\n            be shared between the encoder and decoder. There are two main reasons\n            to set this value to True: (1) saving compute resources. Word embedding\n            tables can be very large and using a single table between the encoder\n            and decoder can cut one's memory usage in half. (2) If the domain of\n            the generated text is highly similar to the input text. For example, if\n            training a Question and Answering (QA) text model, where both the questions\n            and answers are in the same language, the word embeddings used by the\n            encoder are likely usable by the decoder and vice-versa. On the other\n            hand, if training a translation model between two languages, the word\n            embeddings are not likely to be shareable by both model components.\"\n        ui_display_name: null\n    tokenizer_class:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_cache:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nParallelCNN:\n    type:\n        short_description: Default option for processing sequence, audio, and text data types.\n        long_description:\n            The Parallel CNN works by first mapping the input integer sequence b x s (where b is the batch\n            size and s is the length of the sequence) into a sequence of embeddings, then it passes the\n            embedding through a number of parallel 1d convolutional layers with different filter size (by\n            default 4 layers with filter size 2, 3, 4 and 5), followed by max pooling and concatenation.\n            This single vector concatenating the outputs of the parallel convolutional layers is then passed\n            through a stack of fully connected layers and returned as a b x h tensor where h is the output\n            size of the last fully connected layer.\n        compute_tier: 1\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    filter_size:\n        ui_display_name: null\n        expected_impact: 2\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pool_function:\n        ui_display_name: Pooling function\n        expected_impact: 1\n    pool_size:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nPassthroughEncoder:\n    type:\n        short_description: Passes the raw input through to the combiner.\n        long_description:\n            The passthrough encoder simply returns the raw numerical values coming from the input\n            placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is\n            the batch size.\n    input_size:\n        internal_only: true\n        other_information: Internal Only\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\nBinaryPassthroughEncoder:\n    type:\n        short_description: Passes the raw input through to the combiner.\n        long_description:\n            The passthrough encoder simply returns the raw numerical values coming from the input\n            placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is\n            the batch size.\n    input_size:\n        internal_only: true\n        other_information: Internal Only\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\nCategoricalPassthroughEncoder:\n    type:\n        short_description: Passes the raw input through to the combiner.\n        long_description:\n            The passthrough encoder simply returns the raw numerical values coming from the input\n            placeholders as outputs. Inputs are of size `b` while outputs are of size `b x 1` where `b` is\n            the batch size.\n    input_size:\n        internal_only: true\n        other_information: Internal Only\n        related_parameters:\n            - \"No\"\n        ui_display_name: Not Displayed\nResNet:\n    type:\n        short_description: Residual network achieving very high performance on computer vision tasks.\n        long_description:\n            ResNet - short for residual network is part of a family of extremely deep architectures showing\n            compelling accuracy and nice convergence behaviors for computer vision applications. It is a type\n            of CNN architecture designed to support hundreds or thousands of convolutional layers.\n        compute_tier: 2\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    batch_norm_epsilon:\n        ui_display_name: null\n    batch_norm_momentum:\n        ui_display_name: null\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    conv_stride:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    first_pool_kernel_size:\n        ui_display_name: null\n        expected_impact: 1\n    first_pool_stride:\n        ui_display_name: null\n        expected_impact: 1\n    height:\n        internal_only: true\n        ui_display_name: null\n    kernel_size:\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_channels:\n        ui_display_name: null\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    out_channels:\n        ui_display_name: null\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    resnet_size:\n        ui_display_name: null\n    use_bias:\n        ui_display_name: null\n        expected_impact: 1\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\n    width:\n        internal_only: true\n        ui_display_name: null\nDeBERTa:\n    type:\n        short_description: Improved version of BERT and RoBERTa, achieving good baseline performance on many tasks.\n        long_description:\n            The [DeBERTa](https://arxiv.org/abs/2006.03654) encoder improves the BERT and RoBERTa models using\n            disentangled attention and enhanced mask decoder. With those two improvements, DeBERTa out performs RoBERTa\n            on a majority of NLU tasks with 80GB training data.\n\n            In [DeBERTa V3](https://arxiv.org/abs/2111.09543), the authors further improved the efficiency of DeBERTa\n            using ELECTRA-Style pre-training with Gradient Disentangled Embedding Sharing. Compared to DeBERTa,\n            the V3 version significantly improves the model performance on downstream tasks.\n        compute_tier: 2\n        literature_references:\n            - https://arxiv.org/abs/2006.03654\n            - https://arxiv.org/abs/2111.09543\n    pretrained_model_name_or_path:\n        default_value_reasoning:\n            The default model was selected based on the benchmarking work done by IBM's\n            [model recycling](https://ibm.github.io/model-recycling/microsoft_deberta-v3-base_table.html) project.\n            In that study, the selected model ranked first among all variants of the `microsoft/deberta-v3-base`\n            architecture on an evaluation across 36 different datasets.\n        description_implications:\n            Considerations when selecting a pretrained model version include number of parameters (how long the model\n            will take to fine-tuning / perform inference), general model performance on various benchmarks, and\n            specific model performance on the task you wish to fine-tune it on.\n        expected_impact: 2\n        related_parameters:\n            - use_pretrained, trainable, pretrained_kwargs\n        ui_display_name: Pretrained model\nRoBERTa:\n    type:\n        short_description: BERT based model that has higher accuracy and is easier parallelize due to larger mini-batches.\n        long_description:\n            The `roberta` encoder loads a pretrained [RoBERTa](https://arxiv.org/abs/1907.11692) (default `roberta-base`) model\n            using the Hugging Face transformers package. Replication of BERT pretraining which may match or exceed the performance\n            of BERT. RoBERTa builds on BERT and modifies key hyperparameters, removing the\n            next-sentence pretraining objective and training with much larger mini-batches and learning\n            rates.\n        literature_references:\n            - https://arxiv.org/abs/1907.11692\n        compute_tier: 2\n    bos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Beginning-of-Sentence Token Id\n    eos_token_id:\n        default_value_reasoning: <class 'int'>\n        example_value:\n            - Default value used in pre-trained HF encoder.\n        expected_impact: 1\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    pad_token_id:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nSequenceEmbed:\n    type:\n        short_description: Maps each element of the sequence to an embedding.\n        long_description:\n            The embed encoder simply maps each integer in the sequence to an embedding, creating a `b x s x h`\n            tensor where `b` is the batch size, `s` is the length of the sequence and `h` is the embedding\n            size. The tensor is reduced along the `s` dimension to obtain a single vector of size `h` for each\n            element of the batch.\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nSequencePassthrough:\n    type:\n        short_description: Transforms sequence values to a floats then reduces to obtain a vector for each element.\n        long_description:\n            The passthrough encoder simply transforms each input value into a float value and adds a\n            dimension to the input tensor, creating a b x s x 1 tensor where b is the batch size and s is\n            the length of the sequence. The tensor is reduced along the s dimension to obtain a single\n            vector of size h for each element of the batch.\n    encoding_size:\n        default_value_reasoning:\n            The default `reduce_output` method does not use this\n            parameter, so by default this parameter is not set.\n        description_implications:\n            This parameter must be equal to the size of the\n            input. Otherwise, an error will occur.\n        example_value:\n            - 128\n        expected_impact: 1\n        related_parameters:\n            - reduce_output\n        suggested_values_reasoning: NONE\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\nSetSparseEncoder:\n    type:\n        short_description: Maps raw values to sparse integer lists, then maps to dense/sparse embeddings, then reduces to final vector.\n        long_description:\n            The Embed encoder takes the raw binary values coming from the input placeholders and transforms\n            them to sparse integer lists, then they are mapped to either dense or sparse embeddings (one-hot\n            encodings), finally they are reduced on the sequence dimension and returned as an aggregated\n            embedding vector. Inputs are of size b while outputs are of size b x h where b is the batch size\n            and h is the dimensionality of the embeddings.\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nStacked2DCNN:\n    type:\n        short_description: Stack of 2D convolutional layers followed by an optional stack of fully connected layers.\n        long_description:\n            Stack of 2D convolutional layers with optional normalization, dropout, and down-sampling\n            pooling layers, followed by an optional stack of fully connected layers.\n        compute_tier: 1\n    conv_activation:\n        expected_impact: 1\n        ui_display_name: Convolutional Activation\n    conv_bias:\n        expected_impact: 1\n        ui_display_name: Convolutional Bias\n    conv_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Convolutional Dropout\n    conv_norm:\n        expected_impact: 2\n        ui_display_name: Convolutional Normalization\n    conv_norm_params:\n        expected_impact: 1\n        ui_display_name: Convolutional Normalization Parameters\n    dilation:\n        expected_impact: 1\n        ui_display_name: Dilation\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    fc_norm:\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate. See Torch's documentation on batch normalization or for layer see\n            Torch's documentation on layer normalization.\n        expected_impact: 2\n        related_parameters:\n            - fc_norm_params\n        suggested_values: batch\n        ui_display_name: Fully Connected Normalization\n    fc_norm_params:\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        expected_impact: 2\n        related_parameters:\n            - fc_norm\n        suggested_values: Depends on the type of `norm` set.\n        ui_display_name: Fully Connected Normalization Parameters\n    fc_use_bias:\n        expected_impact: 1\n        ui_display_name: FC Use Bias\n    fc_weights_initializer:\n        expected_impact: 1\n        ui_display_name: FC Weights Initializer\n    groups:\n        expected_impact: 1\n        ui_display_name: Groups\n    height:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    kernel_size:\n        expected_impact: 1\n        ui_display_name: Kernel Size\n    num_channels:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        ui_display_name: NOT DISPLAYED\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    out_channels:\n        expected_impact: 2\n        ui_display_name: Number of Output Channels\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    padding:\n        default_value_reasoning:\n            When padding is set to 'valid' like in the default\n            case, no padding is added. As a default value putting in the raw image\n            is the goal here.\n        description_implications:\n            By increasing the amount of padding, you can increase\n            the accuracy of the image analysis for certain circumstances.\n        example_value:\n            - \"'same'\"\n        expected_impact: 1\n        literature_references:\n            - https://www.geeksforgeeks.org/cnn-introduction-to-padding/\n        related_parameters:\n            - \"padding_mode,\n\n              resize method\"\n        suggested_values:\n            \"Same' padding if images are of different dimensions. \\n\\\n            Specific [h, w] entries can be valuable on a per dataset basis.\"\n        suggested_values_reasoning:\n            If your images already have padding, there is\n            no need to add padding, so the default is fine. If your images come in\n            different dimensions, then 'same' padding can help pad the images to standardized\n            dimensions. For certain images, adding padding to the edges can help the\n            CNN process the images better which can improve model performance. This\n            depends on the images however.\n        ui_display_name: Padding\n    padding_mode:\n        expected_impact: 1\n        ui_display_name: Padding Mode\n    pool_dilation:\n        expected_impact: 1\n        ui_display_name: Pool Dilation\n    pool_kernel_size:\n        expected_impact: 1\n        ui_display_name: Pool Kernel Size\n    pool_padding:\n        expected_impact: 1\n        ui_display_name: Pool Padding\n    pool_stride:\n        expected_impact: 1\n        ui_display_name: Pool Stride\n    stride:\n        expected_impact: 1\n        ui_display_name: Stride\n    width:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\nStackedCNN:\n    type:\n        short_description: Maps inputs to embeddings then passes them through a stack of 1d convolutional layers.\n        long_description:\n            The Stacked CNN works by first mapping the input integer sequence b x s (where b is the batch\n            size and s is the length of the sequence) into a sequence of embeddings, then it passes the\n            embedding through a stack of 1d convolutional layers with different filter size (by default 6\n            layers with filter size 7, 7, 3, 3, 3 and 3), followed by an optional final pool and by a\n            flatten operation. This single flatten vector is then passed through a stack of fully connected\n            layers and returned as a b x h tensor where h is the output size of the last fully connected\n            layer.\n        compute_tier: 1\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dilation_rate:\n        default_value_reasoning:\n            The standard discrete convolution is the same as\n            a 1-dilated convolution.\n        description_implications:\n            Higher dilation rates increase the effective size\n            of the convolutional filter.  Dilated convolution may improve performance\n            if the data is very correlated locally and also contains long-term dependencies.\n        example_value:\n            - 2\n        expected_impact: 1\n        other_information: Dilated convolution is also known as atrous convolution.\n        related_parameters:\n            - filter_size\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            The dilation rate is a factor which increases\n            the spacing between elements of the convolutional filter\n        ui_display_name: Dilation Rate\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_filters:\n        ui_display_name: null\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    padding:\n        ui_display_name: null\n    pool_function:\n        ui_display_name: null\n        expected_impact: 1\n    pool_padding:\n        ui_display_name: null\n        expected_impact: 1\n    pool_size:\n        ui_display_name: null\n        expected_impact: 1\n    pool_strides:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    strides:\n        default_value_reasoning:\n            In general, it makes sense to have a smaller stride\n            that fits the input. Imagining the simple 2D image as our input, two pixels\n            next to eachother are strongly correlated while pixels that are further\n            apart will have a comparatively weaker correlation. Consequently, a higher\n            stride may cause significant information loss.\n        description_implications:\n            Changing the stride of a convolutional layer is\n            one form of downsampling (another being pooling). In the case of a large\n            stride, significant amounts of information is thrown away as the filter\n            convolves over its input. This should be usually avoided but may be desirable\n            in cases in which the user has some deep knowledge of the filter or of\n            the rest of the model architecture that makes it comfortable to allow\n            a higher level compression in the output feature map of this layer.\n        example_value:\n            - 1\n        expected_impact: 2\n        literature_references:\n            - \"[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n\n              [machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n\n              [crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)\"\n        related_parameters:\n            - pool_strides, default_strides, default_pool_strides, block_strides\n        suggested_values: 1-2\n        suggested_values_reasoning:\n            In general, points that are closer to eachother\n            in the input feature space will be more strongly correlated to eachother,\n            so it is a good idea to select a stride that captures these neighboring\n            relationships.\n        ui_display_name: Stride\n    use_bias:\n        ui_display_name: null\n        expected_impact: 1\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nStackedCNNRNN:\n    type:\n        short_description: Maps inputs to embeddings, passes them through convolutional layer stack, then recurrent layer stack.\n        long_description:\n            The cnnrnn encoder works by first mapping the input integer sequence b x s (where b is the batch\n            size and s is the length of the sequence) into a sequence of embeddings, then it passes the\n            embedding through a stack of convolutional layers (by default 2), that is followed by a stack of\n            recurrent layers (by default 1), followed by a reduce operation that by default only returns the\n            last output, but can perform other reduce functions.\n        compute_tier: 1\n    activation:\n        ui_display_name: null\n        expected_impact: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    bidirectional:\n        ui_display_name: null\n        expected_impact: 0\n    cell_type:\n        ui_display_name: null\n        expected_impact: 3\n    conv_activation:\n        ui_display_name: null\n        expected_impact: 1\n    conv_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Convolutional Dropout\n    dilation_rate:\n        default_value_reasoning:\n            The standard discrete convolution is the same as\n            a 1-dilated convolution.\n        description_implications:\n            Higher dilation rates increase the effective size\n            of the convolutional filter.  Dilated convolution may improve performance\n            if the data is very correlated locally and also contains long-term dependencies.\n        example_value:\n            - 2\n        expected_impact: 1\n        other_information: Dilated convolution is also known as atrous convolution.\n        related_parameters:\n            - filter_size\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            The dilation rate is a factor which increases\n            the spacing between elements of the convolutional filter\n        ui_display_name: Dilation Rate\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    filter_size:\n        ui_display_name: null\n        expected_impact: 2\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_filters:\n        ui_display_name: null\n    num_rec_layers:\n        ui_display_name: null\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    padding:\n        ui_display_name: null\n    pool_function:\n        ui_display_name: null\n        expected_impact: 1\n    pool_padding:\n        ui_display_name: null\n        expected_impact: 1\n    pool_size:\n        ui_display_name: null\n        expected_impact: 1\n    pool_strides:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    recurrent_activation:\n        default_value_reasoning: sigmoid' is commonly used\n        expected_impact: 1\n        other_information:\n            I don't think that this parameter is used anywhere in the\n            code base. It's being passed down but not used in the actual RNN forwarding\n            functions.\n        suggested_values: sigmoid, ReLu, tanh\n        ui_display_name: null\n    recurrent_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"conv_dropout,\n\n              dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Recurrent Dropout\n    recurrent_initializer:\n        ui_display_name: null\n        expected_impact: 1\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    state_size:\n        ui_display_name: null\n        expected_impact: 3\n    strides:\n        default_value_reasoning:\n            In general, it makes sense to have a smaller stride\n            that fits the input. Imagining the simple 2D image as our input, two pixels\n            next to eachother are strongly correlated while pixels that are further\n            apart will have a comparatively weaker correlation. Consequently, a higher\n            stride may cause significant information loss.\n        description_implications:\n            Changing the stride of a convolutional layer is\n            one form of downsampling (another being pooling). In the case of a large\n            stride, significant amounts of information is thrown away as the filter\n            convolves over its input. This should be usually avoided but may be desirable\n            in cases in which the user has some deep knowledge of the filter or of\n            the rest of the model architecture that makes it comfortable to allow\n            a higher level compression in the output feature map of this layer.\n        example_value:\n            - 1\n        expected_impact: 2\n        literature_references:\n            - \"[d2l.ai blog post](http://d2l.ai/chapter_convolutional-neural-networks/padding-and-strides.html)\n\n\n              [machinelearningmastery blogpost](https://machinelearningmastery.com/padding-and-stride-for-convolutional-neural-networks/)\n\n\n              [crossvalidated discussion](https://stats.stackexchange.com/questions/296027/choosing-filter-size-strides-etc-in-a-cnn)\"\n        related_parameters:\n            - pool_strides, default_strides, default_pool_strides, block_strides\n        suggested_values: 1-2\n        suggested_values_reasoning:\n            In general, points that are closer to eachother\n            in the input feature space will be more strongly correlated to eachother,\n            so it is a good idea to select a stride that captures these neighboring\n            relationships.\n        ui_display_name: Stride\n    unit_forget_bias:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nStackedParallelCNN:\n    type:\n        short_description: Combination of Parallel CNN and Stacked CNN encoders utilizing a stack of parallel convolutional layers.\n        long_description:\n            The stacked parallel cnn encoder is a combination of the Parallel CNN and the Stacked CNN\n            encoders where each layer of the stack is composed of parallel convolutional layers. It works by\n            first mapping the input integer sequence b x s (where b is the batch size and s is the length of\n            the sequence) into a sequence of embeddings, then it passes the embedding through a stack of\n            several parallel 1d convolutional layers with different filter size, followed by an optional\n            final pool and by a flatten operation. This single flattened vector is then passed through a\n            stack of fully connected layers and returned as a b x h tensor where h is the output size of the\n            last fully connected layer.\n        compute_tier: 1\n    activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        expected_impact: 2\n        suggested_values:\n            The default value will work well in the majority of the\n            cases\n        ui_display_name: Activation\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    filter_size:\n        ui_display_name: null\n        expected_impact: 2\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_filters:\n        ui_display_name: null\n    num_stacked_layers:\n        description_implications:\n            While superceded by `stacked_layers`, this can directly\n            change the depth of the current stack of parallel convolutional layers.\n        example_value:\n            - 1\n        expected_impact: 1\n        related_parameters:\n            - stacked_layers\n        ui_display_name: Number of Stacked Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pool_function:\n        ui_display_name: null\n        expected_impact: 1\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    stacked_layers:\n        ui_display_name: null\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nStackedRNN:\n    type:\n        short_description: Utilizes a stack of recurrent layers followed by a reduce operation.\n        long_description:\n            The rnn encoder works by first mapping the input integer sequence b x s (where b is the batch\n            size and s is the length of the sequence) into a sequence of embeddings, then it passes the\n            embedding through a stack of recurrent layers (by default 1 layer), followed by a reduce\n            operation that by default only returns the last output, but can perform other reduce functions.\n        compute_tier: 1\n    activation:\n        ui_display_name: null\n        expected_impact: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    bidirectional:\n        ui_display_name: null\n        expected_impact: 0\n    cell_type:\n        ui_display_name: null\n        expected_impact: 3\n    dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout, recurrent_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n\n\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            Increasing the number of layers may improve model\n            performance for longer sequences or more complex tasks.\n        example_value:\n            - 1\n        expected_impact: 3\n        suggested_values: 1-3\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Recurrent Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    recurrent_activation:\n        default_value_reasoning: sigmoid' is commonly used\n        expected_impact: 1\n        other_information:\n            I don't think that this parameter is used anywhere in the\n            code base. It's being passed down but not used in the actual RNN forwarding\n            functions.\n        suggested_values: sigmoid, ReLu, tanh\n        ui_display_name: null\n    recurrent_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"dropout,\n\n              recurrent_dropout,\n\n              fc_dropout\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Recurrent Dropout\n    recurrent_initializer:\n        ui_display_name: null\n        expected_impact: 1\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    state_size:\n        ui_display_name: null\n        expected_impact: 3\n    unit_forget_bias:\n        ui_display_name: null\n        expected_impact: 1\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nStackedTransformer:\n    type:\n        short_description: Stack of transformer blocks with optional stack of fully connected layers.\n        long_description:\n            The transformer encoder implements a stack of transformer blocks, replicating the architecture\n            introduced in the Attention is all you need paper, and adds am optional stack of fully connected\n            layers at the end.\n        literature_references:\n            - https://arxiv.org/abs/1706.03762\n        compute_tier: 2\n    bias_initializer:\n        default_value_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights.\n        description_implications:\n            It's rare to see any performance gains from choosing\n            a different bias initialization. Some practitioners like to use a small\n            constant value such as 0.01 for all biases to ensure that all ReLU units\n            are activated in the beginning and have some effect on the gradient. However,\n            it's still an open question as to whether this provides consistent improvement.\n        expected_impact: 1\n        literature_references:\n            - https://cs231n.github.io/neural-networks-2/\n        related_parameters:\n            - weights_initializer\n        suggested_values: zeros\n        suggested_values_reasoning:\n            It is possible and common to initialize the biases\n            to be zero, since the asymmetry breaking is provided by the small random\n            numbers in the weights. For ReLU non-linearities, some people like to\n            use small constant value such as 0.01 for all biases because this ensures\n            that all ReLU units fire in the beginning and therefore obtain and propagate\n            some gradient. However, it is not clear if this provides a consistent\n            improvement (in fact some results seem to indicate that this performs\n            worse) and it is more common to simply use 0 bias initialization.\n        ui_display_name: Bias Initializer\n    dropout:\n        default_value_reasoning: Taken from published literature (https://arxiv.org/abs/1908.07442).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - fc_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Dropout\n    embedding_size:\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            'An embedding is a relatively low-dimensional space\n            that is used to translate high-dimensional vectors like words, which can\n            have a large vocbulary size. Ideally, after an embedding is trained, it\n            captures some of the semantics of the input by placing semantically similar\n            inputs close together in the embedding space.\n\n\n            In most cases, the embedding size is chosen empirically, by trial and\n            error. From https://www.amazon.com/dp/1098115783, \"one rule of thumb is\n            to use the fourth root of the total number of unique categorical elements\n            while another is that the embedding dimension should be approximately\n            1.6 times the square root of the number of unique elements in the category,\n            and no less than 600.\"\n\n\n            Increasing the embedding size may cause the model to train more slowly,\n            but the higher dimensionality can also improve overall quality.'\n        expected_impact: 3\n        literature_references:\n            - https://developers.google.com/machine-learning/crash-course/embeddings/video-lecture\n        suggested_values: 1.6 * sqrt(vocab_size)\n        suggested_values_reasoning:\n            Rule of thumb suggested by a deep learning textbook.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Embedding Size\n    embeddings_on_cpu:\n        default_value_reasoning:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access.\n        description_implications:\n            By default embeddings matrices are stored on GPU\n            memory if a GPU is used, as it allows for faster access. However, in some\n            cases when the vocabulary size is very large, the full embedding matrix\n            may be really big and unwieldy to have in GPU memory. This parameter forces\n            the placement of the embedding matrix in regular memory and the CPU is\n            used to access them. This may slow down training due to additional data\n            transfer between CPU and GPU memory, but can lead to healthier GPU memory\n            resource usage.\n        expected_impact: 1\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If GPU memory is not a constraint, having embeddings\n            stored and accessed within the GPU is faster.\n        ui_display_name: Embeddings on CPU\n    embeddings_trainable:\n        ui_display_name: null\n        expected_impact: 1\n    fc_activation:\n        default_value_reasoning:\n            The Rectified Linear Units (ReLU) function is the\n            standard activation function used for adding non-linearity. It is simple,\n            fast, and empirically works well (https://arxiv.org/abs/1803.08375).\n        description_implications:\n            Changing the activation functions has an impact\n            on the computational load of the model and might require further hypterparameter\n            tuning\n        example_value:\n            - relu\n        expected_impact: 1\n        literature_references:\n            - https://ml-cheatsheet.readthedocs.io/en/latest/activation_functions.html\n        related_parameters:\n            - activation, activation_function, conv_activation, recurrent_activation\n        suggested_values: relu, alternatively leakyRelu or elu\n        suggested_values_reasoning:\n            The default value will work well in the majority\n            of the cases\n        ui_display_name: FC Activation\n    fc_dropout:\n        default_value_reasoning:\n            Dropout can cause training to become less stable.\n            Consider start with a dropout-free baseline, and add dropout gradually\n            in subsequent experiments.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: FC Dropout\n    fc_layers:\n        default_value_reasoning:\n            By default the stack is built by using num_fc_layers,\n            output_size, use_bias, weights_initializer, bias_initializer, norm, norm_params,\n            activation, dropout. When a list of dictionaries is provided, the stack\n            is built following the parameters of each dict for building each layer.\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a big anough amount of data is provided,\n            but also makes the model more computationally expensive and potentially\n            more prone to overfitting.\n        example_value:\n            - dropout: 0.1\n              output_size: 128\n            - norm: layer\n              output_size: 64\n        expected_impact: 1\n        related_parameters:\n            - output_size\n            - use_bias\n            - weights_initializer\n            - bias_initializer\n            - norm\n            - norm_params\n            - activation\n            - dropout\n        suggested_values_reasoning:\n            It is easier to define a stack of fully connected\n            layers by just specifying num_fc_layers, output_size and the other individual\n            parameters. It will create a stack of layers with identical properties.\n            Use this parameter only if you need a fine grained level of control of\n            each individual layer in the stack.\n        ui_display_name: Fully Connected Layers\n    hidden_size:\n        default_value_reasoning: Taken from literature (https://arxiv.org/abs/1706.03762)\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 2\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes and the positional embedding matrix are\n            computed accurately.\n        internal_only: true\n        ui_display_name: null\n    norm:\n        default_value_reasoning:\n            While batch normalization and layer normalization\n            usually lead to improvements, it can be useful to start with fewer bells\n            and whistles.\n        description_implications:\n            Normalization helps stabilize the learning process\n            and can have a regularizing effect that can help with generalization.\n            It's often suggested that with normalization, you can use a higher learning\n            rate.\n        example_value:\n            - batch\n        expected_impact: 2\n        literature_references:\n            - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n        related_parameters:\n            - norm_params\n        suggested_values: '\"batch\" or \"layer\"'\n        suggested_values_reasoning:\n            Normalization tries to solve \"internal covariate\n            shift\" that comes from the changing distributions of the inputs to layers\n            deep in the network when weights are updated. For example, batch normalization\n            standardizes the inputs to a layer for each mini-batch. Try out different\n            normalizations to see if that helps with training stability\n        ui_display_name: Normalization Type\n    norm_params:\n        default_value_reasoning:\n            The default parameters that come with Torch's implementation\n            of these normalization types are a trusted starting point.\n        description_implications:\n            There are a variety of ways a certain set of parameters\n            specificed could influence performance here. Broadly speaking the different\n            values passed in here allow for different levels of smoothness to be observed\n            in the learning curves. Since setting this parameters depends on the type\n            of `norm` set, see [BatchNorm2d](https://pytorch.org/docs/stable/generated/torch.nn.BatchNorm2d.html)\n            for more information on the parameters to set for batch normalization,\n            and see [LayerNorm](https://pytorch.org/docs/stable/generated/torch.nn.LayerNorm.html)\n            for more information on the parameters to set for layer normalization.\n        example_value:\n            - affine: false\n              momentum: 0.2\n              num_features: 100\n        expected_impact: 1\n        literature_references:\n            - \"For BatchNorm2d: https://arxiv.org/abs/1502.03167\n              For LayerNorm: https://arxiv.org/abs/1607.06450\"\n        related_parameters:\n            - \"`norm`\"\n        suggested_values: Depends on the type of `norm` set.\n        suggested_values_reasoning: \"NO\"\n        ui_display_name: Normalization Parameters\n    num_fc_layers:\n        default_value_reasoning:\n            The encoder already has learnable parameters.Sometimes\n            the default is 1 for modules where the FC stack is used for shape management,\n            or the only source of learnable parameters.\n        description_implications:\n            Increasing num_fc_layers will increase the capacity\n            of the model. The model will be slower to train, and there's a higher\n            risk of overfitting.\n        example_value:\n            - 1\n        expected_impact: 1\n        other_information:\n            Not all modules that have fc_layers also have an accompanying\n            num_fc_layers parameter. Where both are present, fc_layers takes precedent\n            over num_fc_layers. Specifying num_fc_layers alone uses fully connected\n            layers that are configured by the defaults in FCStack.\n        related_parameters:\n            - fc_layers\n        suggested_values: 0-1\n        suggested_values_reasoning:\n            The full model likely contains many learnable\n            parameters. Consider starting with very few, or without any additional\n            fully connected layers and add them if you observe evidence of limited\n            model capacity. Sometimes the default is 1 for modules where the FC stack\n            is used for shape management, or the only source of learnable parameters.\n        ui_display_name: Number of Fully Connected Layers\n    num_heads:\n        ui_display_name: null\n    num_layers:\n        default_value_reasoning:\n            The ideal number of layers depends on the data. For\n            many data types, one layer is sufficient.\n        description_implications:\n            \"The ideal number of transformer layers depends\n            on the length and complexity of input sequences, as well as the task.\n\n\n            For more complex tasks, and higher number of transformer layers may be\n            useful. However, too many layers will increase memory and slow training\n            while providing diminishing returns of model performance.\"\n        example_value:\n            - 1\n        expected_impact: 3\n        suggested_values: 1 - 12\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Transformer Layers\n    output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 3\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Output Size\n    pretrained_embeddings:\n        ui_display_name: null\n        expected_impact: 0\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    representation:\n        ui_display_name: null\n        expected_impact: 1\n    should_embed:\n        internal_only: true\n        ui_display_name: Not displayed\n    transformer_output_size:\n        default_value_reasoning: A modest value, not too small, not too large.\n        description_implications:\n            If there are fully connected layers in this module,\n            increasing the output size of each fully connected layer will increase\n            the capacity of the model. However, the model may be slower to train,\n            and there's a higher risk of overfitting. If it seems like the model could\n            use even more capacity, consider increasing the number of fully connected\n            layers, or explore other architectures.\n        expected_impact: 2\n        other_information:\n            If num_fc_layers=0 and fc_layers=None, and there are no\n            fully connected layers defined on the module, then this parameter may\n            have no effect on the module's final output shape.\n        related_parameters:\n            - num_fc_layers, fc_layers\n        suggested_values: 10 - 1024\n        suggested_values_reasoning:\n            Increasing the output size increases the capacity\n            of the model. If this seems to have a positive effect, then it could be\n            worth increasing the number of layers, or trying a different architecture\n            with a larger capacity.\n        ui_display_name: Transformer Output Size\n    use_bias:\n        default_value_reasoning:\n            \"Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to use bias terms.\n\n\n            Batch Normalization, however, adds a trainable shift parameter which is\n            added to the activation. When Batch Normalization is used in a layer,\n            bias terms are redundant and may be removed.\"\n        description_implications:\n            Bias terms may improve model accuracy, and don't\n            have much impact in terms of memory or training speed. For most models\n            it is reasonable to leave this parameter set to True.\n        example_value:\n            - true\n        expected_impact: 1\n        other_information:\n            If fc_layers is not specified, or use_bias is not specified\n            for individual layers, the value of use_bias will be used as the default\n            for all layers.\n        related_parameters:\n            - bias_initializer, fc_layers\n        suggested_values:\n            - true\n        ui_display_name: Use Bias\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    weights_initializer:\n        default_value_reasoning: Taken from [this paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf).\n        description_implications:\n            The method you choose to initialize layer weights\n            during training can have a big impact on performance as well as the reproducibility\n            of your final model between runs. As an example, if you were to randomly\n            initialize weights you would risk non-reproducibility (and possibly general\n            training performance), but sticking with constant values for initialization\n            might significantly increase the time needed for model convergence. Generally,\n            choosing one of the probabilistic approaches strikes a balance between\n            the two extremes, and the literature kicked off by the landmark [*Xavier\n            et al.* paper](http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf)\n            provides a few good options. See this nice discussion from [Weights and\n            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.)\n            for more information.\n        expected_impact: 1\n        literature_references:\n            - \"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\n\n              Xavier et al. paper: http://proceedings.mlr.press/v9/glorot10a/glorot10a.pdf\"\n        suggested_values: xavier_uniform\n        suggested_values_reasoning:\n            Changing the weights initialization scheme is\n            something to consider if a model is having trouble with convergence, or\n            otherwise it is something to experiment with after other factors are considered.\n            The default choice (`xavier_uniform`) is a suitable starting point for\n            most tasks.\n        ui_display_name: Layer Weights Initializer\nT5:\n    type:\n        short_description: Text-to-text approach transformer with good transfer performance on multiple tasks.\n        long_description:\n            The `t5` encoder loads a pretrained [T5](https://arxiv.org/pdf/1910.10683.pdf) (default `t5-small`) model using the\n            Hugging Face transformers package. T5 (Text-to-Text Transfer Transformer) is pre-trained on a huge text dataset crawled\n            from the web and shows good transfer performance on multiple tasks.\n        compute_tier: 2\n    d_ff:\n        default_value_reasoning: Default value matches the pre-trained encoder.\n        description_implications:\n            If using a pre-trained encoder, this parameter will\n            be automatically derived from the pre-trained model.\n        expected_impact: 1\n        ui_display_name: Dimensionality of Feed-Forward Layer\n    d_kv:\n        ui_display_name: null\n    d_model:\n        ui_display_name: null\n    dropout_rate:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout_rate\n    feed_forward_proj:\n        ui_display_name: null\n    initializer_factor:\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    num_decoder_layers:\n        ui_display_name: null\n    num_heads:\n        ui_display_name: null\n    num_layers:\n        default_value_reasoning:\n            The default value matches the number of layers in\n            the default pretrained encoder.\n        description_implications:\n            \"The ideal number of transformer layers depends\n            on the length and complexity of input sequences, as well as the task.\n\n\n            If using a pre-trained model, this parameter will be automatically derived\n            from the pre-trained model.\"\n        example_value:\n            - 6\n        expected_impact: 2\n        related_parameters:\n            - pretrained_model_or_path\n        suggested_values: 1 - 12\n        suggested_values_reasoning:\n            Increasing the number of layers may improve encoder\n            performance.  However, more layers will increase training time and may\n            cause overfitting.  Small numbers of layers usually work best.\n        ui_display_name: Number of Transformer Layers\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    relative_attention_num_buckets:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nTransformerXL:\n    type:\n        short_description: Transformer architecture that introduces the notion of recurrence to the deep self-attention network.\n        long_description:\n            The `transformer_xl` encoder loads a pretrained [Transformer-XL](https://arxiv.org/abs/1901.02860)\n            (default `transfo-xl-wt103`) model using the Hugging Face transformers package. Adds novel positional encoding scheme\n            which improves understanding and generation of long-form text up to thousands of tokens. Transformer-XL is a causal (uni-directional)\n            transformer with relative positioning (sinusoïdal) embeddings which can reuse previously\n            computed hidden-states to attend to longer context (memory). This model also uses adaptive\n            softmax inputs and outputs (tied).\n        compute_tier: 2\n    adaptive:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Adaptive softmax is a speedup technique for computing\n            probability distributions over words. For text with large vocabulary,\n            adaptive softmax improves both training speed.\n        expected_impact: 1\n        related_parameters:\n            - vocab_size\n        ui_display_name: Adaptive Softmax\n    attn_type:\n        ui_display_name: null\n    clamp_len:\n        ui_display_name: null\n    cutoffs:\n        ui_display_name: null\n    d_embed:\n        ui_display_name: null\n    d_head:\n        ui_display_name: null\n    d_inner:\n        ui_display_name: null\n    d_model:\n        ui_display_name: null\n    div_val:\n        ui_display_name: null\n    dropatt:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout\n    eos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: End-of-Sequence Token Id\n    init:\n        ui_display_name: null\n    init_range:\n        ui_display_name: null\n    init_std:\n        ui_display_name: null\n    layer_norm_epsilon:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    mem_len:\n        ui_display_name: null\n    n_head:\n        ui_display_name: null\n    n_layer:\n        ui_display_name: null\n    pre_lnorm:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    proj_init_std:\n        ui_display_name: null\n    proj_share_all_but_first:\n        ui_display_name: null\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    same_length:\n        ui_display_name: null\n    sample_softmax:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    untie_r:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nTVAlexNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVBaseEncoder:\n    model_cache_dir:\n        ui_display_name: Model Cache Directory\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: Saved Weights in Checkpoint\n    trainable:\n        default_value_reasoning: By default, model components are trainable.\n        description_implications:\n            The tradeoff when using `trainable` is between speed\n            and flexibility. If False, less weights are subject to change and the\n            model will therefore train faster. However, the representations output\n            by this component are fixed for each input.\n        expected_impact: 3\n        literature_references:\n            - \"https://www.ibm.com/cloud/learn/overfitting\n\n\n              http://d2l.ai/chapter_computer-vision/fine-tuning.html\"\n        related_parameters:\n            - use_pretrained, pretrained_model, saved_weights_in_checkpoint\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Freezing the weights (i.e. `trainable = False`)\n            is only worth trying if you are loading in pretrained weights. In that\n            case, check to see if your model is overfitting. If so, freezing the weights\n            (and therefore reducing model complexity) may be beneficial.\n        ui_display_name: Trainable\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\nTVConvNeXtEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVDenseNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVEfficientNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVGoogLeNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVInceptionV3Encoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVMaxVitEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVMNASNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVMobileNetV2Encoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVMobileNetV3Encoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVRegNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVResNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVResNeXtEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVShuffleNetV2Encoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVSqueezeNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVSwinTransformerEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVViTEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVVGGEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nTVWideResNetEncoder:\n    model_variant:\n        ui_display_name: Model Variant\n    type:\n        ui_display_name: Type\nViT:\n    type:\n        short_description: ViT encoder divides images into patches, performs a linear transformation, and then applies a transformer.\n        long_description:\n            ViT, short for Vision Transformer, divides the image into equal-sized patches, uses a linear\n            transformation to encode each flattened patch, then applies a deep transformer architecture to\n            the sequence of encoded patches.\n        compute_tier: 2\n    attention_probs_dropout_prob:\n        default_value_reasoning: Taken from literature (https://arxiv.org/abs/2010.11929).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"hidden_dropout_prob,\n\n              attention_probs_dropout_prob\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Attention Dropout\n    gradient_checkpointing:\n        ui_display_name: null\n    height:\n        internal_only: true\n        ui_display_name: null\n    hidden_act:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            Changing this activation function will only affect\n            the feed-forward layers of the transformer.\n        example_value:\n            - relu\n        expected_impact: 2\n        literature_references:\n            - \"[Huggingface docs for ViT config](https://huggingface.co/docs/transformers/model_doc/vit#transformers.ViTConfig.hidden_act)\n\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)\"\n        suggested_values: gelu\n        suggested_values_reasoning: Taken from huggingface defaults.\n        ui_display_name: Hidden Layer Activation\n    hidden_dropout_prob:\n        default_value_reasoning: Taken from literature (https://arxiv.org/abs/2010.11929).\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - \"hidden_dropout_prob,\n\n              attention_probs_dropout_prob\"\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: Hidden Dropout\n    hidden_size:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            Increasing the hidden size makes the model larger\n            and slower to train, increases the model's capacity to capture more complexity.\n            It also increases the chance of overfitting.\n        expected_impact: 2\n        suggested_values: 10 - 2048\n        suggested_values_reasoning:\n            Increasing the hidden size makes sense if the\n            model is underfitting. It's useful to train both smaller and larger models\n            to see how model capacity affects performance. This should only be explored\n            after the architecture of the model has been settled.\n        ui_display_name: Hidden Size\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    intermediate_size:\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    num_attention_heads:\n        ui_display_name: null\n    num_channels:\n        ui_display_name: null\n    num_hidden_layers:\n        ui_display_name: null\n    patch_size:\n        default_value_reasoning: Taken from ViT paper.\n        description_implications:\n            \"The implications of the image patch size for this\\\n            \\ layer depend on other factors, such as the true resolution of the incoming\\\n            \\ image dataset. If the patch size is kept consistent but a higher resolution\\\n            \\ image is used as input, then the resulting chunked sequence of tokens\\\n            \\ will be longer than it would have been if the input resolution was lower.\\\n            \\ \\n\\nThe ViT paper notes that decreasing the patch size in this way led\\\n            \\ to robust improvements without introducing other parameters.\"\n        expected_impact: 2\n        literature_references:\n            - \"[Huggingface docs](https://huggingface.co/docs/transformers/model_doc/vit)\n\n\n              [ViT paper](https://arxiv.org/abs/2010.11929)\"\n        suggested_values:\n            - 16\n            - 32\n        suggested_values_reasoning:\n            16 and 32 are the values used in the original\n            ViT paper.\n        ui_display_name: Patch Size\n    pretrained_model:\n        default_value_reasoning:\n            The default model is the canonical model for this\n            model architecture, and is therefore a good starting point for most use\n            cases.\n        description_implications:\n            \"There are two factors to consider when choosing\\\n            \\ a pre-trained model: (1) size, and (2) task similarity. \\n\\nThe larger\\\n            \\ the model, the more subtle its comprehension of inputs can become. However,\\\n            \\ larger models are also more compute and memory-intensive to train.\\n\\\n            \\nModels pretrained on highly-related source tasks are more likely to\\\n            \\ be successful on the target task. Consider searching the HuggingFace\\\n            \\ model repository for models trained on similar tasks.\"\n        expected_impact: 3\n        literature_references:\n            - https://arxiv.org/abs/2010.11929\n        related_parameters:\n            - use_pretrained, trainable, pretrained_kwargs\n        suggested_values: google/vit-large-patch16-224\n        suggested_values_reasoning:\n            \"If you would like better performance and are\n            not compute/memory-constrained, increasing model capacity can potentially\n            provide a richer representation than the default. The suggested value\n            upsizes the model while maintaining the same model architecture.\n\n\n            Model trained on internet-scale datasets typically generalize well. Consider\n            deviating from the default only if the images in the dataset originate\n            from another domain (e.g. medical images, geospatial data).\"\n        ui_display_name: Pretrained model name\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        default_value_reasoning: By default, model components are trainable.\n        description_implications:\n            The tradeoff when using `trainable` is between speed\n            and flexibility. If False, less weights are subject to change and the\n            model will therefore train faster. However, the representations output\n            by this component are fixed for each input.\n        expected_impact: 3\n        literature_references:\n            - \"https://www.ibm.com/cloud/learn/overfitting\n\n\n              http://d2l.ai/chapter_computer-vision/fine-tuning.html\"\n        related_parameters:\n            - use_pretrained, pretrained_model, saved_weights_in_checkpoint\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Freezing the weights (i.e. `trainable = False`)\n            is only worth trying if you are loading in pretrained weights. In that\n            case, check to see if your model is overfitting. If so, freezing the weights\n            (and therefore reducing model complexity) may be beneficial.\n        ui_display_name: Trainable\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    width:\n        internal_only: true\n        ui_display_name: null\nXLM:\n    type:\n        short_description: XLM is pre-trained by cross-language modeling.\n        long_description:\n            The `xlm` encoder loads a pretrained [XLM](https://arxiv.org/abs/1901.07291) (default `xlm-mlm-en-2048`) model using the\n            Hugging Face transformers package. Pre-trained by cross-language modeling.\n        compute_tier: 2\n    asm:\n        ui_display_name: null\n    attention_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: attention_dropout\n    bos_index:\n        ui_display_name: null\n    bos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Beginning-of-Sentence Token Id\n    causal:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - attention_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout\n    emb_dim:\n        ui_display_name: null\n    embed_init_std:\n        ui_display_name: null\n    end_n_top:\n        ui_display_name: null\n    eos_index:\n        ui_display_name: null\n    gelu_activation:\n        ui_display_name: null\n        expected_impact: 1\n    init_std:\n        ui_display_name: null\n    is_encoder:\n        ui_display_name: null\n    lang_id:\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    mask_index:\n        ui_display_name: null\n    mask_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Mask Token ID\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 2\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    n_heads:\n        ui_display_name: null\n    n_langs:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        expected_impact: 1\n        ui_display_name: Number of Languages\n    n_layers:\n        ui_display_name: null\n    pad_index:\n        ui_display_name: null\n    pad_token_id:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    sinusoidal_embeddings:\n        ui_display_name: null\n    start_n_top:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    unk_index:\n        ui_display_name: null\n    use_lang_emb:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nXLMRoBERTa:\n    type:\n        short_description: XLM-RoBERTa a large multi-lingual language model trained on 2.5TB of filtered CommonCrawl data.\n        long_description:\n            The `xlmroberta` encoder loads a pretrained [XLM-RoBERTa](https://arxiv.org/abs/1911.02116)\n            (default `jplu/tf-xlm-reoberta-base`) model using the Hugging Face transformers package. XLM-RoBERTa is a multi-language\n            model similar to BERT, trained on 100 languages. XLM-RoBERTa is based on Facebook’s RoBERTa model\n            released in 2019. It is a large multi-lingual language model, trained on 2.5TB of filtered\n            CommonCrawl data.\n        compute_tier: 2\n    add_pooling_layer:\n        ui_display_name: null\n    bos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Beginning-of-Sentence Token Id\n    eos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: End-of-Sentence Token Id\n    max_position_embeddings:\n        default_value_reasoning: Taken from huggingface.\n        description_implications:\n            The size of the position embeddings table. This typically coincides with the\n            maximum sequence length this model might ever be used with. Typically set this\n            to something large just in case (e.g. 512, 1024, 2048).\n        expected_impact: 1\n        suggested_values: 512\n        suggested_values_reasoning:\n            Out of the box value based on published literature.\n            Try models with smaller or larger embedding sizes to observe relative\n            impact.\n        ui_display_name: Max Position Embeddings\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    pad_token_id:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    type_vocab_size:\n        ui_display_name: null\n        expected_impact: 1\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nXLNet:\n    type:\n        short_description: XLNet is a transformer that outperforms BERT on a variety of benchmarks.\n        long_description:\n            The `xlnet` encoder loads a pretrained [XLNet](https://arxiv.org/abs/1906.08237) (default `xlnet-base-cased`) model\n            using the Hugging Face transformers package. XLnet is an extension of the Transformer-XL model pre-trained using\n            an autoregressive method to learn bidirectional contexts by maximizing the expected likelihood\n            over all permutations of the input sequence factorization order. XLNet outperforms BERT on a\n            variety of benchmarks.\n        compute_tier: 2\n    attn_type:\n        ui_display_name: null\n    bi_data:\n        ui_display_name: null\n    bos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Beginning-of-Sentence Token Id\n    clamp_len:\n        ui_display_name: null\n    d_inner:\n        ui_display_name: null\n    d_model:\n        ui_display_name: null\n    dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 2\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - summary_last_dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: dropout\n    end_n_top:\n        ui_display_name: null\n    eos_token_id:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: End-of-Sequence Token Id\n    ff_activation:\n        ui_display_name: null\n        expected_impact: 1\n    initializer_range:\n        description_implications:\n            There is an ideal value for this variable that doesn't\n            lead to the outputs of these matrices to vanish or explode\n        example_value:\n            - 0.02\n        expected_impact: 1\n        other_information: Must be greater than 0\n        related_parameters:\n            - weights_initializer\n        suggested_values: 0.01-0.05\n        suggested_values_reasoning:\n            Large values will likely lead to very large outputs.\n            Small values will lead to vanishing outputs.\n        ui_display_name: null\n    layer_norm_eps:\n        ui_display_name: null\n    max_sequence_length:\n        default_value_reasoning:\n            Sets the maximum sequence length of the expected\n            inputs, so input/output shapes are computed accurately.\n        internal_only: true\n        ui_display_name: null\n    mem_len:\n        ui_display_name: null\n    n_head:\n        ui_display_name: null\n    n_layer:\n        ui_display_name: null\n    pad_token_id:\n        ui_display_name: null\n    pretrained_kwargs:\n        ui_display_name: null\n    pretrained_model_name_or_path:\n        ui_display_name: null\n        expected_impact: 2\n    reduce_output:\n        ui_display_name: null\n        expected_impact: 1\n    reuse_len:\n        ui_display_name: null\n    same_length:\n        ui_display_name: null\n    saved_weights_in_checkpoint:\n        default_value_reasoning:\n            The weights of the encoder are not necessarily saved\n            in the checkpoint. The user has to save them first.\n        description_implications:\n            The memory footprint for some of these encoders\n            can be large.\n        internal_only: true\n        related_parameters:\n            - skip_save_model\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            Some of these encoders are large, so it might\n            be better to load them as needed, especially if 1. they're not used frequently\n            2. the user doesn't have a lot of storage.\n        ui_display_name: null\n    start_n_top:\n        ui_display_name: null\n    summary_activation:\n        default_value_reasoning: Default value used in pre-trained HF encoder.\n        ui_display_name: Summary Activation Function\n        expected_impact: 1\n    summary_last_dropout:\n        default_value_reasoning: Huggingface default.\n        description_implications:\n            \"Dropout is a computationally cheap regularization\\\n            \\ method where during training, some neurons are randomly ignored or \\u201C\\\n            dropped out\\u201D. Increasing dropout has the effect of making the training\\\n            \\ process more noisy and lowering overall network capacity, but it can\\\n            \\ be an effective regularization method to reduce overfitting and improve\\\n            \\ generalization.\"\n        example_value:\n            - 0.2\n        expected_impact: 1\n        literature_references:\n            - https://pytorch.org/docs/stable/generated/torch.nn.Dropout.html\n        related_parameters:\n            - dropout\n        suggested_values: 0.05 - 0.8\n        suggested_values_reasoning:\n            Tuning dropout is really something to be done\n            when all of the big choices about architecture have been settled. Consider\n            starting with 0.5 and adjusting the dropout depending on observed model\n            performance.\n        ui_display_name: summary_last_dropout\n    summary_type:\n        ui_display_name: null\n    summary_use_proj:\n        ui_display_name: null\n    trainable:\n        expected_impact: 3\n        ui_display_name: null\n    untie_r:\n        ui_display_name: null\n    use_mems_eval:\n        ui_display_name: null\n    use_mems_train:\n        ui_display_name: null\n    use_pretrained:\n        default_value_reasoning:\n            By default, the model is initialized as a pretrained\n            model.\n        description_implications:\n            Pretrained models have typically already learned\n            features that are difficult to learn from scratch. They are particularly\n            beneficial when training on small amounts of data.\n        expected_impact: 3\n        literature_references:\n            - https://machinelearningmastery.com/transfer-learning-for-deep-learning/\n        related_parameters:\n            - trainable, pretrained_model_name, pretrained_model_name_or_path, pretrained_kwargs\n        suggested_values:\n            - false\n        suggested_values_reasoning:\n            If you have a large amount of data and/or you\n            have data that differs from the typical distribution, then it might be\n            worth training the model from scratch.\n        ui_display_name: Use Pretrained\n    vocab:\n        default_value_reasoning:\n            Computed and passed along internally according to\n            preprocessing settings.\n        example_value:\n            - a\n            - b\n            - c\n        internal_only: true\n        ui_display_name: Not Displayed\n    vocab_size:\n        internal_only: true\n        ui_display_name: Not displayed\nconv_params:\n    num_conv_layers:\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a large amount of data is provided, but\n            also makes the model more computationally expensive and potentially more\n            prone to overfitting.\n        expected_impact: 3\n        related_parameters:\n            - conv_layers\n        ui_display_name: Number of Convolutional Layers\n    conv_layers:\n        description_implications:\n            The more layers that are specified the deeper and\n            higher capacity the model will be. This makes it possible to potentially\n            achieve better performance when a large amount of data is provided, but\n            also makes the model more computationally expensive and potentially more\n            prone to overfitting.\n        expected_impact: 1\n        related_parameters:\n            - num_conv_layers\n        ui_display_name: Convolutional Layers\n    pool_function:\n        default_value_reasoning:\n            \"Within a given sliding window (e.g. a \\\"patch\\\"\\\n            \\ of a 3-channel image), the maximum value for each channel is kept. All\\\n            \\ other values in the patch are discarded. Repeat this step for every\\\n            \\ patch and you have a more compact representation of the image. \\n\\n\\\n            Intuitively, each patch encodes the features from a particular part of\\\n            \\ an image, and it is more informative to look at the most prominent features\\\n            \\ of an image than the average of all of them.\"\n        description_implications:\n            Both average and max pooling can achieve strong\n            performance.\n        expected_impact: 1\n        literature_references:\n            - \"https://pytorch.org/docs/stable/generated/torch.nn.MaxPool2d.html\n\n\n              https://machinelearningmastery.com/pooling-layers-for-convolutional-neural-networks/\"\n        suggested_values: Default\n        suggested_values_reasoning: \"No\"\n        ui_display_name: Pooling function\n    pool_size:\n        ui_display_name: null\n        expected_impact: 1\n    num_filters:\n        ui_display_name: null\n    filter_size:\n        ui_display_name: null\n        expected_impact: 2\nUNetEncoder:\n    type:\n        short_description: The UNet encoder convolutional and max pool layers\n        long_description:\n            Stacks of two 2D convolutional layers with optional normalization\n            and relu activation, followed by a max pool layer in all but the\n            final level of the encoder.\n        compute_tier: 1\n    conv_norm:\n        expected_impact: 2\n        ui_display_name: Convolutional Normalization\n    height:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    num_channels:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\n    width:\n        default_value_reasoning:\n            Computed internally, automatically, based on image\n            data preprocessing.\n        internal_only: true\n        ui_display_name: NOT DISPLAYED\nTimmEncoder:\n    model_name:\n        ui_display_name: Model Name\n    use_pretrained:\n        ui_display_name: Use Pretrained\n    saved_weights_in_checkpoint:\n        internal_only: true\n        ui_display_name: Saved Weights in Checkpoint\n    trainable:\n        ui_display_name: Trainable\nTimmCAFormerEncoder:\n    model_name:\n        ui_display_name: Model Name\nTimmConvFormerEncoder:\n    model_name:\n        ui_display_name: Model Name\nTimmPoolFormerEncoder:\n    model_name:\n        ui_display_name: Model Name\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/features.yaml",
    "content": "audio:\n    preprocessing:\n        audio_file_length_limit_in_s:\n            ui_display_name: null\n            expected_impact: 2\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            ui_display_name: Fill Value\n            expected_impact: 2\n        in_memory:\n            ui_display_name: null\n            expected_impact: 1\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            expected_impact: 3\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n        norm:\n            default_value_reasoning:\n                While batch normalization and layer normalization\n                usually lead to improvements, it can be useful to start with fewer\n                bells and whistles.\n            description_implications:\n                Normalization helps stabilize the learning process\n                and can have a regularizing effect that can help with generalization.\n                It's often suggested that with normalization, you can use a higher\n                learning rate.\n            example_value:\n                - batch\n            expected_impact: 2\n            literature_references:\n                - https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/\n            related_parameters:\n                - norm_params\n            suggested_values: '\"batch\" or \"layer\"'\n            suggested_values_reasoning:\n                Normalization tries to solve \"internal covariate\n                shift\" that comes from the changing distributions of the inputs to\n                layers deep in the network when weights are updated. For example,\n                batch normalization standardizes the inputs to a layer for each mini-batch.\n                Try out different normalizations to see if that helps with training\n                stability\n            ui_display_name: Normalization Type\n        num_fft_points:\n            ui_display_name: null\n            expected_impact: 1\n        num_filter_bands:\n            literature_references:\n                - \"https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e \"\n            related_parameters:\n                - window_length_in_s\n                - type\n                - window_shift_in_s\n            ui_display_name: Type\n            expected_impact: 1\n        padding_value:\n            ui_display_name: null\n            expected_impact: 1\n        type:\n            default_value_reasoning:\n                The default type fbank is set based on values\n                that we have tested and determined to be a good starting point for\n                audio feature preprocessing. This is not to say that it is the best\n                way to process every audio feature, it is just a good starting place\n                that performs well in general.\n            description_implications:\n                The different type of audio you select hear\n                will determine how your audio feature is preprocessed and transformed\n                into trainable data for the model.\n            example_value:\n                - stft\n            expected_impact: 3\n            literature_references:\n                - \"https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e \"\n            other_information:\n                Audio feature preprocessing depends heavily on the\n                type of audio data you are dealing with. The type of audio preprocessing\n                you will want to use will be dictated by the audio data you are dealing\n                with.\n            related_parameters:\n                - audio_file_length_limit_in_s\n                - norm\n                - padding_value\n                - in_memory\n            ui_display_name: Type\n        window_length_in_s:\n            literature_references:\n                - \"https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e \"\n            related_parameters:\n                - window_shift_in_s\n                - type\n                - num_filter_bands\n            ui_display_name: Window Length in Seconds\n            expected_impact: 2\n        window_shift_in_s:\n            literature_references:\n                - \"https://medium.com/analytics-vidhya/simplifying-audio-data-fft-stft-mfcc-for-machine-learning-and-deep-learning-443a2f962e0e \"\n            related_parameters:\n                - window_length_in_s\n                - type\n                - num_filter_bands\n            ui_display_name: Window Shift in Seconds\n            expected_impact: 2\n        window_type:\n            ui_display_name: null\n            expected_impact: 2\nbag:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            ui_display_name: Fill Value\n            expected_impact: 2\n        lowercase:\n            ui_display_name: null\n            expected_impact: 2\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            expected_impact: 3\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n        most_common:\n            default_value_reasoning:\n                If there are more than 10000 unique categories\n                in the data, it is likely that they will follow a long-tailed distribution\n                and the least common ones may not provide a lot of information\n            description_implications:\n                A smaller number will reduce the vocabulary,\n                making the embedding matrix smaller and reduce the memory footprint,\n                but will also collapse more tokens into the rare one, so the model\n                may perform worse when rare tokens appear in the data\n            example_value:\n                - 10000\n            expected_impact: 2\n            other_information: Specifying a vocab_file overrides this parameter\n            related_parameters:\n                - vocab_file, pretrained_embeddings\n            suggested_values:\n                A value that covers at least 95% of the tokens in the\n                data\n            suggested_values_reasoning:\n                Depending on the data distribution and how\n                important rare tokens are, 90%, 95% or 99% of the number of tokens\n                will leave out only very rare tokens that should not influence performance\n                substantially\n            ui_display_name: Most common (vocabulary size)\n        tokenizer:\n            ui_display_name: null\n            expected_impact: 3\nbinary:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fallback_true_label:\n            description_implications:\n                Modeling performance should not be affected,\n                but the semantics of some binary metrics may change like for \"false\n                positives\", \"false negatives\", etc. if the true label is pinned to\n                the other value.\n            expected_impact: 2\n            ui_display_name: Fallback True Label\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n    calibration:\n        expected_impact: 3\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\n    threshold:\n        expected_impact: 3\ncategory:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        lowercase:\n            ui_display_name: null\n            expected_impact: 2\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        most_common:\n            default_value_reasoning:\n                If there are more than 10000 unique categories\n                in the data, it is likely that they will follow a long-tailed distribution\n                and the least common ones may not provide a lot of information\n            description_implications:\n                A smaller number will reduce the vocabulary,\n                making the embedding matrix smaller and reduce the memory footprint,\n                but will also collapse more tokens into the rare one, so the model\n                may perform worse when rare tokens appear in the data\n            example_value:\n                - 10000\n            expected_impact: 2\n            other_information: Specifying a vocab_file overrides this parameter\n            related_parameters:\n                - vocab_file, pretrained_embeddings\n            suggested_values:\n                A value that covers at least 95% of the tokens in the\n                data\n            suggested_values_reasoning:\n                Depending on the data distribution and how\n                important rare tokens are, 90%, 95% or 99% of the number of tokens\n                will leave out only very rare tokens that should not influence performance\n                substantially\n            ui_display_name: Most common (vocabulary size)\n    calibration:\n        expected_impact: 3\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\n    top_k:\n        expected_impact: 3\ndate:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        datetime_format:\n            default_value_reasoning:\n                Ludwig will try to infer the date format automatically,\n                but a specific format can be provided. The date string spec is the\n                same as the one described in python's datetime.\n            description_implications:\n                If Ludwig has trouble parsing dates, it could\n                be useful to specify an explicit format that Ludwig should parse date\n                feature values as. This could also serve as a form of normalization,\n                for example, if not all datetimes have the same granularity (some\n                have days, some have times), then the common format (i.e. %d %m %Y)\n                serves as a truncator.\n            example_value:\n                - \"%d %b %Y\"\n            expected_impact: 2\n            suggested_values_reasoning: Have Ludwig figure out the date format automatically.\n            ui_display_name: Datetime format\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\nh3:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\nimage:\n    # TODO: review metadata generated by Copilot\n    augmentation:\n        auto_augmentation_method:\n            default_value_reasoning: Trivial augment is computationally more efficient than the other methods.\n            description_implications:\n                Type of auto-augmentation method to apply to batch of images to improve model generalization\n            example_value:\n                \"trivial_augment\"\n            expected_impact: 1\n            ui_display_name: Auto Augmentation Method\n        max_brightness:\n            default_value_reasoning: The default value of 3.0.\n            description_implications:\n                The maximum factor by which the brightness of\n                the image will be randomly changed.\n            example_value:\n                - 3.9\n            expected_impact: 1\n            ui_display_name: Maximum Brightness\n        min_brightness:\n            default_value_reasoning: The default value of 0.1.\n            description_implications:\n                The minimum brightness factor to apply to the\n                image.\n            example_value:\n                - 0.5\n            expected_impact: 1\n            ui_display_name: Minimum Brightness\n        max_contrast:\n            default_value_reasoning: The default value of 3.0\n            description_implications:\n                The maximum factor by which the contrast of\n                the image will be randomly changed.\n            example_value:\n                - 3.0\n            expected_impact: 1\n            ui_display_name: Maximum Contrast\n        min_contrast:\n            default_value_reasoning: The default value of 0.1.\n            description_implications:\n                The minimum contrast factor to apply to the\n                image.\n            example_value:\n                - 0.1\n            expected_impact: 1\n            ui_display_name: Minimum contrast\n        kernel_size:\n            default_value_reasoning: The default value is 3.\n            description_implications: The kernel size is the size of the filter\n                matrix. A larger kernel size will result in a blurrier image, while a\n                smaller kernel size will result in less blurring.\n            example_value:\n                - 3\n            expected_impact: 2\n            suggested_values:\n                - 3\n                - 5\n                - 7\n            suggested_values_reasoning:\n                The default value is 3, which is a common\n                value for image processing\n            ui_display_name: Kernel Size\n        rotation_degree:\n            default_value_reasoning: The default value of 15 means that the\n                image will be randomly rotated between -15 to +15 degrees.\n            description_implications: The degree of rotation to apply to the image.\n            expected_impact: 1\n            ui_display_name: Rotation Degree\n        type:\n            description_implications: The type of augmentation to perform on the\n                image.\n            expected_impact: 1\n            ui_display_name: Type\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        height:\n            ui_display_name: null\n            expected_impact: 2\n        in_memory:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_dimensions:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_max_height:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_max_width:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_num_channels:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_sample_size:\n            ui_display_name: null\n            expected_impact: 1\n        infer_image_num_classes:\n            ui_display_name: null\n            expected_impact: 1\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        num_channels:\n            ui_display_name: null\n            expected_impact: 2\n        num_classes:\n            ui_display_name: null\n            expected_impact: 2\n        num_processes:\n            ui_display_name: null\n            expected_impact: 2\n        resize_method:\n            default_value_reasoning:\n                Interpolation may stretch or squish the image,\n                but it does not remove content or change the statistical distribution\n                of image values so it is more appropriate for most tasks.\n            description_implications:\n                \"interpolation will not change the content of\n                the image, but it will change the aspect ratio.\n\n\n                crop_or_pad will preserve the aspect ratio of the image, but may remove\n                some content (in the case of cropping).\"\n            expected_impact: 1\n            related_parameters:\n                - height, width\n            ui_display_name: Resize Method\n        standardize_image:\n            ui_display_name: null\n            expected_impact: 1\n        width:\n            ui_display_name: null\n            expected_impact: 2\n        requires_equal_dimensions:\n            ui_display_name: null\n            expected_impact: 1\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\nnumber:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        computed_outlier_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        outlier_strategy:\n            default_value_reasoning:\n                Outlier definitions and how to handle them are very task-specific, so we leave\n                this feature disabled by default and ask the user to choose the strategy that works best for them.\n            description_implications:\n                Determines how outliers will be handled in the dataset. In most cases replacing outliers with the\n                column mean (`fill_with_mean`) will be sufficient, but in others the outliers may be damaging enough\n                to merit dropping the entire row of data (`drop_row`). In some cases, the best way to handle outliers\n                is to leave them in the data, which is the behavior when this parameter is left as `null`.\n            related_parameters:\n                - outlier_threshold\n            suggested_values: fill_with_mean\n            ui_display_name: Outlier Strategy\n            expected_impact: 3\n        outlier_threshold:\n            default_value_reasoning:\n                The definition of an outlier is often dataset and task dependent, but 2 or 3 standard deviations from\n                the mean is a common heuristic.\n            description_implications:\n                \"Determines the threshold past which a number will be considered an outlier in the dataset. The 3-sigma\n                rule in statistics tells us that when data is normally distributed, 95% of the data will lie within 2\n                standard deviations of the mean, and greater than 99% of the data will lie within 3 standard deviations\n                of the mean (see: 68–95–99.7 rule). As such anything farther away than that is highly likely to be an\n                outlier, and may distort the learning process by disproportionately affecting the model.\"\n            related_parameters:\n                - outlier_strategy\n            literature_references:\n                - https://en.wikipedia.org/wiki/68%E2%80%9395%E2%80%9399.7_rule\n            suggested_values: 2 - 3\n            ui_display_name: Outlier Threshold\n            expected_impact: 2\n        normalization:\n            default_value_reasoning:\n                Z-score normalization helps improve the training stability and convergence\n                of neural networks by rescaling the numeric input features to have a mean\n                of 0 and a standard deviation of 1, reducing the variability and distribution\n                of the data. This improves neural network training.\n            description_implications:\n                The goal of normalization is to transform features\n                to be on a similar scale. Normalization can be a form of feature smoothing\n                that improves the performance and training stability of the model.\n                Normalizations may result in different effects on the semantics of\n                your number features. The best normalization technique is one that\n                empirically works well, so try new ideas if you think they'll work\n                well on your feature distribution.\n            expected_impact: 3\n            literature_references:\n                - https://developers.google.com/machine-learning/data-prep/transform/normalization\n            suggested_values: zscore\n            suggested_values_reasoning:\n                \"Z-score is a variation of scaling that represents\\\n                \\ the number of standard deviations away from the mean. You would\\\n                \\ use z-score to ensure your feature distributions have mean = 0 and\\\n                \\ std = 1. It\\u2019s useful when there are a few outliers, but not\\\n                \\ so extreme that you need clipping.\"\n            ui_display_name: Normalization\n    clip:\n        expected_impact: 2\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\nsequence:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        lowercase:\n            ui_display_name: null\n            expected_impact: 2\n        sequence_length:\n            default_value_reasoning:\n                The default value is `None`. Which means that the sequence length will be inferred from the dataset,\n                which may save you compute resources on datasets with short sequence samples.\n            description_implications:\n                A larger sequence length keeps more information\n                from the data, but also makes it more computationally expensive (more\n                memory and longer training time). A smaller sequence length keeps\n                less information from the data, but also makes it less computationally\n                expensive (less memory and shorter training time).\n            expected_impact: 3\n            related_parameters:\n                - max_sequence_length\n            suggested_values:\n                If tying the weights of multiple sequence encoders together,\n                this parameter may need to be set to ensure that all sequence features have the same sequence length.\n            ui_display_name: Sequence Length\n        max_sequence_length:\n            default_value_reasoning:\n                The default value is 256. Every sequence will\n                be truncated to this length.\n            description_implications:\n                A larger sequence length keeps more information\n                from the data, but also makes it more computationally expensive (more\n                memory and longer training time). A smaller sequence length keeps\n                less information from the data, but also makes it less computationally\n                expensive (less memory and shorter training time).\n            expected_impact: 3\n            related_parameters:\n                - vocab_size, embedding_size\n            suggested_values:\n                Use the lowest value that covers most of your input\n                data. Only increase the value if crucial parts of the input data are\n                truncated.\n            ui_display_name: Maximum Sequence Length\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        most_common:\n            default_value_reasoning:\n                If there are more than 10000 unique categories\n                in the data, it is likely that they will follow a long-tailed distribution\n                and the least common ones may not provide a lot of information\n            description_implications:\n                A smaller number will reduce the vocabulary,\n                making the embedding matrix smaller and reduce the memory footprint,\n                but will also collapse more tokens into the rare one, so the model\n                may perform worse when rare tokens appear in the data\n            example_value:\n                - 10000\n            expected_impact: 2\n            other_information: Specifying a vocab_file overrides this parameter\n            related_parameters:\n                - vocab_file, pretrained_embeddings\n            suggested_values:\n                A value that covers at least 95% of the tokens in the\n                data\n            suggested_values_reasoning:\n                Depending on the data distribution and how\n                important rare tokens are, 90%, 95% or 99% of the number of tokens\n                will leave out only very rare tokens that should not influence performance\n                substantially\n            ui_display_name: Most common (vocabulary size)\n        ngram_size:\n            default_value_reasoning: Size of the n-gram when using the `ngram` tokenizer.\n            example_value:\n                - 3\n            ui_display_name: n-gram size\n            expected_impact: 2\n        padding:\n            ui_display_name: null\n            expected_impact: 1\n        padding_symbol:\n            ui_display_name: null\n            expected_impact: 1\n        tokenizer:\n            ui_display_name: null\n            expected_impact: 3\n        unknown_symbol:\n            ui_display_name: null\n            expected_impact: 1\n        vocab_file:\n            default_value_reasoning:\n                The vocabulary can be parsed automatically from\n                the incoming input features.\n            description_implications:\n                It can be useful to specify your own vocabulary\n                list if the vocabulary is very large, there's no out of the box tokenizer\n                that fits your data, or if there are several uncommon or infrequently\n                occurring tokens that we want to guarantee to be a part of the vocabulary,\n                rather than treated as an unknown.\n            expected_impact: 0\n            ui_display_name: Vocab File\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\nset:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        lowercase:\n            ui_display_name: null\n            expected_impact: 2\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            expected_impact: 3\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n        most_common:\n            default_value_reasoning:\n                If there are more than 10000 unique categories\n                in the data, it is likely that they will follow a long-tailed distribution\n                and the least common ones may not provide a lot of information\n            description_implications:\n                A smaller number will reduce the vocabulary,\n                making the embedding matrix smaller and reduce the memory footprint,\n                but will also collapse more tokens into the rare one, so the model\n                may perform worse when rare tokens appear in the data\n            example_value:\n                - 10000\n            expected_impact: 2\n            other_information: Specifying a vocab_file overrides this parameter\n            related_parameters:\n                - vocab_file, pretrained_embeddings\n            suggested_values:\n                A value that covers at least 95% of the tokens in the\n                data\n            suggested_values_reasoning:\n                Depending on the data distribution and how\n                important rare tokens are, 90%, 95% or 99% of the number of tokens\n                will leave out only very rare tokens that should not influence performance\n                substantially\n            ui_display_name: Most common (vocabulary size)\n        tokenizer:\n            ui_display_name: null\n            expected_impact: 3\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\n    threshold:\n        expected_impact: 3\ntext:\n    preprocessing:\n        computed_fill_value:\n            example_value:\n                - Depends on dtype\n            internal_only: true\n            related_parameters:\n                - missing_value_strategy, fill_value\n            ui_display_name: DOCSTRING ONLY\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        lowercase:\n            default_value_reasoning:\n                Reading the text in lowercase enables the model\n                to treat capitalized and lowercase words as the same, effectively\n                increasing the number of data points per word.\n            description_implications:\n                If you set lowercase to False, then capitalized\n                words are seen as completely separate entities than lowercase words.\n            example_value:\n                - true\n            expected_impact: 2\n            related_parameters:\n                - vocab_size\n            suggested_values: \"TRUE\"\n            suggested_values_reasoning:\n                If there is a strong reason to treat capitalized\n                words and lowercased words differently, then set this to False. Otherwise,\n                it is preferable to bucket the words and make the model case-insensitive.\n            ui_display_name: Convert to lowercase\n        sequence_length:\n            default_value_reasoning:\n                The default value is `None`. Which means that the sequence length will be inferred from the dataset,\n                which may save you compute resources on datasets with short text samples.\n            description_implications:\n                A larger sequence length keeps more information\n                from the data, but also makes it more computationally expensive (more\n                memory and longer training time). A smaller sequence length keeps\n                less information from the data, but also makes it less computationally\n                expensive (less memory and shorter training time).\n            expected_impact: 3\n            related_parameters:\n                - max_sequence_length\n            suggested_values:\n                If tying the weights of multiple text encoders together,\n                this parameter may need to be set to ensure that all text features have the same sequence length.\n            ui_display_name: Sequence Length\n        max_sequence_length:\n            default_value_reasoning:\n                The default value is 256. Every sequence will\n                be truncated to this length.\n            description_implications:\n                A larger sequence length keeps more information\n                from the data, but also makes it more computationally expensive (more\n                memory and longer training time). A smaller sequence length keeps\n                less information from the data, but also makes it less computationally\n                expensive (less memory and shorter training time).\n            expected_impact: 3\n            related_parameters:\n                - vocab_size, embedding_size\n            suggested_values:\n                Use the lowest value that covers most of your input\n                data. Only increase the value if crucial parts of the input data are\n                truncated.\n            ui_display_name: Maximum Sequence Length\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        most_common:\n            default_value_reasoning:\n                If there are more than 10000 unique categories\n                in the data, it is likely that they will follow a long-tailed distribution\n                and the least common ones may not provide a lot of information\n            description_implications:\n                A smaller number will reduce the vocabulary,\n                making the embedding matrix smaller and reduce the memory footprint,\n                but will also collapse more tokens into the rare one, so the model\n                may perform worse when rare tokens appear in the data\n            example_value:\n                - 10000\n            expected_impact: 2\n            other_information: Specifying a vocab_file overrides this parameter\n            related_parameters:\n                - vocab_file, pretrained_embeddings\n            suggested_values:\n                A value that covers at least 95% of the tokens in the\n                data\n            suggested_values_reasoning:\n                Depending on the data distribution and how\n                important rare tokens are, 90%, 95% or 99% of the number of tokens\n                will leave out only very rare tokens that should not influence performance\n                substantially\n            ui_display_name: Most common (vocabulary size)\n        ngram_size:\n            default_value_reasoning: Size of the n-gram when using the `ngram` tokenizer.\n            example_value:\n                - 3\n            ui_display_name: n-gram size\n            expected_impact: 2\n        padding:\n            default_value_reasoning:\n                We usually want to add padding to the end of\n                a text sequence to fill in any remaining space as opposed to the beggining\n                so we set the default to right.\n            description_implications:\n                If you pad to the left, the encoded vector will\n                have leading padding tokens as opposed to trailing padding tokens.\n                This could matter based on the type of text input you are expecting.\n            expected_impact: 1\n            related_parameters:\n                - \"padding_symbol,\n\n                  max_sequence_length\"\n            suggested_values: \"'right'\"\n            suggested_values_reasoning:\n                right padding is the usual way to add padding\n                to a text sequence\n            ui_display_name: Padding\n        padding_symbol:\n            ui_display_name: null\n            expected_impact: 1\n        pretrained_model_name_or_path:\n            internal_only: true\n            ui_display_name: null\n            expected_impact: 0\n        tokenizer:\n            default_value_reasoning:\n                'The default tokenizer is `space_punct`, an abbreviation\n                of \"Space punctuation\". This tokenizer creates sub-words by dividing\n                the text on whitespace and punctuation characters. For example: The\n                text `''hello world!isn''t this great?''` would be transformed to\n                `[''hello'', ''world'', ''!'', ''isn'', \"''\", ''t'', ''this'', ''great'',\n                ''?'']`. This is the default value because it is a fast tokenizer\n                that works reasonably well.'\n            description_implications:\n                Choosing a tokenizer can be difficult. The primary\n                thing to check is that the tokenizer you have selected is compatible\n                with the language(s) in your text data. This means either selecting\n                a tokenizer that is language-specific (i.e. `french_tokenize` if working\n                with French text) or general enough that its tokenizations are language-agnostic\n                (i.e. `space_punct`).\n            example_value:\n                - space_punct\n            expected_impact: 3\n            literature_references:\n                - https://huggingface.co/course/chapter2/4?fw=pt\n            related_parameters:\n                - vocab_file, pretrained_model_name_or_path\n            suggested_values: sentencepiece\n            suggested_values_reasoning:\n                \"SentencePiece is a tokenizer developed by\n                Google which utilizes Byte-Pair Encoding (BPE), which strikes a good\n                balance between character-level and word-level tokenization (more\n                info on BPE here: https://towardsdatascience.com/byte-pair-encoding-the-dark-horse-of-modern-nlp-eb36c7df4f10\n                ). This tokenizer is language-agnostic and more sophisticated than\n                the default.\"\n            ui_display_name: Tokenizer\n        unknown_symbol:\n            ui_display_name: null\n            expected_impact: 1\n        vocab_file:\n            default_value_reasoning:\n                The vocabulary can be parsed automatically from\n                the incoming input features.\n            description_implications:\n                It can be useful to specify your own vocabulary\n                list if the vocabulary is very large, there's no out of the box tokenizer\n                that fits your data, or if there are several uncommon or infrequently\n                occurring tokens that we want to guarantee to be a part of the vocabulary,\n                rather than treated as an unknown.\n            expected_impact: 0\n            ui_display_name: Vocab File\n    class_similarities:\n        expected_impact: 1\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\ntimeseries:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n            expected_impact: 3\n        padding:\n            ui_display_name: null\n            expected_impact: 1\n        padding_value:\n            ui_display_name: null\n            expected_impact: 1\n        timeseries_length_limit:\n            ui_display_name: null\n            expected_impact: 2\n        tokenizer:\n            ui_display_name: null\n            expected_impact: 3\nvector:\n    preprocessing:\n        computed_fill_value:\n            internal_only: true\n            ui_display_name: null\n        fill_value:\n            expected_impact: 2\n            ui_display_name: Fill Value\n        missing_value_strategy:\n            default_value_reasoning:\n                The default `fill_with_const` replaces missing\n                values with the value specified by `fill_value`.\n            description_implications:\n                Determines how missing values will be handled\n                in the dataset. Not all strategies are valid for all datatypes. For\n                example, `fill_with_mean` is applicable to continuous numerical data.\n                Note that choosing to drop rows with missing values could result in\n                losing information, especially if there is a high proportion of missing\n                values in the dataset.\n            expected_impact: 3\n            related_parameters:\n                - fill_value\n            ui_display_name: Missing Value Strategy\n        vector_size:\n            ui_display_name: null\n            expected_impact: 3\n    dependencies:\n        expected_impact: 1\n    reduce_dependencies:\n        expected_impact: 1\n    reduce_input:\n        expected_impact: 1\n    softmax:\n        expected_impact: 3\n    vector_size:\n        expected_impact: 3\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/llm.yaml",
    "content": "base_model:\n  _anyOf:\n    preset:\n      ui_display_name: Preset\n      expected_impact: 3\n    custom:\n      ui_display_name: Custom\n      expected_impact: 3\n  _meta:\n    ui_display_name: Model Name\n    expected_impact: 3\n    ui_component_type: radio_string_combined\n    short_description: This can be one of the presets or a fully qualified name of a pretrained model from the HuggingFace Hub\ngeneration:\n  temperature:\n    ui_display_name: Temperature\n    default_value_reasoning:\n      Increasing the temperature will allow the model to generate more diverse sequences,\n      but will also increase the likelihood of generating nonsense. As such, we recommend setting this value to\n      something closer to 0 for classification tasks, and something closer to 1 for text generation tasks where the\n      goal is to generate novel text.\n    expected_impact: 3\n  max_new_tokens:\n    ui_display_name: Max New Tokens\n    default_value_reasoning:\n      Increasing the maximum number of new tokens will allow the model\n      to generate longer sequences, but because inference time scales linearly with the sequence length,\n      longer sequences will be much slower to generate. For classification tasks, it's generally better to\n      use a smaller number of new tokens, while for text generation tasks, it's generally better to use a larger\n      number of new tokens.\n    expected_impact: 3\n  num_beams:\n    ui_display_name: Number of Beams\n    default_value_reasoning:\n      Increasing the number of beams will allow the model to generate more diverse sequences,\n      but will also increase inference time. Some backends (like DeepSpeed) also do not support beam search.\n      As such, we recommend leaving this as 1 in most cases, unless you're finding the quality of the generated\n      sequences to be lacking.\n    expected_impact: 2\n  top_k:\n    ui_display_name: Top K\n    expected_impact: 2\n  top_p:\n    ui_display_name: Top P\n    expected_impact: 2\n  max_length:\n    ui_display_name: Max Length\n    expected_impact: 2\n  min_length:\n    ui_display_name: Min Length\n    expected_impact: 2\n  min_new_tokens:\n    ui_display_name: Min New Tokens\n    expected_impact: 2\n  do_sample:\n    ui_display_name: Do Sample\n    expected_impact: 2\n  use_cache:\n    ui_display_name: Use Cache\n    expected_impact: 2\n  prompt_lookup_num_tokens:\n    ui_display_name: Prompt Lookup Num Tokens\n    expected_impact: 2\nprompt:\n  retrieval:\n    type:\n      ui_display_name: Type\n      expected_impact: 3\n    index_name:\n      ui_display_name: Index Name\n      expected_impact: 2\n    model_name:\n      ui_display_name: Model Name\n      expected_impact: 2\n    k:\n      ui_display_name: Top K\n      expected_impact: 2\n  task:\n    ui_display_name: Task\n    ui_component_type: textarea\n    expected_impact: 3\n  template:\n    ui_display_name: Template\n    ui_component_type: textarea\n    expected_impact: 3\nadapter:\n  _oneOf:\n    allOf:\n      ui_display_name: Perform parameter efficient fine-tuning\n      expected_impact: 3\n    none:\n      ui_display_name: Disabled\n      expected_impact: 3\n  _meta:\n    expected_impact: 3\n    ui_component_type: radio_string_combined\n  lora:\n    type:\n      long_description: |\n        LoRA is a simple, yet effective, method for parameter-efficient fine-tuning of pretrained language models.\n        It works by adding a small number of trainable parameters to the model, which are used to adapt the\n        pretrained parameters to the downstream task. This allows the model to be fine-tuned with a much smaller\n        number of training examples, and can even be used to fine-tune models on tasks that have no training data\n        available at all.\n    r:\n      ui_display_name: R\n      expected_impact: 3\n    alpha:\n      ui_display_name: Alpha\n      expected_impact: 1\n    dropout:\n      ui_display_name: Dropout\n      expected_impact: 2\n    target_modules:\n      ui_display_name: Target Modules\n      expected_impact: 2\n    use_rslora:\n      ui_display_name: Enable RSLora\n      expected_impact: 2\n    use_dora:\n      ui_display_name: Enable DoRa\n      expected_impact: 2\n  adalora:\n    type:\n      long_description: |\n        AdaLoRA is an extension of LoRA that allows the model to adapt the pretrained parameters to the downstream\n        task in a task-specific manner. This is done by adding a small number of trainable parameters to the model,\n        which are used to adapt the pretrained parameters to the downstream task. This allows the model to be\n        fine-tuned with a much smaller number of training examples, and can even be used to fine-tune models on tasks\n        that have no training data available at all.\n  prompt_learning:\n    num_virtual_tokens:\n      ui_display_name: Num Virtual Tokens\n      expected_impact: 3\n  prompt_tuning:\n    prompt_tuning_init:\n      ui_display_name: Prompt Tuning Init\n      expected_impact: 2\n    prompt_tuning_init_text:\n      ui_display_name: Prompt Tuning Init Text\n      expected_impact: 2\n  adaption_prompt:\n    type:\n      long_description: |\n        Adaption Prompt is taken from the paper\n        [LLaMA-Adapter: Efficient Fine-tuning of Language Models with Zero-init Attention](https://arxiv.org/pdf/2303.16199.pdf).\n        It adds a set of learnable adaption prompts and prepends them to the word tokens at higher transformer layers.\n        Then, a zero-initialized attention mechanism with zero gating is introduced, which adaptively injects\n        new instructional cues into LLaMA, while effectively preserving its pre-trained knowledge. According to\n        the paper, LLaMA-Adapter can generate high-quality responses, comparable to Alpaca with fully fine-tuned\n        7B parameters.\n    adapter_len:\n      ui_display_name: Adapter Length\n      expected_impact: 3\n    adapter_layers:\n      ui_display_name: Adapter Layers\n      expected_impact: 3\n  ia3:\n    type:\n      long_description: |\n        [Infused Adapter by Inhibiting and Amplifying Inner Activations](https://arxiv.org/pdf/2205.05638.pdf), or IA3,\n        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.\n    target_modules:\n      ui_display_name: Target Modules\n      expected_impact: 3\n    feedforward_modules:\n      ui_display_name: Feedforward Modules\n      expected_impact: 3\n    fan_in_fan_out:\n      ui_display_name: Fan In Fan Out\n      expected_impact: 3\n    modules_to_save:\n      ui_display_name: Modules to Save\n      expected_impact: 3\n    init_ia3_weights:\n      ui_display_name: Init IA3 Weights\n      expected_impact: 3\nquantization:\n  _oneOf:\n    object:\n      ui_display_name: Quantization\n      expected_impact: 3\n    none:\n      ui_display_name: No Quantization\n      expected_impact: 3\n  _meta:\n    expected_impact: 3\n    ui_component_type: radio_string_combined\n  bits:\n    ui_display_name: Bits per parameter\n    expected_impact: 3\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/loss.yaml",
    "content": "MSELoss:\n  weight:\n    expected_impact: 2\nMAELoss:\n  weight:\n    expected_impact: 2\nRMSELoss:\n  weight:\n    expected_impact: 2\nRMSPELoss:\n  weight:\n    expected_impact: 2\nBWCEWLoss:\n  positive_class_weight:\n    expected_impact: 3\n  robust_lambda:\n    expected_impact: 2\n  confidence_penalty:\n    expected_impact: 2\n  weight:\n    expected_impact: 2\nSoftmaxCrossEntropyLoss:\n  class_weights:\n    expected_impact: 3\n  robust_lambda:\n    expected_impact: 2\n  confidence_penalty:\n    expected_impact: 2\n  class_similarities:\n    expected_impact: 2\n  class_similarities_temperature:\n    expected_impact: 2\n  weight:\n    expected_impact: 2\nSequenceSoftmaxCrossEntropyLoss:\n  class_weights:\n    expected_impact: 3\n  robust_lambda:\n    expected_impact: 2\n  confidence_penalty:\n    expected_impact: 2\n  class_similarities:\n    expected_impact: 2\n  class_similarities_temperature:\n    expected_impact: 2\n  weight:\n    expected_impact: 2\n  unique:\n    expected_impact: 2\nSigmoidCrossEntropyLoss:\n  class_weights:\n    expected_impact: 3\n  weight:\n    expected_impact: 2\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/optimizers.yaml",
    "content": "gradient_clipping:\n    default_value_reasoning:\n        A conservative cap on the maximum gradient size to apply\n        over a single training step.\n    description_implications:\n        Gradient clipping is a technique to prevent exploding\n        gradients in very deep networks. Increasing gradient clipping can help with\n        model training loss curve stability, but it can also make training less efficient\n        as weight at each training step is capped.\n    expected_impact: 1\n    suggested_values_reasoning:\n        It's usually sensible to have some conservative notion\n        of gradient clipping to make modeling robust to a particularly bad or noisy\n        batch of examples.\n    ui_display_name: Gradient Clipping\nmomentum:\n    expected_impact: 1\nweight_decay:\n    expected_impact: 1\ndampening:\n    expected_impact: 1\nnesterov:\n    expected_impact: 1\nmax_iter:\n    expected_impact: 1\nmax_eval:\n    expected_impact: 1\ntolerance_grad:\n    expected_impact: 1\ntolerance_change:\n    expected_impact: 1\nhistory_size:\n    expected_impact: 1\nline_search_fn:\n    expected_impact: 1\nbetas:\n    expected_impact: 1\namsgrad:\n    expected_impact: 1\nrho:\n    expected_impact: 1\ninitial_accumulator_value:\n    expected_impact: 1\nlr_decay:\n    expected_impact: 1\nlearning_rate_power:\n    expected_impact: 1\nl1_regularization_strength:\n    expected_impact: 1\nl2_regularization_strength:\n    expected_impact: 1\nmomentum_decay:\n    expected_impact: 1\nalpha:\n    expected_impact: 1\neps:\n    expected_impact: 1\ncentered:\n    expected_impact: 1\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/preprocessing.yaml",
    "content": "force_split:\n    default_value_reasoning:\n        We do not expect most datasets to have an explicit \"split\"\n        column in the data. Used mostly internally by ludwig datasets.\n    expected_impact: 3\n    related_parameters:\n        - split_probabilities, stratify\n    ui_display_name: Force Split\noversample_minority:\n    default_value_reasoning:\n        We do not want to randomly oversample by default since\n        this is a strategy to deal with imbalanced datasets, but can cause issues\n        if not implemented correctly.\n    description_implications:\n        The higher the value you choose gets to 1, the closer\n        you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative\n        class), however this can lead to problems of overfitting when oversampling\n        is used too liberally. As a rule of thumb, starting oversampling with a very\n        conservative approach and increasing in small incremements is probably the\n        best way to improve your model without experiencing model overfitting.\n    example_value:\n        - 0.5\n    expected_impact: 2\n    literature_references:\n        - https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/\n    other_information:\n        This parameter is one of many strategies to combat issues with\n        class imbalance, though it is not a cure all. Oversampling too much can cause\n        overfitting which can adversely affect your model so use with caution.\n    suggested_values: Depends on imbalance ratio and dataset size\n    ui_display_name: Oversample Minority\nsample_ratio:\n    default_value_reasoning:\n        The default value is 1.0 because we do not want to shrink\n        the dataset by default. In the rare occurences when you do want to downsample\n        the entire dataset, this parameter is available, however it is not enabled\n        by default, hence a default value of 1.0\n    description_implications:\n        Decreases the amount of data you are inputting into\n        the model. Could be useful if you have more data than you need and you are\n        concerned with computational costs.\n    example_value:\n        - 0.8\n    expected_impact: 2\n    suggested_values: Depends on data size\n    ui_display_name: Sample Ratio\nsample_size:\n    default_value_reasoning:\n        The default value is None because we do not want to shrink\n        the dataset by default, and we do not know the size of an arbitrary dataset.\n        By setting the default to None, we fall back on the sample_ratio to determine\n        the size of the dataset.\n    description_implications:\n        Decreases the amount of data you are inputting into\n        the model. Could be useful if you have more data than you need and you are\n        concerned with computational costs. More useful than sample_ratio if you\n        know the exact number of samples you want to train on instead of knowing the proportion.\n    example_value:\n        - 1000\n    expected_impact: 2\n    suggested_values: Depends on data size\n    ui_display_name: Sample Size\ncolumn:\n    expected_impact: 3\n    ui_display_name: Split Column\n    ui_component_type: column_selector\nsplit_probabilities:\n    default_value_reasoning:\n        Most of the dataset should be used for training, with\n        some portion heldout for validation and testing.\n    description_implications:\n        \"In machine learning, data splitting is typically done\n        to avoid overfitting. That is an instance where a machine learning model fits\n        its training data too well and fails to reliably fit additional data.\n\n\n        The training set is the portion of data used to train the model. The model\n        should observe and learn from the training set, optimizing any of its parameters.\n\n\n        The dev set is a data set of examples used to change learning process parameters.\n        It is also called the cross-validation or model validation set. This set of\n        data has the goal of ranking the model's accuracy and can help with model\n        selection.\n\n\n        The testing set is the portion of data that is tested in the final model and\n        is compared against the previous sets of data. The testing set acts as an\n        evaluation of the final mode and algorithm.\"\n    expected_impact: 3\n    literature_references:\n        - \"https://www.techtarget.com/searchenterpriseai/definition/data-splitting#:~:text=Data%20splitting%20is%20when%20data,creating%20models%20based%20on%20data. \"\n    other_information: \"Split data into train, validation, and test.\n\n\n        By default, Ludwig looks for a column named split (case-sensitive) which is\n        expected to consist of 3 possible values that correspond to different datasets:\n\n\n        0: train\n\n        1: validation\n\n        2: test\n\n        If the data does not contain the split column, then data is randomly split\n        based on splitting percentages, defined by split_probabilities.\n\n\n        If force_split is true, the the split column in the dataset is ignored and\n        the dataset is randomly split based on splitting percentages, defined by split_probabilities.\"\n    related_parameters:\n        - force_split, stratify\n    suggested_values:\n        - 0.8\n        - 0.1\n        - 0.1\n    suggested_values_reasoning:\n        For larger datasets, it can be beneficial to use more\n        data for training, since the test and validation sets are still plenty big\n        for getting a good sense of model generalization.\n    ui_display_name: Split Probabilities\nstratify:\n    default_value_reasoning:\n        The default is set to None since we do not want to stratify\n        unless specifically told to do so. There are a variety of reasons for this,\n        but one example is that our data set may not even have a categorical feature\n        to stratify on.\n    description_implications: Depends on dataset\n    example_value:\n        - Category_Feature_A\n    expected_impact: 3\n    literature_references:\n        - https://medium.com/analytics-vidhya/stratified-sampling-in-machine-learning-f5112b5b9cfe\n    related_parameters:\n        - force_split, split_probabilities\n    suggested_values: Depends on dataset\n    ui_display_name: Stratify\nundersample_majority:\n    default_value_reasoning:\n        We do not want to randomly undersample by default since\n        this is a strategy to deal with imbalanced datasets, but can cause issues\n        if not implemented correctly.\n    description_implications:\n        The higher the value you choose gets to 1, the closer\n        you will be to having an equal imbalance ratio (i.e. 1:1 positive to negative\n        class), however this can lead to problems of data loss when undersampling\n        is used too liberally. As a rule of thumb, starting undersampling with a very\n        conservative approach and increasing in small incremements is probably the\n        best way to improve your model without experiencing catastrophic data loss\n        effects.\n    example_value:\n        - 0.5\n    expected_impact: 2\n    literature_references:\n        - https://machinelearningmastery.com/random-oversampling-and-undersampling-for-imbalanced-classification/\n    other_information:\n        This parameter is one of many strategies to combat issues with\n        class imbalance, though it is not a cure all. Undersampling too much can cause\n        loss of data which can adversely affect your model so use with caution.\n    suggested_values: Depends on imbalance ratio and dataset size\n    ui_display_name: Undersample Majority\ncache_encoder_embeddings:\n    default_value_reasoning:\n        Caching encoder embeddings means preprocessed data is not reusable across other model architectures, so\n        it's not always the case that you would always want to enable it when possible.\n    expected_impact: 1\n    ui_display_name: Cache Encoder Embeddings\nglobal_max_sequence_length:\n    expected_impact: 2\n    ui_display_name: Global Max Sequence Length\n    description_implications:\n        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.\n    example_value:\n        - 512\n"
  },
  {
    "path": "ludwig/schema/metadata/configs/trainer.yaml",
    "content": "ecd:\n    effective_batch_size:\n        commonly_used: true\n        expected_impact: 2\n        related_parameters:\n            - batch_size\n        suggested_values: auto\n        ui_display_name: Effective Batch Size\n    batch_size:\n        commonly_used: true\n        default_value_reasoning: Not too big, not too small.\n        description_implications:\n            There's conflicting evidence about what batch size to\n            use. Using a higher batch size will achieve the highest throughput and training\n            efficiency. However, there's also evidence that depending on other hyperparameters,\n            a smaller batch size may produce a higher quality model.\n            Batch size and learning rate are strongly intertwined,\n            so a commonly adopted strategy to set them is to find a the largest batch size\n            that allows the training process not to run out of memory,\n            and then find the best learning rate that makes the training converge\n            with that batch size.\n        expected_impact: 3\n        related_parameters:\n            - eval_batch_size\n            - learning_rate\n        suggested_values: auto\n        suggested_values_reasoning:\n            Auto batch size will determine the largest batch size that allows\n            the training process not to run out of memory.\n            Alternatively, try at least a few different batch sizes to get a\n            sense of whether and how batch size affects model performance.\n        ui_display_name: Batch Size\n    bucketing_field:\n        expected_impact: 1\n        other_information:\n            When not null, when creating batches, instead of shuffling\n            randomly, the length along the last dimension of the matrix of the specified\n            input feature (i.e. the length of a sequence or text)\n            is used for bucketing examples and then randomly shuffled examples\n            from the same bin are sampled. Padding is trimmed to the longest example in\n            the batch. The specified feature should be either a sequence or text feature\n            and the encoder encoding it has to be rnn. When used, bucketing improves speed\n            of rnn encoding up to 1.5x, depending on the length distribution of the inputs.\n        ui_display_name: Bucketing Field\n    checkpoints_per_epoch:\n        default_value_reasoning:\n            Per-epoch behavior, which scales according to the dataset\n            size.\n        description_implications:\n            \"Epoch-based evaluation (using the default: 0) is an\n            appropriate fit for small datasets that fit in memory and\n            train quickly. Commonly available tabular datasets fit in this cateogry.\n            However, this is a poor fit for unstructured datasets, which tend to be much\n            larger, and train more slowly due to larger models.\n            It's important to setup evaluation such that you do not wait several hours\n            before getting a single evaluation result. In general, it is not necessary\n            for models to train over the entirety of a dataset, nor evaluate over the\n            entirety of a test set, to produce useful monitoring metrics and signals to\n            indicate model health.\n            It is also more engaging and more valuable to ensure a frequent pulse of evaluation\n            metrics, even if they are partial.\"\n        expected_impact: 2\n        related_parameters:\n            - train_steps\n            - steps_per_checkpoint\n        suggested_values: 2 - 10, for larger datasets\n        suggested_values_reasoning:\n            Running evaluation too frequently can be wasteful\n            while running evaluation not frequently enough can be prohibitively uninformative.\n            In many large-scale training runs, evaluation is often configured to run on\n            a sub-epoch time scale, or every few thousand steps.\n        ui_display_name: Checkpoints per epoch\n    layers_to_freeze_regex:\n        default_value_reasoning:\n            By default no layers will be frozen when fine-tuning a pretrained model.\n        description_implications:\n            Freezing specific layers can improve a pretrained model's performance in a number\n            of ways. At a basic level, freezing early layers can prevent overfitting by retaining\n            more general features (beneficial for small datasets). Also can reduce computational\n            resource use and lower overall training time due to less gradient calculations.\n        expected_impact: 1\n    early_stop:\n        default_value_reasoning:\n            Deep learning models are prone to overfitting. It's generally\n            a good policy to set up some early stopping criteria as it's not useful to\n            have a model train after it's maximized what it can learn. 5 consecutive rounds\n            of evaluation where there hasn't been any improvement on the validation set\n            (including chance) is a reasonable policy to start with.\n        description_implications:\n            Decreasing this value is a more aggressive policy. Decreasing\n            early stopping makes model training less forgiving, as the model has less\n            runway to demonstrate consecutive metric improvements before the training\n            run is quit. This can be efficient for pruning bad models earlier, but since\n            the training process is inherently non-deterministic and noisy, sometimes\n            improvements happen very gradually over a long period of time.\n            Extending this value leads to longer training times,\n            but potentially also better final performance.\n        expected_impact: 3\n        related_parameters:\n            - epochs\n            - train_steps\n        suggested_values: 5 - 10\n        suggested_values_reasoning:\n            There's potentially a lot of randomness in how models\n            train, but so many consecutive rounds of no improvement is usually a good\n            indicator that the model converged or overfitted.\n        ui_display_name: Early Stop\n    epochs:\n        default_value_reasoning:\n            A very high training length ceiling. Models will almost\n            always hit early stopping criteria before hitting a 100-epoch ceiling.\n        description_implications:\n            Decreasing this will shorten the overall runway for\n            training the model.\n        expected_impact: 3\n        related_parameters:\n            - train_steps\n        suggested_values: 100\n        suggested_values_reasoning:\n            Usually it's sensible to leave this very high and\n            rely on a solid early stopping policy to dictate when the model should stop\n            training. Some models and hyperparameter configurations require many epochs\n            through the dataset to converge while others converge before a single epoch\n            through the data.\n        ui_display_name: Epochs\n    eval_batch_size:\n        default_value_reasoning: Use the same batch size used for training.\n        description_implications:\n            By increasing the `eval_batch_size` past the `batch_size`\n            parameter set value, you allow for more parallelism in the batch evaluation\n            step and speed up evaluation. For example, if you have to evaluate the model\n            on a test set of size 1000, it is faster to evaluate two times with two batches\n            of size 500 as opposed to ten times with ten batches of 100.\n            Setting this parameter higher without getting past out memory limits\n            will speed up the model training process overall.\n        example_value:\n            - 512\n        expected_impact: 1\n        other_information:\n            Should only set the eval_batch_size to a level that you can fit\n            in memory.\n        related_parameters:\n            - batch_size\n        suggested_values:\n            - 256\n            - 512\n            - 1024\n        suggested_values_reasoning:\n            By observing memory consumption on training jobs,\n            you can get a sense of how much extra memory is available for increasing this\n            value. A good rule of thumb can be experimentally doubling the eval batch\n            size if you do not have insight into memory usage.\n        ui_display_name: Evaluation Batch Size\n    evaluate_training_set:\n        default_value_reasoning:\n            It could be useful to monitor evaluation metrics on the\n            training set to understand convergence.\n        description_implications:\n            Running evaluation on the full training set, when your\n            training set is large, can be a huge computational cost. Turning off training\n            set evaluation will lead to significant gains in training throughput and efficiency.\n            For small datasets that train and evaluate quickly, the choice is trivial.\n        expected_impact: 1\n        suggested_values: false\n        suggested_values_reasoning:\n            Running full-scale evaluation on the full training\n            set doesn't usually provide any useful information over the validation dataset.\n            Even with this set to False, continuous training loss metrics are still computed,\n            so it will still be easy to spot signs of overfitting like when the training-validation\n            loss curves diverge.\n        ui_display_name: Evaluate Training Set\n    gradient_clipping:\n        default_value_reasoning:\n            A conservative cap on the maximum gradient size to apply\n            over a single training step.\n        description_implications:\n            Gradient clipping is a technique to prevent exploding\n            gradients in very deep networks. Increasing gradient clipping can help with\n            model training loss curve stability, but it can also make training slower\n            as weights may not be updated as fast.\n        expected_impact: 2\n        suggested_values_reasoning:\n            It's usually sensible to enable gradient clipping to make modeling robust\n            to particularly bad or noisy batches of examples.\n        ui_display_name: Gradient Clipping\n    increase_batch_size_eval_metric:\n        expected_impact: 1\n        ui_display_name: \"Batch Size Increase: Evaluation Metric\"\n    increase_batch_size_eval_split:\n        expected_impact: 1\n        ui_display_name: \"Batch Size Increase: Evaluation Split\"\n    increase_batch_size_on_plateau:\n        expected_impact: 1\n        ui_display_name: Batch Size Increase On Plateau\n    increase_batch_size_on_plateau_patience:\n        expected_impact: 1\n        ui_display_name: \"Batch Size Increase On Plateau: Patience\"\n    increase_batch_size_on_plateau_rate:\n        expected_impact: 1\n        ui_display_name: \"Batch Size Increase On Plateau: Rate\"\n    learning_rate:\n        commonly_used: true\n        default_value_reasoning: Middle of the road learning rate to start with.\n        description_implications:\n            The learning rate is a hyperparameter that controls\n            how much to change the model in response to the estimated error each time\n            the model weights are updated. Increasing the learning rate may decrease learning\n            curve stability but also increase learning speed and efficiency, leading to\n            faster model convergence. Decreasing the learning rate can help stabilize\n            learning curves at the cost of slower time to convergence.\n        expected_impact: 3\n        suggested_values: 0.00001 - 0.1 or auto\n        related_parameters:\n            - decay\n        suggested_values_reasoning:\n            Tabular models trained from scratch typically use\n            learning rates around 1e-3 while learning rates for pre-trained models should\n            be much smaller, typically around 1e-5, which is important to mitigate catastrophic\n            forgetting. To make the model more robust to any specific choice of learning\n            rate, consider turning enabling learning rate decay.\n        ui_display_name: Learning Rate\n    learning_rate_scaling:\n        default_value_reasoning:\n            Traditionally the learning rate is scaled linearly with\n            the number of workers to reflect the proportion by which the effective batch\n            size is increased.\n        description_implications:\n            Traditionally the learning rate is scaled linearly with\n            the number of workers to reflect the proportion by which the effective batch\n            size is increased. For very large batch sizes, a softer square-root scale\n            can sometimes lead to better model performance. If the learning rate is hand-tuned\n            for a given number of workers, setting this value to constant can be used\n            to disable scale-up.\n        expected_impact: 1\n        suggested_values: linear or sqrt\n        suggested_values_reasoning:\n            Traditionally the learning rate is scaled linearly\n            with the number of workers to reflect the proportion by which the effective\n            batch size is increased. For very large batch sizes, a softer square-root\n            scale can sometimes lead to better model performance. If the learning rate\n            is hand-tuned for a given number of workers, setting this value to constant\n            can be used to disable scale-up.\n        ui_display_name: Learning Rate Scaling\n    max_batch_size:\n        default_value_reasoning: Not typically required.\n        description_implications:\n            Value used to manually limit the batch sizes explored\n            by auto batch size tuning and batch size increasing on plateau.\n        example_value:\n            - 1024\n        expected_impact: 1\n        related_parameters:\n            - batch_size\n            - increase_batch_size_on_plateau\n        ui_display_name: Max Batch Size\n    optimizer:\n        default_value_reasoning:\n            First try Adam because it is shown to return good\n            results without an advanced fine tuning.\n        description_implications:\n            \"Choosing a good optimizer for your machine learning\n            project can be overwhelming. Popular deep learning libraries such as PyTorch\n            or TensorFLow offer a broad selection of different optimizers, each\n            with its own strengths and weaknesses. However, picking the wrong optimizer\n            can have a substantial negative impact on the performance of your machine\n            learning model [1][2]. This makes optimizers a critical design choice in\n            the process of building, testing, and deploying your machine learning model.\"\n        expected_impact: 3\n        literature_references:\n            - https://www.youtube.com/watch?v=mdKjMPmcWjY\n        suggested_values: adam, adamw\n        suggested_values_reasoning:\n            \"As a rule of thumb: If you have the resources to\n            find a good learning rate schedule, SGD with momentum is a solid choice. If\n            you are in need of quick results without extensive hyperparameter tuning,\n            adaptive gradient methods like adam or adamw are good choices.\"\n        ui_display_name: Optimizer\n    regularization_lambda:\n        default_value_reasoning:\n            How to tune the overall impact of the regularization\n            term by multiplying its value by a scalar known as lambda (also called the\n            regularization rate).\n        description_implications:\n            \"When choosing a lambda value, the goal is to strike\n            the right balance between simplicity and training-data fit:\n            If your lambda value is too high, your model will be simple, but you run the\n            risk of underfitting your data. Your model won't learn enough about the training\n            data to make useful predictions.\n            If your lambda value is too low, your model will be more complex, and you\n            run the risk of overfitting your data. Your model will learn too much about\n            the particularities of the training data, and won't be able to generalize\n            to new data. The ideal value of lambda produces a model that generalizes well\n            to new, previously unseen data. Unfortunately, that ideal value of lambda\n            is data-dependent, so you'll need to do some tuning. We recommend trying\n            a handful of values (0.001, 0.02, ... 0.4) gradually increasing the value until\n            training curves get worse\"\n        expected_impact: 2\n        literature_references:\n            - \"https://developers.google.com/machine-learning/crash-course/regularization-for-simplicity/lambda \"\n        related_parameters:\n            - regularization_type\n        suggested_values: 0.1\n        suggested_values_reasoning:\n            \"The most common type of regularization is L2, also\n            called weight decay, with values often on a logarithmic\n            scale between 0 and 0.1, such as 0.1, 0.001, 0.0001, etc.\"\n        ui_display_name: Regularization Lambda\n    regularization_type:\n        default_value_reasoning: L2 is a standard regularization to start with.\n        description_implications:\n            \"L1 regularization penalizes the sum of absolute values\n            of the weights, whereas L2 regularization penalizes the sum of squares of\n            the weights.\n            The L1 regularization solution is sparse, meaning some weights will be zero,\n            others will be large.\n            The L2 regularization solution is non-sparse, most weights will be small.\n            L2 regularization does not perform feature\n            selection, since weights are only reduced to values near 0 instead of 0.\n            L1 regularization implicitly performs feature selection. L1 regularization is more\n            robust to outliers.\"\n        expected_impact: 3\n        literature_references:\n            - \"https://neptune.ai/blog/fighting-overfitting-with-l1-or-l2-regularization#:~:text=The%20differences%20between%20L1%20and,regularization%20solution%20is%20non%2Dsparse. \"\n        related_parameters:\n            - regularization_lambda\n        suggested_values: L2\n        ui_display_name: Regularization Type\n    should_shuffle:\n        default_value_reasoning:\n            In general, it's a good idea to mix up data on each batch\n            so that the neural network gets the broadest exposure to the dataset.\n        description_implications:\n            Turning off mini-batch shuffling can make training faster,\n            but it may lead to worse performance overall as shuffling helps mitigate overfitting.\n        expected_impact: 1\n        literature_references:\n            - \"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 \"\n        suggested_values: true\n        suggested_values_reasoning:\n            One of the most powerful things about neural networks\n            is that they can be very complex functions, allowing one to learn very complex\n            relationships between your input and output data. These relationships can\n            include things you would never expect, such as the order in which data is\n            fed in per epoch. If the order of data within each epoch is the same, then\n            the model may use this as a way of reducing the training error, which is a\n            sort of overfitting.\n        ui_display_name: Should Shuffle\n    steps_per_checkpoint:\n        default_value_reasoning:\n            By default, we evaluate once per epoch, which scales\n            according to the dataset size.\n        description_implications:\n            \"Epoch-based evaluation (using the default: 0) is an\n            appropriate fit for tabular datasets, which are small, fit in memory, and\n            train quickly.\n            However, this is a poor fit for unstructured datasets, which tend to be much\n            larger, and train more slowly due to larger models.\n            It's important to setup evaluation such that you do not wait several hours\n            before getting a single evaluation result. In general, it is not necessary\n            for models to train over the entirety of a dataset, nor evaluate over the\n            entirety of a test set, to produce useful monitoring metrics and signals to\n            indicate model health.\n            It is also more engaging and more valuable to ensure a frequent pulse of evaluation\n            metrics, even if they are partial.\"\n        expected_impact: 1\n        related_parameters:\n            - checkpoints_per_epoch\n        suggested_values: 1000-10000 for larger datasets\n        suggested_values_reasoning:\n            Running evaluation too frequently can be wasteful\n            while running evaluation not frequently enough can be prohibitively uninformative.\n            In many large-scale training runs, evaluation is often configured to run on\n            a sub-epoch time scale, or every few thousand steps.\n        ui_display_name: Steps Per Checkpoint\n    train_steps:\n        default_value_reasoning:\n            This defaults to `epochs`, which is a very high training\n            length ceiling. Models will almost always hit early stopping criteria before\n            reaching the absolute end of the training runway.\n        description_implications:\n            Decreasing this parameter will shorten the overall runway for\n            training the model.\n        expected_impact: 1\n        related_parameters:\n            - epochs\n        suggested_values: Leave unset, or 1000000, 1 for debugging\n        suggested_values_reasoning:\n            Usually it's sensible to leave the value of this parameter very high and\n            rely on a solid early stopping policy to dictate when the model should stop\n            training. Some models and hyperparameter configurations require many epochs\n            through the dataset to converge while others converge before a single epoch\n            through the data.\n        ui_display_name: Train Steps\n    eval_steps:\n        default_value_reasoning:\n            The default value is None because we do not want to lower the number of evaluation steps\n            by default, and we do not know the size of an arbitrary dataset.\n            By setting the default to None, we simply evaluate on the full evaluation set.\n        description_implications:\n            The smaller this value of this parameter, the less time evaluation will take.\n        expected_impact: 2\n        suggested_values: Depends on data size and prioritization of quality vs. speed\n        suggested_values_reasoning:\n            Normally, evaluation should use the entire evaluation set, and this is\n            recommended to achieve the highest quality evaluation. However, using\n            the full evaluation set can be slow, so the value of this parameter should\n            be set depending on which is more important for the task at hand -- quality\n            or speed.\n        ui_display_name: Evaluation Steps\n    use_mixed_precision:\n        default_value_reasoning:\n            Speed up training by using float16 parameters where it\n            makes sense.\n        description_implications:\n            Mixed precision training on GPU can dramatically speedup\n            training, with some risks to model convergence.\n        expected_impact: 3\n        literature_references:\n            - https://pytorch.org/blog/what-every-user-should-know-about-mixed-precision-training-in-pytorch/\n        suggested_values: false\n        suggested_values_reasoning:\n            Suggested to enable this if training is taking too\n            long on GPU.\n        ui_display_name: Use Mixed Precision\n    compile:\n        default_value_reasoning:\n            Model compilation has been shown to significantly speedup training by upwards of 20%, but does impose\n            some delay to compile the model at the beginning of training. This feature is experimental for now,\n            but may become the default in future versions.\n        description_implications:\n            Model compilation on GPU, when used in conjunction with automatic mixed precision, can speed up training\n            by upwards of 20%.\n        expected_impact: 3\n        suggested_values: false\n        suggested_values_reasoning:\n            Suggested to enable this if training is taking too\n            long on GPU.\n        ui_display_name: Compile\n    gradient_accumulation_steps:\n        default_value_reasoning:\n            Gradient accumulation is something that should be enabled only once it has been observed that either GPU\n            utilization is low due to low bandwidth between distributed workers, or that there is too much variance\n            in the training process due to very low batch sizes.\n        description_implications:\n            Gradient accumulation is useful to (1) reduce network bandwidth overhead in multi-node distributed training\n            scenarios where bandwidth is the bottleneck, and (2) train with larger effective batch sizes when the max\n            batch size the GPU can accommodate is very small. The first scenario occurs when the interconnect between\n            nodes is slow, so performing gradient synchronization (allreduce) less frequently will speed up training.\n            The second scenario occurs in cases where the model being trained is very large (e.g., LLM) so training with\n            a larger batch size will help to smooth out the variance from training with a very small batch size.\n        expected_impact: 2\n        suggested_values: false\n        suggested_values_reasoning:\n            Suggested to enable this if training is proceeding very slowly in distributed training (and GPU\n            utilization is low), or the batch size is very small and the loss curves look very spiky.\n        ui_display_name: Gradient Accumulation Steps\n    enable_gradient_checkpointing:\n        expected_impact: 2\n        ui_display_name: Enable Gradient Checkpointing\n        default_value_reasoning:\n            Gradient checkpointing is a technique to reduce the memory footprint of the model by\n            trading compute for memory. This is useful when training very large models that run into out of memory\n            errors very quickly during training. It is particularly helpful when doing non-quantization based training\n            (adapter based or full fine-tuning). Gradient checkpointing works by recomputing the activations of the\n            model during the backward pass, rather than storing them in memory during the forward pass.\n            This is a tradeoff between compute and memory, as the activations need to be recomputed during\n            the backward pass, but the memory footprint is reduced. This is set to false by default because\n            it is not always beneficial to use gradient checkpointing, and it can sometimes slow down training.\n    validation_field:\n        default_value_reasoning:\n            Concrete evaluation metrics are usually better than loss,\n            the penalty for a bad prediction, which is only a proxy for prediction correctness.\n        description_implications:\n            This parameter affects 1) what the early stopping policy\n            looks at to determine when to early stop and 2) hyperparameter optimization\n            for determining the best trial.\n        expected_impact: 1\n        related_parameters:\n            - validation_metric\n        suggested_values: default behavior\n        ui_display_name: Validation Field\n    validation_metric:\n        description_implications:\n            This parameter affects 1) what the early stopping policy\n            looks at to determine when to early stop and 2) hyperparameter optimization\n            for determining the best trial.\n        expected_impact: 1\n        related_parameters:\n            - validation_field\n        suggested_values: default behavior\n        ui_display_name: Validation Metric\n    learning_rate_scheduler:\n        warmup_evaluations:\n            default_value_reasoning:\n                \"Learning rate warmup is most commonly used when training with large batch sizes / distributed\n                training to avoid taking overly large steps at the beginning of training that might result in the\n                process getting stuck in a local optimum. Conventional wisdom when training with large batch sizes is\n                to use a larger learning rate (see: `learning_rate_scaling`) but gradually warm up to the larger learning\n                rate over a few epochs of training in the beginning.\n                Even when not training with large batch sizes, the randomness of how weights are initialized can result\n                in strange, noisy gradient updates during the beginning of your training run. As such, it's generally\n                recommended to use a small amount of warmup (e.g., 1 epoch / evaluation) even when the batch size is\n                relatively small.\"\n            description_implications:\n                Learning rate warmup sets a very low learning rate at the beginning of training and gradually\n                (linearly) increases to the base learning rate each step (batch) during training.\n                After your warmup steps you use your \"regular\" learning rate or learning rate scheduler.\n            expected_impact: 2\n            related_parameters:\n                - warmup_fraction\n                - learning_rate_scaling\n            literature_references:\n                - https://arxiv.org/abs/1711.00489\n                - https://datascience.stackexchange.com/questions/55991/in-the-context-of-deep-learning-what-is-training-warmup-steps\n            suggested_values: 0 - 5\n            suggested_values_reasoning:\n                You don't want to warm up for too long, as after the model is starting to hill climb, you want to use the\n                full weight of the learning rate to descend into good loss minima.\n\n                If you observe your loss curve converging very early into training, within the first few epochs, then\n                increasing learning rate warmup may help to mitigate this effect. Pretrained models can benefit from more\n                warmup to help offset the effects of catastrophic forgetting due to an overly high learning rate.\n            ui_display_name: Warmup Evaluations\n        warmup_fraction:\n            default_value_reasoning:\n                Similar to `warmup_evaluations` but expressed as a fraction of the total number of training steps, rather\n                that a certain number of evaluation phases.\n            description_implications: See `warmup_evaluations`.\n            expected_impact: 2\n            related_parameters:\n                - warmup_evaluations\n                - learning_rate_scaling\n            suggested_values: 0.05 - 0.2\n            suggested_values_reasoning:\n                You don't want to warm up for too long, as after the\n                model is starting to hill climb, you want to use the full weight of the learning\n                rate to descend into good loss minima.\n            ui_display_name: Warmup Fraction\n        decay:\n            description_implications:\n                \"It\\u2019s almost always a good idea to use a schedule.\\\n                \\ For most models, try the exponential decay schedule first.\\n\\nThe exponential\\\n                \\ schedule divides the learning rate by the same factor (%) every epoch. This\\\n                \\ means that the learning rate will decrease rapidly in the first few epochs,\\\n                \\ and spend more epochs with a lower value, but never reach exactly zero.\\\n                \\ As a rule of thumb, compared to training without a schedule, you can use\\\n                \\ a slightly higher maximum learning rate. Since the learning rate changes\\\n                \\ over time, the whole training is not so sensitive to the value picked.\"\n            expected_impact: 3\n            literature_references:\n                - \"https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule \"\n            related_parameters:\n                - decay_rate\n                - decay_steps\n                - learning_rate\n            suggested_values: exponential\n            suggested_values_reasoning:\n                Starting with exponential decay is a safe place to start, as it is a \"softer\" decrease in the learning\n                rate over time, as compared with linear, which is more steep after the initial drop. Linear decay is\n                most useful when the risk of catastrophic forgetting is very high (e.g, for fine-tuning pretrained\n                models). Cosine annealing is a type of learning rate schedule that has the effect of starting with a\n                large learning rate that is relatively rapidly decreased to a minimum value before being increased\n                rapidly again. The resetting of the learning rate acts like a simulated restart of the learning process.\n                If you observe your loss curves shooting up (even on the training set) in later epochs, increasing the\n                decay rate may help mitigate this effect.\n            ui_display_name: Decay\n        decay_rate:\n            default_value_reasoning:\n                4-5% decay each step is an empirically useful decay\n                rate to start with.\n            description_implications:\n                Increasing the decay rate will lower the learning rate\n                faster. This could make the model more robust to a bad (too high) initial\n                learning rate, but a decay rate that is too high could prohibit the model\n                from learning anything at all.\n            expected_impact: 2\n            literature_references:\n                - \"https://peltarion.com/knowledge-center/documentation/modeling-view/run-a-model/optimization-principles-(in-deep-learning)/learning-rate-schedule \"\n            related_parameters:\n                - decay_steps\n                - learning_rate\n            suggested_values: 0.9 - 0.96\n            suggested_values_reasoning:\n                Since this controls exponential decay, even a small\n                decay rate will still be strongly impactful.\n            ui_display_name: Decay Rate\n        decay_steps:\n            default_value_reasoning:\n                This default essentially enables the `learning_rate`\n                to decay by a factor of the `decay_rate` at 10000 training steps.\n            description_implications:\n                By increasing the value of decay steps, you are increasing\n                the number of training steps it takes to decay the learning rate by a factor\n                of `decay_rate`. In other words, the bigger this parameter, the slower the\n                learning rate decays.\n            example_value:\n                - 5000\n            expected_impact: 2\n            related_parameters:\n                - decay_rate\n                - learning_rate\n            suggested_values: 10000 +/- 500 at a time\n            suggested_values_reasoning:\n                The decay in the learning rate is calculated as the\n                training step divided by the `decay_steps` plus one. Then the `decay_rate`\n                is raised to the power of this exponent which is then multiplied to the current\n                learning rate. All this to say that the learning rate is only decayed by a\n                factor of the set `decay_rate` when the training step reaches the `decay_steps`\n                and then subsequently when it reaches any multiple of `decay_steps`. You can\n                think of `decay_steps` as a rate of decay for the `decay_rate`.\n            ui_display_name: Decay Steps\n        staircase:\n            default_value_reasoning: Performs learning rate decay in stepwise discrete manner.\n            description_implications:\n                An excessively aggressive decay results in optimizers\n                never reaching the minima, whereas a slow decay leads to chaotic updates without\n                significant improvement. Discrete learning rate decay is another parameter to help\n                tune a balance.\n            expected_impact: 1\n            literature_references:\n                - https://neptune.ai/blog/how-to-choose-a-learning-rate-scheduler\n            suggested_values: false\n            suggested_values_reasoning:\n                We have not found strong evidence that discretely\n                decaying the learning rate is superior to doing so continuously in general,\n                but in specific tasks it might have a positive impact.\n            ui_display_name: Staircase\n        reduce_on_plateau:\n            expected_impact: 3\n            ui_display_name: Reduce On Plateau\n        reduce_on_plateau_patience:\n            expected_impact: 2\n            ui_display_name: Reduce On Plateau Patience\n        reduce_on_plateau_rate:\n            expected_impact: 2\n            ui_display_name: Reduce On Plateau Rate\n        reduce_eval_metric:\n            expected_impact: 1\n            ui_display_name: Reduce Eval Metric\n        reduce_eval_split:\n            expected_impact: 1\n            ui_display_name: Reduce Eval Split\n        t_0:\n            expected_impact: 1\n            ui_display_name: T_0\n        t_mult:\n            expected_impact: 1\n            ui_display_name: T_mult\n        eta_min:\n            expected_impact: 1\n            ui_display_name: Eta Min\ngbm:\n    learning_rate:\n        commonly_used: true\n        default_value_reasoning: Middle of the road learning rate to start with.\n        description_implications:\n            The learning rate is a hyperparameter that controls\n            how much to change the model in response to the estimated error each time\n            the model weights are updated. Increasing the learning rate may decrease learning\n            curve stability but also increase learning speed and efficiency, leading to\n            faster model convergence. Decreasing the learning rate can help stabilize\n            learning curves at the cost of slower time to convergence.\n        expected_impact: 3\n        suggested_values: 0.00001 - 0.1 or auto\n        related_parameters:\n            - decay\n        suggested_values_reasoning:\n            Tabular models trained from scratch typically use\n            learning rates around 1e-3 while learning rates for pre-trained models should\n            be much smaller, typically around 1e-5, which is important to mitigate catastrophic\n            forgetting. To make the model more robust to any specific choice of learning\n            rate, consider turning enabling learning rate decay.\n        ui_display_name: Learning Rate\n    early_stop:\n        default_value_reasoning:\n            Deep learning models are prone to overfitting. It's generally\n            a good policy to set up some early stopping criteria as it's not useful to\n            have a model train after it's maximized what it can learn. 5 consecutive rounds\n            of evaluation where there hasn't been any improvement on the validation set\n            (including chance) is a reasonable policy to start with.\n        description_implications:\n            Decreasing this value is a more aggressive policy. Decreasing\n            early stopping makes model training less forgiving, as the model has less\n            runway to demonstrate consecutive metric improvements before the training\n            run is quit. This can be efficient for pruning bad models earlier, but since\n            the training process is inherently non-deterministic and noisy, sometimes\n            improvements happen very gradually over a long period of time.\n            Extending this value leads to longer training times,\n            but potentially also better final performance.\n        expected_impact: 3\n        related_parameters:\n            - epochs\n            - train_steps\n        suggested_values: 5 - 10\n        suggested_values_reasoning:\n            There's potentially a lot of randomness in how models\n            train, but so many consecutive rounds of no improvement is usually a good\n            indicator that the model converged or overfitted.\n        ui_display_name: Early Stop\n    eval_batch_size:\n        default_value_reasoning: Use the same batch size used for training.\n        description_implications:\n            By increasing the `eval_batch_size` past the `batch_size`\n            parameter set value, you allow for more parallelism in the batch evaluation\n            step and speed up evaluation. For example, if you have to evaluate the model\n            on a test set of size 1000, it is faster to evaluate two times with two batches\n            of size 500 as opposed to ten times with ten batches of 100.\n            Setting this parameter higher without getting past out memory limits\n            will speed up the model training process overall.\n        example_value:\n            - 512\n        expected_impact: 1\n        other_information:\n            Should only set the eval_batch_size to a level that you can fit\n            in memory.\n        related_parameters:\n            - batch_size\n        suggested_values:\n            - 256\n            - 512\n            - 1024\n        suggested_values_reasoning:\n            By observing memory consumption on training jobs,\n            you can get a sense of how much extra memory is available for increasing this\n            value. A good rule of thumb can be experimentally doubling the eval batch\n            size if you do not have insight into memory usage.\n        ui_display_name: Evaluation Batch Size\n    evaluate_training_set:\n        default_value_reasoning:\n            It could be useful to monitor evaluation metrics on the\n            training set to understand convergence.\n        description_implications:\n            Running evaluation on the full training set, when your\n            training set is large, can be a huge computational cost. Turning off training\n            set evaluation will lead to significant gains in training throughput and efficiency.\n            For small datasets that train and evaluate quickly, the choice is trivial.\n        expected_impact: 1\n        suggested_values: false\n        suggested_values_reasoning:\n            Running full-scale evaluation on the full training\n            set doesn't usually provide any useful information over the validation dataset.\n            Even with this set to False, continuous training loss metrics are still computed,\n            so it will still be easy to spot signs of overfitting like when the training-validation\n            loss curves diverge.\n        ui_display_name: Evaluate Training Set\n    validation_field:\n        default_value_reasoning:\n            Concrete evaluation metrics are usually better than loss,\n            the penalty for a bad prediction, which is only a proxy for prediction correctness.\n        description_implications:\n            This parameter affects 1) what the early stopping policy\n            looks at to determine when to early stop and 2) hyperparameter optimization\n            for determining the best trial.\n        expected_impact: 1\n        related_parameters:\n            - validation_metric\n        suggested_values: default behavior\n        ui_display_name: Validation Field\n    validation_metric:\n        description_implications:\n            This parameter affects 1) what the early stopping policy\n            looks at to determine when to early stop and 2) hyperparameter optimization\n            for determining the best trial.\n        expected_impact: 1\n        related_parameters:\n            - validation_field\n        suggested_values: default behavior\n        ui_display_name: Validation Metric\n    max_depth:\n        expected_impact: 3\n    drop_rate:\n        expected_impact: 2\n    tree_learner:\n        expected_impact: 2\n    boosting_type:\n        expected_impact: 3\n    boosting_rounds_per_checkpoint:\n        expected_impact: 2\n    num_boost_round:\n        expected_impact: 2\n    num_leaves:\n        expected_impact: 2\n    min_data_in_leaf:\n        expected_impact: 2\n    min_sum_hessian_in_leaf:\n        expected_impact: 1\n    bagging_fraction:\n        expected_impact: 3\n    pos_bagging_fraction:\n        expected_impact: 2\n    neg_bagging_fraction:\n        expected_impact: 2\n    bagging_freq:\n        expected_impact: 2\n    bagging_seed:\n        expected_impact: 2\n    feature_fraction:\n        expected_impact: 3\n    feature_fraction_bynode:\n        expected_impact: 2\n    feature_fraction_seed:\n        expected_impact: 2\n    extra_trees:\n        expected_impact: 3\n    extra_seed:\n        expected_impact: 2\n    max_delta_step:\n        expected_impact: 1\n    lambda_l1:\n        expected_impact: 3\n    lambda_l2:\n        expected_impact: 3\n    linear_lambda:\n        expected_impact: 2\n    min_gain_to_split:\n        expected_impact: 1\n    max_drop:\n        expected_impact: 2\n    skip_drop:\n        expected_impact: 2\n    xgboost_dart_mode:\n        expected_impact: 1\n    uniform_drop:\n        expected_impact: 2\n    drop_seed:\n        expected_impact: 2\n    top_rate:\n        expected_impact: 1\n    other_rate:\n        expected_impact: 1\n    min_data_per_group:\n        expected_impact: 1\n    max_cat_threshold:\n        expected_impact: 1\n    cat_l2:\n        expected_impact: 1\n    cat_smooth:\n        expected_impact: 1\n    max_cat_to_onehot:\n        expected_impact: 1\n    cegb_tradeoff:\n        expected_impact: 1\n    cegb_penalty_split:\n        expected_impact: 1\n    path_smooth:\n        expected_impact: 1\n    verbose:\n        expected_impact: 1\n    max_bin:\n        expected_impact: 1\n    feature_pre_filter:\n        expected_impact: 1\nllm:\n    type:\n        commonly_used: true\n        default_value_reasoning:\n            It's useful to start with zero-shot or few-shot learning to see what the model\n            can do as a baseline before fine-tuning.\n        suggested_values: none or finetune\n        suggested_values_reasoning:\n            If you want to perform zero shot learning or few shot learning, you should set this to `none`.\n            If you want to perform fine-tuning, you should set this to `finetune`.\n        ui_display_name: Trainer Type\n        expected_impact: 3\n"
  },
  {
    "path": "ludwig/schema/metadata/feature_metadata.py",
    "content": ""
  },
  {
    "path": "ludwig/schema/metadata/parameter_metadata.py",
    "content": "import json\nfrom dataclasses import dataclass\nfrom enum import Enum\nfrom typing import Any\n\nfrom dataclasses_json import dataclass_json\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.misc_utils import memoized_method\n\n\n@DeveloperAPI\nclass ExpectedImpact(int, Enum):\n    \"\"\"The expected impact of determining a \"good\" value for a specific parameter.\n\n    - HIGH: this parameter should almost always be included in a hyperopt run and can make or break a good model.\n    - MEDIUM: this parameter can sometimes make or break a good model.\n    - LOW: this parameter usually does not have a significant impact on model performance.\n    \"\"\"\n\n    UNKNOWN = 0\n    LOW = 1\n    MEDIUM = 2\n    HIGH = 3\n\n\n@DeveloperAPI\nclass ComputeTier(int, Enum):\n    \"\"\"The compute tier defines the type of compute resources that a model typically needs to get good\n    throughput.\"\"\"\n\n    CPU = 0\n    \"\"\"Model can train effectively on CPU hardware.\"\"\"\n\n    GPU_LOW = 1\n    \"\"\"Model can train effectively on commodity GPU hardware, or inference optimized SKUs like NVIDIA T4.\"\"\"\n\n    GPU_MEDIUM = 2\n    \"\"\"Model can train effectively on training-optimized GPU hardware like V100, A10G, or A5000.\"\"\"\n\n    GPU_HIGH = 3\n    \"\"\"Model requires high-end GPUs like A100 or H100 to achieve good throughput.\"\"\"\n\n\n@DeveloperAPI\n@dataclass_json()\n@dataclass\nclass ParameterMetadata:\n    \"\"\"Contains descriptive information that pertains to a Ludwig configuration parameter.\"\"\"\n\n    short_description: str = \"\"\n    \"\"\"Quick description generally for UI display.\"\"\"\n\n    long_description: str = \"\"\n    \"\"\"In depth description generally for documentation purposes.\"\"\"\n\n    ui_display_name: str | None = \"\"\n    \"\"\"How this parameter can be displayed in a human-readable form.\"\"\"\n\n    default_value_reasoning: str | None = None\n    \"\"\"The reasoning behind the default value for this parameter.\"\"\"\n\n    example_value: list[Any] | None = None\n    \"\"\"Examples of other values that can be used for this parameter.\"\"\"\n\n    related_parameters: list[str] | None = None\n    \"\"\"List of related parameters that this parameter interacts with or depends on.\"\"\"\n\n    other_information: str | None = None\n    \"\"\"Other information that is relevant for this parameter.\"\"\"\n\n    description_implications: str | None = None\n    \"\"\"The intuition for how model performance would change if this parameter is changed.\"\"\"\n\n    suggested_values: Any = None\n    \"\"\"What values would a machine learning expert suggest users try to help improve their model?\n\n    Should cover 95% (2-sigma) worth of use-cases.\n    \"\"\"\n\n    suggested_values_reasoning: str | None = None\n    \"\"\"The reasoning behind the suggested values, as well as model performance indicators or other intuition that\n    could help inform a user to make an educated decision about what values to experiment with for this\n    parameter.\"\"\"\n\n    commonly_used: bool = False\n    \"\"\"True if this parameter could be frequently used, would have a high impact, and/or would be interesting for a\n    machine learning practitioner.\"\"\"\n\n    expected_impact: ExpectedImpact = ExpectedImpact.UNKNOWN\n    \"\"\"The expected impact of determining a \"good\" value for this parameter.\"\"\"\n\n    literature_references: list[str] | None = None\n    \"\"\"List of links, papers, and blog posts to learn more.\"\"\"\n\n    internal_only: bool = False\n    \"\"\"True if this parameter is used strictly internally and should not be exposed to users.\"\"\"\n\n    compute_tier: ComputeTier = ComputeTier.CPU\n    \"\"\"The compute tier defines the type of compute resources that a model typically needs to get good\n    throughput.\"\"\"\n\n    ui_component_type: str | None = None\n    \"\"\"Override for HTML component type that should be used to render this field in UIs.\"\"\"\n\n    @memoized_method(maxsize=1)\n    def to_json_dict(self) -> dict[str, Any]:\n        return json.loads(self.to_json())\n\n\n@DeveloperAPI\ndef convert_metadata_to_json(pm: ParameterMetadata) -> dict[str, Any]:\n    \"\"\"Converts a ParameterMetadata dict to a normal JSON dict.\n\n    NOTE: Without the json.loads call, to_json() returns\n    a string repr that is improperly parsed.\n    \"\"\"\n    if not pm:\n        return ParameterMetadata().to_json_dict()\n    return pm.to_json_dict()\n\n\n# This is a quick way to flag schema parameters as internal only via the `parameter_metadata` argument\nINTERNAL_ONLY = ParameterMetadata(internal_only=True)\n"
  },
  {
    "path": "ludwig/schema/model_config.py",
    "content": "# TODO(travis) consider removing this in the future after deprecation period\nfrom ludwig.schema.model_types.base import ModelConfig  # noqa\n"
  },
  {
    "path": "ludwig/schema/model_types/__init__.py",
    "content": "import ludwig.schema.model_types.ecd  # noqa\nimport ludwig.schema.model_types.llm  # noqa\n"
  },
  {
    "path": "ludwig/schema/model_types/base.py",
    "content": "import copy\nfrom abc import ABC\nfrom typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.config_validation.checks import get_config_check_registry\nfrom ludwig.config_validation.validation import check_schema\nfrom ludwig.constants import (\n    BACKEND,\n    COLUMN,\n    DEPENDENCIES,\n    ENCODER,\n    INPUT_FEATURES,\n    MODEL_ECD,\n    NAME,\n    OUTPUT_FEATURES,\n    TIED,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.defaults.base import BaseDefaultsConfig\nfrom ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig, FeatureCollection\nfrom ludwig.schema.hyperopt import HyperoptConfig\nfrom ludwig.schema.model_types.utils import (\n    merge_fixed_preprocessing_params,\n    merge_with_defaults,\n    sanitize_and_filter_combiner_entities_,\n    set_derived_feature_columns_,\n    set_hyperopt_defaults_,\n    set_llm_parameters,\n    set_preprocessing_parameters,\n    set_tagger_decoder_parameters,\n    set_validation_parameters,\n)\nfrom ludwig.schema.preprocessing import PreprocessingConfig\nfrom ludwig.schema.trainer import BaseTrainerConfig\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom ludwig.utils.data_utils import get_sanitized_feature_name, load_yaml\nfrom ludwig.utils.registry import Registry\n\nmodel_type_schema_registry = Registry()\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ModelConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    input_features: FeatureCollection[BaseInputFeatureConfig]\n    output_features: FeatureCollection[BaseOutputFeatureConfig]\n\n    model_type: str\n\n    trainer: BaseTrainerConfig\n    preprocessing: PreprocessingConfig\n    defaults: BaseDefaultsConfig\n    hyperopt: HyperoptConfig | None = None\n\n    backend: dict[str, Any] = schema_utils.Dict()  # TODO(jeffkinnison): Add backend schema\n    ludwig_version: str = schema_utils.ProtectedString(LUDWIG_VERSION)\n\n    def __post_init__(self):\n        merge_fixed_preprocessing_params(self)\n        set_validation_parameters(self)\n        set_hyperopt_defaults_(self)\n        set_tagger_decoder_parameters(self)\n        sanitize_and_filter_combiner_entities_(self)\n\n        # Reconcile LLM parameters\n        set_llm_parameters(self)\n\n        # Reconcile conflicting preprocessing parameters\n        set_preprocessing_parameters(self)\n\n        # Derive proc_col for each feature from the feature's preprocessing parameters\n        # after all preprocessing parameters have been set\n        set_derived_feature_columns_(self)\n\n        # Auxiliary checks.\n        get_config_check_registry().check_config(self)\n\n    @staticmethod\n    def from_dict(config: ModelConfigDict) -> \"ModelConfig\":\n        config = copy.deepcopy(config)\n        config = upgrade_config_dict_to_latest_version(config)\n\n        # Use sanitized feature names.\n        # NOTE: This must be kept consistent with build_dataset()\n        for input_feature in config[INPUT_FEATURES]:\n            input_feature[NAME] = get_sanitized_feature_name(input_feature[NAME])\n            if COLUMN in input_feature and input_feature[COLUMN]:\n                input_feature[COLUMN] = get_sanitized_feature_name(input_feature[COLUMN])\n        for output_feature in config[OUTPUT_FEATURES]:\n            output_feature[NAME] = get_sanitized_feature_name(output_feature[NAME])\n            if COLUMN in output_feature and output_feature[COLUMN]:\n                output_feature[COLUMN] = get_sanitized_feature_name(output_feature[COLUMN])\n\n        # Sanitize tied feature names.\n        for input_feature in config[INPUT_FEATURES]:\n            if TIED in input_feature and input_feature[TIED]:\n                input_feature[TIED] = get_sanitized_feature_name(input_feature[TIED])\n\n        # Sanitize dependent feature names.\n        for output_feature in config[OUTPUT_FEATURES]:\n            if DEPENDENCIES in output_feature and output_feature[DEPENDENCIES]:\n                output_feature[DEPENDENCIES] = [\n                    get_sanitized_feature_name(feature_name) for feature_name in output_feature[DEPENDENCIES]\n                ]\n\n        config[\"model_type\"] = config.get(\"model_type\", MODEL_ECD)\n        model_type = config[\"model_type\"]\n        if model_type not in model_type_schema_registry:\n            raise ConfigValidationError(\n                f\"Invalid model type: '{model_type}', expected one of: {list(model_type_schema_registry.keys())}\"\n            )\n\n        config = merge_with_defaults(config)\n\n        # TODO(travis): handle this with helper function\n        backend = config.get(BACKEND)\n        if isinstance(backend, str):\n            config[BACKEND] = {\"type\": backend}\n\n        # JSON schema validation. Note that this is desireable on top of `schema.load(config)` below because marshmallow\n        # deserialization permits additional properties while JSON schema validation, for schema (e.g. `trainer`) that\n        # have `additionalProperties=False`, does not.\n        #\n        # Illustrative example: test_validate_config_misc.py::test_validate_no_trainer_type\n        #\n        # TODO: Set `additionalProperties=False` for all Ludwig schema, and look into passing in `unknown='RAISE'` to\n        # marshmallow.load(), which raises an error for unknown fields during deserialization.\n        # https://marshmallow.readthedocs.io/en/stable/marshmallow.schema.html#marshmallow.schema.Schema.load\n        check_schema(config)\n\n        cls = model_type_schema_registry[model_type]\n        schema = cls.get_class_schema()()\n        try:\n            config_obj: ModelConfig = schema.load(config)\n        except ConfigValidationError:\n            raise\n        except ValueError as e:\n            raise ConfigValidationError(f\"Config validation error raised during config deserialization: {e}\") from e\n        except (OSError, ValueError) as e:\n            raise ConfigValidationError(f\"Config validation error raised during config post-init: {e}\") from e\n\n        return config_obj\n\n    @staticmethod\n    def from_yaml(config_path: str) -> \"ModelConfig\":\n        return ModelConfig.from_dict(load_yaml(config_path))\n\n    def get_feature_names(self) -> set[str]:\n        \"\"\"Returns a set of all feature names.\"\"\"\n        feature_names = set()\n        feature_names.update([f.column for f in self.input_features])\n        feature_names.update([f.column for f in self.output_features])\n        return feature_names\n\n    def get_feature_config(self, feature_column_name: str) -> BaseInputFeatureConfig | None:\n        \"\"\"Returns the feature config for the given feature name.\"\"\"\n        for feature in self.input_features:\n            if feature.column == feature_column_name:\n                return feature\n        for feature in self.output_features:\n            if feature.column == feature_column_name:\n                return feature\n\n\n@DeveloperAPI\ndef register_model_type(name: str):\n    def wrap(model_type_config: ModelConfig) -> ModelConfig:\n        model_type_schema_registry[name] = model_type_config\n        return model_type_config\n\n    return wrap\n\n\ndef _merge_encoder_cache_params(preprocessing_params: dict[str, Any], encoder_params: dict[str, Any]) -> dict[str, Any]:\n    if preprocessing_params.get(\"cache_encoder_embeddings\"):\n        preprocessing_params[ENCODER] = encoder_params\n    return preprocessing_params\n"
  },
  {
    "path": "ludwig/schema/model_types/ecd.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import CombinerSelection\nfrom ludwig.schema.defaults.ecd import ECDDefaultsConfig, ECDDefaultsField\nfrom ludwig.schema.features.base import (\n    BaseInputFeatureConfig,\n    BaseOutputFeatureConfig,\n    ECDInputFeatureSelection,\n    ECDOutputFeatureSelection,\n    FeatureCollection,\n)\nfrom ludwig.schema.hyperopt import HyperoptConfig, HyperoptField\nfrom ludwig.schema.model_types.base import ModelConfig, register_model_type\nfrom ludwig.schema.preprocessing import PreprocessingConfig, PreprocessingField\nfrom ludwig.schema.trainer import ECDTrainerConfig, ECDTrainerField\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_model_type(name=\"ecd\")\n@ludwig_dataclass\nclass ECDModelConfig(ModelConfig):\n    \"\"\"Parameters for ECD.\"\"\"\n\n    model_type: str = schema_utils.ProtectedString(\"ecd\")\n\n    input_features: FeatureCollection[BaseInputFeatureConfig] = ECDInputFeatureSelection().get_list_field()\n    output_features: FeatureCollection[BaseOutputFeatureConfig] = ECDOutputFeatureSelection().get_list_field()\n\n    combiner: BaseCombinerConfig = CombinerSelection().get_default_field()\n\n    trainer: ECDTrainerConfig = ECDTrainerField().get_default_field()\n    preprocessing: PreprocessingConfig = PreprocessingField().get_default_field()\n    defaults: ECDDefaultsConfig = ECDDefaultsField().get_default_field()\n    hyperopt: HyperoptConfig | None = HyperoptField().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/model_types/llm.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.defaults.llm import LLMDefaultsConfig, LLMDefaultsField\nfrom ludwig.schema.features.base import (\n    BaseInputFeatureConfig,\n    BaseOutputFeatureConfig,\n    FeatureCollection,\n    LLMInputFeatureSelection,\n    LLMOutputFeatureSelection,\n)\nfrom ludwig.schema.hyperopt import HyperoptConfig, HyperoptField\nfrom ludwig.schema.llms.base_model import BaseModelDataclassField\nfrom ludwig.schema.llms.generation import LLMGenerationConfig, LLMGenerationConfigField\nfrom ludwig.schema.llms.model_parameters import ModelParametersConfig, ModelParametersConfigField\nfrom ludwig.schema.llms.peft import AdapterDataclassField, BaseAdapterConfig\nfrom ludwig.schema.llms.prompt import PromptConfig, PromptConfigField\nfrom ludwig.schema.llms.quantization import QuantizationConfig, QuantizationConfigField\nfrom ludwig.schema.model_types.base import ModelConfig, register_model_type\nfrom ludwig.schema.preprocessing import PreprocessingConfig, PreprocessingField\nfrom ludwig.schema.trainer import LLMTrainerConfig, LLMTrainerDataclassField\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@register_model_type(name=\"llm\")\n@ludwig_dataclass\nclass LLMModelConfig(ModelConfig):\n    \"\"\"Parameters for LLM Model Type.\"\"\"\n\n    model_type: str = schema_utils.ProtectedString(\"llm\")\n\n    base_model: str = BaseModelDataclassField()\n\n    input_features: FeatureCollection[BaseInputFeatureConfig] = LLMInputFeatureSelection().get_list_field()\n    output_features: FeatureCollection[BaseOutputFeatureConfig] = LLMOutputFeatureSelection().get_list_field()\n\n    preprocessing: PreprocessingConfig = PreprocessingField().get_default_field()\n    defaults: LLMDefaultsConfig | None = LLMDefaultsField().get_default_field()\n    hyperopt: HyperoptConfig | None = HyperoptField().get_default_field()\n\n    prompt: PromptConfig = PromptConfigField().get_default_field()\n\n    # trainer: LLMTrainerConfig = LLMTrainerField().get_default_field()\n    trainer: LLMTrainerConfig = LLMTrainerDataclassField(\n        description=\"The trainer to use for the model\",\n    )\n\n    generation: LLMGenerationConfig = LLMGenerationConfigField().get_default_field()\n\n    adapter: BaseAdapterConfig | None = AdapterDataclassField()\n    quantization: QuantizationConfig | None = QuantizationConfigField().get_default_field()\n    model_parameters: ModelParametersConfig | None = ModelParametersConfigField().get_default_field()\n\n    trust_remote_code: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Whether to trust and execute remote code from the HuggingFace model repository. \"\n            \"Required for some models (e.g. Phi-2, Qwen) that use custom architectures. \"\n            \"Only enable this for models you trust.\"\n        ),\n    )\n"
  },
  {
    "path": "ludwig/schema/model_types/utils.py",
    "content": "import copy\nimport logging\nimport sys\nimport warnings\nfrom collections.abc import Mapping\nfrom typing import Any, TYPE_CHECKING\n\nfrom transformers import AutoConfig\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    CATEGORY,\n    COMBINED,\n    DECODER,\n    DEFAULTS,\n    ENCODER,\n    GRID_SEARCH,\n    INPUT_FEATURES,\n    LOSS,\n    MODEL_ECD,\n    MODEL_LLM,\n    OUTPUT_FEATURES,\n    PARAMETERS,\n    PREPROCESSING,\n    SEQUENCE,\n    SPACE,\n    TEXT,\n    TYPE,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.features.feature_utils import compute_feature_hash\nfrom ludwig.schema.features.utils import output_config_registry\nfrom ludwig.schema.hyperopt.scheduler import BaseHyperbandSchedulerConfig\nfrom ludwig.schema.llms.generation import LLMGenerationConfig\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.types import HyperoptConfigDict, ModelConfigDict\nfrom ludwig.utils.data_utils import get_sanitized_feature_name\nfrom ludwig.utils.llm_utils import get_context_len\n\nif TYPE_CHECKING:\n    from ludwig.schema.model_types.base import ModelConfig\n\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\ndef merge_with_defaults(config_dict: ModelConfigDict) -> ModelConfigDict:\n    # Recursive merge of the features, except that if we find a dictionary containing\n    # an explicit \"type\" key, we ignore defaults if they don't match.\n    defaults = config_dict.get(DEFAULTS)\n    if not defaults:\n        return config_dict\n\n    config_dict = copy.deepcopy(config_dict)\n    _merge_features_(config_dict.get(INPUT_FEATURES, []), defaults, {DECODER, LOSS})\n    _merge_features_(config_dict.get(OUTPUT_FEATURES, []), defaults, {ENCODER, PREPROCESSING})\n    return config_dict\n\n\ndef _merge_features_(features: list[dict[str, Any]], defaults: dict[str, Any], exclude_keys: set[str]):\n    for feature in features:\n        ftype = feature.get(TYPE)\n        if not ftype:\n            continue\n\n        default_feature = defaults.get(ftype, {})\n        merged_feature = _merge_dict_with_types(default_feature, feature, exclude_keys)\n\n        # In-place replacement of the old feature with the new\n        feature.clear()\n        feature.update(merged_feature)\n\n\ndef _merge_dict_with_types(dct: dict[str, Any], merge_dct: dict[str, Any], exclude_keys: set[str]) -> dict[str, Any]:\n    dct = copy.deepcopy(dct)\n    dct = {k: v for k, v in dct.items() if k not in exclude_keys}\n\n    for k, v in merge_dct.items():\n        # TODO(travis): below type comparison is not perfect, as it doesn't consider the case where the default type\n        # is omitted while the encoder type is explicitly set to the default type, in which case they\n        # should resolve to equal, but will be considered different.\n        if (\n            k in dct\n            and isinstance(dct[k], dict)\n            and isinstance(v, Mapping)\n            and dct[k].get(TYPE) == v.get(TYPE, dct[k].get(TYPE))\n        ):\n            dct[k] = _merge_dict_with_types(dct[k], v, exclude_keys)\n        else:\n            dct[k] = v\n    return dct\n\n\n@DeveloperAPI\ndef merge_fixed_preprocessing_params(config: \"ModelConfig\"):\n    \"\"\"Update preprocessing parameters if encoders require fixed preprocessing parameters.\"\"\"\n    for feature in config.input_features:\n        feature.encoder.set_fixed_preprocessing_params(config.model_type, feature.preprocessing)\n\n\ndef set_validation_parameters(config: \"ModelConfig\"):\n    \"\"\"Sets validation-related parameters used for early stopping, determining the best hyperopt trial, etc.\"\"\"\n    if not config.output_features:\n        return\n\n    # First set the validation field so we know what feature we're validating on\n    if not config.trainer.validation_field:\n        if config.trainer.validation_metric is None or config.trainer.validation_metric == LOSS:\n            # Loss is valid for all features.\n            config.trainer.validation_field = config.output_features[0].name\n        else:\n            # Determine the proper validation field for the user, like if the user specifies \"accuracy\" but forgets to\n            # change the validation field from \"combined\" to the name of the feature that produces accuracy metrics.\n            from ludwig.utils.metric_utils import get_feature_to_metric_names_map\n\n            feature_to_metric_names_map = get_feature_to_metric_names_map(config.output_features.to_list())\n            validation_field = None\n            for feature_name, metric_names in feature_to_metric_names_map.items():\n                if config.trainer.validation_metric in metric_names:\n                    if validation_field is None:\n                        validation_field = feature_name\n                    else:\n                        raise ConfigValidationError(\n                            f\"The validation_metric: '{config.trainer.validation_metric}' corresponds to multiple \"\n                            f\"possible validation_fields, '{validation_field}' and '{feature_name}'. Please explicitly \"\n                            \"specify the validation_field that should be used with the validation_metric \"\n                            f\"'{config.trainer.validation_metric}'.\"\n                        )\n            if validation_field is None:\n                raise ConfigValidationError(\n                    \"User-specified trainer.validation_metric is not valid for any output feature.\"\n                )\n\n            config.trainer.validation_field = validation_field\n\n    # If the field is combined, then make sure the metric is loss and then return\n    if config.trainer.validation_field == COMBINED:\n        # Only loss is supported for combined\n        if not config.trainer.validation_metric:\n            config.trainer.validation_metric = LOSS\n        elif config.trainer.validation_metric != LOSS:\n            raise ConfigValidationError(\n                f\"Must set validation_metric=loss when validation_field=combined, \"\n                f\"found validation_metric={config.trainer.validation_metric}\"\n            )\n        return\n\n    # Field is not combined, so use the default validation metric for the single feature\n    validation_features = [f for f in config.output_features if f.name == config.trainer.validation_field]\n    if len(validation_features) > 1:\n        raise ConfigValidationError(\n            f\"Found more than one feature matching validation field: {config.trainer.validation_field}\"\n        )\n    if len(validation_features) == 0:\n        raise ConfigValidationError(\n            f\"No output feature found matching validation field: {config.trainer.validation_field}\"\n        )\n\n    validation_feature = validation_features[0]\n    if not config.trainer.validation_metric:\n        # The user has not explicitly set any validation fields.\n        # Default to using the first output feature's default validation metric.\n        out_type = validation_feature.type\n        config.trainer.validation_metric = output_config_registry(config.model_type)[out_type].default_validation_metric\n\n\ndef set_derived_feature_columns_(config_obj: \"ModelConfig\"):\n    \"\"\"Assigns column and proc_column values to features that do not have them set.\n\n    Proc_column is set to a hash of the feature's preprocessing configuration.\n    \"\"\"\n    for feature in config_obj.input_features:\n        if feature.column is None:\n            feature.column = feature.name\n        if feature.proc_column is None:\n            feature.proc_column = compute_feature_hash(feature.to_dict())\n\n    for feature in config_obj.output_features:\n        if feature.column is None:\n            feature.column = feature.name\n        if feature.proc_column is None:\n            feature.proc_column = compute_feature_hash(feature.to_dict())\n\n\ndef sanitize_and_filter_combiner_entities_(config: \"ModelConfig\"):\n    if config.model_type != MODEL_ECD or config.combiner.type != \"comparator\":\n        return\n\n    input_feature_names = {input_feature.name for input_feature in config.input_features}\n\n    # Sanitize feature names.\n    config.combiner.entity_1 = [get_sanitized_feature_name(fname) for fname in config.combiner.entity_1]\n    config.combiner.entity_2 = [get_sanitized_feature_name(fname) for fname in config.combiner.entity_2]\n\n    entity_1_excluded = {fname for fname in config.combiner.entity_1 if fname not in input_feature_names}\n    if entity_1_excluded:\n        logger.warning(\n            f\"Excluding `entity_1` features {entity_1_excluded} from the comparator combiner because they are not \"\n            f\"present in the `input_features`.\"\n        )\n\n    config.combiner.entity_1 = [fname for fname in config.combiner.entity_1 if fname not in entity_1_excluded]\n\n    entity_2_excluded = {fname for fname in config.combiner.entity_2 if fname not in input_feature_names}\n    if entity_2_excluded:\n        logger.warning(\n            f\"Excluding `entity_2` features {entity_2_excluded} from the comparator combiner because they are not \"\n            f\"present in the `input_features`.\"\n        )\n\n    config.combiner.entity_2 = [fname for fname in config.combiner.entity_2 if fname not in entity_2_excluded]\n\n\ndef set_hyperopt_defaults_(config: \"ModelConfig\"):\n    \"\"\"This function was migrated from defaults.py with the intention of setting some hyperopt defaults while the\n    hyperopt section of the config object is not fully complete.\n\n    Returns:\n        None -> modifies trainer and hyperopt sections\n    \"\"\"\n    if not config.hyperopt:\n        return\n\n    # Set default num_samples based on search space if not set by user\n    if config.hyperopt.executor.num_samples is None:\n        _contains_grid_search_params = contains_grid_search_parameters(config.hyperopt.to_dict())\n        if _contains_grid_search_params:\n            logger.info(\n                \"Setting hyperopt num_samples to 1 to prevent duplicate trials from being run. Duplicate trials are\"\n                \" created when there are hyperopt parameters that use the `grid_search` search space.\",\n            )\n            config.hyperopt.executor.num_samples = 1\n        else:\n            logger.info(\"Setting hyperopt num_samples to 10.\")\n            config.hyperopt.executor.num_samples = 10\n\n    scheduler = config.hyperopt.executor.scheduler\n    if scheduler.type == \"fifo\":\n        # FIFO scheduler has no constraints\n        return\n\n    # Disable early stopping when using a scheduler. We achieve this by setting the parameter\n    # to -1, which ensures the condition to apply early stopping is never met.\n    early_stop = config.trainer.early_stop\n    if early_stop is not None and early_stop != -1:\n        warnings.warn(\"Can't utilize `early_stop` while using a hyperopt scheduler. Setting early stop to -1.\")\n    config.trainer.early_stop = -1\n\n    if isinstance(config.trainer, ECDTrainerConfig) and isinstance(scheduler, BaseHyperbandSchedulerConfig):\n        # TODO(travis): explore similar constraints for other model types that may not have epochs\n        max_t = scheduler.max_t\n        time_attr = scheduler.time_attr\n        epochs = config.trainer.epochs\n        if max_t is not None:\n            if time_attr == \"time_total_s\":\n                if epochs is None:\n                    # Continue training until time limit hit\n                    config.trainer.epochs = sys.maxsize\n                # else continue training until either time or trainer epochs limit hit\n            elif epochs is not None and epochs != max_t:\n                raise ValueError(\n                    \"Cannot set trainer `epochs` when using hyperopt scheduler w/different training_iteration `max_t`. \"\n                    \"Unset one of these parameters in your config or make sure their values match.\"\n                )\n            else:\n                # Run trainer until scheduler epochs limit hit\n                config.trainer.epochs = max_t\n        elif epochs is not None:\n            scheduler.max_t = epochs  # run scheduler until trainer epochs limit hit\n\n\ndef set_preprocessing_parameters(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Reconcile conflicting preprocessing parameters in place.\"\"\"\n    _set_max_sequence_length(config)\n\n\ndef _set_max_sequence_length(config: \"ModelConfig\") -> None:  # noqa: F821\n    \"\"\"Ensures that `max_sequence_length` is never less than `sequence_length`.\"\"\"\n\n    types_with_sequence_length = [SEQUENCE, TEXT]\n    for input_feature in config.input_features:\n        if input_feature.type in types_with_sequence_length:\n            sequence_length = input_feature.preprocessing.sequence_length\n            max_sequence_length = input_feature.preprocessing.max_sequence_length\n            if sequence_length is not None and sequence_length > max_sequence_length:\n                warnings.warn(\n                    \"if `sequence_length` is not None, `max_sequence_length` must be greater than or equal \"\n                    \"to `sequence_length`. Setting `max_sequence_length` to `sequence_length`.\"\n                )\n                input_feature.preprocessing.max_sequence_length = sequence_length\n\n\ndef set_tagger_decoder_parameters(config: \"ModelConfig\") -> None:\n    \"\"\"Overrides the reduce_input parameter for text and sequence output features when a tagger decoder is used.\n    This is done to ensure that the decoder correctly gets a 3D tensor as input.\n\n    Returns:\n        None -> modifies output_features\n    \"\"\"\n    for output_feature in config.output_features:\n        if output_feature.type in {TEXT, SEQUENCE} and output_feature.decoder.type == \"tagger\":\n            if output_feature.reduce_input is not None:\n                warnings.warn(\n                    \"reduce_input must be set to `None` when using a tagger decoder for your output feature. \"\n                    f\"Setting reduce_input to `None` for `{output_feature.name}`.\"\n                )\n                output_feature.reduce_input = None\n\n\ndef set_llm_parameters(config: \"ModelConfig\") -> None:\n    if config.model_type != MODEL_LLM:\n        return\n\n    # Set preprocessing parameters for text features for LLM model type\n    _set_llm_tokenizers(config)\n\n    # Set max_new_tokens in generation config to the max sequence length of the output features\n    _set_generation_max_new_tokens(config)\n\n    # HACK(Arnav): Set Mixtral target modules when using LoRA\n    # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3853\n    # PEFT PR: https://github.com/huggingface/peft/pull/1376\n    _set_mixtral_target_modules(config)\n\n    # HACK(Arnav): Set Phi-2 target modules when using LoRA\n    # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3910\n    # PEFT PR: https://github.com/huggingface/peft/pull/1375\n    _set_phi2_target_modules(config)\n\n    # HACK(Arnav): Set Phi-3 target modules when using LoRA\n    _set_phi3_target_modules(config)\n\n    # HACK(Arnav): Set Gemma target modules when using LoRA\n    # GitHub issue: https://github.com/ludwig-ai/ludwig/issues/3937\n    # PEFT PR: https://github.com/huggingface/peft/pull/1499\n    _set_gemma_target_modules(config)\n\n\ndef _set_llm_tokenizers(config: \"ModelConfig\") -> None:\n    \"\"\"Sets the tokenizers for the LLM model to the pretrained model name or path. This ensures that they use the\n    correct shared vocabulary from the tokenizer.\n\n    This also ensures padding is correctly set to left padding to prevent the LLM from trying to continue to sequence\n    based on the right padding tokens, which might exist based on sequence length.\n    \"\"\"\n    pretrained_model_name_or_path = config.base_model\n    if not isinstance(pretrained_model_name_or_path, str) or pretrained_model_name_or_path is None:\n        raise ValueError(\"Must set `base_model` when using the LLM model.\")\n\n    for input_feature in config.input_features:\n        if input_feature.type == TEXT:\n            input_feature.preprocessing.tokenizer = \"hf_tokenizer\"\n            input_feature.preprocessing.pretrained_model_name_or_path = pretrained_model_name_or_path\n            input_feature.preprocessing.padding = \"left\"\n\n    for output_feature in config.output_features:\n        if output_feature.type == TEXT:\n            # Add tokenizer parameters to preprocessing so it can be used during post processing\n            output_feature.preprocessing.tokenizer = \"hf_tokenizer\"\n            output_feature.preprocessing.pretrained_model_name_or_path = pretrained_model_name_or_path\n            output_feature.preprocessing.padding = \"left\"\n\n            # Add tokenizer parameters to decoder so it can be used during the forward pass\n            output_feature.decoder.pretrained_model_name_or_path = pretrained_model_name_or_path\n            output_feature.decoder.max_new_tokens = config.generation.max_new_tokens\n        elif output_feature.type == CATEGORY:\n            # Tokenizer parameters\n            output_feature.decoder.tokenizer = \"hf_tokenizer\"\n            output_feature.decoder.pretrained_model_name_or_path = pretrained_model_name_or_path\n            # Parameters for building decoder vocabulary\n            output_feature.decoder.fallback_label = output_feature.preprocessing.fallback_label\n\n\ndef _get_maximum_possible_sequence_length(config: \"ModelConfig\", default_max_sequence_length: int) -> int:\n    \"\"\"Returns the maximum possible sequence length for the LLM model based on the model config.\"\"\"\n    max_possible_sequence_length = default_max_sequence_length\n    if config.output_features[0].preprocessing.max_sequence_length is not None:\n        # Note: We don't need to check for max between feature.preprocessing.max_sequence_length and\n        # defaults.text.preprocessing.max_sequence_length because the latter is only applied to input features.\n        max_possible_sequence_length = max(\n            default_max_sequence_length, config.output_features[0].preprocessing.max_sequence_length\n        )\n    elif config.preprocessing.global_max_sequence_length is not None:\n        # This is not perfect since it includes tokens from both input + output features, but this at least\n        # ensures that max possible of the sequence length is used. It is very likely that the model learns\n        # to generate sequences than this value.\n        max_possible_sequence_length = max(\n            max_possible_sequence_length, config.preprocessing.global_max_sequence_length\n        )\n    elif max_possible_sequence_length == default_max_sequence_length:\n        # It's possible that both max_sequence_length and global_max_sequence_length are not set, in which case\n        # we should fall back to the window size of the pretrained model. By this point, because of schema validation\n        # checks, we know that the base_model exists so we can safely grab the base model's config.\n        # TODO (Arnav): Figure out how to factor in rope scaling factor into this calculation.\n        model_config = AutoConfig.from_pretrained(config.base_model)\n        max_possible_sequence_length = get_context_len(model_config)\n        # Artifically leave a buffer of half the total model window size to trade off\n        # runtime while likely covering a majority of the max sequence length.\n        max_possible_sequence_length = max_possible_sequence_length // 2\n    return max_possible_sequence_length\n\n\ndef _set_generation_max_new_tokens(config: \"ModelConfig\") -> None:\n    \"\"\"Sets the max_new_tokens parameter in the generation config to the max sequence length of the output\n    features.\n\n    This ensures that the generation config is set to the correct value for the LLM model type.\n    \"\"\"\n    _DEFAULT_MAX_SEQUENCE_LENGTH = LLMGenerationConfig().max_new_tokens\n    if config.generation.max_new_tokens != _DEFAULT_MAX_SEQUENCE_LENGTH:\n        # Max new tokens is explicitly set by user, so don't override\n        return\n\n    if config.output_features[0].type != TEXT:\n        # This is trickier to set for other output features, so don't override for now.\n        # TODO: Add better support for category output features\n        return\n\n    max_possible_sequence_length = _get_maximum_possible_sequence_length(config, _DEFAULT_MAX_SEQUENCE_LENGTH)\n\n    logger.info(\n        f\"Setting generation max_new_tokens to {max_possible_sequence_length} to correspond with the max \"\n        \"sequence length assigned to the output feature or the global max sequence length. This will ensure that \"\n        \"the correct number of tokens are generated at inference time. To override this behavior, set \"\n        \"`generation.max_new_tokens` to a different value in your Ludwig config.\"\n    )\n    config.generation.max_new_tokens = max_possible_sequence_length\n\n\ndef _set_mixtral_target_modules(config: \"ModelConfig\") -> None:\n    \"\"\"If the base model is Mixtral 7x8, LoRA is enabled and the target modules are not set, set the target modules\n    to q_proj and v_proj.\"\"\"\n    if config.base_model not in {\"mistralai/Mixtral-8x7B-v0.1\", \"mistralai/Mixtral-8x7B-Instruct-v0.1\"}:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"lora\" or config.adapter.target_modules:\n        return\n\n    target_modules = [\"q_proj\", \"v_proj\"]\n\n    logger.info(f\"Setting adapter target modules to {target_modules} for Mixtral 7x8 base model with LoRA adapter.\")\n    config.adapter.target_modules = target_modules\n\n\ndef _set_phi2_target_modules(config: \"ModelConfig\") -> None:\n    \"\"\"If the base model is Phi-2, LoRA is enabled and the target modules are not set, set the target modules to\n    maximize performance.\"\"\"\n    if config.base_model not in {\n        \"microsoft/phi-1\",\n        \"microsoft/phi-1_5\",\n        \"microsoft/phi-2\",\n    }:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"lora\" or config.adapter.target_modules:\n        return\n\n    target_modules = [\"q_proj\", \"k_proj\", \"v_proj\", \"dense\", \"fc1\", \"fc2\"]\n\n    logger.info(f\"Setting adapter target modules to {target_modules} for Phi-2 base model with LoRA adapter.\")\n    config.adapter.target_modules = target_modules\n\n\ndef _set_phi3_target_modules(config: \"ModelConfig\") -> None:\n    if config.base_model not in {\n        \"microsoft/Phi-3-mini-4k-instruct\",\n        \"microsoft/Phi-3-mini-128k-instruct\",\n    }:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"lora\" or config.adapter.target_modules:\n        return\n\n    target_modules = [\"qkv_proj\", \"o_proj\", \"gate_up_proj\", \"down_proj\"]\n\n    logger.info(f\"Setting adapter target modules to {target_modules} for Phi-3 base model with LoRA adapter.\")\n    config.adapter.target_modules = target_modules\n\n\ndef _set_gemma_target_modules(config: \"ModelConfig\") -> None:\n    \"\"\"If the base model is Gemma, LoRA is enabled and the target modules are not set, set the target modules to\n    maximize performance.\"\"\"\n    if config.base_model not in {\"google/gemma-2b\", \"google/gemma-2b-it\", \"google/gemma-7b\", \"google/gemma-7b-it\"}:\n        return\n\n    if not config.adapter:\n        return\n\n    if config.adapter.type != \"lora\" or config.adapter.target_modules:\n        return\n\n    target_modules = [\"q_proj\", \"v_proj\"]\n    config.adapter.target_modules = target_modules\n\n\n@DeveloperAPI\ndef contains_grid_search_parameters(hyperopt_config: HyperoptConfigDict) -> bool:\n    \"\"\"Returns True if any hyperopt parameter in the config is using the grid_search space.\"\"\"\n    for _, param_info in hyperopt_config[PARAMETERS].items():\n        if param_info.get(SPACE, None) == GRID_SEARCH:\n            return True\n    return False\n"
  },
  {
    "path": "ludwig/schema/optimizers.py",
    "content": "from abc import ABC\nfrom dataclasses import field\nfrom typing import ClassVar\n\nimport torch\n\ntry:\n    import bitsandbytes as bnb\nexcept Exception:\n    bnb = None\nimport ludwig.schema.utils as schema_utils\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.metadata import OPTIMIZER_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.registry import Registry\n\noptimizer_registry = Registry()\n\n\n@DeveloperAPI\ndef register_optimizer(name: str):\n    def wrap(optimizer_config: BaseOptimizerConfig):\n        optimizer_registry[name] = (optimizer_config.optimizer_class, optimizer_config)\n        return optimizer_config\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_optimizer_cls(name: str):\n    \"\"\"Get the optimizer schema class from the optimizer schema class registry.\"\"\"\n    return optimizer_registry[name][1]\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseOptimizerConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Base class for optimizers. Not meant to be used directly.\n\n    The dataclass format prevents arbitrary properties from being set. Consequently, in child classes, all properties\n    from the corresponding `torch.optim.Optimizer` class are copied over: check each class to check which attributes are\n    different from the torch-specified defaults.\n    \"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer | None] = None\n    \"Class variable pointing to the corresponding `torch.optim.Optimizer` class.\"\n\n    type: str\n    \"\"\"Name corresponding to an optimizer `ludwig.modules.optimization_modules.optimizer_registry`.\n\n    Technically mutable, but attempting to load a derived optimizer with `type` set to a mismatched value will result in\n    a `ValidationError`.\n    \"\"\"\n\n    @property\n    def is_paged(self) -> bool:\n        \"\"\"Returns True if the optimizer is a Paged optimizer.\"\"\"\n        return False\n\n    @property\n    def is_8bit(self) -> bool:\n        \"\"\"Returns True if the optimizer is an 8-bit optimizer.\"\"\"\n        return False\n\n\n@DeveloperAPI\n@register_optimizer(name=\"sgd\")\n@ludwig_dataclass\nclass SGDOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for stochastic gradient descent.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.SGD\n    \"\"\"Points to `torch.optim.SGD`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"sgd\")\n    \"\"\"Must be 'sgd' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default:\n       'sgd')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.SGD.html#torch.optim.SGD :\n    momentum: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Momentum factor.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"momentum\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Weight decay ($L2$ penalty).\",\n        parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"],\n    )\n\n    dampening: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Dampening for momentum.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"dampening\"],\n    )\n\n    nesterov: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Enables Nesterov momentum.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"nesterov\"],\n    )\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"sgd_8bit\")\n    @ludwig_dataclass\n    class SGD8BitOptimizerConfig(SGDOptimizerConfig):\n        \"\"\"Parameters for stochastic gradient descent.\"\"\"\n\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.SGD8bit\n\n        type: str = schema_utils.ProtectedString(\"sgd_8bit\")\n\n        block_wise: bool = schema_utils.Boolean(\n            default=False,\n            description=\"Whether to use block wise update.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\n@DeveloperAPI\n@register_optimizer(name=\"lbfgs\")\n@ludwig_dataclass\nclass LBFGSOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for stochastic gradient descent.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.LBFGS\n    \"\"\"Points to `torch.optim.LBFGS`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"lbfgs\")\n    \"\"\"Must be 'lbfgs' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default:\n       'lbfgs')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.LBFGS.html#torch.optim.LBFGS\n    max_iter: int = schema_utils.Integer(\n        default=20,\n        description=\"Maximum number of iterations per optimization step.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"max_iter\"],\n    )\n\n    max_eval: int = schema_utils.Integer(\n        default=None,\n        allow_none=True,\n        description=\"Maximum number of function evaluations per optimization step. Default: `max_iter` * 1.25.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"max_eval\"],\n    )\n\n    tolerance_grad: float = schema_utils.NonNegativeFloat(\n        default=1e-07,\n        description=\"Termination tolerance on first order optimality.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"tolerance_grad\"],\n    )\n\n    tolerance_change: float = schema_utils.NonNegativeFloat(\n        default=1e-09,\n        description=\"Termination tolerance on function value/parameter changes.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"tolerance_change\"],\n    )\n\n    history_size: int = schema_utils.Integer(\n        default=100, description=\"Update history size.\", parameter_metadata=OPTIMIZER_METADATA[\"history_size\"]\n    )\n\n    line_search_fn: str = schema_utils.StringOptions(\n        [\"strong_wolfe\"],\n        default=None,\n        allow_none=True,\n        description=\"Line search function to use.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"line_search_fn\"],\n    )\n\n\n@DeveloperAPI\n@register_optimizer(name=\"adam\")\n@ludwig_dataclass\nclass AdamOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for adam optimization.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adam\n    \"\"\"Points to `torch.optim.Adam`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"adam\")\n    \"\"\"Must be 'adam' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'adam')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam :\n    betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n        default=(0.9, 0.999),\n        description=\"Coefficients used for computing running averages of gradient and its square.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-08,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0, description=\"Weight decay (L2 penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n    amsgrad: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam \"\n        \"and Beyond'.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"amsgrad\"],\n    )\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"adam_8bit\")\n    @ludwig_dataclass\n    class Adam8BitOptimizerConfig(AdamOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adam8bit\n\n        type: str = schema_utils.ProtectedString(\"adam_8bit\")\n\n        block_wise: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use block wise update.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_adam\")\n    @ludwig_dataclass\n    class PagedAdamOptimizerConfig(Adam8BitOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam\n\n        type: str = schema_utils.ProtectedString(\"paged_adam\")\n\n        @property\n        def is_paged(self) -> bool:\n            return True\n\n        @property\n        def is_8bit(self) -> bool:\n            return False\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_adam_8bit\")\n    @ludwig_dataclass\n    class PagedAdam8BitOptimizerConfig(PagedAdamOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam8bit\n\n        type: str = schema_utils.ProtectedString(\"paged_adam_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\n@DeveloperAPI\n@register_optimizer(name=\"adamw\")\n@ludwig_dataclass\nclass AdamWOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for adamw optimization.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.AdamW\n    \"\"\"Points to `torch.optim.AdamW`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"adamw\")\n    \"\"\"Must be 'adamw' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'adamw')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam :\n    betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n        default=(0.9, 0.999),\n        description=\"Coefficients used for computing running averages of gradient and its square.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-08,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0, description=\"Weight decay ($L2$ penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n    amsgrad: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam \"\n        \"and Beyond'. \",\n        parameter_metadata=OPTIMIZER_METADATA[\"amsgrad\"],\n    )\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"adamw_8bit\")\n    @ludwig_dataclass\n    class AdamW8BitOptimizerConfig(AdamWOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.AdamW8bit\n\n        type: str = schema_utils.ProtectedString(\"adamw_8bit\")\n\n        block_wise: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use block wise update.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_adamw\")\n    @ludwig_dataclass\n    class PagedAdamWOptimizerConfig(AdamW8BitOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW\n\n        type: str = schema_utils.ProtectedString(\"paged_adamw\")\n\n        @property\n        def is_paged(self) -> bool:\n            return True\n\n        @property\n        def is_8bit(self) -> bool:\n            return False\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_adamw_8bit\")\n    @ludwig_dataclass\n    class PagedAdamW8BitOptimizerConfig(PagedAdamWOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW8bit\n\n        type: str = schema_utils.ProtectedString(\"paged_adamw_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\n@DeveloperAPI\n@register_optimizer(name=\"adadelta\")\n@ludwig_dataclass\nclass AdadeltaOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for adadelta optimization.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adadelta\n    \"\"\"Points to `torch.optim.Adadelta`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"adadelta\")\n    \"\"\"Must be 'adadelta' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'adadelta')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adadelta.html#torch.optim.Adadelta :\n    rho: float = schema_utils.FloatRange(\n        default=0.9,\n        min=0,\n        max=1,\n        description=\"Coefficient used for computing a running average of squared gradients.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"rho\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-06,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0, description=\"Weight decay ($L2$ penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n\n@DeveloperAPI\n@register_optimizer(name=\"adagrad\")\n@ludwig_dataclass\nclass AdagradOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for adagrad optimization.\"\"\"\n\n    # Example docstring\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adagrad\n    \"\"\"Points to `torch.optim.Adagrad`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"adagrad\")\n    \"\"\"Must be 'adagrad' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'adagrad')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adagrad.html#torch.optim.Adagrad :\n    initial_accumulator_value: float = schema_utils.NonNegativeFloat(\n        default=0, description=\"\", parameter_metadata=OPTIMIZER_METADATA[\"initial_accumulator_value\"]\n    )\n\n    lr_decay: float = schema_utils.FloatRange(\n        default=0, description=\"Learning rate decay.\", parameter_metadata=OPTIMIZER_METADATA[\"lr_decay\"]\n    )\n\n    weight_decay: float = schema_utils.FloatRange(\n        default=0, description=\"Weight decay ($L2$ penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n    eps: float = schema_utils.FloatRange(\n        default=1e-10,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"adagrad_8bit\")\n    @ludwig_dataclass\n    class Adagrad8BitOptimizerConfig(AdagradOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adagrad8bit\n\n        type: str = schema_utils.ProtectedString(\"adagrad_8bit\")\n\n        block_wise: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use block wise update.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\n@DeveloperAPI\n@register_optimizer(name=\"adamax\")\n@ludwig_dataclass\nclass AdamaxOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for adamax optimization.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adamax\n    \"\"\"Points to `torch.optim.Adamax`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"adamax\")\n    \"\"\"Must be 'adamax' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'adamax')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adamax.html#torch.optim.Adamax :\n    betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n        default=(0.9, 0.999),\n        description=\"Coefficients used for computing running averages of gradient and its square.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-08,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0, description=\"Weight decay ($L2$ penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n\n# NOTE: keep ftrl and nadam optimizers out of registry:\n# @register_optimizer(name=\"ftrl\")\n@DeveloperAPI\n@ludwig_dataclass\nclass FtrlOptimizerConfig(BaseOptimizerConfig):\n    # optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Ftrl\n    type: str = schema_utils.ProtectedString(\"ftrl\")\n\n    learning_rate_power: float = schema_utils.FloatRange(\n        default=-0.5, max=0, parameter_metadata=OPTIMIZER_METADATA[\"learning_rate_power\"]\n    )\n\n    initial_accumulator_value: float = schema_utils.NonNegativeFloat(\n        default=0.1, parameter_metadata=OPTIMIZER_METADATA[\"initial_accumulator_value\"]\n    )\n\n    l1_regularization_strength: float = schema_utils.NonNegativeFloat(\n        default=0.0, parameter_metadata=OPTIMIZER_METADATA[\"l1_regularization_strength\"]\n    )\n\n    l2_regularization_strength: float = schema_utils.NonNegativeFloat(\n        default=0.0, parameter_metadata=OPTIMIZER_METADATA[\"l2_regularization_strength\"]\n    )\n\n\n@DeveloperAPI\n@register_optimizer(name=\"nadam\")\n@ludwig_dataclass\nclass NadamOptimizerConfig(BaseOptimizerConfig):\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.NAdam\n    \"\"\"Points to `torch.optim.NAdam`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"nadam\")\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.NAdam.html#torch.optim.NAdam :\n\n    betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n        default=(0.9, 0.999),\n        description=\"Coefficients used for computing running averages of gradient and its square.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-08,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(\n        default=0.0, description=\"Weight decay ($L2$ penalty).\", parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"]\n    )\n\n    momentum_decay: float = schema_utils.NonNegativeFloat(\n        default=4e-3, description=\"Momentum decay.\", parameter_metadata=OPTIMIZER_METADATA[\"momentum_decay\"]\n    )\n\n\n@DeveloperAPI\n@register_optimizer(name=\"rmsprop\")\n@ludwig_dataclass\nclass RMSPropOptimizerConfig(BaseOptimizerConfig):\n    \"\"\"Parameters for rmsprop optimization.\"\"\"\n\n    optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.RMSprop\n    \"\"\"Points to `torch.optim.RMSprop`.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\"rmsprop\")\n    \"\"\"Must be 'rmsprop' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`\n       (default: 'rmsprop')\"\"\"\n\n    # Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.RMSprop.html#torch.optim.RMSprop:\n    momentum: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Momentum factor.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"momentum\"],\n    )\n\n    alpha: float = schema_utils.NonNegativeFloat(\n        default=0.99,\n        description=\"Smoothing constant.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"alpha\"],\n    )\n\n    eps: float = schema_utils.NonNegativeFloat(\n        default=1e-08,\n        description=\"Term added to the denominator to improve numerical stability.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n    )\n\n    centered: bool = schema_utils.Boolean(\n        default=False,\n        description=\"If True, computes the centered RMSProp, and the gradient is normalized by an estimation of its \"\n        \"variance.\",\n        parameter_metadata=OPTIMIZER_METADATA[\"centered\"],\n    )\n\n    weight_decay: float = schema_utils.NonNegativeFloat(default=0.0, description=\"Weight decay ($L2$ penalty).\")\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"rmsprop_8bit\")\n    @ludwig_dataclass\n    class RMSProp8BitOptimizerConfig(RMSPropOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.RMSprop8bit\n\n        type: str = schema_utils.ProtectedString(\"rmsprop_8bit\")\n\n        block_wise: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use block wise update.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lamb\")\n    @ludwig_dataclass\n    class LAMBOptimizerConfig(BaseOptimizerConfig):\n        \"\"\"Layer-wise Adaptive Moments optimizer for Batch training.\n\n        Paper: https://arxiv.org/pdf/1904.00962.pdf\n        \"\"\"\n\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB\n\n        type: str = schema_utils.ProtectedString(\"lamb\")\n\n        bias_correction: bool = schema_utils.Boolean(\n            default=True,\n        )\n\n        betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n            default=(0.9, 0.999),\n            description=\"Coefficients used for computing running averages of gradient and its square.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n        )\n\n        eps: float = schema_utils.NonNegativeFloat(\n            default=1e-08,\n            description=\"Term added to the denominator to improve numerical stability.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"eps\"],\n        )\n\n        weight_decay: float = schema_utils.NonNegativeFloat(\n            default=0.0,\n            description=\"Weight decay (L2 penalty).\",\n            parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"],\n        )\n\n        amsgrad: bool = schema_utils.Boolean(\n            default=False,\n            description=(\n                \"Whether to use the AMSGrad variant of this algorithm from the paper \"\n                \"'On the Convergence of Adam and Beyond'.\"\n            ),\n            parameter_metadata=OPTIMIZER_METADATA[\"amsgrad\"],\n        )\n\n        adam_w_mode: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use the AdamW mode of this algorithm from the paper \"\n            \"'Decoupled Weight Decay Regularization'.\",\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        block_wise: bool = schema_utils.Boolean(\n            default=False,\n            description=\"Whether to use block wise update.\",\n        )\n\n        max_unorm: float = schema_utils.FloatRange(\n            default=1.0,\n            min=0.0,\n            max=1.0,\n        )\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lamb_8bit\")\n    @ludwig_dataclass\n    class LAMB8BitOptimizerConfig(LAMBOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB8bit\n\n        type: str = schema_utils.ProtectedString(\"lamb_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lars\")\n    @ludwig_dataclass\n    class LARSOptimizerConfig(BaseOptimizerConfig):\n        \"\"\"Layerwise Adaptive Rate Scaling.\n\n        Paper: https://arxiv.org/pdf/1708.03888.pdf\n        \"\"\"\n\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS\n\n        type: str = schema_utils.ProtectedString(\"lars\")\n\n        # 0.9 taken from the original paper - momentum requires a non zero value\n        # https://arxiv.org/pdf/1708.03888v3.pdf\n        momentum: float = schema_utils.FloatRange(\n            default=0.9,\n            min=0.0,\n            max=1.0,\n            min_inclusive=False,\n            description=\"Momentum factor.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"momentum\"],\n        )\n\n        dampening: float = schema_utils.FloatRange(\n            default=0.0,\n            min=0.0,\n            max=1.0,\n            description=\"Dampening for momentum.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"dampening\"],\n        )\n\n        weight_decay: float = schema_utils.NonNegativeFloat(\n            default=0.0,\n            description=\"Weight decay (L2 penalty).\",\n            parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"],\n        )\n\n        nesterov: bool = schema_utils.Boolean(\n            default=False,\n            description=\"Enables Nesterov momentum.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"nesterov\"],\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        max_unorm: float = schema_utils.FloatRange(\n            default=1.0,\n            min=0.0,\n            max=1.0,\n        )\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lars_8bit\")\n    @ludwig_dataclass\n    class LARS8BitOptimizerConfig(LARSOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS8bit\n\n        type: str = schema_utils.ProtectedString(\"lars_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\nif bnb is not None:\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lion\")\n    @ludwig_dataclass\n    class LIONOptimizerConfig(BaseOptimizerConfig):\n        \"\"\"Evolved Sign Momentum.\n\n        Paper: https://arxiv.org/pdf/2302.06675.pdf\n        \"\"\"\n\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion\n\n        type: str = schema_utils.ProtectedString(\"lion\")\n\n        betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(\n            default=(0.9, 0.999),\n            description=\"Coefficients used for computing running averages of gradient and its square.\",\n            parameter_metadata=OPTIMIZER_METADATA[\"betas\"],\n        )\n\n        weight_decay: float = schema_utils.NonNegativeFloat(\n            default=0.0,\n            description=\"Weight decay (L2 penalty).\",\n            parameter_metadata=OPTIMIZER_METADATA[\"weight_decay\"],\n        )\n\n        percentile_clipping: int = schema_utils.IntegerRange(\n            default=100,\n            min=0,\n            max=100,\n            description=\"Percentile clipping.\",\n        )\n\n        block_wise: bool = schema_utils.Boolean(\n            default=True,\n            description=\"Whether to use block wise update.\",\n        )\n\n    @DeveloperAPI\n    @register_optimizer(name=\"lion_8bit\")\n    @ludwig_dataclass\n    class LION8BitOptimizerConfig(LIONOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion8bit\n\n        type: str = schema_utils.ProtectedString(\"lion_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_lion\")\n    @ludwig_dataclass\n    class PagedLionOptimizerConfig(LIONOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion\n\n        type: str = schema_utils.ProtectedString(\"paged_lion\")\n\n        @property\n        def is_paged(self) -> bool:\n            return True\n\n    @DeveloperAPI\n    @register_optimizer(name=\"paged_lion_8bit\")\n    @ludwig_dataclass\n    class PagedLion8BitOptimizerConfig(PagedLionOptimizerConfig):\n        optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion8bit\n\n        type: str = schema_utils.ProtectedString(\"paged_lion_8bit\")\n\n        @property\n        def is_8bit(self) -> bool:\n            return True\n\n\n@DeveloperAPI\ndef get_optimizer_conds():\n    \"\"\"Returns a JSON schema of conditionals to validate against optimizer types defined in\n    `ludwig.modules.optimization_modules.optimizer_registry`.\"\"\"\n    conds = []\n    for optimizer in optimizer_registry:\n        optimizer_cls = optimizer_registry[optimizer][1]\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(optimizer_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        preproc_cond = schema_utils.create_cond(\n            {\"type\": optimizer},\n            other_props,\n        )\n        conds.append(preproc_cond)\n    return conds\n\n\n@DeveloperAPI\ndef OptimizerDataclassField(default=\"adam\", description=\"\", parameter_metadata: ParameterMetadata = None):\n    \"\"\"Custom dataclass field that when used inside of a dataclass will allow any optimizer in\n    `ludwig.modules.optimization_modules.optimizer_registry`.\n\n    Sets default optimizer to 'adam'.\n\n    :param default: Dict specifying an optimizer with a `type` field and its associated parameters. Will attempt to use\n           `type` to load optimizer from registry with given params. (default: {\"type\": \"adam\"}).\n    :return: Initialized dataclass field that converts untyped dicts with params to optimizer dataclass instances.\n    \"\"\"\n\n    class OptimizerSelection(schema_utils.TypeSelection):\n        \"\"\"Custom marshmallow field that deserializes a dict to a valid optimizer from\n        `ludwig.modules.optimization_modules.optimizer_registry` and creates a corresponding `oneOf` JSON schema\n        for external usage.\"\"\"\n\n        def __init__(self):\n            super().__init__(\n                registry=optimizer_registry,\n                default_value=default,\n                description=description,\n                parameter_metadata=parameter_metadata,\n            )\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return get_optimizer_cls(key)\n\n        def _jsonschema_type_mapping(self):\n            # Note that this uses the same conditional pattern as combiners:\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": list(optimizer_registry.keys()),\n                        \"default\": default,\n                        \"description\": \"The type of optimizer to use during the learning process\",\n                    },\n                },\n                \"title\": \"optimizer_options\",\n                \"allOf\": get_optimizer_conds(),\n                \"required\": [\"type\"],\n                \"description\": description,\n            }\n\n    return OptimizerSelection().get_default_field()\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass GradientClippingConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Dataclass that holds gradient clipping parameters.\"\"\"\n\n    clipglobalnorm: float | None = schema_utils.FloatRange(\n        default=0.5,\n        allow_none=True,\n        description=\"Maximum allowed norm of the gradients\",\n        parameter_metadata=OPTIMIZER_METADATA[\"gradient_clipping\"],\n    )\n\n    # TODO(travis): is this redundant with `clipglobalnorm`?\n    clipnorm: float | None = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        description=\"Maximum allowed norm of the gradients\",\n        parameter_metadata=OPTIMIZER_METADATA[\"gradient_clipping\"],\n    )\n\n    clipvalue: float | None = schema_utils.FloatRange(\n        default=None,\n        allow_none=True,\n        description=\"Maximum allowed value of the gradients\",\n        parameter_metadata=OPTIMIZER_METADATA[\"gradient_clipping\"],\n    )\n\n\n@DeveloperAPI\ndef GradientClippingDataclassField(description: str, default: dict = {}):\n    \"\"\"Returns custom dataclass field for `ludwig.modules.optimization_modules.GradientClippingConfig`. Allows\n    `None` by default.\n\n    :param description: Description of the gradient dataclass field\n    :param default: dict that specifies clipping param values that will be loaded by its schema class (default: {}).\n    \"\"\"\n    allow_none = True\n\n    class GradientClippingMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field class for gradient clipping.\n\n        Deserializes a dict to a valid instance of `ludwig.modules.optimization_modules.GradientClippingConfig` and\n        creates a corresponding JSON schema for external usage.\n        \"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return value\n            if isinstance(value, dict):\n                try:\n                    return GradientClippingConfig.Schema().load(value)\n                except (TypeError, ConfigValidationError):\n                    raise ConfigValidationError(\n                        f\"Invalid params for gradient clipping: {value}, see GradientClippingConfig class.\"\n                    )\n            raise ConfigValidationError(\"Field should be None or dict\")\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"oneOf\": [\n                    {\"type\": \"null\", \"title\": \"disabled\", \"description\": \"Disable gradient clipping.\"},\n                    {\n                        **schema_utils.unload_jsonschema_from_marshmallow_class(GradientClippingConfig),\n                        \"title\": \"enabled_options\",\n                    },\n                ],\n                \"title\": \"gradient_clipping_options\",\n                \"description\": description,\n            }\n\n    if not isinstance(default, dict):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n\n    def load_default():\n        return GradientClippingConfig.Schema().load(default)\n\n    dump_default = GradientClippingConfig.Schema().dump(default)\n\n    return field(\n        metadata={\n            \"marshmallow_field\": GradientClippingMarshmallowField(\n                allow_none=allow_none,\n                load_default=load_default,\n                dump_default=dump_default,\n                metadata={\n                    \"description\": description,\n                    \"parameter_metadata\": convert_metadata_to_json(OPTIMIZER_METADATA[\"gradient_clipping\"]),\n                },\n            )\n        },\n        default_factory=load_default,\n    )\n"
  },
  {
    "path": "ludwig/schema/preprocessing.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import RANDOM\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import PREPROCESSING_METADATA\nfrom ludwig.schema.split import BaseSplitConfig, SplitDataclassField\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass PreprocessingConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Global preprocessing config is a dataclass that configures the parameters used for global preprocessing.\"\"\"\n\n    sample_ratio: float = schema_utils.NonNegativeFloat(\n        default=1.0,\n        description=\"The ratio of the dataset to use. For instance, if 0.5, half of the dataset \"\n        \"provided will be used.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"sample_ratio\"],\n    )\n\n    sample_size: float = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"The maximum number of samples from the dataset to use. Cannot be set if sample_ratio is set to be \"\n        \"< 1.0. If sample_ratio is set to 1.0, this will override the number of samples to used.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"sample_size\"],\n    )\n\n    oversample_minority: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"If not None, the minority class will be oversampled to reach the specified ratio respective to \"\n        \"the majority class. \",\n        parameter_metadata=PREPROCESSING_METADATA[\"oversample_minority\"],\n    )\n\n    undersample_majority: float = schema_utils.NonNegativeFloat(\n        default=None,\n        allow_none=True,\n        description=\"If not None, the majority class will be undersampled to reach the specified ratio respective \"\n        \"to the minority class. \",\n        parameter_metadata=PREPROCESSING_METADATA[\"undersample_majority\"],\n    )\n\n    split: BaseSplitConfig = SplitDataclassField(\n        default=RANDOM,\n    )\n\n    global_max_sequence_length: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Specifically for LLMs. This is the maximum length of the input sequence going into the model's \"\n        \"forward pass during training. Sequences will be truncated to this length after merging inputs and targets. \"\n        \"If not set, the total length of the merged input and target token sequences will be used.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"global_max_sequence_length\"],\n    )\n\n\n@DeveloperAPI\nclass PreprocessingField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(PreprocessingConfig)\n"
  },
  {
    "path": "ludwig/schema/profiler.py",
    "content": "from dataclasses import field\n\nimport ludwig.schema.utils as schema_utils\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass ProfilerConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"Dataclass that holds profiling parameters for torch profile scheduler.\n\n    The profiler will skip the first skip_first steps, then wait for wait steps, then do the warmup for the next warmup\n    steps, then do the active recording for the next active steps and then repeat the cycle starting with wait steps.\n    The optional number of cycles is specified with the repeat parameter, the zero value means that the cycles will\n    continue until the profiling is finished.\n    \"\"\"\n\n    wait: int = schema_utils.IntegerRange(\n        default=1,\n        min=0,\n        description=\"The number of steps to wait profiling.\",\n    )\n\n    warmup: int = schema_utils.IntegerRange(\n        default=1,\n        min=0,\n        description=\"The number of steps for profiler warmup after waiting finishes.\",\n    )\n\n    active: int = schema_utils.IntegerRange(\n        default=3,\n        min=0,\n        description=\"The number of steps that are actively recorded. Values more than 10 wil dramatically slow down \"\n        \"tensorboard loading.\",\n    )\n\n    repeat: int = schema_utils.IntegerRange(\n        default=5,\n        min=0,\n        description=\"The optional number of profiling cycles. Use 0 to profile the entire training run.\",\n    )\n\n    skip_first: int = schema_utils.IntegerRange(\n        default=0,\n        min=0,\n        max=100,\n        description=\"The number of steps to skip in the beginning of training.\",\n    )\n\n\n@DeveloperAPI\ndef ProfilerDataclassField(description: str, default: dict = {}):\n    \"\"\"Returns custom dataclass field for `ludwig.modules.profiler.ProfilerConfig`. Allows `None` by default.\n\n    :param description: Description of the torch profiler field\n    :param default: dict that specifies clipping param values that will be loaded by its schema class (default: {}).\n    \"\"\"\n    allow_none = True\n\n    class ProfilingMarshmallowField(schema_utils.LudwigSchemaField):\n        \"\"\"Custom field class for the torch profiler.\n\n        Deserializes a dict to a valid instance of `ludwig.modules.optimization_modules.ProfilerConfig` and\n        creates a corresponding JSON schema for external usage.\n        \"\"\"\n\n        def _deserialize(self, value, attr, data, **kwargs):\n            if value is None:\n                return value\n            if isinstance(value, dict):\n                try:\n                    return ProfilerConfig.Schema().load(value)\n                except (TypeError, ConfigValidationError):\n                    raise ConfigValidationError(\n                        f\"Invalid params for profiling config: {value}, see ProfilerConfig class.\"\n                    )\n            raise ConfigValidationError(\"Field should be None or dict\")\n\n        def _jsonschema_type_mapping(self):\n            return {\n                **schema_utils.unload_jsonschema_from_marshmallow_class(ProfilerConfig),\n                \"title\": \"profiler_options\",\n                \"description\": description,\n            }\n\n    if not isinstance(default, dict):\n        raise ConfigValidationError(f\"Invalid default: `{default}`\")\n\n    def load_default():\n        return ProfilerConfig.Schema().load(default)\n\n    dump_default = ProfilerConfig.Schema().dump(default)\n\n    return field(\n        metadata={\n            \"marshmallow_field\": ProfilingMarshmallowField(\n                allow_none=allow_none,\n                load_default=load_default,\n                dump_default=dump_default,\n                metadata={\n                    \"description\": description,\n                    \"parameter_metadata\": None,\n                },\n            )\n        },\n        default_factory=load_default,\n    )\n"
  },
  {
    "path": "ludwig/schema/split.py",
    "content": "from dataclasses import Field\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import SPLIT, TYPE\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.metadata import PREPROCESSING_METADATA\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.registry import Registry\n\nsplit_config_registry = Registry()\nDEFAULT_PROBABILITIES = [0.7, 0.1, 0.2]\n\n\n@DeveloperAPI\ndef get_split_cls(name: str):\n    return split_config_registry[name]\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseSplitConfig(schema_utils.BaseMarshmallowConfig):\n    \"\"\"This Dataclass is a base schema for the nested split config under preprocessing.\"\"\"\n\n    type: str\n    \"Name corresponding to the splitting type.\"\n\n\n@DeveloperAPI\n@split_config_registry.register(\"random\")\n@ludwig_dataclass\nclass RandomSplitConfig(BaseSplitConfig):\n    \"\"\"This Dataclass generates a schema for the random splitting config.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"random\",\n        description=\"Type of splitting to use during preprocessing.\",\n    )\n\n    probabilities: list = schema_utils.List(\n        list_type=float,\n        default=DEFAULT_PROBABILITIES,\n        description=\"Probabilities for splitting data into train, validation, and test sets.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"split_probabilities\"],\n    )\n\n\n@DeveloperAPI\n@split_config_registry.register(\"fixed\")\n@ludwig_dataclass\nclass FixedSplitConfig(BaseSplitConfig):\n    \"\"\"This Dataclass generates a schema for the fixed splitting config.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"fixed\",\n        description=\"Type of splitting to use during preprocessing.\",\n    )\n\n    column: str = schema_utils.String(\n        default=SPLIT,\n        allow_none=False,\n        description=\"The column name to use for fixed splitting.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"column\"],\n    )\n\n\n@DeveloperAPI\n@split_config_registry.register(\"stratify\")\n@ludwig_dataclass\nclass StratifySplitConfig(BaseSplitConfig):\n    \"\"\"This Dataclass generates a schema for the fixed splitting config.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"stratify\",\n        description=\"Type of splitting to use during preprocessing.\",\n    )\n\n    column: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The column name to base the stratified splitting on.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"column\"],\n    )\n\n    probabilities: list = schema_utils.List(\n        list_type=float,\n        default=DEFAULT_PROBABILITIES,\n        description=\"Probabilities for splitting data into train, validation, and test sets.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"split_probabilities\"],\n    )\n\n\n@DeveloperAPI\n@split_config_registry.register(\"datetime\")\n@ludwig_dataclass\nclass DateTimeSplitConfig(BaseSplitConfig):\n    \"\"\"This Dataclass generates a schema for the fixed splitting config.\"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"datetime\",\n        description=\"Type of splitting to use during preprocessing.\",\n    )\n\n    column: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The column name to perform datetime splitting on.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"column\"],\n    )\n\n    probabilities: list = schema_utils.List(\n        list_type=float,\n        default=DEFAULT_PROBABILITIES,\n        description=\"Proportion of data to split into train, validation, and test sets.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"split_probabilities\"],\n    )\n\n\n@DeveloperAPI\n@split_config_registry.register(\"hash\")\n@ludwig_dataclass\nclass HashSplitConfig(BaseSplitConfig):\n    \"\"\"This Dataclass generates a schema for the hash splitting config.\n\n    This is useful for deterministically splitting on a unique ID. Even when additional rows are added to the dataset in\n    the future, each ID will retain its original split assignment.\n\n    This approach does not guarantee that the split proportions will be assigned exactly, but the larger the dataset,\n    the more closely the assignment should match the given proportions.\n\n    This approach can be used on a column with duplicates, but it will further skew the assignments of rows to splits.\n    \"\"\"\n\n    type: str = schema_utils.ProtectedString(\n        \"hash\",\n        description=\"Type of splitting to use during preprocessing.\",\n    )\n\n    column: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The column name to perform hash splitting on.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"column\"],\n    )\n\n    probabilities: list = schema_utils.List(\n        list_type=float,\n        default=DEFAULT_PROBABILITIES,\n        description=\"Proportion of data to split into train, validation, and test sets.\",\n        parameter_metadata=PREPROCESSING_METADATA[\"split_probabilities\"],\n    )\n\n\n@DeveloperAPI\ndef get_split_conds():\n    \"\"\"Returns a JSON schema of conditionals to validate against optimizer types defined in\n    `ludwig.modules.optimization_modules.optimizer_registry`.\"\"\"\n    conds = []\n    for splitter in split_config_registry.data:\n        splitter_cls = split_config_registry.data[splitter]\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(splitter_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props, [TYPE])\n        splitter_cond = schema_utils.create_cond(\n            {\"type\": splitter},\n            other_props,\n        )\n        conds.append(splitter_cond)\n    return conds\n\n\n@DeveloperAPI\ndef SplitDataclassField(default: str) -> Field:\n    \"\"\"Custom dataclass field that when used inside a dataclass will allow the user to specify a nested split\n    config.\n\n    Returns: Initialized dataclass field that converts an untyped dict with params to a split config.\n    \"\"\"\n\n    class SplitSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(registry=split_config_registry.data, default_value=default)\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return split_config_registry.data[key]\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"description\": \"Type of splitting to use during preprocessing.\",\n                        \"enum\": list(split_config_registry.data.keys()),\n                        \"default\": default,\n                    },\n                },\n                \"title\": \"split_options\",\n                \"allOf\": get_split_conds(),\n            }\n\n    return SplitSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/trainer.py",
    "content": "import re\nfrom abc import ABC\n\nimport torch\nfrom packaging.version import parse as parse_version\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUTO,\n    EFFECTIVE_BATCH_SIZE,\n    LOSS,\n    MAX_BATCH_SIZE,\n    MAX_POSSIBLE_BATCH_SIZE,\n    MODEL_ECD,\n    MODEL_LLM,\n    TRAINING,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.lr_scheduler import LRSchedulerConfig, LRSchedulerDataclassField\nfrom ludwig.schema.metadata import TRAINER_METADATA\nfrom ludwig.schema.optimizers import (\n    BaseOptimizerConfig,\n    GradientClippingConfig,\n    GradientClippingDataclassField,\n    OptimizerDataclassField,\n)\nfrom ludwig.schema.profiler import ProfilerConfig, ProfilerDataclassField\nfrom ludwig.schema.utils import ludwig_dataclass\nfrom ludwig.utils.registry import Registry\n\n_torch_200 = parse_version(torch.__version__) >= parse_version(\"2.0\")\n\n\ntrainer_schema_registry = Registry()\n_llm_trainer_schema_registry = Registry()\n\n\n@DeveloperAPI\ndef register_trainer_schema(model_type: str):\n    def wrap(trainer_config: BaseTrainerConfig):\n        trainer_schema_registry[model_type] = trainer_config\n        return trainer_config\n\n    return wrap\n\n\n@DeveloperAPI\ndef register_llm_trainer_schema(trainer_type: str):\n    def wrap(trainer_config: BaseTrainerConfig):\n        _llm_trainer_schema_registry[trainer_type] = trainer_config\n        return trainer_config\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_llm_trainer_cls(trainer_type: str):\n    \"\"\"Returns the adapter config class registered with the given name.\"\"\"\n    return _llm_trainer_schema_registry[trainer_type]\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass BaseTrainerConfig(schema_utils.BaseMarshmallowConfig, ABC):\n    \"\"\"Common trainer parameter values.\"\"\"\n\n    validation_field: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The field for which the `validation_metric` is used for validation-related mechanics like early \"\n        \"stopping, parameter change plateaus, as well as what hyperparameter optimization uses to determine the best \"\n        \"trial. If unset (default), the first output feature is used. If explicitly specified, neither \"\n        \"`validation_field` nor `validation_metric` are overwritten.\",\n    )\n\n    validation_metric: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Metric from `validation_field` that is used. If validation_field is not explicitly specified, this is \"\n            \"overwritten to be the first output feature type's `default_validation_metric`, consistent with \"\n            \"validation_field. If the validation_metric is specified, then we will use the first output feature that \"\n            \"produces this metric as the `validation_field`.\"\n        ),\n    )\n\n    early_stop: int = schema_utils.IntegerRange(\n        default=5,\n        min=-1,\n        description=(\n            \"Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that \"\n            \"triggers training to stop. Can be set to -1, which disables early stopping entirely.\"\n        ),\n    )\n\n    skip_all_evaluation: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Whether to skip evaluation entirely. If you are training a model with a well-known configuration on a \"\n            \"well-known dataset and are confident about the expected results, you might skip all evaluation. Moreover, \"\n            \"evaluating a model, especially on large validation or test sets, can be time-consuming.\"\n        ),\n    )\n\n    enable_profiling: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to enable profiling of the training process using torch.profiler.profile.\",\n    )\n\n    profiler: ProfilerConfig | None = ProfilerDataclassField(\n        description=\"Parameter values for profiling config.\",\n        default={},\n    )\n\n    def can_tune_batch_size(self) -> bool:\n        return True\n\n\n@DeveloperAPI\n@register_trainer_schema(MODEL_ECD)\n@ludwig_dataclass\nclass ECDTrainerConfig(BaseTrainerConfig):\n    \"\"\"Dataclass that configures most of the hyperparameters used for ECD model training.\"\"\"\n\n    def __post_init__(self):\n        if self.compile and not _torch_200:\n            raise ConfigValidationError(\n                \"Trainer param `compile: true` requires PyTorch 2.0.0 or higher. Please upgrade PyTorch and try again.\"\n            )\n\n        if self.effective_batch_size != AUTO and self.max_batch_size < self.effective_batch_size:\n            raise ConfigValidationError(\n                f\"`max_batch_size` ({self.max_batch_size}) must be greater than or equal to \"\n                f\"`effective_batch_size` ({self.effective_batch_size}).\"\n            )\n\n        if self.effective_batch_size != AUTO and self.batch_size != AUTO:\n            if self.effective_batch_size < self.batch_size:\n                raise ConfigValidationError(\n                    f\"`effective_batch_size` ({self.effective_batch_size}) \"\n                    f\"must be greater than or equal to `batch_size` ({self.batch_size}).\"\n                )\n\n            if self.effective_batch_size % self.batch_size != 0:\n                raise ConfigValidationError(\n                    f\"`effective_batch_size` ({self.effective_batch_size}) \"\n                    f\"must be divisible by `batch_size` ({self.batch_size}).\"\n                )\n\n        if self.effective_batch_size != AUTO and self.gradient_accumulation_steps != AUTO:\n            if self.effective_batch_size < self.gradient_accumulation_steps:\n                raise ConfigValidationError(\n                    f\"`effective_batch_size` ({self.effective_batch_size}) must be greater than or equal to \"\n                    f\"`gradient_accumulation_steps` ({self.gradient_accumulation_steps}).\"\n                )\n\n            if self.effective_batch_size % self.gradient_accumulation_steps != 0:\n                raise ConfigValidationError(\n                    f\"`effective_batch_size` ({self.effective_batch_size}) must be divisible by \"\n                    f\"`gradient_accumulation_steps` ({self.gradient_accumulation_steps}).\"\n                )\n\n        if self.layers_to_freeze_regex:\n            try:\n                re.compile(self.layers_to_freeze_regex)\n            except re.error:\n                raise ConfigValidationError(\n                    f\"`layers_to_freeze_regex` ({self.layers_to_freeze_regex}) must be a valid regular expression.\"\n                )\n\n    learning_rate: float | str = schema_utils.OneOfOptionsField(\n        default=0.001,\n        allow_none=False,\n        description=(\n            \"Controls how much to change the model in response to the estimated error each time the model weights are \"\n            \"updated. If 'auto', the optimal learning rate is estimated by choosing the learning rate that produces \"\n            \"the smallest non-diverging gradient update.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate\"],\n        field_options=[\n            schema_utils.FloatRange(default=0.001, allow_none=False, min=0, max=1),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    learning_rate_scheduler: LRSchedulerConfig = LRSchedulerDataclassField(\n        description=\"Parameter values for learning rate scheduler.\",\n        default=None,\n    )\n\n    epochs: int = schema_utils.PositiveInteger(\n        default=100,\n        description=\"Number of epochs the algorithm is intended to be run over. Overridden if `train_steps` is set\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"epochs\"],\n    )\n\n    checkpoints_per_epoch: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=(\n            \"Number of checkpoints per epoch. For example, 2 -> checkpoints are written every half of an epoch. Note \"\n            \"that it is invalid to specify both non-zero `steps_per_checkpoint` and non-zero `checkpoints_per_epoch`.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"checkpoints_per_epoch\"],\n    )\n\n    train_steps: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Maximum number of training steps the algorithm is intended to be run over. Unset by default. \"\n            \"If set, will override `epochs` and if left unset then `epochs` is used to determine training length.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"train_steps\"],\n    )\n\n    eval_steps: float = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of steps to use for evaluation. If None, the entire evaluation set will be used.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"eval_steps\"],\n    )\n\n    steps_per_checkpoint: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=(\n            \"How often the model is checkpointed. Also dictates maximum evaluation frequency. If 0 the model is \"\n            \"checkpointed after every epoch.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"steps_per_checkpoint\"],\n    )\n\n    effective_batch_size: int | str = schema_utils.OneOfOptionsField(\n        default=AUTO,\n        allow_none=False,\n        description=(\n            \"The effective batch size is the total number of samples used to compute a single gradient update \"\n            \"to the model weights. This differs from `batch_size` by taking `gradient_accumulation_steps` and number \"\n            \"of training worker processes into account. In practice, \"\n            \"`effective_batch_size = batch_size * gradient_accumulation_steps * num_workers`. \"\n            \"If 'auto', the effective batch size is derivied implicitly from `batch_size`, but if set explicitly, then \"\n            \"one of `batch_size` or `gradient_accumulation_steps` must be set to something other than 'auto', and \"\n            \"consequently will be set following the formula given above.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][EFFECTIVE_BATCH_SIZE],\n        field_options=[\n            schema_utils.PositiveInteger(default=128, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    batch_size: int | str = schema_utils.OneOfOptionsField(\n        default=AUTO,\n        allow_none=False,\n        description=(\n            \"The number of training examples utilized in one training step of the model. If ’auto’, the \"\n            \"batch size that maximized training throughput (samples / sec) will be used. For CPU training, the \"\n            \"tuned batch size is capped at 128 as throughput benefits of large batch sizes are less noticeable without \"\n            \"a GPU.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"batch_size\"],\n        field_options=[\n            schema_utils.PositiveInteger(default=128, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    max_batch_size: int = schema_utils.PositiveInteger(\n        default=MAX_POSSIBLE_BATCH_SIZE,\n        allow_none=True,\n        description=(\n            \"Auto batch size tuning and increasing batch size on plateau will be capped at this value. The default \"\n            \"value is 2^40.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][MAX_BATCH_SIZE],\n    )\n\n    gradient_accumulation_steps: int | str = schema_utils.OneOfOptionsField(\n        default=AUTO,\n        allow_none=False,\n        description=\"Number of steps to accumulate gradients over before performing a weight update.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"gradient_accumulation_steps\"],\n        field_options=[\n            schema_utils.PositiveInteger(default=1, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    early_stop: int = schema_utils.IntegerRange(\n        default=5,\n        min=-1,\n        description=(\n            \"Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that \"\n            \"triggers training to stop. Can be set to -1, which disables early stopping entirely.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"early_stop\"],\n    )\n\n    eval_batch_size: None | int | str = schema_utils.OneOfOptionsField(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Size of batch to pass to the model for evaluation. If it is `0` or `None`, the same value of `batch_size` \"\n            \"is used. This is useful to speedup evaluation with a much bigger batch size than training, if enough \"\n            \"memory is available. If ’auto’, the biggest batch size (power of 2) that can fit in memory will be used.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"eval_batch_size\"],\n        field_options=[\n            schema_utils.PositiveInteger(default=128, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    evaluate_training_set: bool = schema_utils.Boolean(\n        default=False,\n        description=(\n            \"Whether to evaluate on the entire training set during evaluation. By default, training metrics will be \"\n            \"computed at the end of each training step, and accumulated up to the evaluation phase. In practice, \"\n            \"computing training set metrics during training is up to 30% faster than running a separate evaluation \"\n            \"pass over the training set, but results in more noisy training metrics, particularly during the earlier \"\n            \"epochs. It's recommended to only set this to True if you need very exact training set metrics, and are \"\n            \"willing to pay a significant performance penalty for them.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"evaluate_training_set\"],\n    )\n\n    validation_field: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"The field for which the `validation_metric` is used for validation-related mechanics like early \"\n        \"stopping, parameter change plateaus, as well as what hyperparameter optimization uses to determine the best \"\n        \"trial. If unset (default), the first output feature is used. If explicitly specified, neither \"\n        \"`validation_field` nor `validation_metric` are overwritten.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"validation_field\"],\n    )\n\n    validation_metric: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Metric from `validation_field` that is used. If validation_field is not explicitly specified, this is \"\n            \"overwritten to be the first output feature type's `default_validation_metric`, consistent with \"\n            \"validation_field. If the validation_metric is specified, then we will use the first output feature that \"\n            \"produces this metric as the `validation_field`.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"validation_metric\"],\n    )\n\n    optimizer: BaseOptimizerConfig = OptimizerDataclassField(\n        default=\"adam\",\n        description=(\n            \"Optimizer type and its parameters. The optimizer is responsble for applying the gradients computed \"\n            \"from the loss during backpropagation as updates to the model weights.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"optimizer\"],\n    )\n\n    regularization_type: str | None = schema_utils.RegularizerOptions(\n        default=\"l2\",\n        allow_none=True,\n        description=\"Type of regularization.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"regularization_type\"],\n    )\n\n    regularization_lambda: float = schema_utils.FloatRange(\n        default=0.0,\n        min=0,\n        max=1,\n        description=\"Strength of the regularization.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"regularization_lambda\"],\n    )\n\n    should_shuffle: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to shuffle batches during training when true.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"should_shuffle\"],\n    )\n\n    increase_batch_size_on_plateau: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"The number of times to increase the batch size on a plateau.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"increase_batch_size_on_plateau\"],\n    )\n\n    increase_batch_size_on_plateau_patience: int = schema_utils.NonNegativeInteger(\n        default=5,\n        description=\"How many epochs to wait for before increasing the batch size.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"increase_batch_size_on_plateau_patience\"],\n    )\n\n    increase_batch_size_on_plateau_rate: float = schema_utils.NonNegativeFloat(\n        default=2.0,\n        description=\"Rate at which the batch size increases.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"increase_batch_size_on_plateau_rate\"],\n    )\n\n    increase_batch_size_eval_metric: str = schema_utils.String(\n        default=LOSS,\n        description=\"Which metric to listen on for increasing the batch size.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"increase_batch_size_eval_metric\"],\n    )\n\n    increase_batch_size_eval_split: str = schema_utils.String(\n        default=TRAINING,\n        description=\"Which dataset split to listen on for increasing the batch size.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"increase_batch_size_eval_split\"],\n    )\n\n    gradient_clipping: GradientClippingConfig | None = GradientClippingDataclassField(\n        description=\"Parameter values for gradient clipping.\",\n        default={},\n    )\n\n    learning_rate_scaling: str = schema_utils.StringOptions(\n        [\"constant\", \"sqrt\", \"linear\"],\n        default=\"linear\",\n        description=\"Scale by which to increase the learning rate as the number of distributed workers increases. \"\n        \"Traditionally the learning rate is scaled linearly with the number of workers to reflect the \"\n        \"proportion by\"\n        \" which the effective batch size is increased. For very large batch sizes, a softer square-root \"\n        \"scale can \"\n        \"sometimes lead to better model performance. If the learning rate is hand-tuned for a given \"\n        \"number of \"\n        \"workers, setting this value to constant can be used to disable scale-up.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate_scaling\"],\n    )\n\n    bucketing_field: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=\"Feature to use for bucketing datapoints\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"bucketing_field\"],\n    )\n\n    use_mixed_precision: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Enable automatic mixed-precision (AMP) during training.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"use_mixed_precision\"],\n    )\n\n    compile: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to compile the model before training.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"compile\"],\n    )\n\n    enable_gradient_checkpointing: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to enable gradient checkpointing, which trades compute for memory.\"\n        \"This is useful for training very deep models with limited memory.\",\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"enable_gradient_checkpointing\"],\n    )\n\n    layers_to_freeze_regex: str = schema_utils.String(\n        default=None,\n        allow_none=True,\n        description=(\n            \"Freeze specific layers based on provided regex. Freezing specific layers can improve a  \"\n            \"pretrained model's performance in a number of ways. At a basic level, freezing early layers can  \"\n            \"prevent overfitting by retaining more general features (beneficial for small datasets). Also can  \"\n            \"reduce computational resource use and lower overall training time due to less gradient calculations. \"\n        ),\n    )\n\n    def update_batch_size_grad_accum(self, num_workers: int):\n        from ludwig.utils.trainer_utils import get_rendered_batch_size_grad_accum\n\n        self.batch_size, self.gradient_accumulation_steps = get_rendered_batch_size_grad_accum(self, num_workers)\n\n\n@DeveloperAPI\n@ludwig_dataclass\nclass LLMTrainerConfig(BaseTrainerConfig):\n    \"\"\"Base class for all LLM trainer configs.\"\"\"\n\n    learning_rate: float | str = schema_utils.OneOfOptionsField(\n        default=0.0002,\n        allow_none=False,\n        description=(\n            \"Controls how much to change the model in response to the estimated error each time the model weights are \"\n            \"updated. If 'auto', the optimal learning rate is estimated by choosing the learning rate that produces \"\n            \"the smallest non-diverging gradient update.\"\n        ),\n        parameter_metadata=TRAINER_METADATA[MODEL_ECD][\"learning_rate\"],\n        field_options=[\n            schema_utils.FloatRange(default=0.001, allow_none=False, min=0, max=1),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    batch_size: int = schema_utils.PositiveInteger(\n        default=1,\n        description=\"Batch size used for training in the LLM trainer.\",\n    )\n\n    base_learning_rate: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Base learning rate used for training in the LLM trainer.\",\n    )\n\n    should_shuffle: bool = schema_utils.Boolean(\n        default=True,\n        description=\"Whether to shuffle the training data in the LLM trainer.\",\n    )\n\n    epochs: int = schema_utils.PositiveInteger(\n        default=3,\n        description=\"Number of epochs to train in the LLM trainer.\",\n    )\n\n    train_steps: int = schema_utils.PositiveInteger(\n        default=None,\n        allow_none=True,\n        description=\"Number of training steps to train in the LLM trainer.\",\n    )\n\n    eval_steps: float = schema_utils.NonNegativeInteger(\n        default=None,\n        allow_none=True,\n        description=\"The number of steps to evaluate in the LLM trainer.\",\n    )\n\n    steps_per_checkpoint: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"Number of steps per checkpoint in the LLM trainer.\",\n    )\n\n    checkpoints_per_epoch: int = schema_utils.NonNegativeInteger(\n        default=0,\n        description=\"Number of checkpoints per epoch in the LLM trainer.\",\n    )\n\n    early_stop: int = schema_utils.IntegerRange(\n        default=-1,\n        min=-1,\n        description=(\n            \"Number of consecutive rounds of evaluation without any improvement on the `validation_metric` that \"\n            \"triggers training to stop. Can be set to -1, which disables early stopping entirely.\"\n        ),\n    )\n\n    eval_batch_size: int = schema_utils.PositiveInteger(\n        default=2,\n        description=\"Batch size used for evaluation in the LLM trainer.\",\n    )\n\n    evaluate_training_set: bool = schema_utils.Boolean(\n        default=False,\n        description=\"Whether to evaluate the training set in the LLM trainer. Note: this operation may be slow.\",\n    )\n\n\n@DeveloperAPI\n@register_llm_trainer_schema(\"none\")\n@ludwig_dataclass\nclass NoneTrainerConfig(LLMTrainerConfig):\n    \"\"\"Dataclass that configures most of the hyperparameters used for zero-shot / few-shot LLM model training.\"\"\"\n\n    # Required for lookup during trainer initialization\n    type: str = schema_utils.ProtectedString(\n        \"none\",\n        description=\"The type of trainer used to train the model. \",\n        parameter_metadata=TRAINER_METADATA[MODEL_LLM][\"type\"],\n    )\n\n    def can_tune_batch_size(self) -> bool:\n        return False\n\n\n@DeveloperAPI\n@register_llm_trainer_schema(\"finetune\")\n@ludwig_dataclass\nclass FineTuneTrainerConfig(ECDTrainerConfig):\n    \"\"\"Dataclass that configures most of the hyperparameters used for fine-tuning LLM model training.\"\"\"\n\n    # Required for lookup during trainer initialization\n    type: str = schema_utils.ProtectedString(\"finetune\")\n\n    base_learning_rate: float = schema_utils.NonNegativeFloat(\n        default=0.0,\n        description=\"Base learning rate used for training in the LLM trainer.\",\n    )\n\n    batch_size: int | str | None = schema_utils.OneOfOptionsField(\n        default=1,\n        allow_none=False,\n        description=(\n            \"The number of training examples utilized in one training step of the model. If `auto`, the \"\n            \"batch size that maximized training throughput (samples / sec) will be used.\"\n        ),\n        field_options=[\n            schema_utils.PositiveInteger(default=1, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n    eval_batch_size: int | str | None = schema_utils.OneOfOptionsField(\n        default=2,\n        allow_none=True,\n        description=(\n            \"Size of batch to pass to the model for evaluation. If it is `0` or `None`, the same value of `batch_size` \"\n            \"is used. This is useful to speedup evaluation with a much bigger batch size than training, if enough \"\n            \"memory is available. If `auto`, the biggest batch size (power of 2) that can fit in memory will be used.\"\n        ),\n        field_options=[\n            schema_utils.PositiveInteger(default=2, description=\"\", allow_none=False),\n            schema_utils.StringOptions(options=[\"auto\"], default=\"auto\", allow_none=False),\n        ],\n    )\n\n\n@DeveloperAPI\ndef get_model_type_jsonschema(model_type: str = MODEL_ECD):\n    if model_type == MODEL_LLM:\n        enum = [MODEL_LLM]\n    else:\n        enum = [MODEL_ECD]\n\n    return {\n        \"type\": \"string\",\n        \"enum\": enum,\n        \"default\": MODEL_ECD,\n        \"title\": \"model_type\",\n        \"description\": \"Select the model type.\",\n    }\n\n\n@DeveloperAPI\ndef get_trainer_jsonschema(model_type: str):\n    trainer_cls = trainer_schema_registry[model_type]\n    props = schema_utils.unload_jsonschema_from_marshmallow_class(trainer_cls)[\"properties\"]\n\n    return {\n        \"type\": \"object\",\n        \"properties\": props,\n        \"title\": \"trainer_options\",\n        \"additionalProperties\": False,\n        \"description\": \"Schema for trainer determined by Model Type\",\n    }\n\n\n@DeveloperAPI\nclass ECDTrainerField(schema_utils.DictMarshmallowField):\n    def __init__(self):\n        super().__init__(ECDTrainerConfig)\n\n    def _jsonschema_type_mapping(self):\n        return get_trainer_jsonschema(MODEL_ECD)\n\n\n@DeveloperAPI\ndef get_llm_trainer_conds():\n    \"\"\"Returns a JSON schema of conditionals to validate against adapter types.\"\"\"\n    conds = []\n    for trainer in _llm_trainer_schema_registry:\n        trainer_cls = _llm_trainer_schema_registry[trainer]\n        other_props = schema_utils.unload_jsonschema_from_marshmallow_class(trainer_cls)[\"properties\"]\n        schema_utils.remove_duplicate_fields(other_props)\n        preproc_cond = schema_utils.create_cond(\n            {\"type\": trainer},\n            other_props,\n        )\n        conds.append(preproc_cond)\n    return conds\n\n\n@DeveloperAPI\ndef LLMTrainerDataclassField(default=\"none\", description=\"\"):\n    class LLMTrainerSelection(schema_utils.TypeSelection):\n        def __init__(self):\n            super().__init__(\n                registry=_llm_trainer_schema_registry,\n                default_value=default,\n                description=description,\n            )\n\n        def get_schema_from_registry(self, key: str) -> type[schema_utils.BaseMarshmallowConfig]:\n            return get_llm_trainer_cls(key)\n\n        def _jsonschema_type_mapping(self):\n            return {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"type\": {\n                        \"type\": \"string\",\n                        \"enum\": list(_llm_trainer_schema_registry.keys()),\n                        \"default\": default,\n                        \"description\": \"The type of LLM trainer to use\",\n                    },\n                },\n                \"title\": \"llm_trainer_options\",\n                \"allOf\": get_llm_trainer_conds(),\n                \"required\": [\"type\"],\n                \"description\": description,\n            }\n\n    return LLMTrainerSelection().get_default_field()\n"
  },
  {
    "path": "ludwig/schema/utils.py",
    "content": "\"\"\"Ludwig schema utilities - pydantic 2 based.\n\nThis module provides the foundation for Ludwig's declarative config system.\nAll config classes inherit from BaseMarshmallowConfig (a pydantic BaseModel)\nand use field factory functions (String, Integer, Float, etc.) that return\npydantic Field() objects.\n\"\"\"\n\nimport copy\nimport logging\nimport os\nimport warnings\nfrom abc import ABC, abstractmethod\nfrom functools import lru_cache\nfrom typing import Any\n\nimport yaml\nfrom pydantic import BaseModel, ConfigDict, Field, model_validator\nfrom pydantic import ValidationError as PydanticValidationError\nfrom pydantic.fields import FieldInfo\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ACTIVE, COLUMN, LUDWIG_SCHEMA_VALIDATION_POLICY, NAME, PROC_COLUMN, TYPE\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.modules.reduction_modules import reduce_mode_registry\nfrom ludwig.schema.metadata import COMMON_METADATA\nfrom ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata\nfrom ludwig.utils.misc_utils import scrub_creds\nfrom ludwig.utils.registry import Registry\nfrom ludwig.utils.torch_utils import activations, initializer_registry\n\n# ============================================================================\n# LudwigSchemaField - base class replacing marshmallow fields.Field\n# ============================================================================\n\n\nclass LudwigSchemaField:\n    \"\"\"Plain Python base class for Ludwig schema fields.\n\n    Replaces marshmallow fields.Field as the base for TypeSelection, DictMarshmallowField (NestedConfigField), and all\n    custom field classes. The contract (get_default_field, _jsonschema_type_mapping, _deserialize) stays identical.\n    \"\"\"\n\n    def __init__(self, **kwargs):\n        # Store all keyword arguments as attributes for backward compat\n        for k, v in kwargs.items():\n            setattr(self, k, v)\n\n    def get_default_field(self) -> FieldInfo:\n        \"\"\"Create a pydantic FieldInfo for this field.\n\n        Override in subclasses.\n        \"\"\"\n        return Field(default=None)\n\n    def _jsonschema_type_mapping(self):\n        \"\"\"Return a JSON schema dict for this field.\n\n        Override in subclasses.\n        \"\"\"\n        return None\n\n    def _deserialize(self, value, attr, data, **kwargs):\n        \"\"\"Deserialize a raw value.\n\n        Override in subclasses.\n        \"\"\"\n        return value\n\n\nlogger = logging.getLogger(__name__)\n\nRECURSION_STOP_ENUM = {\"weights_initializer\", \"bias_initializer\", \"norm_params\"}\n\n\ndef ludwig_dataclass(cls):\n    \"\"\"No-op decorator.\n\n    Config classes now inherit directly from BaseMarshmallowConfig (pydantic BaseModel).\n    \"\"\"\n    return cls\n\n\n# TODO: Change to RAISE and update descriptions once we want to enforce strict schemas.\nLUDWIG_SCHEMA_VALIDATION_POLICY_VAR = os.environ.get(LUDWIG_SCHEMA_VALIDATION_POLICY, \"exclude\").lower()\n\n\nclass _SchemaAdapter:\n    \"\"\"Adapts pydantic model to marshmallow-like Schema interface for backward compatibility.\n\n    This allows existing code that calls cls.Schema().load(data), cls.Schema().dump(data), and cls.Schema().fields to\n    continue working.\n    \"\"\"\n\n    def __init__(self, cls):\n        self._cls = cls\n\n    def __call__(self):\n        \"\"\"Allow Schema()() pattern (double-call).\"\"\"\n        return self\n\n    def load(self, data):\n        \"\"\"Validate and create a config instance from a dict.\"\"\"\n        return self._cls.model_validate(data)\n\n    def dump(self, data):\n        \"\"\"Serialize a config instance or dict to a plain dict.\"\"\"\n        if isinstance(data, BaseMarshmallowConfig):\n            return data.to_dict()\n        if isinstance(data, dict):\n            try:\n                instance = self._cls.model_validate(data)\n                return instance.to_dict()\n            except Exception:\n                return data\n        return data\n\n    @property\n    def fields(self):\n        \"\"\"Return field info dict (pydantic model_fields).\"\"\"\n        return self._cls.model_fields\n\n\n# Sentinel for TypeSelection and DictMarshmallowField metadata markers\nclass _TypeSelectionMarker:\n    \"\"\"Marker stored in Field.metadata to indicate this field uses TypeSelection dispatch.\"\"\"\n\n    def __init__(self, type_selection):\n        self.type_selection = type_selection\n\n\nclass _NestedConfigMarker:\n    \"\"\"Marker stored in Field.metadata to indicate this field uses DictMarshmallowField dispatch.\"\"\"\n\n    def __init__(self, cls, allow_none=True):\n        self.cls = cls\n        self.allow_none = allow_none\n\n\nConfigT = Any  # TypeVar(\"ConfigT\", bound=\"BaseMarshmallowConfig\")\n\n\ndef _convert_dataclass_field_to_pydantic(dc_field) -> FieldInfo:\n    \"\"\"Convert a dataclasses.Field to a pydantic FieldInfo.\n\n    This is the bridge that allows old marshmallow-style field definitions\n    (using dataclasses.field(metadata={\"marshmallow_field\": ...})) to work\n    with pydantic BaseModel classes during the migration period.\n    \"\"\"\n    import dataclasses as _dc\n\n    metadata_list = []\n    marshmallow_field = None\n\n    # Extract marshmallow_field from metadata\n    if dc_field.metadata:\n        marshmallow_field = dc_field.metadata.get(\"marshmallow_field\")\n        if marshmallow_field is not None:\n            # Store as a marker so model_validator can use it for dispatch\n            if isinstance(marshmallow_field, TypeSelection):\n                metadata_list.append(_TypeSelectionMarker(marshmallow_field))\n            elif isinstance(marshmallow_field, DictMarshmallowField):\n                # Check if the subclass overrides _jsonschema_type_mapping\n                has_custom_schema = (\n                    type(marshmallow_field)._jsonschema_type_mapping\n                    is not DictMarshmallowField._jsonschema_type_mapping\n                )\n                if has_custom_schema:\n                    # Store as MarshmallowFieldMarker to preserve custom JSON schema generation\n                    metadata_list.append(_MarshmallowFieldMarker(marshmallow_field))\n                else:\n                    metadata_list.append(_NestedConfigMarker(marshmallow_field.cls, marshmallow_field.allow_none))\n            else:\n                # Generic marshmallow field - store for reference\n                metadata_list.append(_MarshmallowFieldMarker(marshmallow_field))\n\n    # Extract default and create FieldInfo.\n    # Note: pydantic 2's Field() does not accept a `metadata` kwarg — set it on the FieldInfo after creation.\n    if dc_field.default is not _dc.MISSING:\n        fi = Field(default=dc_field.default)\n    elif dc_field.default_factory is not _dc.MISSING:\n        fi = Field(default_factory=dc_field.default_factory)\n    else:\n        # No default - this is a required field\n        fi = Field()\n    if metadata_list:\n        fi.metadata = metadata_list\n    return fi\n\n\nclass _MarshmallowFieldMarker:\n    \"\"\"Stores a marshmallow field for backward compat during migration.\"\"\"\n\n    def __init__(self, marshmallow_field):\n        self.marshmallow_field = marshmallow_field\n\n\nclass _LudwigModelMeta(type(BaseModel)):\n    \"\"\"Metaclass that bridges marshmallow-dataclass patterns to pydantic 2.\n\n    Handles two key behaviors:\n    1. Converts dataclasses.Field objects to pydantic FieldInfo in __new__\n    2. Allows class-level access to field defaults via __getattr__\n    \"\"\"\n\n    def __new__(mcs, name, bases, namespace, **kwargs):\n        import dataclasses as _dc\n\n        annotations = namespace.get(\"__annotations__\", {})\n\n        # Detect @property definitions and prevent pydantic from treating them as field defaults.\n        # Properties that don't shadow inherited fields work fine as-is because pydantic\n        # only processes annotated attributes. Properties that DO shadow inherited fields\n        # should be converted to fields with constant defaults instead (done at the schema\n        # class level, not here).\n        _saved_properties: dict[str, property] = {}\n        for attr_name, value in list(namespace.items()):\n            if isinstance(value, property) and attr_name in annotations:\n                # A property in this class's own annotations would confuse pydantic.\n                # Remove it from annotations (it won't become a field).\n                _saved_properties[attr_name] = value\n                del namespace[attr_name]\n                annotations.pop(attr_name, None)\n\n        # Convert dataclass field() objects and marshmallow field descriptors to pydantic Field()\n        for attr_name in list(annotations.keys()):\n            if attr_name in namespace:\n                value = namespace[attr_name]\n                if isinstance(value, _dc.Field):\n                    namespace[attr_name] = _convert_dataclass_field_to_pydantic(value)\n                elif isinstance(value, LudwigSchemaField) and hasattr(value, \"get_default_field\"):\n                    # TypeSelection and DictMarshmallowField instances need conversion\n                    namespace[attr_name] = value.get_default_field()\n\n        # Auto-widen annotations to bridge marshmallow→pydantic gap.\n        # In marshmallow, annotations were decorative. In pydantic, they're enforced.\n        import types\n        import typing\n\n        for attr_name, ann in list(annotations.items()):\n            # Skip ClassVar annotations\n            origin = getattr(ann, \"__origin__\", None)\n            if origin is typing.ClassVar:\n                continue\n\n            if attr_name not in namespace:\n                continue\n\n            value = namespace[attr_name]\n\n            # For fields with markers (TypeSelection/DictMarshmallowField/MarshmallowField),\n            # set annotation to Any since the actual validation happens in the marker\n            if isinstance(value, FieldInfo):\n                jse = getattr(value, \"json_schema_extra\", None)\n                has_marker = False\n                if isinstance(jse, dict) and \"metadata\" in jse:\n                    has_marker = any(\n                        isinstance(m, (_TypeSelectionMarker, _NestedConfigMarker, _MarshmallowFieldMarker))\n                        for m in jse[\"metadata\"]\n                    )\n                for meta in getattr(value, \"metadata\", None) or []:\n                    if isinstance(meta, (_TypeSelectionMarker, _NestedConfigMarker, _MarshmallowFieldMarker)):\n                        has_marker = True\n                        break\n\n                if has_marker:\n                    annotations[attr_name] = Any\n                    continue\n\n                # Widen to include None if default is None or enum contains None\n                from pydantic_core import PydanticUndefined\n\n                should_widen = value.default is None and value.default is not PydanticUndefined\n                if not should_widen:\n                    # Also widen if the enum (from allow_none=True in StringOptions etc.) contains None\n                    jse_enum = (jse or {}).get(\"enum\") if isinstance(jse, dict) else None\n                    if isinstance(jse_enum, list) and None in jse_enum:\n                        should_widen = True\n                if not should_widen:\n                    # Also widen if allow_none=True was explicitly set in the field factory\n                    if isinstance(jse, dict) and jse.get(\"allow_none\"):\n                        should_widen = True\n\n                if should_widen:\n                    is_union = origin in (types.UnionType,)\n                    try:\n                        is_union = is_union or origin is typing.Union\n                    except (AttributeError, TypeError):\n                        pass\n\n                    has_none = False\n                    if is_union:\n                        has_none = type(None) in getattr(ann, \"__args__\", ())\n\n                    if not has_none:\n                        try:\n                            annotations[attr_name] = ann | None\n                        except TypeError:\n                            pass\n\n            elif value is None:\n                # Plain None default\n                try:\n                    annotations[attr_name] = ann | None\n                except TypeError:\n                    pass\n\n        namespace[\"__annotations__\"] = annotations\n        import warnings\n\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\"ignore\", message=\"Field name .* shadows an attribute in parent\")\n            cls = super().__new__(mcs, name, bases, namespace, **kwargs)\n\n        # Restore @property descriptors that we removed from namespace.\n        if _saved_properties:\n            for pname, prop in _saved_properties.items():\n                setattr(cls, pname, prop)\n\n        return cls\n\n    def __getattr__(cls, name: str) -> Any:\n        \"\"\"Allow accessing field defaults as class attributes (e.g., cls.type).\"\"\"\n        for klass in cls.__mro__:\n            pf = vars(klass).get(\"__pydantic_fields__\")\n            if pf is not None and isinstance(pf, dict) and name in pf:\n                field_info = pf[name]\n                from pydantic_core import PydanticUndefined\n\n                if field_info.default is not PydanticUndefined:\n                    return field_info.default\n                break\n        raise AttributeError(name)\n\n\n@DeveloperAPI\nclass BaseMarshmallowConfig(BaseModel, metaclass=_LudwigModelMeta):\n    \"\"\"Base pydantic model for all Ludwig config classes.\n\n    Maintains backward-compatible API (from_dict, to_dict, Schema, etc.) while using pydantic 2 internally for\n    validation and serialization.\n    \"\"\"\n\n    model_config = ConfigDict(\n        extra=\"ignore\" if LUDWIG_SCHEMA_VALIDATION_POLICY_VAR == \"exclude\" else \"forbid\",\n        arbitrary_types_allowed=True,\n        validate_default=False,\n        revalidate_instances=\"never\",\n        populate_by_name=True,\n        strict=False,\n    )\n\n    @model_validator(mode=\"before\")\n    @classmethod\n    def _pre_validate(cls, data: Any) -> Any:\n        \"\"\"Pre-validation: log deprecation warnings, resolve TypeSelection/nested fields.\"\"\"\n        if not isinstance(data, dict):\n            return data\n\n        # Log deprecation warnings for unknown fields\n        valid_fields = set(cls.model_fields.keys())\n        for key in list(data.keys()):\n            if key not in valid_fields and key != \"type\":\n                warnings.warn(\n                    f'\"{key}\" is not a valid parameter for the \"{cls.__name__}\" schema, will be flagged '\n                    \"as an error in a future version\",\n                    DeprecationWarning,\n                )\n\n        # Resolve TypeSelection, DictMarshmallowField, and legacy marshmallow fields\n        for fname, finfo in cls.model_fields.items():\n            if fname not in data:\n                continue\n            value = data[fname]\n\n            # Get markers from both metadata and json_schema_extra\n            markers = list(finfo.metadata or [])\n            jse = finfo.json_schema_extra\n            if isinstance(jse, dict) and \"metadata\" in jse:\n                markers.extend(jse[\"metadata\"])\n\n            for meta in markers:\n                if isinstance(meta, _TypeSelectionMarker):\n                    data[fname] = meta.type_selection.resolve(value)\n                    break\n                elif isinstance(meta, _NestedConfigMarker):\n                    if isinstance(value, BaseMarshmallowConfig):\n                        break  # Already a config instance, skip re-validation\n                    if isinstance(value, dict):\n                        try:\n                            data[fname] = meta.cls.model_validate(value)\n                        except Exception as e:\n                            raise ConfigValidationError(\n                                f\"Invalid params: {value}, see `{meta.cls}` definition. Error: {e}\"\n                            )\n                    break\n                elif isinstance(meta, _MarshmallowFieldMarker):\n                    # Legacy marshmallow field - use its _deserialize for validation\n                    # Skip if value is already a config instance (avoid double-validation)\n                    if isinstance(value, BaseMarshmallowConfig):\n                        break\n                    mfield = meta.marshmallow_field\n                    if hasattr(mfield, \"_deserialize\") and value is not None:\n                        try:\n                            data[fname] = mfield._deserialize(value, fname, data)\n                        except Exception as e:\n                            # Re-raise ConfigValidationError (from __post_init__) and\n                            # from _deserialize rather than swallowing them\n                            if isinstance(e, ConfigValidationError):\n                                raise\n                            pass  # Let pydantic handle other validation errors\n                    break\n\n        return data\n\n    @model_validator(mode=\"after\")\n    def _validate_field_constraints(self):\n        \"\"\"Post-validation: enforce enum constraints stored in json_schema_extra.\"\"\"\n        for fname, finfo in type(self).model_fields.items():\n            value = getattr(self, fname, None)\n            extra = finfo.json_schema_extra\n            if not isinstance(extra, dict):\n                continue\n\n            # Validate enum constraints (from StringOptions, IntegerOptions)\n            if \"enum\" in extra and value is not None:\n                allowed = extra[\"enum\"]\n                if value not in allowed:\n                    raise ValueError(f\"Field '{fname}': value {value!r} not in allowed options {allowed}\")\n\n            # Validate float tuple range constraints\n            if \"_float_tuple_range\" in extra and value is not None:\n                spec = extra[\"_float_tuple_range\"]\n                if not isinstance(value, (tuple, list)) or len(value) != spec[\"n\"]:\n                    raise ValueError(f\"Field '{fname}': expected {spec['n']}-tuple, got {value!r}\")\n                for v in value:\n                    if spec.get(\"min\") is not None and v < spec[\"min\"]:\n                        raise ValueError(f\"Field '{fname}': value {v} below minimum {spec['min']}\")\n                    if spec.get(\"max\") is not None and v > spec[\"max\"]:\n                        raise ValueError(f\"Field '{fname}': value {v} above maximum {spec['max']}\")\n\n            # Validate embed field (int or str from options)\n            if \"_embed_options\" in extra and value is not None:\n                embed_options = extra[\"_embed_options\"]\n                if isinstance(value, str) and value not in embed_options:\n                    raise ValueError(f\"Field '{fname}': string value {value!r} not in {embed_options}\")\n                if not isinstance(value, (str, int)):\n                    raise ValueError(f\"Field '{fname}': expected str, int, or None, got {type(value).__name__}\")\n\n            # Validate initializer_or_dict field\n            if \"_initializer_options\" in extra and value is not None:\n                init_options = extra[\"_initializer_options\"]\n                if isinstance(value, str) and value not in init_options:\n                    raise ValueError(f\"Field '{fname}': initializer {value!r} not in {init_options}\")\n                if isinstance(value, dict):\n                    if \"type\" not in value:\n                        raise ValueError(f\"Field '{fname}': dict must contain 'type' key\")\n                    if value[\"type\"] not in init_options:\n                        raise ValueError(f\"Field '{fname}': initializer type {value['type']!r} not in {init_options}\")\n                if not isinstance(value, (str, dict)):\n                    raise ValueError(f\"Field '{fname}': expected str or dict, got {type(value).__name__}\")\n\n        return self\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        \"\"\"Allow setting arbitrary attributes on config instances.\n\n        Ludwig code dynamically sets attributes like saved_weights_in_checkpoint, proc_column, etc. on config objects.\n        Pydantic 2 normally rejects setting attributes not defined as fields, so we override to allow it.\n        \"\"\"\n        try:\n            super().__setattr__(name, value)\n        except ValueError:\n            # Attribute not in model fields - allow it anyway (dataclass behavior)\n            object.__setattr__(self, name, value)\n\n    def model_post_init(self, __context: Any) -> None:\n        \"\"\"Bridge: call __post_init__ if defined by subclass (dataclass convention).\"\"\"\n        super().model_post_init(__context)\n        # Check if THIS class (or a parent) defines __post_init__\n        post_init = getattr(type(self), \"__post_init__\", None)\n        if post_init is not None:\n            post_init(self)\n\n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"Get a dictionary representation of this config.\n\n        Recursively converts nested config objects and scrubs credentials.\n        \"\"\"\n        return scrub_creds(convert_submodules(vars(self)))\n\n    @classmethod\n    def from_dict(cls, d: dict[str, Any]) -> \"BaseMarshmallowConfig\":\n        \"\"\"Create a config instance from a dictionary.\"\"\"\n        return cls.model_validate(d)\n\n    @classmethod\n    @lru_cache(maxsize=None)\n    def get_valid_field_names(cls) -> set[str]:\n        \"\"\"Return the set of valid field names for this config class.\"\"\"\n        return set(cls.model_fields.keys())\n\n    @classmethod\n    @lru_cache(maxsize=None)\n    def get_class_schema(cls):\n        \"\"\"Return a schema adapter for backward compatibility.\n\n        Returns an object with .load() and .fields methods.\n        \"\"\"\n        return _SchemaAdapter(cls)\n\n    @classmethod\n    def Schema(cls):\n        \"\"\"Backward compatibility: return a schema adapter with .load(), .dump(), .fields.\"\"\"\n        return _SchemaAdapter(cls)\n\n    def __repr__(self):\n        return yaml.dump(self.to_dict(), sort_keys=False)\n\n\n@DeveloperAPI\ndef get_marshmallow_field_class_name(field_info):\n    \"\"\"Returns a human-readable string of the field class name.\n\n    For backward compat, checks both pydantic metadata and marshmallow_field.\n    \"\"\"\n    # Check for marshmallow_field in metadata (legacy)\n    if hasattr(field_info, \"metadata\"):\n        for meta in field_info.metadata or []:\n            if hasattr(meta, \"__class__\"):\n                return meta.__class__.__name__\n    # For pydantic FieldInfo, return the annotation name\n    if hasattr(field_info, \"annotation\"):\n        return str(field_info.annotation)\n    return \"Unknown\"\n\n\n@DeveloperAPI\ndef load_config(cls: type[\"BaseMarshmallowConfig\"], **kwargs) -> \"BaseMarshmallowConfig\":\n    \"\"\"Takes a config class and instantiates it with the given keyword args as parameters.\"\"\"\n    assert_is_a_marshmallow_class(cls)\n    return cls.model_validate(kwargs)\n\n\n@DeveloperAPI\ndef load_trainer_with_kwargs(model_type: str, kwargs: dict) -> tuple[\"BaseMarshmallowConfig\", dict[str, Any]]:\n    \"\"\"Special case of `load_config_with_kwargs` for the trainer schemas.\"\"\"\n    from ludwig.constants import MODEL_LLM\n    from ludwig.schema.trainer import ECDTrainerConfig, LLMTrainerConfig\n\n    if model_type == MODEL_LLM:\n        trainer_schema = LLMTrainerConfig\n    else:\n        trainer_schema = ECDTrainerConfig\n\n    return load_config_with_kwargs(trainer_schema, kwargs)\n\n\n@DeveloperAPI\ndef load_config_with_kwargs(\n    cls: type[\"BaseMarshmallowConfig\"], kwargs_overrides\n) -> tuple[\"BaseMarshmallowConfig\", dict[str, Any]]:\n    \"\"\"Instantiates a config class filtering kwargs to only valid fields.\n\n    Returns a tuple of (config, remaining_kwargs).\n    \"\"\"\n    assert_is_a_marshmallow_class(cls)\n    fields = cls.model_fields.keys()\n    return load_config(cls, **{k: v for k, v in kwargs_overrides.items() if k in fields}), {\n        k: v for k, v in kwargs_overrides.items() if k not in fields\n    }\n\n\n@DeveloperAPI\ndef convert_submodules(config_dict: dict) -> dict[str, Any]:\n    \"\"\"Helper for converting submodules to dictionaries during config serialization.\"\"\"\n    output_dict = copy.deepcopy(config_dict)\n\n    for k, v in output_dict.items():\n        if isinstance(v, dict):\n            convert_submodules(v)\n        elif isinstance(v, BaseMarshmallowConfig):\n            output_dict[k] = v.to_dict()\n            convert_submodules(output_dict[k])\n        elif isinstance(v, list):\n            output_dict[k] = [x.to_dict() if isinstance(x, BaseMarshmallowConfig) else x for x in v]\n        elif isinstance(v, ListSerializable):\n            output_dict[k] = v.to_list()\n\n    return output_dict\n\n\n@DeveloperAPI\ndef create_cond(if_pred: dict, then_pred: dict):\n    \"\"\"Returns a JSONSchema conditional for the given if-then predicates.\"\"\"\n    return {\n        \"if\": {\"properties\": {k: {\"const\": v} for k, v in if_pred.items()}},\n        \"then\": {\"properties\": then_pred},\n    }\n\n\n@DeveloperAPI\ndef remove_duplicate_fields(properties: dict, fields: list[str] | None = None) -> None:\n    \"\"\"Util function for removing duplicated schema elements.\"\"\"\n    duplicate_fields = [NAME, TYPE, COLUMN, PROC_COLUMN, ACTIVE] if fields is None else fields\n    for key in duplicate_fields:\n        if key in properties:\n            del properties[key]\n\n\n@DeveloperAPI\nclass ListSerializable(ABC):\n    @abstractmethod\n    def to_list(self) -> list:\n        pass\n\n\n@DeveloperAPI\ndef assert_is_a_marshmallow_class(cls):\n    \"\"\"Assert that cls is a Ludwig config class (pydantic BaseModel).\"\"\"\n    assert issubclass(\n        cls, BaseMarshmallowConfig\n    ), f\"Expected a Ludwig config class (BaseMarshmallowConfig subclass), but `{cls}` is not.\"\n\n\ndef _default_matches_json_type(default_val, type_str) -> bool:\n    \"\"\"Check if a default value is consistent with a JSON schema type string.\n\n    Returns True if the default value matches the type string, False otherwise. This is used to avoid emitting 'type':\n    'integer' when the default is 7.5 (float), which was a common pattern in the marshmallow era where type enforcement\n    was looser.\n    \"\"\"\n    if isinstance(type_str, list):\n        # Union type like [\"integer\", \"null\"]\n        return any(_default_matches_json_type(default_val, t) for t in type_str)\n    _CHECKS = {\n        \"string\": lambda v: isinstance(v, str),\n        \"integer\": lambda v: isinstance(v, int) and not isinstance(v, bool),\n        \"number\": lambda v: isinstance(v, (int, float)) and not isinstance(v, bool),\n        \"boolean\": lambda v: isinstance(v, bool),\n        \"object\": lambda v: isinstance(v, dict),\n        \"array\": lambda v: isinstance(v, (list, tuple)),\n        \"null\": lambda v: v is None,\n    }\n    check = _CHECKS.get(type_str)\n    if check is None:\n        return True  # Unknown type, don't block\n    return check(default_val)\n\n\ndef _field_info_to_jsonschema(fname: str, finfo: FieldInfo, annotation: type | None = None) -> dict:\n    \"\"\"Convert a pydantic FieldInfo to a JSON schema fragment.\n\n    Checks metadata markers for TypeSelection/DictMarshmallowField/legacy marshmallow fields, and falls back to type-\n    based mapping for plain fields.\n    \"\"\"\n    # Check for markers in both metadata and json_schema_extra\n    markers = list(finfo.metadata or [])\n    jse = finfo.json_schema_extra\n    if isinstance(jse, dict) and \"metadata\" in jse:\n        markers.extend(jse[\"metadata\"])\n\n    for meta in markers:\n        if isinstance(meta, _TypeSelectionMarker):\n            ts = meta.type_selection\n            custom = ts._jsonschema_type_mapping()\n            if custom is not None:\n                return custom\n            return {\"type\": \"object\"}\n\n        if isinstance(meta, _NestedConfigMarker):\n            return unload_jsonschema_from_marshmallow_class(meta.cls)\n\n        if isinstance(meta, _MarshmallowFieldMarker):\n            mf = meta.marshmallow_field\n            if hasattr(mf, \"_jsonschema_type_mapping\"):\n                custom = mf._jsonschema_type_mapping()\n                if custom is not None:\n                    return custom\n            # Handle FeatureList-style fields with inner and length constraints\n            if hasattr(mf, \"inner\") and mf.inner is not None:\n                inner_schema = {}\n                if hasattr(mf.inner, \"_jsonschema_type_mapping\"):\n                    inner_schema = mf.inner._jsonschema_type_mapping() or {}\n                result = {\"type\": \"array\", \"items\": inner_schema}\n                if hasattr(mf, \"min_length\") and mf.min_length is not None:\n                    result[\"minItems\"] = mf.min_length\n                if hasattr(mf, \"max_length\") and mf.max_length is not None:\n                    result[\"maxItems\"] = mf.max_length\n                return result\n            return {\"type\": \"object\"}\n\n    # Handle InitializerOrDict fields\n    from pydantic_core import PydanticUndefined\n\n    extra = finfo.json_schema_extra\n    if isinstance(extra, dict) and \"_initializer_options\" in extra:\n        init_options = extra[\"_initializer_options\"]\n        return {\n            \"oneOf\": [\n                {\"type\": \"string\", \"enum\": init_options},\n                {\n                    \"type\": \"object\",\n                    \"properties\": {\"type\": {\"type\": \"string\", \"enum\": init_options}},\n                    \"required\": [\"type\"],\n                    \"additionalProperties\": True,\n                },\n                {\"type\": \"null\"},\n            ],\n            \"default\": finfo.default if finfo.default is not PydanticUndefined else \"xavier_uniform\",\n            \"description\": finfo.description or \"\",\n        }\n\n    # Build schema from field info\n    schema: dict[str, Any] = {}\n\n    # Description\n    desc = finfo.description or \"\"\n    if desc:\n        schema[\"description\"] = desc\n\n    # Default value\n    from pydantic_core import PydanticUndefined\n\n    if finfo.default is not PydanticUndefined:\n        if not callable(finfo.default) and not isinstance(finfo.default, property):\n            schema[\"default\"] = finfo.default\n\n    # Enum constraint from json_schema_extra\n    extra = finfo.json_schema_extra\n    if isinstance(extra, dict):\n        if \"enum\" in extra:\n            schema[\"enum\"] = extra[\"enum\"]\n        if \"parameter_metadata\" in extra:\n            schema[\"parameter_metadata\"] = copy.deepcopy(extra[\"parameter_metadata\"])\n\n    # Always include parameter_metadata (default if not explicitly provided)\n    if \"parameter_metadata\" not in schema:\n        schema[\"parameter_metadata\"] = convert_metadata_to_json(None)\n\n    # Map type annotation to JSON schema type\n    # Only emit type if annotation and default are consistent (avoid mismatches\n    # like annotation=int but default=7.5 which was common in marshmallow era)\n    if annotation is not None:\n        type_str = _annotation_to_json_type(annotation)\n        if type_str:\n            # If the enum contains None, the JSON schema type must include \"null\"\n            enum_vals = schema.get(\"enum\")\n            if enum_vals is not None and None in enum_vals:\n                if isinstance(type_str, list):\n                    if \"null\" not in type_str:\n                        type_str = type_str + [\"null\"]\n                elif type_str != \"null\":\n                    type_str = [type_str, \"null\"]\n\n            # Check for mismatch between annotation type and default value\n            from pydantic_core import PydanticUndefined\n\n            default_val = finfo.default if finfo.default is not PydanticUndefined else None\n            if default_val is not None and not _default_matches_json_type(default_val, type_str):\n                pass  # Skip emitting type to avoid JSON schema validation failures\n            else:\n                schema[\"type\"] = type_str\n\n    # Range constraints and pattern from pydantic Field metadata\n    from annotated_types import Ge, Gt, Le, Lt\n\n    for meta in finfo.metadata or []:\n        if isinstance(meta, Ge):\n            schema[\"minimum\"] = meta.ge\n        elif isinstance(meta, Gt):\n            schema[\"exclusiveMinimum\"] = meta.gt\n        elif isinstance(meta, Le):\n            schema[\"maximum\"] = meta.le\n        elif isinstance(meta, Lt):\n            schema[\"exclusiveMaximum\"] = meta.lt\n        elif hasattr(meta, \"pattern\") and getattr(meta, \"pattern\", None) is not None:\n            schema[\"pattern\"] = meta.pattern\n\n    return schema\n\n\ndef _annotation_to_json_type(annotation) -> str | list | None:\n    \"\"\"Map a Python type annotation to a JSON schema type string.\"\"\"\n    import types\n\n    origin = getattr(annotation, \"__origin__\", None)\n\n    # Handle Python 3.10+ union types (e.g. float | None) which are instances of\n    # types.UnionType directly, without __origin__\n    if isinstance(annotation, types.UnionType):\n        args = annotation.__args__\n        has_none = type(None) in args\n        non_none = [a for a in args if a is not type(None)]\n        if len(non_none) == 1:\n            base = _annotation_to_json_type(non_none[0])\n            if has_none and base:\n                return [base, \"null\"]\n            return base\n        return None\n\n    # Also handle typing.Union\n    try:\n        import typing\n\n        if origin is typing.Union:\n            args = annotation.__args__\n            has_none = type(None) in args\n            non_none = [a for a in args if a is not type(None)]\n            if len(non_none) == 1:\n                base = _annotation_to_json_type(non_none[0])\n                if has_none and base:\n                    return [base, \"null\"]\n                return base\n            return None\n    except (AttributeError, TypeError):\n        pass\n\n    _TYPE_MAP = {\n        str: \"string\",\n        int: \"integer\",\n        float: \"number\",\n        bool: \"boolean\",\n        dict: \"object\",\n        list: \"array\",\n        tuple: \"array\",\n    }\n\n    if annotation in _TYPE_MAP:\n        return _TYPE_MAP[annotation]\n\n    return None\n\n\n@DeveloperAPI\ndef unload_jsonschema_from_marshmallow_class(mclass, additional_properties: bool = True, title: str = None) -> dict:\n    \"\"\"Get a JSON schema dict for a Ludwig config class.\n\n    Iterates over pydantic model_fields and checks metadata markers for TypeSelection, DictMarshmallowField, and legacy\n    marshmallow fields.\n    \"\"\"\n    assert_is_a_marshmallow_class(mclass)\n\n    properties = {}\n    annotations = {}\n\n    # Gather annotations from the class and its MRO\n    for klass in reversed(mclass.__mro__):\n        annotations.update(getattr(klass, \"__annotations__\", {}))\n\n    for fname, finfo in mclass.model_fields.items():\n        ann = annotations.get(fname)\n        properties[fname] = _field_info_to_jsonschema(fname, finfo, ann)\n\n    schema = {\n        \"type\": \"object\",\n        \"properties\": properties,\n        \"additionalProperties\": additional_properties,\n    }\n    if title is not None:\n        schema[\"title\"] = title\n    return schema\n\n\n# ============================================================================\n# Field Factory Functions\n# ============================================================================\n# All return pydantic Field() objects (FieldInfo) that can be used as class\n# variable defaults in BaseMarshmallowConfig subclasses.\n# ============================================================================\n\n\ndef _make_json_schema_extra(\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n    **extra,\n) -> dict | None:\n    \"\"\"Build json_schema_extra dict for Field(), returning None if empty.\"\"\"\n    result = {}\n    if parameter_metadata:\n        result[\"parameter_metadata\"] = convert_metadata_to_json(parameter_metadata)\n    result.update(extra)\n    return result or None\n\n\n@DeveloperAPI\ndef InitializerOptions(default: str = \"xavier_uniform\", description=\"\", parameter_metadata: ParameterMetadata = None):\n    \"\"\"Utility wrapper that returns a `StringOptions` field with keys from `initializer_registry`.\"\"\"\n    return StringOptions(\n        list(initializer_registry.keys()),\n        default=default,\n        allow_none=False,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\n@DeveloperAPI\ndef ActivationOptions(default: str | None = \"relu\", description=None, parameter_metadata: ParameterMetadata = None):\n    \"\"\"Utility wrapper that returns a `StringOptions` field with keys from `activations` registry.\"\"\"\n    description = description or \"Default activation function applied to the output of the fully connected layers.\"\n    parameter_metadata = parameter_metadata or COMMON_METADATA[\"activation\"]\n    return StringOptions(\n        list(activations.keys()),\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\n@DeveloperAPI\ndef ReductionOptions(default: None | str = None, description=\"\", parameter_metadata: ParameterMetadata = None):\n    \"\"\"Utility wrapper that returns a `StringOptions` field with keys from `reduce_mode_registry`.\"\"\"\n    return StringOptions(\n        list(reduce_mode_registry.keys()),\n        default=default,\n        allow_none=True,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\n@DeveloperAPI\ndef RegularizerOptions(\n    default: None | str,\n    allow_none: bool = False,\n    description=\"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Utility wrapper that returns a `StringOptions` field with prefilled regularizer options.\"\"\"\n    return StringOptions(\n        [\"l1\", \"l2\", \"l1_l2\"],\n        default=default,\n        allow_none=allow_none,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\n@DeveloperAPI\ndef String(\n    description: str,\n    default: None | str,\n    allow_none: bool = False,\n    pattern: str = None,\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for string values.\"\"\"\n    if not allow_none and default is not None and not isinstance(default, str):\n        raise ValueError(f\"Provided default `{default}` should be a string!\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    kwargs = {}\n    if pattern is not None:\n        kwargs[\"pattern\"] = pattern\n\n    return Field(\n        default=default,\n        description=description,\n        json_schema_extra=json_extra,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef StringOptions(\n    options: list[str],\n    default: None | str,\n    allow_none: bool = False,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field that enforces string inputs must be one of `options`.\"\"\"\n    options = list(options)  # ensure list, not dict_keys or other iterable\n    assert len(options) > 0, \"Must provide non-empty list of options!\"\n\n    if default is not None:\n        assert isinstance(default, str), f\"Provided default `{default}` should be a string!\"\n\n    if allow_none and None not in options:\n        options = options + [None]\n    if not allow_none and None in options:\n        options = [o for o in options if o is not None]\n\n    assert len(options) == len(\n        {o for o in options if o is not None} | ({None} if None in options else set())\n    ), f\"Provided options must be unique! See: {options}\"\n    assert default in options, f\"Provided default `{default}` is not one of allowed options: {options}\"\n\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        enum=options,\n    )\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef ProtectedString(\n    pstring: str,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Alias for a `StringOptions` field with only one option.\"\"\"\n    return StringOptions(\n        options=[pstring],\n        default=pstring,\n        allow_none=False,\n        description=description,\n        parameter_metadata=parameter_metadata,\n    )\n\n\n@DeveloperAPI\ndef IntegerOptions(\n    options: list[int],\n    default: None | int,\n    allow_none: bool = False,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field that enforces integer inputs must be one of `options`.\"\"\"\n    if len(options) <= 0:\n        raise ValueError(\"Must provide non-empty list of options!\")\n    if default is not None and not isinstance(default, int):\n        raise ValueError(f\"Provided default `{default}` should be an int!\")\n    if allow_none and None not in options:\n        options = list(options) + [None]\n    if not allow_none and None in options:\n        options = [o for o in options if o is not None]\n    if default not in options:\n        raise ValueError(f\"Provided default `{default}` is not one of allowed options: {options}\")\n\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        enum=options,\n    )\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef Boolean(default: bool, description: str = \"\", parameter_metadata: ParameterMetadata = None):\n    \"\"\"Returns a pydantic Field for boolean values.\"\"\"\n    if default is not None and not isinstance(default, bool):\n        raise ValueError(f\"Invalid default: `{default}`\")\n\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata)\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef Integer(\n    default: None | int,\n    allow_none=False,\n    description=\"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field strictly enforcing integer inputs.\"\"\"\n    if default is not None and not isinstance(default, int):\n        raise ValueError(f\"Invalid default: `{default}`\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef PositiveInteger(\n    description: str,\n    default: None | int,\n    allow_none: bool = False,\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field enforcing positive integer inputs (>= 1).\"\"\"\n    if default is not None:\n        if not isinstance(default, int) or default < 1:\n            raise ValueError(f\"Invalid default: `{default}`\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, ge=1, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef NonNegativeInteger(\n    description: str,\n    default: None | int,\n    allow_none: bool = False,\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field enforcing nonnegative integer inputs (>= 0).\"\"\"\n    if default is not None:\n        if not isinstance(default, int) or default < 0:\n            raise ValueError(f\"Invalid default: `{default}`\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, ge=0, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef IntegerRange(\n    description: str,\n    default: None | int,\n    allow_none: bool = False,\n    parameter_metadata: ParameterMetadata = None,\n    min: int = None,\n    max: int = None,\n    min_inclusive: bool = True,\n    max_inclusive: bool = True,\n):\n    \"\"\"Returns a pydantic Field enforcing integer inputs within a range.\"\"\"\n    if default is not None:\n        if not isinstance(default, int):\n            raise ValueError(f\"Invalid default: `{default}`\")\n        if min is not None and ((min_inclusive and default < min) or (not min_inclusive and default <= min)):\n            raise ValueError(f\"Invalid default: `{default}` (below min {min})\")\n        if max is not None and ((max_inclusive and default > max) or (not max_inclusive and default >= max)):\n            raise ValueError(f\"Invalid default: `{default}` (above max {max})\")\n\n    kwargs = {}\n    if min is not None:\n        kwargs[\"ge\" if min_inclusive else \"gt\"] = min\n    if max is not None:\n        kwargs[\"le\" if max_inclusive else \"lt\"] = max\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs)\n\n\n@DeveloperAPI\ndef Float(\n    default: None | float | int,\n    allow_none=False,\n    description=\"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for float inputs.\"\"\"\n    if default is not None and not isinstance(default, (float, int)):\n        raise ValueError(f\"Invalid default: `{default}`\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef NonNegativeFloat(\n    default: None | float,\n    allow_none: bool = False,\n    description: str = \"\",\n    max: float | None = None,\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field enforcing nonnegative float inputs.\"\"\"\n    if default is not None:\n        if not isinstance(default, (float, int)) or default < 0:\n            raise ValueError(f\"Invalid default: `{default}`\")\n        if max is not None and default > max:\n            raise ValueError(f\"Invalid default: `{default}` (above max {max})\")\n\n    kwargs = {\"ge\": 0.0}\n    if max is not None:\n        kwargs[\"le\"] = max\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs)\n\n\n@DeveloperAPI\ndef FloatRange(\n    default: None | float,\n    allow_none: bool = False,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n    min: int = None,\n    max: int = None,\n    min_inclusive: bool = True,\n    max_inclusive: bool = True,\n):\n    \"\"\"Returns a pydantic Field enforcing float inputs within a range.\"\"\"\n    if default is not None:\n        if not isinstance(default, (float, int)):\n            raise ValueError(f\"Invalid default: `{default}`\")\n\n    kwargs = {}\n    if min is not None:\n        kwargs[\"ge\" if min_inclusive else \"gt\"] = min\n    if max is not None:\n        kwargs[\"le\" if max_inclusive else \"lt\"] = max\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata, **extra_kwargs)\n    return Field(default=default, description=description, json_schema_extra=json_extra, **kwargs)\n\n\n@DeveloperAPI\ndef Dict(\n    default: None | dict = None,\n    allow_none: bool = True,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for dict values.\"\"\"\n    allow_none = allow_none or default is None\n\n    if default is not None:\n        if not isinstance(default, dict):\n            raise ValueError(f\"Invalid default: `{default}`\")\n        if not all(isinstance(k, str) for k in default.keys()):\n            raise ValueError(f\"Invalid default: `{default}` (non-string keys)\")\n    elif not allow_none:\n        default = {}\n\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata)\n\n    if default is None:\n        return Field(default=None, description=description, json_schema_extra=json_extra)\n    return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef List(\n    list_type: type[str] | type[int] | type[float] | type[list] = str,\n    inner_type: type[str] | type[int] | type[float] | type[dict] = float,\n    default: None | list[Any] = None,\n    allow_none: bool = True,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for list values.\"\"\"\n    if default is not None:\n        if not isinstance(default, list):\n            raise ValueError(f\"Invalid default: `{default}`\")\n    elif not allow_none:\n        default = []\n\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata)\n\n    if default is None:\n        return Field(default=None, description=description, json_schema_extra=json_extra)\n    return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef DictList(\n    default: None | list[dict] = None,\n    allow_none: bool = True,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for list-of-dicts values.\"\"\"\n    if default is not None:\n        if not isinstance(default, list) or not all(isinstance(d, dict) for d in default):\n            raise ValueError(f\"Invalid default: `{default}`\")\n    elif not allow_none:\n        default = []\n\n    json_extra = _make_json_schema_extra(description=description, parameter_metadata=parameter_metadata)\n\n    if default is None:\n        return Field(default=None, description=description, json_schema_extra=json_extra)\n    return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef Embed(description: str = \"\", parameter_metadata: ParameterMetadata = None):\n    \"\"\"Returns a pydantic Field for embedding input feature names (int, str, or None).\"\"\"\n    _embed_options = [\"add\"]\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        _embed_options=_embed_options,\n    )\n    return Field(default=None, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef InitializerOrDict(\n    default: str = \"xavier_uniform\", description: str = \"\", parameter_metadata: ParameterMetadata = None\n):\n    \"\"\"Returns a pydantic Field allowing str or dict initializer values.\"\"\"\n    initializers = list(initializer_registry.keys())\n    if not isinstance(default, str) or default not in initializers:\n        raise ValueError(f\"Invalid default: `{default}`\")\n\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        _initializer_options=initializers,\n    )\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef FloatRangeTupleDataclassField(\n    n: int = 2,\n    default: tuple | None = (0.9, 0.999),\n    allow_none: bool = False,\n    min: int | None = 0,\n    max: int | None = 1,\n    description: str = \"\",\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field for an N-dim tuple with values in a range.\"\"\"\n    if default is not None:\n        if n != len(default):\n            raise ValueError(f\"Dimension of tuple '{n}' must match dimension of default val. '{default}'\")\n        for v in default:\n            if min is not None and v < min:\n                raise ValueError(f\"Invalid default: value {v} below minimum {min}\")\n            if max is not None and v > max:\n                raise ValueError(f\"Invalid default: value {v} above maximum {max}\")\n    if default is None and not allow_none:\n        raise ValueError(\"Default value must not be None if allow_none is False\")\n\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        _float_tuple_range={\"n\": n, \"min\": min, \"max\": max},\n        **extra_kwargs,\n    )\n    return Field(default=default, description=description, json_schema_extra=json_extra)\n\n\n@DeveloperAPI\ndef OneOfOptionsField(\n    default: Any,\n    description: str,\n    field_options: list,\n    allow_none: bool = False,\n    parameter_metadata: ParameterMetadata = None,\n):\n    \"\"\"Returns a pydantic Field that accepts values matching any of the field_options.\n\n    Pydantic union validation handles the multi-type dispatch. The field_options are stored in json_schema_extra for\n    JSON schema generation.\n    \"\"\"\n    extra_kwargs = {}\n    if allow_none:\n        extra_kwargs[\"allow_none\"] = True\n    json_extra = _make_json_schema_extra(\n        description=description,\n        parameter_metadata=parameter_metadata,\n        _oneof_options=True,\n        **extra_kwargs,\n    )\n\n    if default is None or isinstance(default, (int, str, bool)):\n        return Field(default=default, description=description, json_schema_extra=json_extra)\n    return Field(default_factory=lambda: copy.deepcopy(default), description=description, json_schema_extra=json_extra)\n\n\n# ============================================================================\n# TypeSelection - Polymorphic config dispatch based on registry\n# ============================================================================\n\n\nclass TypeSelection(LudwigSchemaField):\n    \"\"\"Resolves polymorphic config types from a registry based on a key field.\n\n    Used for fields like encoder, decoder, optimizer where the config class depends on a \"type\" key in the dict value.\n    \"\"\"\n\n    def __init__(\n        self,\n        registry: Registry,\n        default_value: str | None = None,\n        key: str = \"type\",\n        description: str = \"\",\n        parameter_metadata: ParameterMetadata = None,\n        allow_str_value: bool = False,\n        allow_none: bool = False,\n        **kwargs,\n    ):\n        self.registry = registry\n        self.default_value = default_value\n        self.key = key\n        self.allow_str_value = allow_str_value\n        self.allow_none = allow_none\n        self.description = description\n        self.parameter_metadata = parameter_metadata\n\n    def _deserialize(self, value, attr, data, **kwargs):\n        \"\"\"Marshmallow deserialization - delegates to resolve().\"\"\"\n        return self.resolve(value)\n\n    def resolve(self, value):\n        \"\"\"Resolve a raw value (dict, str, None) to a config instance.\"\"\"\n        if value is None:\n            if self.allow_none:\n                return None\n            return None\n\n        # Already a config instance\n        if isinstance(value, BaseMarshmallowConfig):\n            return value\n\n        if self.allow_str_value and isinstance(value, str):\n            value = self.str_value_to_object(value)\n\n        if isinstance(value, dict):\n            cls_type = value.get(self.key)\n            cls_type = cls_type.lower() if cls_type else self.default_value\n            if cls_type and cls_type in self.registry:\n                cls = self.get_schema_from_registry(cls_type)\n                try:\n                    return cls.model_validate(value)\n                except (TypeError, PydanticValidationError) as e:\n                    raise ConfigValidationError(f\"Invalid params: {value}, see `{cls}` definition\") from e\n            raise ConfigValidationError(f\"Invalid type: '{cls_type}', expected one of: {list(self.registry.keys())}\")\n\n        maybe_str = \", `str`,\" if self.allow_str_value else \"\"\n        raise ConfigValidationError(f\"Invalid param {value}, expected `None`{maybe_str} or `dict`\")\n\n    def str_value_to_object(self, value: str) -> dict:\n        \"\"\"Convert a string shorthand to a dict with the type key.\"\"\"\n        return {self.key: value}\n\n    def get_schema_from_registry(self, key: str) -> type[BaseMarshmallowConfig]:\n        \"\"\"Look up a config class from the registry.\"\"\"\n        return self.registry[key]\n\n    def get_default_field(self) -> FieldInfo:\n        \"\"\"Create a pydantic Field wrapping this TypeSelection.\n\n        The TypeSelection instance is stored in Field.metadata so the base class's model_validator can use it for\n        dispatch.\n        \"\"\"\n        if self.default_value is not None:\n            cls = self.get_schema_from_registry(self.default_value.lower())\n            key = self.key\n            dv = self.default_value\n\n            def default_factory(cls=cls, key=key, dv=dv):\n                return cls.model_validate({key: dv})\n\n        else:\n\n            def default_factory():\n                return None\n\n        fi = Field(default_factory=default_factory)\n        fi.metadata = [_TypeSelectionMarker(self)]\n        return fi\n\n    def _jsonschema_type_mapping(self):\n        \"\"\"Override in subclass for custom JSON schema.\"\"\"\n        return None\n\n\n@DeveloperAPI\nclass DictMarshmallowField(LudwigSchemaField):\n    \"\"\"Validates a dict as a specific config class (non-polymorphic).\n\n    Used for fields where a dict should be deserialized into a fixed config class.\n    \"\"\"\n\n    def __init__(\n        self,\n        cls: type[BaseMarshmallowConfig],\n        allow_none: bool = True,\n        default_missing: bool = False,\n        description: str = \"\",\n        **kwargs,\n    ):\n        self.cls = cls\n        self.allow_none = allow_none\n        self.default_missing = default_missing\n        self.description = description\n\n    def _deserialize(self, value, attr, data, **kwargs):\n        \"\"\"Deserialize a dict to a config instance via pydantic model_validate.\"\"\"\n        if value is None:\n            return value\n        if isinstance(value, dict):\n            try:\n                return self.cls.model_validate(value)\n            except (TypeError, PydanticValidationError) as e:\n                raise ConfigValidationError(f\"Invalid params: {value}, see `{self.cls}` definition\") from e\n        raise ConfigValidationError(\"Field should be None or dict\")\n\n    def get_default_field(self) -> FieldInfo:\n        \"\"\"Create a pydantic Field wrapping this DictMarshmallowField.\"\"\"\n        if not self.default_missing:\n            cls = self.cls\n\n            def default_factory(cls=cls):\n                return cls.model_validate({})\n\n        else:\n\n            def default_factory():\n                return None\n\n        # Check if subclass overrides _jsonschema_type_mapping - if so, use\n        # MarshmallowFieldMarker to preserve custom JSON schema generation\n        has_custom_schema = type(self)._jsonschema_type_mapping is not DictMarshmallowField._jsonschema_type_mapping\n        if has_custom_schema:\n            marker = _MarshmallowFieldMarker(self)\n        else:\n            marker = _NestedConfigMarker(self.cls, self.allow_none)\n\n        fi = Field(default_factory=default_factory)\n        fi.metadata = [marker]\n        return fi\n\n    def _jsonschema_type_mapping(self):\n        return unload_jsonschema_from_marshmallow_class(self.cls)\n\n\n# Backward compatibility aliases\nValidationError = ConfigValidationError\nNestedConfigField = DictMarshmallowField\nLudwigConfig = BaseMarshmallowConfig\nunload_jsonschema_from_config_class = unload_jsonschema_from_marshmallow_class\n"
  },
  {
    "path": "ludwig/serve.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport io\nimport json\nimport logging\nimport os\nimport sys\nimport tempfile\n\nimport pandas as pd\nimport torch\nfrom torchvision.io import decode_image\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import AUDIO, COLUMN\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig\nfrom ludwig.utils.server_utils import NumpyJSONResponse\n\nlogger = logging.getLogger(__name__)\n\ntry:\n    import uvicorn\n    from fastapi import FastAPI\n    from starlette.datastructures import UploadFile\n    from starlette.middleware import Middleware\n    from starlette.middleware.cors import CORSMiddleware\n    from starlette.requests import Request\nexcept ImportError as e:\n    logger.error(e)\n    logger.error(\n        \" fastapi and other serving dependencies cannot be loaded\"\n        \"and may have not been installed. \"\n        \"In order to install all serving dependencies run \"\n        \"pip install ludwig[serve]\"\n    )\n    sys.exit(-1)\n\nALL_FEATURES_PRESENT_ERROR = {\"error\": \"entry must contain all input features\"}\n\nCOULD_NOT_RUN_INFERENCE_ERROR = {\"error\": \"Unexpected Error: could not run inference on model\"}\n\n\ndef server(model, allowed_origins=None):\n    middleware = [Middleware(CORSMiddleware, allow_origins=allowed_origins)] if allowed_origins else None\n    app = FastAPI(middleware=middleware)\n\n    config = model.config\n    input_features = {f[COLUMN] for f in config[\"input_features\"]}\n\n    @app.get(\"/\")\n    def check_health():\n        return NumpyJSONResponse({\"message\": \"Ludwig server is up\"})\n\n    @app.post(\"/predict\")\n    async def predict(request: Request):\n        try:\n            form = await request.form()\n            entry, files = convert_input(form, model.model.input_features)\n        except Exception:\n            logger.exception(\"Failed to parse predict form\")\n            return NumpyJSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500)\n\n        try:\n            if (entry.keys() & input_features) != input_features:\n                missing_features = set(input_features) - set(entry.keys())\n                return NumpyJSONResponse(\n                    {\n                        \"error\": \"Data received does not contain all input features. \"\n                        f\"Missing features: {missing_features}.\"\n                    },\n                    status_code=400,\n                )\n            try:\n                resp, _ = model.predict(dataset=[entry], data_format=dict)\n                resp = resp.to_dict(\"records\")[0]\n                return NumpyJSONResponse(resp)\n            except Exception as exc:\n                logger.exception(f\"Failed to run predict: {exc}\")\n                return NumpyJSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500)\n        finally:\n            for f in files:\n                os.remove(f.name)\n\n    @app.post(\"/batch_predict\")\n    async def batch_predict(request: Request):\n        try:\n            form = await request.form()\n            data, files = convert_batch_input(form, model.model.input_features)\n            data_df = pd.DataFrame.from_records(data[\"data\"], index=data.get(\"index\"), columns=data[\"columns\"])\n        except Exception:\n            logger.exception(\"Failed to parse batch_predict form\")\n            return NumpyJSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500)\n\n        if (set(data_df.columns) & input_features) != input_features:\n            missing_features = set(input_features) - set(data_df.columns)\n            return NumpyJSONResponse(\n                {\n                    \"error\": \"Data received does not contain all input features. \"\n                    f\"Missing features: {missing_features}.\"\n                },\n                status_code=400,\n            )\n        try:\n            resp, _ = model.predict(dataset=data_df)\n            resp = resp.to_dict(\"split\")\n            return NumpyJSONResponse(resp)\n        except Exception:\n            logger.exception(\"Failed to run batch_predict: {}\")\n            return NumpyJSONResponse(COULD_NOT_RUN_INFERENCE_ERROR, status_code=500)\n\n    return app\n\n\ndef _write_file(v, files):\n    # Convert UploadFile to a NamedTemporaryFile to ensure it's on the disk\n    suffix = os.path.splitext(v.filename)[1]\n    named_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)\n    files.append(named_file)\n    named_file.write(v.file.read())\n    named_file.close()\n    return named_file.name\n\n\ndef _read_image_buffer(v):\n    # read bytes sent via REST API and convert to image tensor\n    # in [channels, height, width] format\n    byte_string = io.BytesIO(v.file.read()).read()\n    image = decode_image(torch.frombuffer(byte_string, dtype=torch.uint8))\n    return image  # channels, height, width\n\n\ndef convert_input(form, input_features):\n    \"\"\"Returns a new input and a list of files to be cleaned up.\"\"\"\n    new_input = {}\n    files = []\n    for k, v in form.multi_items():\n        if isinstance(v, UploadFile):\n            # check if audio or image file\n            if input_features.get(k).type() == AUDIO:\n                new_input[k] = _write_file(v, files)\n            else:\n                new_input[k] = _read_image_buffer(v)\n        else:\n            new_input[k] = v\n\n    return new_input, files\n\n\ndef convert_batch_input(form, input_features):\n    \"\"\"Returns a new input and a list of files to be cleaned up.\"\"\"\n    file_index = {}\n    files = []\n    for k, v in form.multi_items():\n        if isinstance(v, UploadFile):\n            file_index[v.filename] = v\n\n    data = json.loads(form[\"dataset\"])\n    for row in data[\"data\"]:\n        for i, value in enumerate(row):\n            if value in file_index:\n                feature_name = data[\"columns\"][i]\n                if input_features.get(feature_name).type() == AUDIO:\n                    row[i] = _write_file(file_index[value], files)\n                else:\n                    row[i] = _read_image_buffer(file_index[value])\n\n    return data, files\n\n\ndef run_server(\n    model_path: str,\n    host: str,\n    port: int,\n    allowed_origins: list,\n) -> None:\n    \"\"\"Loads a pre-trained model and serve it on an http server.\n\n    # Inputs\n\n    :param model_path: (str) filepath to pre-trained model.\n    :param host: (str, default: `0.0.0.0`) host ip address for the server to use.\n    :param port: (int, default: `8000`) port number for the server to use.\n    :param allowed_origins: (list) list of origins allowed to make cross-origin requests.\n\n    # Return\n\n    :return: (`None`)\n    \"\"\"\n    # Use local backend for serving to use pandas DataFrames.\n    model = LudwigModel.load(model_path, backend=\"local\")\n    app = server(model, allowed_origins)\n    uvicorn.run(app, host=host, port=port)\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script serves a pretrained model\", prog=\"ludwig serve\", usage=\"%(prog)s [options]\"\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    parser.add_argument(\"-m\", \"--model_path\", help=\"model to load\", required=True)\n\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    # ----------------\n    # Server parameters\n    # ----------------\n    parser.add_argument(\n        \"-p\",\n        \"--port\",\n        help=\"port for server (default: 8000)\",\n        default=8000,\n        type=int,\n    )\n\n    parser.add_argument(\"-H\", \"--host\", help=\"host for server (default: 0.0.0.0)\", default=\"0.0.0.0\")\n\n    parser.add_argument(\n        \"-ao\",\n        \"--allowed_origins\",\n        nargs=\"*\",\n        help=\"A list of origins that should be permitted to make cross-origin requests. \"\n        'Use \"*\" to allow any origin. See https://www.starlette.io/middleware/#corsmiddleware.',\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"serve\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.serve\")\n\n    print_ludwig(\"Serve\", LUDWIG_VERSION)\n\n    run_server(args.model_path, args.host, args.port, args.allowed_origins)\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/train.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport logging\nimport sys\n\nimport pandas as pd\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import ALL_BACKENDS, Backend, initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import CONTINUE_PROMPT, HYPEROPT, HYPEROPT_WARNING\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.data_utils import load_config_from_str, load_yaml\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.print_utils import get_logging_level_registry, print_ludwig, query_yes_no\n\nlogger = logging.getLogger(__name__)\n\n\ndef train_cli(\n    config: str | dict = None,\n    dataset: str | dict | pd.DataFrame = None,\n    training_set: str | dict | pd.DataFrame = None,\n    validation_set: str | dict | pd.DataFrame = None,\n    test_set: str | dict | pd.DataFrame = None,\n    training_set_metadata: str | dict = None,\n    data_format: str = None,\n    experiment_name: str = \"api_experiment\",\n    model_name: str = \"run\",\n    model_load_path: str = None,\n    model_resume_path: str = None,\n    skip_save_training_description: bool = False,\n    skip_save_training_statistics: bool = False,\n    skip_save_model: bool = False,\n    skip_save_progress: bool = False,\n    skip_save_log: bool = False,\n    skip_save_processed_input: bool = False,\n    output_directory: str = \"results\",\n    gpus: str | int | list[int] = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n    callbacks: list[Callback] = None,\n    backend: Backend | str = None,\n    random_seed: int = default_random_seed,\n    logging_level: int = logging.INFO,\n    **kwargs\n) -> None:\n    \"\"\"*train* defines the entire training procedure used by Ludwig's internals. Requires most of the parameters\n    that are taken into the model. Builds a full ludwig model and performs the training.\n\n    :param config: (Union[str, dict]) in-memory representation of\n            config or string path to a YAML config file.\n    :param dataset: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing the entire dataset to be used for training.\n        If it has a split column, it will be used for splitting (0 for train,\n        1 for validation, 2 for test), otherwise the dataset will be\n        randomly split.\n    :param training_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing training data.\n    :param validation_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing validation data.\n    :param test_set: (Union[str, dict, pandas.DataFrame], default: `None`)\n        source containing test data.\n    :param training_set_metadata: (Union[str, dict], default: `None`)\n        metadata JSON file or loaded metadata.  Intermediate preprocessed\n        structure containing the mappings of the input\n        dataset created the first time an input file is used in the same\n        directory with the same name and a '.meta.json' extension.\n    :param data_format: (str, default: `None`) format to interpret data\n        sources. Will be inferred automatically if not specified.  Valid\n        formats are `'auto'`, `'csv'`, `'excel'`, `'feather'`,\n        `'fwf'`, `'hdf5'` (cache file produced during previous training),\n        `'html'` (file containing a single HTML `<table>`), `'json'`, `'jsonl'`,\n        `'parquet'`, `'pickle'` (pickled Pandas DataFrame), `'sas'`, `'spss'`,\n        `'stata'`, `'tsv'`.\n    :param experiment_name: (str, default: `'experiment'`) name for\n        the experiment.\n    :param model_name: (str, default: `'run'`) name of the model that is\n        being used.\n    :param model_load_path: (str, default: `None`) if this is specified the\n        loaded model will be used as initialization\n        (useful for transfer learning).\n    :param model_resume_path: (str, default: `None`) resumes training of\n        the model from the path specified. The config is restored.\n        In addition to config, training statistics, loss for each\n        epoch and the state of the optimizer are restored such that\n        training can be effectively continued from a previously interrupted\n        training process.\n    :param skip_save_training_description: (bool, default: `False`) disables\n        saving the description JSON file.\n    :param skip_save_training_statistics: (bool, default: `False`) disables\n        saving training statistics JSON file.\n    :param skip_save_model: (bool, default: `False`) disables\n        saving model weights and hyperparameters each time the model\n        improves. By default Ludwig saves model weights after each epoch\n        the validation metric improves, but if the model is really big\n        that can be time consuming. If you do not want to keep\n        the weights and just find out what performance a model can get\n        with a set of hyperparameters, use this parameter to skip it,\n        but the model will not be loadable later on and the returned model\n        will have the weights obtained at the end of training, instead of\n        the weights of the epoch with the best validation performance.\n    :param skip_save_progress: (bool, default: `False`) disables saving\n        progress each epoch. By default Ludwig saves weights and stats\n        after each epoch for enabling resuming of training, but if\n        the model is really big that can be time consuming and will uses\n        twice as much space, use this parameter to skip it, but training\n        cannot be resumed later on.\n    :param skip_save_log: (bool, default: `False`) disables saving\n        TensorBoard logs. By default Ludwig saves logs for the TensorBoard,\n        but if it is not needed turning it off can slightly increase the\n        overall speed.\n    :param skip_save_processed_input: (bool, default: `False`) if input\n        dataset is provided it is preprocessed and cached by saving an HDF5\n        and JSON files to avoid running the preprocessing again. If this\n        parameter is `False`, the HDF5 and JSON file are not saved.\n    :param output_directory: (str, default: `'results'`) the directory that\n        will contain the training statistics, TensorBoard logs, the saved\n        model and the training progress files.\n    :param gpus: (list, default: `None`) list of GPUs that are available\n        for training.\n    :param gpu_memory_limit: (float: default: `None`) maximum memory fraction\n        [0, 1] allowed to allocate per GPU device.\n    :param allow_parallel_threads: (bool, default: `True`) allow PyTorch\n        to use multithreading parallelism to improve performance at\n        the cost of determinism.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n        of backend to use to execute preprocessing / training steps.\n    :param random_seed: (int: default: 42) random seed used for weights\n        initialization, splits and any other random function.\n    :param logging_level: (int) Log level that will be sent to stderr.\n\n    # Return\n\n    :return: (`None`)\n    \"\"\"\n    if HYPEROPT in config:\n        if not query_yes_no(HYPEROPT_WARNING + CONTINUE_PROMPT):\n            exit(1)\n        # Stop gap: remove hyperopt from the config to prevent interference with training step sizes\n        # TODO: https://github.com/ludwig-ai/ludwig/issues/2633\n        # Need to investigate why the presence of hyperopt in the config interferes with training step sizes\n        config.pop(HYPEROPT)\n\n    if model_load_path:\n        model = LudwigModel.load(\n            model_load_path,\n            logging_level=logging_level,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n    else:\n        model = LudwigModel(\n            config=config,\n            logging_level=logging_level,\n            backend=backend,\n            gpus=gpus,\n            gpu_memory_limit=gpu_memory_limit,\n            allow_parallel_threads=allow_parallel_threads,\n            callbacks=callbacks,\n        )\n    model.train(\n        dataset=dataset,\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        training_set_metadata=training_set_metadata,\n        data_format=data_format,\n        experiment_name=experiment_name,\n        model_name=model_name,\n        model_resume_path=model_resume_path,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        output_directory=output_directory,\n        random_seed=random_seed,\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script trains a model\", prog=\"ludwig train\", usage=\"%(prog)s [options]\"\n    )\n\n    # ----------------------------\n    # Experiment naming parameters\n    # ----------------------------\n    parser.add_argument(\"--output_directory\", type=str, default=\"results\", help=\"directory that contains the results\")\n    parser.add_argument(\"--experiment_name\", type=str, default=\"experiment\", help=\"experiment name\")\n    parser.add_argument(\"--model_name\", type=str, default=\"run\", help=\"name for the model\")\n\n    # ---------------\n    # Data parameters\n    # ---------------\n    parser.add_argument(\n        \"--dataset\",\n        help=\"input data file path. \"\n        \"If it has a split column, it will be used for splitting \"\n        \"(0: train, 1: validation, 2: test), \"\n        \"otherwise the dataset will be randomly split\",\n    )\n    parser.add_argument(\"--training_set\", help=\"input train data file path\")\n    parser.add_argument(\"--validation_set\", help=\"input validation data file path\")\n    parser.add_argument(\"--test_set\", help=\"input test data file path\")\n\n    parser.add_argument(\n        \"--training_set_metadata\",\n        help=\"input metadata JSON file path. An intermediate preprocessed file \"\n        \"containing the mappings of the input file created \"\n        \"the first time a file is used, in the same directory \"\n        \"with the same name and a .json extension\",\n    )\n\n    parser.add_argument(\n        \"--data_format\",\n        help=\"format of the input data\",\n        default=\"auto\",\n        choices=[\n            \"auto\",\n            \"csv\",\n            \"excel\",\n            \"feather\",\n            \"fwf\",\n            \"hdf5\",\n            \"html\" \"tables\",\n            \"json\",\n            \"jsonl\",\n            \"parquet\",\n            \"pickle\",\n            \"sas\",\n            \"spss\",\n            \"stata\",\n            \"tsv\",\n        ],\n    )\n\n    parser.add_argument(\n        \"-sspi\",\n        \"--skip_save_processed_input\",\n        help=\"skips saving intermediate HDF5 and JSON files\",\n        action=\"store_true\",\n        default=False,\n    )\n\n    # ----------------\n    # Model parameters\n    # ----------------\n    config = parser.add_mutually_exclusive_group(required=True)\n    config.add_argument(\n        \"-c\",\n        \"--config\",\n        type=load_yaml,\n        help=\"Path to the YAML file containing the model configuration\",\n    )\n    config.add_argument(\n        \"-cs\",\n        \"--config_str\",\n        dest=\"config\",\n        type=load_config_from_str,\n        help=\"JSON or YAML serialized string of the model configuration\",\n    )\n\n    parser.add_argument(\"-mlp\", \"--model_load_path\", help=\"path of a pretrained model to load as initialization\")\n    parser.add_argument(\"-mrp\", \"--model_resume_path\", help=\"path of the model directory to resume training of\")\n    parser.add_argument(\n        \"-sstd\",\n        \"--skip_save_training_description\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving the description JSON file\",\n    )\n    parser.add_argument(\n        \"-ssts\",\n        \"--skip_save_training_statistics\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving training statistics JSON file\",\n    )\n    parser.add_argument(\n        \"-ssm\",\n        \"--skip_save_model\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving weights each time the model improves. \"\n        \"By default Ludwig saves  weights after each epoch \"\n        \"the validation metric (improves, but  if the model is really big \"\n        \"that can be time consuming. If you do not want to keep \"\n        \"the weights and just find out what performance a model can get \"\n        \"with a set of hyperparameters, use this parameter to skip it\",\n    )\n    parser.add_argument(\n        \"-ssp\",\n        \"--skip_save_progress\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving weights after each epoch. By default ludwig saves \"\n        \"weights after each epoch for enabling resuming of training, but \"\n        \"if the model is really big that can be time consuming and will \"\n        \"save twice as much space, use this parameter to skip it\",\n    )\n    parser.add_argument(\n        \"-ssl\",\n        \"--skip_save_log\",\n        action=\"store_true\",\n        default=False,\n        help=\"disables saving TensorBoard logs. By default Ludwig saves \"\n        \"logs for the TensorBoard, but if it is not needed turning it off \"\n        \"can slightly increase the overall speed\",\n    )\n\n    # ------------------\n    # Runtime parameters\n    # ------------------\n    parser.add_argument(\n        \"-rs\",\n        \"--random_seed\",\n        type=int,\n        default=42,\n        help=\"a random seed that is going to be used anywhere there is a call \"\n        \"to a random number generator: data splitting, parameter \"\n        \"initialization and training set shuffling\",\n    )\n    parser.add_argument(\"-g\", \"--gpus\", nargs=\"+\", type=int, default=None, help=\"list of gpus to use\")\n    parser.add_argument(\n        \"-gml\",\n        \"--gpu_memory_limit\",\n        type=float,\n        default=None,\n        help=\"maximum memory fraction [0, 1] allowed to allocate per GPU device\",\n    )\n    parser.add_argument(\n        \"-dpt\",\n        \"--disable_parallel_threads\",\n        action=\"store_false\",\n        dest=\"allow_parallel_threads\",\n        help=\"disable PyTorch from using multithreading for reproducibility\",\n    )\n    parser.add_argument(\n        \"-b\",\n        \"--backend\",\n        help=\"specifies backend to use for parallel / distributed execution, \" \"defaults to local execution\",\n        choices=ALL_BACKENDS,\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"train\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.train\")\n\n    args.backend = initialize_backend(args.backend or args.config.get(\"backend\"))\n    if args.backend.is_coordinator():\n        print_ludwig(\"Train\", LUDWIG_VERSION)\n\n    train_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/trainers/__init__.py",
    "content": "# register trainers\n\nimport ludwig.trainers.trainer  # noqa: F401\n\ntry:\n    import ludwig.trainers.trainer_llm  # noqa: F401\nexcept ImportError:\n    pass\n"
  },
  {
    "path": "ludwig/trainers/base.py",
    "content": "from abc import ABC, abstractmethod\n\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.schema.trainer import BaseTrainerConfig\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.defaults import default_random_seed\n\n\nclass BaseTrainer(ABC):\n    @abstractmethod\n    def train(self, training_set, validation_set=None, test_set=None, save_path=MODEL_FILE_NAME, **kwargs):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def train_online(\n        self,\n        dataset,\n    ):\n        raise NotImplementedError()\n\n    @abstractmethod\n    def tune_batch_size(\n        self,\n        config: ModelConfigDict,\n        training_set: Dataset,\n        random_seed: int = default_random_seed,\n        max_trials: int = 10,\n        halving_limit: int = 3,\n        tune_for_training: bool = True,\n    ) -> int:\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def validation_field(self):\n        raise NotImplementedError()\n\n    @property\n    @abstractmethod\n    def validation_metric(self):\n        raise NotImplementedError()\n\n    # Remote implementations may override this\n    def shutdown(self):\n        pass\n\n    @property\n    def local_rank(self) -> int:\n        return 0\n\n    def barrier(self):\n        pass\n\n    # Functions needed to treat Trainer as a context manager\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.shutdown()\n\n    @staticmethod\n    @abstractmethod\n    def get_schema_cls() -> BaseTrainerConfig:\n        raise NotImplementedError()\n"
  },
  {
    "path": "ludwig/trainers/registry.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.registry import DEFAULT_KEYS, Registry\n\n_trainers_registry = Registry()\n_ray_trainers_registry = Registry()\n\n_llm_trainers_registry = Registry()\n_llm_ray_trainers_registry = Registry()\n\n\n@DeveloperAPI\ndef get_trainers_registry() -> Registry:\n    return _trainers_registry\n\n\n@DeveloperAPI\ndef get_ray_trainers_registry() -> Registry:\n    return _ray_trainers_registry\n\n\n@DeveloperAPI\ndef get_llm_trainers_registry() -> Registry:\n    return _llm_trainers_registry\n\n\n@DeveloperAPI\ndef get_llm_ray_trainers_registry() -> Registry:\n    return _llm_ray_trainers_registry\n\n\n@DeveloperAPI\ndef register_trainer(model_type: str, default=False):\n    \"\"\"Register a trainer class that supports training the given model types.\n\n    Using default=True will make the trainer the default trainer for the model type.\n\n    Args:\n        model_type: The model_type which dictates the trainer type to use.\n        default: Whether the trainer should be the default trainer for the model type.\n    \"\"\"\n\n    def wrap(cls):\n        _trainers_registry[model_type] = cls\n        if default:\n            if DEFAULT_KEYS[0] in _trainers_registry:\n                raise ValueError(f\"Default trainer already registered for model type {model_type}\")\n            for key in DEFAULT_KEYS:\n                _trainers_registry[key] = cls\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef register_ray_trainer(model_type: str, default=False):\n    \"\"\"Register a trainer class that supports training the given model types with Ray backend.\n\n    Using default=True will make the trainer the default trainer for the model type.\n\n    Args:\n        model_type: The model_type which dictates the trainer type to use.\n        default: Whether the trainer should be the default trainer for the model type.\n    \"\"\"\n\n    def wrap(cls):\n        _ray_trainers_registry[model_type] = cls\n        if default:\n            if DEFAULT_KEYS[0] in _ray_trainers_registry:\n                raise ValueError(f\"Default trainer already registered for model type {model_type}\")\n            for key in DEFAULT_KEYS:\n                _ray_trainers_registry[key] = cls\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef register_llm_trainer(trainer_type: str, default=False):\n    \"\"\"Register a trainer class that supports training the specific type of training strategy for LLM Models.\n\n    Using default=True will make the trainer the default trainer for the LLM model type.\n\n    Args:\n        trainer_type: The trainer_type which dictates what training strategy to use.\n        default: Whether the trainer should be the default trainer for LLMs.\n    \"\"\"\n\n    def wrap(cls):\n        _llm_trainers_registry[trainer_type] = cls\n        if default:\n            if DEFAULT_KEYS[0] in _trainers_registry:\n                raise ValueError(f\"Default trainer {trainer_type} already registered for LLM\")\n            for key in DEFAULT_KEYS:\n                _llm_trainers_registry[key] = cls\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef register_llm_ray_trainer(trainer_type: str, default=False):\n    \"\"\"Register a trainer class that supports training the specific type of training strategy for LLM Models with\n    Ray backend.\n\n    Using default=True will make the trainer the default trainer for the LLM model type.\n\n    Args:\n        trainer_type: The trainer_type which dictates what training strategy to use.\n        default: Whether the trainer should be the default trainer for LLMs.\n    \"\"\"\n\n    def wrap(cls):\n        _llm_ray_trainers_registry[trainer_type] = cls\n        if default:\n            if DEFAULT_KEYS[0] in _trainers_registry:\n                raise ValueError(f\"Default ray trainer {trainer_type} already registered for LLM\")\n            for key in DEFAULT_KEYS:\n                _llm_ray_trainers_registry[key] = cls\n        return cls\n\n    return wrap\n"
  },
  {
    "path": "ludwig/trainers/trainer.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\"\"\"This module contains the class and auxiliary methods of a model.\"\"\"\n\nimport contextlib\nimport csv\nimport logging\nimport math\nimport os\nimport os.path\nimport signal\nimport sys\nimport tempfile\nimport threading\nimport time\nfrom collections.abc import Callable\n\nimport numpy as np\nimport packaging\nimport pandas as pd\nimport psutil\nimport torch\nfrom torch.utils.tensorboard import SummaryWriter\n\nfrom ludwig.constants import (\n    AUTO,\n    LOSS,\n    MAX_CPU_BATCH_SIZE,\n    MINIMIZE,\n    MODEL_ECD,\n    MODEL_LLM,\n    TEST,\n    TRAINING,\n    USED_TOKENS,\n    VALIDATION,\n)\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.distributed.base import DistributedStrategy, LocalStrategy\nfrom ludwig.globals import (\n    is_progressbar_disabled,\n    MODEL_FILE_NAME,\n    MODEL_HYPERPARAMETERS_FILE_NAME,\n    TRAINING_CHECKPOINTS_DIR_PATH,\n    TRAINING_PROGRESS_TRACKER_FILE_NAME,\n)\nfrom ludwig.models.ecd import ECD\nfrom ludwig.models.llm import LLM\nfrom ludwig.models.predictor import Predictor\nfrom ludwig.modules.lr_scheduler import LRScheduler\nfrom ludwig.modules.metric_modules import get_improved_fn, get_initial_validation_value\nfrom ludwig.modules.metric_registry import get_metric_objective\nfrom ludwig.modules.optimization_modules import create_clipper\nfrom ludwig.progress_bar import LudwigProgressBar\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.trainers.base import BaseTrainer\nfrom ludwig.trainers.registry import register_trainer\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils import time_utils\nfrom ludwig.utils.batch_size_tuner import BatchSizeEvaluator\nfrom ludwig.utils.checkpoint_utils import Checkpoint, CheckpointManager\nfrom ludwig.utils.config_utils import get_quantization\nfrom ludwig.utils.data_utils import load_json\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.fs_utils import path_exists\nfrom ludwig.utils.llm_utils import update_embedding_layer\nfrom ludwig.utils.metric_utils import get_metric_names, TrainerMetric\nfrom ludwig.utils.metrics_printed_table import print_metrics_table\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.model_utils import contains_nan_or_inf_tensors\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom ludwig.utils.trainer_utils import (\n    append_metrics,\n    freeze_layers_regex,\n    get_final_steps_per_checkpoint,\n    get_latest_metrics_dict,\n    get_new_progress_tracker,\n    get_total_expected_checkpoints,\n    get_total_steps,\n    ProgressTracker,\n)\n\nlogger = logging.getLogger(__name__)\n\n\n_TORCH210 = packaging.version.parse(torch.__version__) >= packaging.version.parse(\"2.1.0\")\n\n\n@register_trainer(MODEL_ECD, default=True)\nclass Trainer(BaseTrainer):\n    \"\"\"Trainer is a class that trains a model.\"\"\"\n\n    @staticmethod\n    def get_schema_cls():\n        return ECDTrainerConfig\n\n    def __init__(\n        self,\n        config: ECDTrainerConfig,\n        model: ECD,\n        resume: float = False,\n        skip_save_model: bool = False,\n        skip_save_progress: bool = False,\n        skip_save_log: bool = False,\n        callbacks: list = None,\n        report_tqdm_to_ray=False,\n        random_seed: float = default_random_seed,\n        distributed: DistributedStrategy | None = None,\n        device: str | None = None,\n        **kwargs,\n    ):\n        \"\"\"Trains a model with a set of options and hyperparameters listed below. Customizable.\n\n        :param model: Underlying Ludwig model\n        :type model: `ludwig.models.ecd.ECD`\n        :param resume: Resume training a model that was being trained. (default: False).\n        :type resume: Boolean\n        :param skip_save_model: Disables saving model weights and hyperparameters each time the model improves. By\n                default Ludwig saves model weights after each round of evaluation the validation metric (improves, but\n                if the model is really big that can be time consuming. If you do not want to keep the weights and just\n                find out what performance a model can get with a set of hyperparameters, use this parameter to skip it,\n                but the model will not be loadable later on. (default: False).\n        :type skip_save_model: Boolean\n        :param skip_save_progress: Disables saving progress each round of evaluation. By default Ludwig saves weights\n                and stats after each round of evaluation for enabling resuming of training, but if the model is really\n                big that can be time consuming and will uses twice as much space, use this parameter to skip it, but\n                training cannot be resumed later on. (default: False).\n        :type skip_save_progress: Boolean\n        :param skip_save_log: Disables saving TensorBoard logs. By default Ludwig saves logs for the TensorBoard, but if\n                it is not needed turning it off can slightly increase the overall speed. (default: False).\n        :type skip_save_log: Boolean\n        :param callbacks: List of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline.\n                (default: None).\n        :type callbacks: list\n        :param report_tqdm_to_ray: Enables using the ray based tqdm Callback for progress bar reporting\n        :param random_seed: Default initialization for the random seeds (default: 42).\n        :type random_seed: Float\n        :param distributed: Distributed strategy (default: None).\n        :type distributed: `DistributedStrategy`\n        :param device: Device to load the model on from a saved checkpoint (default: None).\n        :type device: str\n        :param config: `ludwig.schema.trainer.BaseTrainerConfig` instance that specifies training hyperparameters\n                (default: `ludwig.schema.trainer.ECDTrainerConfig()`).\n        \"\"\"\n        self.distributed = distributed if distributed is not None else LocalStrategy()\n\n        self.epochs = config.epochs\n        self.train_steps = config.train_steps\n        self.enable_profiling = config.enable_profiling\n        self.steps_per_epoch = 0  # Computed during training, after batcher has been initialized.\n        self.total_steps = 0  # Computed during training, after batcher has been initialized.\n        self.total_expected_checkpoints = 0  # Computed during training, after batcher has been initialized.\n\n        self.regularization_lambda = config.regularization_lambda\n        self.regularization_type = config.regularization_type\n        self.batch_size = config.batch_size\n        self.effective_batch_size = config.effective_batch_size\n        self.max_batch_size = config.max_batch_size\n        self.eval_batch_size = config.batch_size if config.eval_batch_size is None else config.eval_batch_size\n        self.should_shuffle = config.should_shuffle\n        self._validation_field = config.validation_field\n        self._validation_metric = config.validation_metric\n        self.early_stop = config.early_stop\n        self.layers_to_freeze_regex = config.layers_to_freeze_regex\n        self.steps_per_checkpoint = config.steps_per_checkpoint\n        self.checkpoints_per_epoch = config.checkpoints_per_epoch\n        self.evaluate_training_set = config.evaluate_training_set\n        self.skip_all_evaluation = config.skip_all_evaluation\n        self.increase_batch_size_on_plateau = config.increase_batch_size_on_plateau\n        self.increase_batch_size_on_plateau_patience = config.increase_batch_size_on_plateau_patience\n        self.increase_batch_size_on_plateau_rate = config.increase_batch_size_on_plateau_rate\n        self.increase_batch_size_eval_metric = config.increase_batch_size_eval_metric\n        self.increase_batch_size_eval_split = config.increase_batch_size_eval_split\n        self.gradient_accumulation_steps = (\n            config.gradient_accumulation_steps\n            if self.distributed.allow_gradient_accumulation() and config.gradient_accumulation_steps != AUTO\n            else 1\n        )\n        self.resume = resume\n        self.skip_save_model = skip_save_model\n        self.skip_save_progress = skip_save_progress\n        self.skip_save_log = skip_save_log\n        self.random_seed = random_seed\n        self.received_sigint = False\n        self.report_tqdm_to_ray = report_tqdm_to_ray\n        self.callbacks = callbacks or []\n        self.device = device\n        if self.device is None:\n            self.device = get_torch_device()\n\n        self.model = model\n        self.model.prepare_for_training()\n        self.model = self.distributed.to_device(self.model)\n        self.model.metrics_to_device(self.device)\n\n        self.compiled_model = self.model\n        if config.compile:\n            self.compiled_model = torch.compile(self.model)\n            logger.info(\"Training with torchdynamo compiled model\")\n\n        # ================ Optimizer tuning ================\n        self.gradient_clipping_config = create_clipper(config.gradient_clipping)\n\n        self.config = config\n\n        self.base_learning_rate = None\n        self.dist_model = None\n        self.optimizer = None\n        self.scheduler = None\n\n        self.prepare()\n\n        # Setup for automatic mixed precision (AMP)\n        self.use_amp = config.use_mixed_precision and self.distributed.allow_mixed_precision()\n        if self.use_amp:\n            if torch.cuda.is_available():\n                logger.info(\"Enabling automatic mixed precision (AMP)\")\n            else:\n                logger.info(\"`trainer.use_mixed_precision=True`, but no GPU device found. Setting to `False`\")\n                self.use_amp = False\n        self.scaler = torch.amp.GradScaler(\"cuda\") if self.use_amp else None\n\n        # when training starts the sigint handler will be replaced with\n        # set_steps_to_1_or_quit so this is needed to remember\n        # the original sigint to restore at the end of training\n        # and before set_steps_to_1_or_quit returns\n        self.original_sigint_handler = None\n\n    def prepare(self):\n        base_learning_rate = self.config.learning_rate\n        if self.distributed:\n            lr_scale_fn = learning_rate_scale_fns[self.config.learning_rate_scaling]\n            base_learning_rate *= lr_scale_fn(self.distributed.size())\n        self.base_learning_rate = base_learning_rate\n\n        # Given that regex is supplied, freeze layers\n        if self.config.layers_to_freeze_regex:\n            freeze_layers_regex(self.config, self.model)\n\n        # We may need to replace the embedding layer when using 8-bit optimizers from bitsandbytes.\n        update_embedding_layer(self.compiled_model, self.config)\n\n        # Register any post forward hooks for the model\n        self.compiled_model._activate_forward_hooks()\n\n        # Enable gradient checkpointing if configured\n        if self.config.enable_gradient_checkpointing:\n            # TODO(Arnav): Add support for gradient checkpointing in the compiled model\n            # when the model is an ECD model using torch.utils.checkpoint (torch.utils.checkpoint.sequential())\n            if not isinstance(self.compiled_model, LLM):\n                logger.warning(\"Gradient checkpointing is currently only supported for model_type: llm. Skipping...\")\n            elif not hasattr(self.compiled_model, \"model\") and not hasattr(\n                self.compiled_model.model, \"gradient_checkpointing_enable\"\n            ):\n                logger.warning(\"Gradient checkpointing is not supported by this model. Skipping...\")\n            elif hasattr(self.compiled_model.model, \"gradient_checkpointing_enable\"):\n                if _TORCH210:\n                    # https://pytorch.org/docs/stable/checkpoint.html\n                    # https://github.com/huggingface/transformers/blob/02f8738ef8c674300c314d004ba436cb5aaca165/src/transformers/modeling_utils.py#L2094 # noqa: E501\n                    self.compiled_model.model.gradient_checkpointing_enable(\n                        gradient_checkpointing_kwargs={\"use_reentrant\": True}\n                    )\n                else:\n                    self.compiled_model.model.gradient_checkpointing_enable()\n                # `use_cache=True` is incompatible with gradient checkpointing.\n                self.compiled_model.model.config.use_cache = False\n                self.compiled_model.model.enable_input_require_grads()\n                logger.info(\"Gradient checkpointing enabled for training.\")\n            else:\n                raise RuntimeError(\"Error when trying to enable gradient checkpointing.\")\n\n        self.dist_model, self.optimizer = self.distributed.prepare(\n            self.compiled_model,\n            self.config,\n            self.base_learning_rate,\n        )\n\n        # NOTE: This is a partially configured LRScheduler. It will be updated in the first call to train_step.\n        self.scheduler = LRScheduler(self.config.learning_rate_scheduler, self.optimizer, 0, 0)\n\n    def train_step(\n        self,\n        inputs: dict[str, torch.Tensor],\n        targets: dict[str, torch.Tensor],\n        should_step: bool = True,\n        profiler: torch.profiler.profile | None = None,\n    ) -> tuple[torch.Tensor, dict[str, torch.Tensor], torch.Tensor]:\n        \"\"\"Performs a single training step.\n\n        Params:\n            inputs: A dictionary of input data, from feature name to tensor.\n            targets: A dictionary of target data, from feature name to tensor.\n            should_step: Whether to perform a step of the optimizer after computing gradients.\n\n        Returns:\n            A tuple of:\n                1. loss tensor\n                2. dictionary of loss for every output feature.\n                3. tokens usage tensor\n        \"\"\"\n        if isinstance(self.optimizer, torch.optim.LBFGS):\n            # NOTE: AMP is not supported for L-BFGS yet.\n            # NOTE: gradient accumulation is not supported for L-BFGS yet.\n\n            def closure():\n                # Allows L-BFGS to reevaluate the loss function\n                self.distributed.zero_grad(self.optimizer)\n                model_outputs = self.dist_model((inputs, targets))\n                loss, _ = self.model.train_loss(\n                    targets, model_outputs, self.regularization_type, self.regularization_lambda\n                )\n                loss.backward()\n                return loss\n\n            self.distributed.step(self.optimizer, closure)\n\n            # Obtain model predictions and loss\n            model_outputs = self.dist_model((inputs, targets))\n            loss, all_losses = self.model.train_loss(\n                targets, model_outputs, self.regularization_type, self.regularization_lambda\n            )\n\n            if not self.evaluate_training_set:\n                # Update evaluation metrics with current model params:\n                # noisy but fast way to get metrics on the training set\n                predictions = self.model.outputs_to_predictions(model_outputs)\n                self.model.update_metrics(targets, predictions)\n\n            return loss, all_losses, model_outputs[USED_TOKENS]\n\n        with torch.amp.autocast(\"cuda\") if self.use_amp else contextlib.nullcontext():\n            with self.distributed.prepare_model_update(self.dist_model, should_step=should_step):\n                # Obtain model predictions and loss\n                model_outputs = self.dist_model((inputs, targets))\n                loss, all_losses = self.model.train_loss(\n                    targets, model_outputs, self.regularization_type, self.regularization_lambda\n                )\n                loss = loss / self.gradient_accumulation_steps\n\n        used_tokens = model_outputs[USED_TOKENS]\n\n        # Begin the backward pass\n        variables = self.dist_model.parameters()\n        if self.use_amp:\n            self.scaler.scale(loss).backward()\n        else:\n            self.distributed.backward(loss, self.dist_model)\n\n        if not should_step:\n            # Short-circuit the parameter updates if we are still accumulating gradients\n            return loss, all_losses, used_tokens\n\n        # Wait for gradient aggregation to complete before clipping the gradients.\n        # When using AMP, we need to do this before unscaling.\n        self.distributed.wait_optimizer_synced(self.optimizer)\n\n        if self.use_amp:\n            # In-place unscaling of all gradients before weights update\n            # Do this before gradient clipping per docs:\n            # https://pytorch.org/docs/master/notes/amp_examples.html#gradient-clipping\n            self.scaler.unscale_(self.optimizer)\n\n        if self.distributed.allow_clip_gradients():\n            # Clip gradients\n            self.clip_grads(variables)\n\n        # Apply gradient updates\n        with self.distributed.prepare_optimizer_update(self.optimizer):\n            # Because we already synchronized above, we skip doing so here\n            if self.use_amp:\n                self.scaler.step(self.optimizer)\n            else:\n                self.distributed.step(self.optimizer)\n\n        if self.use_amp:\n            # Update scaler in case of overflow/underflow\n            self.scaler.update()\n\n        if not self.evaluate_training_set:\n            # Update evaluation metrics with current model params:\n            # noisy but fast way to get metrics on the training set\n            predictions = self.model.outputs_to_predictions(model_outputs)\n            self.model.update_metrics(targets, predictions)\n\n        self.distributed.zero_grad(self.optimizer)\n\n        if profiler:\n            profiler.step()\n\n        return loss, all_losses, used_tokens\n\n    def clip_grads(self, variables):\n        \"\"\"Applies gradient clipping.\"\"\"\n        if self.gradient_clipping_config.clipglobalnorm:\n            torch.nn.utils.clip_grad_norm_(variables, self.gradient_clipping_config.clipglobalnorm)\n        if self.gradient_clipping_config.clipnorm:\n            torch.nn.utils.clip_grad_norm_(variables, self.gradient_clipping_config.clipnorm)\n        if self.gradient_clipping_config.clipvalue:\n            torch.nn.utils.clip_grad_value_(variables, self.gradient_clipping_config.clipvalue)\n\n    @classmethod\n    def write_eval_summary(\n        cls,\n        summary_writer,\n        metrics,\n        step,\n    ):\n        if not summary_writer:\n            return\n\n        for feature_name, output_feature in metrics.items():\n            for metric_name, metrics in output_feature.items():\n                if metrics:\n                    metric_tag = f\"{feature_name}/epoch_{metric_name}\"\n                    metric_val = metrics[-1][-1]\n                    summary_writer.add_scalar(metric_tag, metric_val, global_step=step)\n        summary_writer.flush()\n\n    @classmethod\n    def write_step_summary(\n        cls, train_summary_writer, combined_loss, all_losses, step, used_tokens, total_tokens_used, learning_rate=None\n    ):\n        if not train_summary_writer:\n            return\n\n        # token information.\n        train_summary_writer.add_scalar(\"tokens/tokens\", used_tokens, global_step=step)\n        train_summary_writer.add_scalar(\"tokens/total_tokens_used\", total_tokens_used, global_step=step)\n\n        # combined loss\n        train_summary_writer.add_scalar(\"combined/step_training_loss\", combined_loss, global_step=step)\n\n        # all other losses\n        for feature_name, loss in all_losses.items():\n            loss_tag = f\"{feature_name}/step_training_loss\"\n            train_summary_writer.add_scalar(loss_tag, loss.detach().float(), global_step=step)\n\n        if learning_rate:\n            train_summary_writer.add_scalar(\"combined/step_learning_rate\", learning_rate, global_step=step)\n\n        # Log CUDA memory stats.\n        if torch.cuda.is_available():\n            for i in range(torch.cuda.device_count()):\n                device = torch.device(f\"cuda:{i}\")\n                memory_stats = torch.cuda.memory_stats(device=device)\n                gb_memory_stats = {k: v / (1000**3) for k, v in memory_stats.items()}\n                # Allocated bytes.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/allocated_gb.all.current\",\n                    gb_memory_stats[\"allocated_bytes.all.current\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/allocated_gb.all.peak\",\n                    gb_memory_stats[\"allocated_bytes.all.peak\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/allocated_gb.all.allocated\",\n                    gb_memory_stats[\"allocated_bytes.all.allocated\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/allocated_gb.all.freed\",\n                    gb_memory_stats[\"allocated_bytes.all.freed\"],\n                    global_step=step,\n                )\n\n                # Reserved bytes.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/reserved_gb.all.current\",\n                    gb_memory_stats[\"reserved_bytes.all.current\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/reserved_gb.all.peak\", gb_memory_stats[\"reserved_bytes.all.peak\"], global_step=step\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/reserved_gb.all.allocated\",\n                    gb_memory_stats[\"reserved_bytes.all.allocated\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/reserved_gb.all.freed\",\n                    gb_memory_stats[\"reserved_bytes.all.freed\"],\n                    global_step=step,\n                )\n\n                # Active bytes.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/active_gb.all.current\",\n                    gb_memory_stats[\"active_bytes.all.current\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/active_gb.all.peak\", gb_memory_stats[\"active_bytes.all.peak\"], global_step=step\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/active_gb.all.allocated\",\n                    gb_memory_stats[\"active_bytes.all.allocated\"],\n                    global_step=step,\n                )\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/active_gb.all.freed\", gb_memory_stats[\"active_bytes.all.freed\"], global_step=step\n                )\n\n                # Global free memory.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/global_free_memory_gb\",\n                    torch.cuda.mem_get_info(device=device)[0] / (1000**3),\n                    global_step=step,\n                )\n\n                # Total memory occupied.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/total_memory_occupied_gb\",\n                    torch.cuda.mem_get_info(device=device)[1] / (1000**3),\n                    global_step=step,\n                )\n\n                # Total memory used.\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/total_memory_used_gb\",\n                    (torch.cuda.mem_get_info(device=device)[1] - torch.cuda.mem_get_info(device=device)[0]) / (1000**3),\n                    global_step=step,\n                )\n\n                # Utilization.\n                # https://pytorch.org/docs/stable/generated/torch.cuda.utilization.html#torch.cuda.utilization\n                train_summary_writer.add_scalar(\n                    f\"cuda/device{i}/utilization\",\n                    torch.cuda.utilization(device=device),\n                    global_step=step,\n                )\n        train_summary_writer.flush()\n\n    def is_cpu_training(self):\n        return torch.device(self.device) == torch.device(\"cpu\")\n\n    def tune_batch_size(\n        self,\n        config: ModelConfigDict,\n        training_set: Dataset,\n        random_seed: int = default_random_seed,\n        max_trials: int = 20,\n        halving_limit: int = 3,\n        snapshot_weights: bool = True,\n        on_best_batch_size_updated: Callable[[int, float, int], None] | None = None,\n        tune_for_training: bool = True,\n        global_max_sequence_length: int | None = None,\n    ) -> int:\n        logger.info(\"Tuning batch size...\")\n        skip_save_model = self.skip_save_model\n        skip_save_progress = self.skip_save_progress\n        skip_save_log = self.skip_save_log\n        # Set temporary values\n        self.skip_save_model = True\n        self.skip_save_progress = True\n        self.skip_save_log = True\n\n        # When training on CPU, larger batch sizes offer limited benefits due to lack of effective\n        # parallelization within a batch. As such, to increase chances of stable training, we cap the maximum\n        # batch size at MAX_CPU_BATCH_SIZE\n        max_batch_size = (\n            self.max_batch_size if torch.cuda.is_available() else min(self.max_batch_size, MAX_CPU_BATCH_SIZE)\n        )\n\n        if self.effective_batch_size != AUTO:\n            # If an effective batch size is set, we must ensure that batch size tuning doesn't exceed it\n            max_batch_size = min(self.effective_batch_size, max_batch_size)\n\n        if not tune_for_training:\n            # No need to save and restore model and optimizer states, as they aren't modified during predict\n            snapshot_weights = False\n\n        self.dist_model.train()  # Sets model training mode.\n        evaluator = (\n            self._create_batch_size_evaluator() if tune_for_training else self._create_predict_batch_size_evaluator()\n        )\n        with tempfile.TemporaryDirectory() as tmpdir:\n            if snapshot_weights:\n                # Save a snapshot of the model and optimizer state to restore later, as they will be modified\n                # when we call the train step as part of the auto-tuning. This is undesirable, particularly for\n                # pretrained models.\n                checkpoint = self.distributed.create_checkpoint_handle(\n                    dist_model=self.dist_model, model=self.model, optimizer=self.optimizer, scheduler=self.scheduler\n                )\n                checkpoint.save(os.path.join(tmpdir, \"latest.ckpt\"), global_step=0)\n            try:\n                best_batch_size = evaluator.select_best_batch_size(\n                    len(training_set), max_batch_size, max_trials, self.is_coordinator(), global_max_sequence_length\n                )\n                best_batch_size = self.distributed.broadcast_object(best_batch_size)\n\n                if tune_for_training:\n                    # Update batch size / gradient accumulation before preparing the trainer. This is needed primarily\n                    # for DeepSpeed, which needs to know the batch size and gradient accumulation steps before init\n                    self.config.batch_size = best_batch_size\n                    self.config.update_batch_size_grad_accum(self.distributed.size())\n                    self.batch_size = self.config.batch_size\n                    self.gradient_accumulation_steps = self.config.gradient_accumulation_steps\n\n                return best_batch_size\n            finally:\n                # Restore original parameters to defaults\n                self.skip_save_model = skip_save_model\n                self.skip_save_progress = skip_save_progress\n                self.skip_save_log = skip_save_log\n\n                if snapshot_weights:\n                    # Restore the model weights prior to batch size tuning to undo any updates made to the weights\n                    if self.distributed.prepare_before_load():\n                        # Some distributed strategies, like DeepSpeed, need to re-init before loading the model\n                        self.prepare()\n                    self.resume_weights_and_optimizer(str(tmpdir), checkpoint)\n\n    def _create_batch_size_evaluator(self) -> BatchSizeEvaluator:\n        trainer = self\n\n        class _TrainerBatchSizeEvaluator(BatchSizeEvaluator):\n            def reset(self):\n                trainer.model.reset_metrics()\n                trainer.optimizer.zero_grad()\n\n            def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n                trainer.distributed.set_batch_size(trainer.dist_model, batch_size)\n                inputs = {\n                    input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(trainer.device)\n                    for input_feature_name, input_feature in trainer.model.input_features.items()\n                }\n                targets = {\n                    output_feature_name: output_feature.create_sample_output(batch_size=batch_size).to(trainer.device)\n                    for output_feature_name, output_feature in trainer.model.output_features.items()\n                }\n                trainer.train_step(inputs, targets)\n\n        return _TrainerBatchSizeEvaluator()\n\n    def _create_predict_batch_size_evaluator(self) -> BatchSizeEvaluator:\n        trainer = self\n\n        class _PredictBatchSizeEvaluator(BatchSizeEvaluator):\n            def reset(self):\n                trainer.model.reset_metrics()\n                trainer.optimizer.zero_grad()\n\n            def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n                trainer.distributed.set_batch_size(trainer.dist_model, batch_size)\n                inputs = {\n                    input_feature_name: input_feature.create_sample_input(batch_size=batch_size).to(trainer.device)\n                    for input_feature_name, input_feature in trainer.model.input_features.items()\n                }\n                targets = {\n                    output_feature_name: output_feature.create_sample_output(batch_size=batch_size).to(trainer.device)\n                    for output_feature_name, output_feature in trainer.model.output_features.items()\n                }\n                with torch.no_grad():\n                    trainer.dist_model((inputs, targets))\n\n        return _PredictBatchSizeEvaluator()\n\n    def run_evaluation(\n        self,\n        training_set,\n        validation_set,\n        test_set,\n        progress_tracker: ProgressTracker,\n        train_summary_writer,\n        validation_summary_writer,\n        test_summary_writer,\n        model_hyperparameters_path,\n        output_features,\n        metrics_names,\n        save_path,\n        loss: torch.Tensor,\n        all_losses: dict[str, torch.Tensor],\n        early_stopping_steps: int,\n        checkpoint_manager: CheckpointManager,\n    ) -> bool:\n        \"\"\"Runs evaluation over training, validation, and test sets.\n\n        Also:\n        - Prints results, saves results to the progress tracker.\n        - Saves the model if the validation score is the best so far\n        - If there is no validation set, the model is always saved.\n\n        Returns whether the trainer should early stop, based on validation metrics history.\n        \"\"\"\n        start_time = time.time()\n        self.callback(lambda c: c.on_eval_start(self, progress_tracker, save_path))\n\n        if self.is_coordinator():\n            logger.info(f\"\\nRunning evaluation for step: {progress_tracker.steps}, epoch: {progress_tracker.epoch}\")\n\n        # ================ Eval ================\n        # eval metrics on train\n        self.eval_batch_size = max(self.eval_batch_size, progress_tracker.batch_size)\n\n        if self.evaluate_training_set:\n            # Run a separate pass over the training data to compute metrics\n            self.evaluation(\n                training_set, \"train\", progress_tracker.train_metrics, self.eval_batch_size, progress_tracker\n            )\n        else:\n            # Use metrics accumulated during training\n            metrics = self.model.get_metrics()\n            append_metrics(self.model, \"train\", metrics, progress_tracker.train_metrics, progress_tracker)\n            self.model.reset_metrics()\n\n        self.write_eval_summary(\n            summary_writer=train_summary_writer,\n            metrics=progress_tracker.train_metrics,\n            step=progress_tracker.steps,\n        )\n\n        if validation_set is not None:\n            self.callback(lambda c: c.on_validation_start(self, progress_tracker, save_path))\n\n            # eval metrics on validation set\n            self.evaluation(\n                validation_set,\n                VALIDATION,\n                progress_tracker.validation_metrics,\n                self.eval_batch_size,\n                progress_tracker,\n            )\n\n            llm_eval_examples = progress_tracker.llm_eval_examples\n            dict_save_dir = os.path.join(os.path.dirname(checkpoint_manager.directory), \"llm_eval_examples\")\n            os.makedirs(dict_save_dir, exist_ok=True)\n            dict_save_path = os.path.join(dict_save_dir, f\"{progress_tracker.checkpoint_number}.csv\")\n            llm_eval_examples = pd.DataFrame(llm_eval_examples).to_dict(orient=\"records\")\n            with open(dict_save_path, \"w\", encoding=\"utf-8\") as outfile:\n                writer = csv.DictWriter(outfile, fieldnames=[\"inputs\", \"targets\", \"outputs\"])\n                writer.writeheader()\n                writer.writerows(llm_eval_examples)\n\n            self.write_eval_summary(\n                summary_writer=validation_summary_writer,\n                metrics=progress_tracker.validation_metrics,\n                step=progress_tracker.steps,\n            )\n\n            self.callback(lambda c: c.on_validation_end(self, progress_tracker, save_path))\n\n        if test_set is not None:\n            self.callback(lambda c: c.on_test_start(self, progress_tracker, save_path))\n\n            # eval metrics on test set\n            self.evaluation(test_set, TEST, progress_tracker.test_metrics, self.eval_batch_size, progress_tracker)\n\n            self.write_eval_summary(\n                summary_writer=test_summary_writer,\n                metrics=progress_tracker.test_metrics,\n                step=progress_tracker.steps,\n            )\n\n            self.callback(lambda c: c.on_test_end(self, progress_tracker, save_path))\n\n        elapsed_time = (time.time() - start_time) * 1000.0\n\n        if self.is_coordinator():\n            logger.info(f\"Evaluation took {time_utils.strdelta(elapsed_time)}\\n\")\n            print_metrics_table(\n                output_features,\n                progress_tracker.train_metrics,\n                progress_tracker.validation_metrics,\n                progress_tracker.test_metrics,\n            )\n\n        # ================ Validation Logic ================\n        should_break = False\n        if validation_set is not None and validation_set.size > 0:\n            should_break = self.check_progress_on_validation(\n                progress_tracker,\n                self.validation_field,\n                self.validation_metric,\n                save_path,\n                model_hyperparameters_path,\n                self.increase_batch_size_on_plateau,\n                self.increase_batch_size_on_plateau_patience,\n                self.increase_batch_size_on_plateau_rate,\n                self.max_batch_size,\n                self.increase_batch_size_eval_metric,\n                self.increase_batch_size_eval_split,\n                early_stopping_steps,\n                self.skip_save_model,\n                checkpoint_manager,\n            )\n        else:\n            # There's no validation, so we save the model.\n            if not self.skip_save_model:\n                if self.is_coordinator():\n                    logger.info(\"Saving model.\\n\")\n                checkpoint_manager.save_best(progress_tracker.steps)\n                self.callback(lambda c: c.on_save_best_checkpoint(self, progress_tracker, save_path))\n\n        # Trigger eval end callback after any model weights save for complete checkpoint\n        self.callback(lambda c: c.on_eval_end(self, progress_tracker, save_path))\n\n        # Clear the CUDA cache to free up memory\n        torch.cuda.empty_cache()\n\n        return should_break\n\n    def save_checkpoint(self, progress_tracker: ProgressTracker, save_path: str, checkpoint_manager: CheckpointManager):\n        \"\"\"Checkpoints the model, progress tracker, and invokes the checkpoint callback.\"\"\"\n        progress_tracker.increment_checkpoint()\n\n        checkpoint_manager.save(progress_tracker.steps)\n        if self.is_coordinator():\n            progress_tracker.save(os.path.join(save_path, TRAINING_PROGRESS_TRACKER_FILE_NAME))\n\n        # Callback that the checkpoint was reached, regardless of whether the model was evaluated.\n        self.callback(lambda c: c.on_checkpoint(self, progress_tracker))\n\n    def create_checkpoint_handle(self):\n        return self.distributed.create_checkpoint_handle(\n            dist_model=self.dist_model, model=self.model, optimizer=self.optimizer, scheduler=self.scheduler\n        )\n\n    def train(\n        self,\n        training_set,\n        validation_set=None,\n        test_set=None,\n        save_path=MODEL_FILE_NAME,\n        return_state_dict: bool = False,\n        **kwargs,\n    ):\n        \"\"\"Trains a model with a set of hyperparameters listed below. Customizable.\n\n        :param training_set: The training set\n        :param validation_set: The validation dataset\n        :param test_set: The test dataset\n        :param save_path: The directory that will contain the saved model\n        :param return_state_dict: Whether to return the state dict of the model instead of the model itself\n        \"\"\"\n        # ====== General setup =======\n        output_features = self.model.output_features\n\n        # Only use signals when on the main thread to avoid issues with CherryPy\n        # https://github.com/ludwig-ai/ludwig/issues/286\n        if threading.current_thread() == threading.main_thread():\n            # set the original sigint signal handler\n            # as we want to restore it at the end of training\n            self.original_sigint_handler = signal.getsignal(signal.SIGINT)\n            signal.signal(signal.SIGINT, self.set_steps_to_1_or_quit)\n\n        metrics_names = get_metric_names(output_features)\n\n        # ====== Setup file names =======\n        model_hyperparameters_path = None\n        tensorboard_log_dir = None\n        if self.is_coordinator():\n            os.makedirs(save_path, exist_ok=True)\n            model_hyperparameters_path = os.path.join(save_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n            tensorboard_log_dir = os.path.join(save_path, \"logs\")\n\n        # Sync save_path across the workers\n        save_path = self.distributed.broadcast_object(save_path or \"\")\n\n        training_progress_tracker_path = None\n        training_checkpoints_path = None\n        if save_path:\n            training_progress_tracker_path = os.path.join(save_path, TRAINING_PROGRESS_TRACKER_FILE_NAME)\n            training_checkpoints_path = os.path.join(save_path, TRAINING_CHECKPOINTS_DIR_PATH)\n\n        self.callback(\n            lambda c: c.on_trainer_train_setup(self, save_path, self.is_coordinator()), coordinator_only=False\n        )\n\n        # ====== Setup session =======\n        checkpoint = self.create_checkpoint_handle()\n        checkpoint_manager = CheckpointManager(checkpoint, training_checkpoints_path, device=self.device)\n\n        # ====== Setup Tensorboard writers =======\n        train_summary_writer = None\n        validation_summary_writer = None\n        test_summary_writer = None\n        if self.is_coordinator() and not self.skip_save_log and tensorboard_log_dir:\n            train_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TRAINING))\n            if validation_set is not None and validation_set.size > 0:\n                validation_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, VALIDATION))\n            if test_set is not None and test_set.size > 0:\n                test_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TEST))\n\n        # ================ Resume logic ================\n        self.callback(lambda c: c.on_resume_training(self.is_coordinator()))\n\n        should_resume = self.resume and self.resume_files_exist(\n            training_progress_tracker_path, training_checkpoints_path\n        )\n        # make sure all workers are on the same page about resuming.\n        should_resume = self.distributed.broadcast_object(should_resume, name=\"should_resume\")\n\n        if should_resume:\n            try:\n                progress_tracker = self.resume_training_progress_tracker(training_progress_tracker_path)\n                self.resume_weights_and_optimizer(training_checkpoints_path, checkpoint)\n                if self.is_coordinator():\n                    logger.info(\"Resuming training from previous run.\")\n            except Exception:\n                # This may happen if model training is interrupted after the progress tracker is initialized\n                # but before any real training progress is made.\n                progress_tracker = get_new_progress_tracker(\n                    batch_size=self.batch_size,\n                    learning_rate=self.base_learning_rate,\n                    best_eval_metric_value=get_initial_validation_value(self.validation_metric),\n                    best_increase_batch_size_eval_metric=get_initial_validation_value(\n                        self.increase_batch_size_eval_metric\n                    ),\n                    output_features=output_features,\n                )\n                if self.is_coordinator():\n                    logger.info(\"Failed to resume training from previous run. Creating fresh model training run.\")\n        else:\n            progress_tracker = get_new_progress_tracker(\n                batch_size=self.batch_size,\n                learning_rate=self.base_learning_rate,\n                best_eval_metric_value=get_initial_validation_value(self.validation_metric),\n                best_increase_batch_size_eval_metric=get_initial_validation_value(self.increase_batch_size_eval_metric),\n                output_features=output_features,\n            )\n            if self.is_coordinator():\n                logger.info(\"Creating fresh model training run.\")\n\n        # Distributed: broadcast initial variable states from rank 0 to all other processes.\n        # This is necessary to ensure consistent initialization of all workers when\n        # training is started with random weights or restored from a checkpoint.\n        self.distributed.sync_model(self.dist_model)\n        self.distributed.sync_optimizer(self.optimizer)\n        self.scheduler.load_state_dict(self.distributed.broadcast_object(self.scheduler.state_dict()))\n\n        # For DeepSpeed, we need to set the batch size here in case it was modfied during auto-tuning\n        self.distributed.set_batch_size(self.dist_model, self.batch_size)\n\n        set_random_seed(self.random_seed)\n\n        if self.enable_profiling:\n            logger.warning(\"Full torch profiler is enabled. Training may be significantly slower.\")\n            profiler = torch.profiler.profile(\n                schedule=torch.profiler.schedule(\n                    wait=self.config.profiler.wait,\n                    warmup=self.config.profiler.warmup,\n                    active=self.config.profiler.active,\n                    repeat=self.config.profiler.repeat,\n                ),\n                on_trace_ready=torch.profiler.tensorboard_trace_handler(os.path.join(tensorboard_log_dir, \"profiling\")),\n                record_shapes=True,\n                with_stack=True,\n                profile_memory=True,\n            )\n        else:\n            profiler = None\n\n        try:\n            with training_set.initialize_batcher(\n                batch_size=self.batch_size,\n                should_shuffle=self.should_shuffle,\n                random_seed=self.random_seed,\n                distributed=self.distributed,\n                ignore_last=True,\n                augmentation_pipeline=self.model.get_augmentation_pipelines(),\n            ) as batcher:\n                # ================ Training Loop ================\n                self.steps_per_epoch = batcher.steps_per_epoch\n                self.total_steps = get_total_steps(self.epochs, batcher.steps_per_epoch, self.train_steps)\n                # NOTE(geoffrey): this ensures that the total number of epochs coincides with the number of\n                # times `batcher.set_epoch` is called.\n                old_epochs = self.epochs\n                self.epochs = math.ceil(self.total_steps / self.steps_per_epoch)\n                if old_epochs != self.epochs:\n                    logger.warning(\n                        f\"The number of epochs has been adjusted from config-specified {old_epochs} \"\n                        f\"to {self.epochs} to match the total number of steps.\"\n                    )\n\n                # Get the terminal steps per checkpoint.\n                final_steps_per_checkpoint = get_final_steps_per_checkpoint(\n                    batcher.steps_per_epoch,\n                    self.steps_per_checkpoint,\n                    self.checkpoints_per_epoch,\n                    self.is_coordinator(),\n                )\n                final_steps_per_checkpoint = min(final_steps_per_checkpoint, self.total_steps)\n                early_stopping_steps = final_steps_per_checkpoint * self.early_stop\n                if not self.skip_save_progress:\n                    self.total_expected_checkpoints = get_total_expected_checkpoints(\n                        self.total_steps, final_steps_per_checkpoint, self.epochs\n                    )\n\n                # Initialize the learning rate scheduler.\n                self.scheduler = LRScheduler(\n                    self.config.learning_rate_scheduler,\n                    self.optimizer,\n                    steps_per_checkpoint=final_steps_per_checkpoint,\n                    total_steps=self.total_steps,\n                )\n\n                if self.is_coordinator():\n                    logger.info(\n                        f\"Training for {self.total_steps} step(s), approximately \"\n                        f\"{int(self.total_steps / batcher.steps_per_epoch)} epoch(s).\"\n                    )\n                    if self.early_stop < 0:\n                        logger.info(\"Early stopping policy: None\")\n                    else:\n                        logger.info(\n                            f\"Early stopping policy: {self.early_stop} round(s) of evaluation, or \"\n                            f\"{early_stopping_steps} step(s), approximately \"\n                            f\"{int(early_stopping_steps / batcher.steps_per_epoch)} epoch(s).\\n\"\n                        )\n                    logger.info(f\"Starting with step {progress_tracker.steps}, epoch: {progress_tracker.epoch}\")\n\n                progress_bar_config = {\n                    \"desc\": \"Training\",\n                    \"initial\": progress_tracker.steps,\n                    \"total\": self.total_steps,\n                    \"disable\": is_progressbar_disabled(),\n                    \"file\": sys.stdout,\n                }\n                progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n\n                if profiler:\n                    profiler.start()\n\n                while progress_tracker.steps < self.total_steps:\n                    # note that batch size may change over epochs\n                    batcher.set_epoch(progress_tracker.epoch, progress_tracker.batch_size)\n\n                    # epoch init\n                    start_time = time.time()\n\n                    # Reset the metrics at the start of the next epoch\n                    self.dist_model.train()  # Sets model to training mode.\n                    self.model.reset_metrics()\n\n                    self.callback(lambda c: c.on_epoch_start(self, progress_tracker, save_path))\n\n                    # Trains over a full epoch of data or up to the last training step, whichever is sooner.\n                    should_break, has_nan_or_inf_tensors = self._train_loop(\n                        batcher,\n                        progress_tracker,\n                        save_path,\n                        train_summary_writer,\n                        progress_bar,\n                        training_set,\n                        validation_set,\n                        test_set,\n                        start_time,\n                        validation_summary_writer,\n                        test_summary_writer,\n                        model_hyperparameters_path,\n                        output_features,\n                        metrics_names,\n                        checkpoint_manager,\n                        final_steps_per_checkpoint,\n                        early_stopping_steps,\n                        profiler,\n                    )\n                    if self.is_coordinator():\n                        # ========== Save training progress ==========\n                        logger.debug(\n                            f\"Epoch {progress_tracker.epoch} took: \"\n                            f\"{time_utils.strdelta((time.time() - start_time) * 1000.0)}.\"\n                        )\n\n                    # Skip saving progress if we're not saving the model. We should do this so as to not overwrite the\n                    # best model checkpoint from the previous round of evaluation so that the previous best model\n                    # weights can be used for inference instead of the current weights which are in a bad state.\n                    if has_nan_or_inf_tensors:\n                        break\n\n                    if not self.skip_save_progress:\n                        self.save_checkpoint(progress_tracker, save_path, checkpoint_manager)\n\n                    if not self.skip_save_model and self.skip_all_evaluation:\n                        # All evaluation was skipped, so save the current step as the best so far.\n                        checkpoint_manager.save_best(progress_tracker.steps)\n\n                    # Early stop if needed.\n                    if should_break:\n                        break\n        finally:\n            # ================ Finished Training ================\n            self.callback(\n                lambda c: c.on_trainer_train_teardown(self, progress_tracker, save_path, self.is_coordinator()),\n                coordinator_only=False,\n            )\n\n            # Deactivate any forward hooks for the model used at training time.\n            self.compiled_model._deactivate_forward_hooks()\n\n            # Stop the profiler.\n            if profiler:\n                profiler.stop()\n\n            # Close the summary writers.\n            if train_summary_writer is not None:\n                train_summary_writer.close()\n            if validation_summary_writer is not None:\n                validation_summary_writer.close()\n            if test_summary_writer is not None:\n                test_summary_writer.close()\n\n            if not self.skip_save_model and self.skip_all_evaluation and not has_nan_or_inf_tensors:\n                # All evaluation was skipped, so save the current step as the best so far.\n                checkpoint_manager.save_best(progress_tracker.steps)\n\n            if not self.skip_save_progress:\n                checkpoint_manager.close()\n\n        # Load the best weights from saved checkpoint\n        state_dict = None\n        if self.distributed.is_coordinator():\n            if not self.skip_save_model:\n                state_dict = checkpoint_manager.get_best_checkpoint_state_for_inference(self.return_device)\n                if not state_dict:\n                    error_message = \"Training ran into an error. No checkpoint was saved.\"\n                    if has_nan_or_inf_tensors:\n                        error_message += (\n                            \" This is because training was terminated early due to the presence of NaN or \"\n                            \"Inf values in the model weights before a single valid checkpoint could be saved.\"\n                        )\n                    raise RuntimeError(error_message)\n                if not return_state_dict:\n                    if self.distributed.is_model_parallel():\n                        # Assume the full weights cannot fit in memory on GPU\n                        self.model = self.model.cpu()\n\n                    # For a full explanation of this 8-bit workaround, see https://github.com/ludwig-ai/ludwig/pull/3606\n                    # TODO (jeffkinnison): Determine why `SCB` and `CB` are deleted from parameter state\n                    quantization = get_quantization(self.model.config_obj)\n                    uses_quantization = bool(quantization) if not isinstance(quantization, list) else any(quantization)\n                    if uses_quantization and 8 in quantization:\n                        # If the model was previously placed on GPU, 8-bit parameter state will be updated with several\n                        # matrices containing quantization information. These are recorded matrices are recorded in the\n                        # training checkpoint state dicts, but do not necessarily exist in the parameter object, leading\n                        # to a RuntimeError in `load_state_dict`. Explicitly call `model.cuda()` to make sure the\n                        # matrices are part of model state. This workaround is necessary because the matrices are\n                        # deleted during the model's forward pass.\n                        if self.model.config_obj.model_type == MODEL_LLM and self.model.model.device.type == \"cuda\":\n                            self.model.model.cuda()\n                        elif self.model.config_obj.model_type == MODEL_ECD and self.model.device.type == \"cuda\":\n                            self.model.cuda()\n                        _, unexpected_keys = self.model.load_state_dict(state_dict, strict=False)\n                        only_weights_format_keys = [\"weights_format\" in k for k in unexpected_keys]\n\n                        # bitsandbytes adds a number of `weights_format` metadata fields to the state dict in\n                        # `Linear8bitLt._save_to_state_dict`. These contain information about how the 8-bit tensors\n                        # are tiled, but the fields themselves never exist in the module and get returned as unexpected\n                        # keys when loading the state dict. The\n                        assert (\n                            unexpected_keys == [] or only_weights_format_keys\n                        ), f\"Unexpected keys found in state dict: {unexpected_keys}\"\n                    else:\n                        _, unexpected_keys = self.model.load_state_dict(state_dict, strict=False)\n                        assert unexpected_keys == [], f\"Unexpected keys found in state dict: {unexpected_keys}\"\n            elif return_state_dict:\n                state_dict = self.model.cpu().state_dict()\n\n        # When running with Ray, we only need to return the state dict, as it's faster and cheaper to send the\n        # state dict over the network than to load the model state here, serialize it back to a state dict, then\n        # load it back on the head node.\n        return_value = self.model if not return_state_dict else state_dict\n\n        # restore original sigint signal handler\n        if self.original_sigint_handler and threading.current_thread() == threading.main_thread():\n            signal.signal(signal.SIGINT, self.original_sigint_handler)\n\n        return (\n            return_value,\n            progress_tracker.train_metrics,\n            progress_tracker.validation_metrics,\n            progress_tracker.test_metrics,\n        )\n\n    def _train_loop(\n        self,\n        batcher,\n        progress_tracker: ProgressTracker,\n        save_path,\n        train_summary_writer,\n        progress_bar: LudwigProgressBar,\n        training_set,\n        validation_set,\n        test_set,\n        start_time,\n        validation_summary_writer,\n        test_summary_writer,\n        model_hyperparameters_path,\n        output_features,\n        metrics_names,\n        checkpoint_manager: CheckpointManager,\n        final_steps_per_checkpoint: int,\n        early_stopping_steps: int,\n        profiler: torch.profiler.profile | None,\n    ) -> tuple[bool, bool]:\n        \"\"\"Completes up to one epoch through the data.\n\n        This function completes a single pass (epoch) through the training data and returns\n        two boolean values:\n\n        Returns:\n            should_break (bool):\n                Indicates whether the training loop should be terminated prematurely.\n\n            has_nan_or_inf_tensors (bool):\n                Indicates whether the model weights contain NaN or Inf values.\n        \"\"\"\n        self.distributed.zero_grad(self.optimizer)\n        batch_idx = 0\n        should_break = False\n        has_nan_or_inf_tensors = False\n        while not batcher.last_batch() and progress_tracker.steps < self.total_steps and not should_break:\n            progress_tracker.learning_rate = self.optimizer.param_groups[0][\"lr\"]\n            self.callback(lambda c: c.on_batch_start(self, progress_tracker, save_path))\n\n            # obtain batch\n            batch = batcher.next_batch()\n\n            # determine whether we need to accumulate gradients as trigger a full parameter update\n            should_sync_grads = (batch_idx + 1) % self.gradient_accumulation_steps == 0\n            is_checkpoint_step = (progress_tracker.steps + 1) % final_steps_per_checkpoint == 0\n            should_step = should_sync_grads or is_checkpoint_step\n            batch_idx += 1\n\n            # Move tensors to cuda here.\n            inputs = {\n                i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(self.device)\n                for i_feat in self.model.input_features.values()\n            }\n            targets = {\n                o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(self.device)\n                for o_feat in self.model.output_features.values()\n            }\n\n            loss, all_losses, used_tokens = self.train_step(inputs, targets, should_step=should_step, profiler=profiler)\n\n            # Update LR schduler here instead of train loop to avoid updating during batch size tuning, etc.\n            self.scheduler.step()\n\n            # Update progress tracker with token information.\n            progress_tracker.set_token_usage_for_this_step(used_tokens)\n\n            if self.is_coordinator() and not self.skip_save_log:\n                self.write_step_summary(\n                    train_summary_writer=train_summary_writer,\n                    combined_loss=loss.detach().float(),\n                    all_losses=all_losses,\n                    step=progress_tracker.steps,\n                    used_tokens=used_tokens,\n                    total_tokens_used=progress_tracker.total_tokens_used,\n                    learning_rate=progress_tracker.learning_rate,\n                )\n\n            progress_tracker.steps += 1\n            progress_bar.set_postfix({\"loss\": loss.detach().item()})\n            progress_bar.update(1)\n            if self.is_coordinator():\n                logger.debug(\n                    \"training: completed batch %s memory used: %.2fMB\",\n                    progress_bar.total_steps,\n                    psutil.Process(os.getpid()).memory_info()[0] / 1e6,\n                )\n\n            # Executing `on_batch_end` calls before `run_evaluation` enables more accurate\n            # batch duration measurements when using timer callbacks.\n            self.callback(lambda c: c.on_batch_end(self, progress_tracker, save_path, sync_step=should_step))\n\n            # If this is the last batch in the epoch, increment before running evaluation so that metrics are reported\n            # with the correct epoch.\n            if batcher.last_batch():\n                progress_tracker.epoch += 1\n\n            if progress_tracker.steps % final_steps_per_checkpoint == 0:\n                # Before continuing to evaluation or skipping evaluation altogether, we should use this point to\n                # ensure that the model weights are not NaN or Inf.\n                has_nan_or_inf_tensors = self._has_nan_or_inf_weights(self.dist_model)\n                # If a nan/inf tensor is detected, we should break out of the training loop immediately and raise an #\n                # error. There is no point in running evaluation for this step as the model weights are already in\n                # a bad state. Theere is also no point in continuing to train the model since the loss will always be\n                # NaN or Inf from this point forward.\n                if has_nan_or_inf_tensors:\n                    return True, has_nan_or_inf_tensors\n\n                if not self.skip_all_evaluation:\n                    # Publishes metrics to MLFLow if there are any MLFlow callbacks.\n                    should_break = self.run_evaluation(\n                        training_set,\n                        validation_set,\n                        test_set,\n                        progress_tracker,\n                        train_summary_writer,\n                        validation_summary_writer,\n                        test_summary_writer,\n                        model_hyperparameters_path,\n                        output_features,\n                        metrics_names,\n                        save_path,\n                        loss,\n                        all_losses,\n                        early_stopping_steps,\n                        checkpoint_manager,\n                    )\n                else:\n                    should_break = False\n\n                # Checkpoint the model.\n                # NOTE: Ideally we would do this before evaluation, but for some reason DeepSpeed will complain\n                # about inflight params if we do that, which is why we checkpoint after eval instead. In practice,\n                # this should not make a difference, except in the unlikely event an error occurs during eval and we\n                # want to resume from the last checkpoint, in which case we will lose slightly more progress this way.\n                if not self.skip_save_progress:\n                    self.save_checkpoint(progress_tracker, save_path, checkpoint_manager)\n\n            # If this was the last batch, then increment the epoch counter and invoke the `on_epoch_end` callback.\n            if batcher.last_batch():\n                self.callback(lambda c: c.on_epoch_end(self, progress_tracker, save_path))\n\n        return should_break, has_nan_or_inf_tensors\n\n    def _has_nan_or_inf_weights(self, model: torch.nn.Module) -> bool:\n        \"\"\"Check for NaN or infinity (inf) values in the weights (parameters and buffers) of a PyTorch model in a\n        local or distributed training environment. It is called to ensure the model's numerical stability during\n        training. It works for both model parallel and data parallel training.\n\n        This function recursively inspects the model's parameters and buffers to identify NaN or inf values. It\n        communicates and aggregates the results across all distributed processes using the `all_reduce` operation. If\n        any process finds NaN or inf values, it is considered a critical error, and the main coordinator process will\n        return True to halt training in the main training loop.\n\n        Parameters:\n            model (torch.nn.Module): The PyTorch model to check for NaN or inf weights.\n\n        Returns:\n            bool: Returns True if any NaN or inf tensors are found in the model's weights. Otherwise, returns False.\n        \"\"\"\n        local_has_nan_or_inf = contains_nan_or_inf_tensors(model)\n\n        # Use all_reduce to aggregate local_has_nan across all processes and sum the result into global_has_nan, which\n        # will be a tensor with a single element on all processes after the all_reduce operation.\n        global_has_nan_or_inf = torch.tensor(int(local_has_nan_or_inf), device=self.device)\n        self.distributed.allreduce(global_has_nan_or_inf)\n\n        # The main coordinator process will raise a runtime error if any of the processes found NaN or inf weights.\n        if self.distributed.local_rank() == 0:\n            if global_has_nan_or_inf.item() > 0:\n                logger.warning(\"NaN or inf tensors found in the model. Stopping training.\")\n                return True\n            return False\n\n    def train_online(self, dataset):\n        self.dist_model.train()  # Sets model training mode.\n        with dataset.initialize_batcher(\n            batch_size=self.batch_size,\n            should_shuffle=self.should_shuffle,\n            distributed=self.distributed,\n            ignore_last=True,\n        ) as batcher:\n            # training step loop\n            progress_bar_config = {\n                \"desc\": \"Training online\",\n                \"total\": batcher.steps_per_epoch,\n                \"file\": sys.stdout,\n                \"disable\": is_progressbar_disabled(),\n            }\n            progress_bar = LudwigProgressBar(self.report_tqdm_to_ray, progress_bar_config, self.is_coordinator())\n\n            while not batcher.last_batch():\n                batch = batcher.next_batch()\n                inputs = {\n                    i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(\n                        self.device\n                    )\n                    for i_feat in self.model.input_features.values()\n                }\n                targets = {\n                    o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(\n                        self.device\n                    )\n                    for o_feat in self.model.output_features.values()\n                }\n\n                self.train_step(\n                    inputs,\n                    targets,\n                )\n\n                progress_bar.update(1)\n\n            progress_bar.close()\n        return self.model\n\n    @property\n    def validation_field(self):\n        return self._validation_field\n\n    @property\n    def validation_metric(self):\n        return self._validation_metric\n\n    def evaluation(self, dataset, dataset_name, metrics_log, batch_size, progress_tracker):\n        predictor = Predictor(\n            self.dist_model,\n            batch_size=batch_size,\n            distributed=self.distributed,\n            report_tqdm_to_ray=self.report_tqdm_to_ray,\n            model=self.model,\n        )\n        metrics, _ = predictor.batch_evaluation(dataset, collect_predictions=False, dataset_name=dataset_name)\n\n        return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker)\n\n    def check_progress_on_validation(\n        self,\n        progress_tracker,\n        validation_output_feature_name,\n        validation_metric: str,\n        save_path,\n        model_hyperparameters_path,\n        increase_batch_size_on_plateau,\n        increase_batch_size_on_plateau_patience,\n        increase_batch_size_on_plateau_rate,\n        increase_batch_size_on_plateau_max,\n        increase_batch_size_eval_metric,\n        increase_batch_size_eval_split,\n        early_stopping_steps: int,\n        skip_save_model,\n        checkpoint_manager: CheckpointManager,\n    ) -> bool:\n        \"\"\"Checks the history of validation scores.\n\n        Uses history of validation scores to reduce learning rate, increase batch size, and decide whether training\n        should stop.\n\n        Saves the model if scores have improved.\n\n        Returns whether the model should stop training.\n        \"\"\"\n        should_break = False\n        improved_fn = get_improved_fn(validation_metric)\n\n        all_validation_metrics = progress_tracker.validation_metrics[validation_output_feature_name]\n        # The most recent validation_metric metric.\n        eval_metric: TrainerMetric = all_validation_metrics[validation_metric][-1]\n        eval_metric_value = eval_metric[-1]\n\n        if eval_metric_value != eval_metric_value:\n            # Fallback to 0 if the validation metric value is a NaN.\n            # This is potentially relevant for small datasets like those used in testing where if there's only a\n            # single output label, some metrics like ROC may turn out to be NaN.\n            # However, we want to guarantee that the model will be saved at least once over a full\n            # training-checkpoint-eval-loop.\n            eval_metric_value = 0\n\n        if improved_fn(eval_metric_value, progress_tracker.best_eval_metric_value):\n            previous_best_eval_metric_value = progress_tracker.best_eval_metric_value\n\n            # Save the value, steps, epoch, and checkpoint number.\n            progress_tracker.best_eval_metric_value = eval_metric_value\n            progress_tracker.best_eval_metric_steps = progress_tracker.steps\n            progress_tracker.best_eval_metric_epoch = progress_tracker.epoch\n            progress_tracker.best_eval_metric_checkpoint_number = progress_tracker.checkpoint_number\n\n            # Save best metrics for all data subsets.\n            progress_tracker.best_eval_train_metrics = get_latest_metrics_dict(progress_tracker.train_metrics)\n            progress_tracker.best_eval_validation_metrics = get_latest_metrics_dict(progress_tracker.validation_metrics)\n            progress_tracker.best_eval_test_metrics = get_latest_metrics_dict(progress_tracker.test_metrics)\n\n            if self.is_coordinator():\n                logger.info(\n                    f\"Evaluation validation metric: '{validation_output_feature_name}' '{validation_metric}' improved.\"\n                )\n                absolute_eval_metric_value_change = round(\n                    abs(previous_best_eval_metric_value - progress_tracker.best_eval_metric_value), 3\n                )\n                if get_metric_objective(validation_metric) == MINIMIZE:\n                    logger.info(\n                        f\"'{validation_output_feature_name}' '{validation_metric}' decreased by \"\n                        f\"{absolute_eval_metric_value_change}.\"\n                    )\n                else:\n                    logger.info(\n                        f\"'{validation_output_feature_name}' '{validation_metric}' increased by \"\n                        f\"{absolute_eval_metric_value_change}.\"\n                    )\n\n            # Save the model.\n            if not skip_save_model:\n                logger.info(\"New best model saved.\\n\")\n                checkpoint_manager.save_best(progress_tracker.steps)\n                self.callback(lambda c: c.on_save_best_checkpoint(self, progress_tracker, save_path))\n\n        last_improvement_in_steps = progress_tracker.steps - progress_tracker.best_eval_metric_steps\n        progress_tracker.last_improvement_steps = last_improvement_in_steps\n\n        if last_improvement_in_steps != 0 and self.is_coordinator():\n            logger.info(\n                f\"Last improvement of {validation_output_feature_name} validation {validation_metric} happened \"\n                + f\"{last_improvement_in_steps} step(s) ago.\\n\"\n            )\n\n        # ========== Learning Rate Schedule evaluation updates ========\n        self.scheduler.eval_step(progress_tracker, validation_output_feature_name)\n\n        # ========== Increase Batch Size Plateau logic =========\n        if increase_batch_size_on_plateau > 0:\n            self.increase_batch_size(\n                progress_tracker,\n                validation_output_feature_name,\n                increase_batch_size_on_plateau,\n                increase_batch_size_on_plateau_patience,\n                increase_batch_size_on_plateau_rate,\n                increase_batch_size_on_plateau_max,\n                increase_batch_size_eval_metric,\n                increase_batch_size_eval_split,\n            )\n            progress_tracker.last_increase_batch_size = (\n                progress_tracker.steps - progress_tracker.last_increase_batch_size_steps\n            )\n            if (\n                progress_tracker.last_increase_batch_size > 0\n                and progress_tracker.last_increase_batch_size_eval_metric_improvement > 0\n                and not progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau\n                and not progress_tracker.batch_size >= increase_batch_size_on_plateau_max\n            ):\n                logger.info(\n                    \"Last batch size increase \"\n                    f\"happened {progress_tracker.last_increase_batch_size} step(s) ago, \"\n                    f\"improvement of {validation_output_feature_name} {increase_batch_size_eval_split} \"\n                    f\"{increase_batch_size_eval_metric} happened \"\n                    f\"{progress_tracker.last_increase_batch_size_eval_metric_improvement} step(s) ago.\"\n                )\n\n        # ========== Early Stop logic ==========\n        # If any early stopping condition is satisfied, either lack of improvement for many steps, or via callbacks on\n        # any worker, then trigger early stopping.\n        early_stop_bool = 0 < early_stopping_steps <= last_improvement_in_steps\n        if not early_stop_bool:\n            for callback in self.callbacks:\n                if callback.should_early_stop(self, progress_tracker, self.is_coordinator()):\n                    early_stop_bool = True\n                    break\n\n        should_early_stop = torch.as_tensor([early_stop_bool], dtype=torch.int, device=self.device)\n        should_early_stop = self.distributed.allreduce(should_early_stop)\n        if should_early_stop.item():\n            if self.is_coordinator():\n                logger.info(\n                    f\"\\nEARLY STOPPING due to lack of validation improvement. It has been {last_improvement_in_steps} \"\n                    \"step(s) since last validation improvement.\"\n                )\n            should_break = True\n        return should_break\n\n    def set_steps_to_1_or_quit(self, signum, frame):\n        \"\"\"Custom SIGINT handler used to elegantly exit training.\n\n        A single SIGINT will stop training after the next training step. A second SIGINT will stop training immediately.\n        \"\"\"\n        if not self.received_sigint:\n            self.total_steps = 1\n            self.received_sigint = True\n            logger.critical(\"\\nReceived SIGINT, will finish this training step and then conclude training.\")\n            logger.critical(\"Send another SIGINT to immediately interrupt the process.\")\n        else:\n            logger.critical(\"\\nReceived a second SIGINT, will now quit\")\n            if self.original_sigint_handler:\n                signal.signal(signal.SIGINT, self.original_sigint_handler)\n            sys.exit(1)\n\n    @staticmethod\n    def resume_files_exist(\n        training_progress_tracker_path: str,\n        training_checkpoint_path: str,\n    ) -> bool:\n        missing_files = []\n        # training_progress.json\n        if not path_exists(training_progress_tracker_path):\n            missing_files.append(training_progress_tracker_path)\n        # latest.ckpt in training_checkpoints/\n        latest_ckpt = os.path.join(training_checkpoint_path, \"latest.ckpt\")\n        if not path_exists(latest_ckpt):\n            missing_files.append(latest_ckpt)\n        if missing_files:\n            logger.warning(f\"Could not find {missing_files} while trying to resume model training.\")\n            return False\n        return True\n\n    def resume_training_progress_tracker(self, training_progress_tracker_path):\n        progress_tracker_dict = None\n        if self.is_coordinator():\n            logger.info(f\"Loading progress tracker for model: {training_progress_tracker_path}\")\n            progress_tracker_dict = load_json(training_progress_tracker_path)\n\n        logger.debug(\"Broadcasting model progress tracker dict to all workers\")\n        progress_tracker_dict = self.distributed.broadcast_object(\n            progress_tracker_dict, name=\"broadcast_progress_tracker\"\n        )\n\n        progress_tracker = ProgressTracker.load(progress_tracker_dict)\n        return progress_tracker\n\n    def resume_weights_and_optimizer(\n        self,\n        model_weights_progress_path: str,\n        checkpoint: Checkpoint,\n    ):\n        CheckpointManager.load_latest_checkpoint(checkpoint, model_weights_progress_path, self.device)\n\n    def increase_batch_size(\n        self,\n        progress_tracker: ProgressTracker,\n        validation_output_feature_name: str,\n        increase_batch_size_on_plateau: int,\n        increase_batch_size_on_plateau_patience: int,\n        increase_batch_size_on_plateau_rate: float,\n        increase_batch_size_on_plateau_max: int,\n        increase_batch_size_eval_metric: str = LOSS,\n        increase_batch_size_eval_split: str = TRAINING,\n    ):\n        \"\"\"Uses the progress tracker to determine if the batch size should be increased.\"\"\"\n        if (\n            not progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau\n            and not progress_tracker.batch_size == increase_batch_size_on_plateau_max\n        ):\n            if increase_batch_size_eval_split == TRAINING:\n                split_metrics = progress_tracker.train_metrics\n            elif increase_batch_size_eval_split == VALIDATION:\n                split_metrics = progress_tracker.validation_metrics\n            else:  # if increase_batch_size_eval_split == TEST:\n                split_metrics = progress_tracker.test_metrics\n\n            validation_metric = increase_batch_size_eval_metric\n            last_metric = split_metrics[validation_output_feature_name][validation_metric][-1]\n            last_metric_value = last_metric[-1]\n\n            improved_fn = get_improved_fn(validation_metric)\n            is_improved = improved_fn(last_metric_value, progress_tracker.best_increase_batch_size_eval_metric)\n            if is_improved:\n                # We update the best metric value and set it to the current one, and reset last\n                # improvement step count\n                progress_tracker.best_increase_batch_size_eval_metric = last_metric_value\n                progress_tracker.last_increase_batch_size_eval_metric_improvement = 0\n            else:\n                progress_tracker.last_increase_batch_size_eval_metric_improvement += 1\n                if not is_improved and (\n                    # Batch size increase happened more than N steps ago\n                    progress_tracker.last_increase_batch_size >= increase_batch_size_on_plateau_patience\n                    and (\n                        # No improvement of the evaluation metric since more than N steps ago\n                        progress_tracker.last_increase_batch_size_eval_metric_improvement\n                        >= increase_batch_size_on_plateau_patience\n                    )\n                ):\n                    progress_tracker.batch_size = min(\n                        int(increase_batch_size_on_plateau_rate * progress_tracker.batch_size),\n                        increase_batch_size_on_plateau_max,\n                    )\n\n                    if self.is_coordinator():\n                        logger.info(\n                            f\"PLATEAU REACHED, increasing batch size to {progress_tracker.batch_size} due to lack of \"\n                            f\"improvement of {validation_output_feature_name} {increase_batch_size_eval_split} \"\n                            f\"{validation_metric}.\"\n                        )\n\n                    progress_tracker.last_increase_batch_size_steps = progress_tracker.steps\n                    progress_tracker.last_increase_batch_size = 0\n                    progress_tracker.num_increases_batch_size += 1\n\n                    if progress_tracker.num_increases_batch_size >= increase_batch_size_on_plateau:\n                        if self.is_coordinator():\n                            logger.info(\n                                f\"Batch size was already increased {progress_tracker.num_increases_batch_size} times, \"\n                                \"not increasing it anymore.\"\n                            )\n                    elif progress_tracker.batch_size >= increase_batch_size_on_plateau_max:\n                        if self.is_coordinator():\n                            logger.info(\n                                f\"Batch size was already increased {progress_tracker.num_increases_batch_size} times, \"\n                                f\"currently it is {progress_tracker.batch_size}, the maximum allowed.\"\n                            )\n\n    def is_coordinator(self):\n        return self.distributed.rank() == 0\n\n    @property\n    def local_rank(self) -> int:\n        return self.distributed.local_rank()\n\n    def barrier(self):\n        self.distributed.barrier()\n\n    def callback(self, fn, coordinator_only=True):\n        if not coordinator_only or self.is_coordinator():\n            for callback in self.callbacks:\n                fn(callback)\n\n    @property\n    def return_device(self):\n        return self.device\n\n\nclass RemoteTrainer(Trainer):\n    def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs):\n        super().__init__(**kwargs)\n\n        # Only return results from rank 0 to reduce network overhead\n        self.train = self.distributed.return_first(self.train)\n        self.train_online = self.distributed.return_first(self.train_online)\n\n    @property\n    def return_device(self):\n        # When returning the model weights from remote to driver, place them on CPU,\n        # as the driver likely doesn't have a GPU.\n        return \"cpu\"\n\n\nlearning_rate_scale_fns = {\n    \"linear\": lambda n: n,\n    \"sqrt\": lambda n: math.sqrt(n),\n    \"constant\": lambda n: 1,\n}\n"
  },
  {
    "path": "ludwig/trainers/trainer_llm.py",
    "content": "import logging\nimport os\nimport time\nfrom collections.abc import Callable\nfrom typing import Union\n\nfrom torch.utils.tensorboard import SummaryWriter\n\nfrom ludwig.constants import MINIMUM_BATCH_SIZE, TEST, TRAINING, VALIDATION\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.distributed.base import DistributedStrategy, LocalStrategy\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.models.llm import LLM\nfrom ludwig.models.predictor import LlmFineTunePredictor, LlmPredictor\nfrom ludwig.modules.metric_modules import get_initial_validation_value\nfrom ludwig.schema.trainer import BaseTrainerConfig, FineTuneTrainerConfig, NoneTrainerConfig\nfrom ludwig.trainers.base import BaseTrainer\nfrom ludwig.trainers.registry import register_llm_ray_trainer, register_llm_trainer\nfrom ludwig.trainers.trainer import Trainer\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils import time_utils\nfrom ludwig.utils.batch_size_tuner import (\n    BatchSizeEvaluator,\n    LLMFinetunePredictBatchSizeEvaluator,\n    LLMFinetuneTrainerBatchSizeEvaluator,\n)\nfrom ludwig.utils.defaults import default_random_seed\nfrom ludwig.utils.metric_utils import TrainerMetric\nfrom ludwig.utils.metrics_printed_table import print_metrics_table\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom ludwig.utils.trainer_utils import append_metrics, get_new_progress_tracker, ProgressTracker\n\nlogger = logging.getLogger(__name__)\n\nMAX_EVALUATION_EXAMPLES = 1000\nMAX_EVALUATION_EXAMPLES_SHOWN = 5\n\n\n@register_llm_trainer(\"none\")\n@register_llm_ray_trainer(\"none\")\nclass NoneTrainer(BaseTrainer):\n    \"\"\"NoneTrainer is a trainer that does not train a model, only runs evaluation.\"\"\"\n\n    def __init__(\n        self,\n        config: NoneTrainerConfig,\n        model: LLM,\n        resume: float = False,\n        skip_save_model: bool = False,\n        skip_save_progress: bool = False,\n        skip_save_log: bool = False,\n        callbacks: list = None,\n        report_tqdm_to_ray=False,\n        random_seed: float = default_random_seed,\n        distributed: DistributedStrategy | None = None,\n        device: str | None = None,\n        **kwargs,\n    ):\n        \"\"\"\n        :param config: `ludwig.schema.trainer.NoneTrainerConfig` instance that specifies training hyperparameters\n        (default: `ludwig.schema.trainer.NoneTrainerConfig()`).\n        :param model: Underlying Ludwig model\n        :type model: `ludwig.models.llm.LLM`\n        :param resume: Resume training a model that was being trained. (default: False).\n        :type resume: Boolean\n        :param skip_save_model: Disables saving model weights and hyperparameters each time the model improves. By\n                default Ludwig saves model weights after each round of evaluation the validation metric (improves, but\n                if the model is really big that can be time consuming. If you do not want to keep the weights and just\n                find out what performance a model can get with a set of hyperparameters, use this parameter to skip it,\n                but the model will not be loadable later on. (default: False).\n        :type skip_save_model: Boolean\n        :param skip_save_progress: Disables saving progress each round of evaluation. By default Ludwig saves weights\n                and stats after each round of evaluation for enabling resuming of training, but if the model is really\n                big that can be time consuming and will uses twice as much space, use this parameter to skip it, but\n                training cannot be resumed later on. (default: False).\n        :type skip_save_progress: Boolean\n        :param skip_save_log: Disables saving TensorBoard logs. By default Ludwig saves logs for the TensorBoard, but if\n                it is not needed turning it off can slightly increase the overall speed. (default: False).\n        :type skip_save_log: Boolean\n        :param callbacks: List of `ludwig.callbacks.Callback` objects that provide hooks into the Ludwig pipeline.\n                (default: None).\n        :type callbacks: list\n        :param report_tqdm_to_ray: Enables using the ray based tqdm Callback for progress bar reporting\n        :param random_seed: Default initialization for the random seeds (default: 42).\n        :type random_seed: Float\n        :param distributed: Distributed strategy (default: None).\n        :type distributed: `DistributedStrategy`\n        :param device: Device to load the model on from a saved checkpoint (default: None).\n        :type device: str\n        \"\"\"\n\n        super().__init__()\n\n        # Ensure distributed strategy is initialized for metric sync_context.\n        # NoneTrainer may run on the head node (not in a Ray Train worker),\n        # so init_dist_strategy may not have been called yet.\n        from ludwig.distributed import init_dist_strategy\n\n        init_dist_strategy(\"local\")\n\n        self.config = config\n        self.distributed = distributed if distributed is not None else LocalStrategy()\n        self.skip_save_log = skip_save_log\n        self.resume = resume\n        self.skip_save_model = skip_save_model\n        self.skip_save_progress = skip_save_progress\n        self.random_seed = random_seed\n        self.callbacks = callbacks or []\n        self.report_tqdm_to_ray = report_tqdm_to_ray\n\n        self.device = device if device is not None else get_torch_device()\n        self.model = model.to_device(self.device)\n        self.model.metrics_to_device(self.device)\n\n        # Since we are only running evaluation without training, set the model to evaluation mode.\n        self.model.eval()\n\n        self.batch_size = self.config.batch_size\n        self.eval_batch_size = self.config.eval_batch_size\n        self.base_learning_rate = self.config.base_learning_rate\n        self.should_shuffle = self.config.should_shuffle\n        self.epochs = self.config.epochs\n        self.train_steps = self.config.train_steps\n        self.steps_per_checkpoint = self.config.steps_per_checkpoint\n        self.checkpoints_per_epoch = self.config.checkpoints_per_epoch\n        self.early_stop = self.config.early_stop\n        self.evaluate_training_set = self.config.evaluate_training_set\n        self.skip_all_evaluation = self.config.skip_all_evaluation\n\n    def close_writers(\n        self, progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer\n    ):\n        # ================ Finished Training ================\n        self.callback(\n            lambda c: c.on_trainer_train_teardown(self, progress_tracker, save_path, self.is_coordinator()),\n            coordinator_only=False,\n        )\n\n        if train_summary_writer is not None:\n            train_summary_writer.close()\n        if validation_summary_writer is not None:\n            validation_summary_writer.close()\n        if test_summary_writer is not None:\n            test_summary_writer.close()\n\n    def train(\n        self,\n        training_set: Dataset,\n        validation_set: Dataset | None = None,\n        test_set: Dataset | None = None,\n        save_path: str = MODEL_FILE_NAME,\n        return_state_dict: bool = False,\n        **kwargs,\n    ):\n        output_features = self.model.output_features\n\n        # ====== Setup file names =======\n        tensorboard_log_dir = None\n        if self.is_coordinator():\n            os.makedirs(save_path, exist_ok=True)\n            tensorboard_log_dir = os.path.join(save_path, \"logs\")\n\n        self.callback(\n            lambda c: c.on_trainer_train_setup(self, save_path, self.is_coordinator()), coordinator_only=False\n        )\n\n        train_summary_writer = None\n        validation_summary_writer = None\n        test_summary_writer = None\n        if self.is_coordinator() and not self.skip_save_log and tensorboard_log_dir:\n            train_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TRAINING))\n            if validation_set is not None and validation_set.size > 0:\n                validation_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, VALIDATION))\n            if test_set is not None and test_set.size > 0:\n                test_summary_writer = SummaryWriter(os.path.join(tensorboard_log_dir, TEST))\n\n        set_random_seed(self.random_seed)\n\n        progress_tracker = get_new_progress_tracker(\n            batch_size=self.batch_size,\n            learning_rate=self.base_learning_rate,\n            best_eval_metric_value=get_initial_validation_value(self.validation_metric),\n            best_increase_batch_size_eval_metric=get_initial_validation_value(self.validation_metric),\n            output_features=output_features,\n        )\n\n        # When running with Ray, we only need to return the state dict, as it's faster and cheaper to send the\n        # state dict over the network than to load the model state here, serialize it back to a state dict, then\n        # load it back on the head node.\n        return_value = self.model if not return_state_dict else self.model.cpu().state_dict()\n\n        if self.skip_all_evaluation:\n            self.close_writers(\n                progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer\n            )\n            return (\n                return_value,\n                progress_tracker.train_metrics,\n                progress_tracker.validation_metrics,\n                progress_tracker.test_metrics,\n            )\n\n        try:\n            self.run_evaluation(\n                training_set,\n                validation_set,\n                test_set,\n                progress_tracker,\n                train_summary_writer,\n                validation_summary_writer,\n                test_summary_writer,\n                output_features,\n                save_path,\n            )\n        finally:\n            self.close_writers(\n                progress_tracker, save_path, train_summary_writer, validation_summary_writer, test_summary_writer\n            )\n\n        return (\n            return_value,\n            progress_tracker.train_metrics,\n            progress_tracker.validation_metrics,\n            progress_tracker.test_metrics,\n        )\n\n    def train_online(\n        self,\n        dataset,\n    ):\n        pass\n\n    def tune_batch_size(\n        self,\n        config: ModelConfigDict,\n        training_set: Dataset,\n        random_seed: int = default_random_seed,\n        max_trials: int = 20,\n        halving_limit: int = 3,\n        snapshot_weights: bool = True,\n        on_best_batch_size_updated: Callable[[int, float, int], None] | None = None,\n        tune_for_training: bool = True,\n    ) -> int:\n        # TODO: Implement batch size tuning for LLM, currently just returns the default batch size\n        # Compared to ECD, this just requires forward passes till we OOM.\n        # https://github.com/ludwig-ai/ludwig/issues/3525\n        return MINIMUM_BATCH_SIZE\n\n    @property\n    def validation_field(self):\n        return self.config.validation_field\n\n    @property\n    def validation_metric(self):\n        return self.config.validation_metric\n\n    # Remote implementations may override this\n    def shutdown(self):\n        pass\n\n    @property\n    def local_rank(self) -> int:\n        return 0\n\n    def barrier(self):\n        pass\n\n    # Functions needed to treat Trainer as a context manager\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_val, exc_tb):\n        self.shutdown()\n\n    @staticmethod\n    def get_schema_cls() -> BaseTrainerConfig:\n        return NoneTrainerConfig\n\n    def is_coordinator(self) -> bool:\n        return self.distributed.rank() == 0\n\n    def callback(self, fn, coordinator_only=True):\n        if not coordinator_only or self.is_coordinator():\n            for callback in self.callbacks:\n                fn(callback)\n\n    def evaluation(\n        self,\n        dataset: \"Dataset\",  # noqa: F821\n        dataset_name: str,\n        metrics_log: dict[str, dict[str, list[TrainerMetric]]],\n        batch_size: int,\n        progress_tracker: ProgressTracker,\n    ):\n        predictor = LlmPredictor(\n            self.model, batch_size=batch_size, distributed=self.distributed, report_tqdm_to_ray=self.report_tqdm_to_ray\n        )\n        metrics, _ = predictor.batch_evaluation(dataset, collect_predictions=False, dataset_name=dataset_name)\n\n        return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker)\n\n    @classmethod\n    def write_eval_summary(\n        cls,\n        summary_writer,\n        metrics,\n        step,\n    ):\n        if not summary_writer:\n            return\n\n        for feature_name, output_feature in metrics.items():\n            for metric_name, metrics in output_feature.items():\n                if metrics:\n                    metric_tag = f\"{feature_name}/epoch_{metric_name}\"\n                    metric_val = metrics[-1][-1]\n                    summary_writer.add_scalar(metric_tag, metric_val, global_step=step)\n        summary_writer.flush()\n\n    def run_evaluation(\n        self,\n        training_set: Union[\"Dataset\", \"RayDataset\"],  # noqa: F821\n        validation_set: Union[\"Dataset\", \"RayDataset\"] | None,  # noqa: F821\n        test_set: Union[\"Dataset\", \"RayDataset\"] | None,  # noqa: F821\n        progress_tracker: ProgressTracker,\n        train_summary_writer: SummaryWriter,\n        validation_summary_writer: SummaryWriter,\n        test_summary_writer: SummaryWriter,\n        output_features: LudwigFeatureDict,\n        save_path: str,\n    ) -> bool:\n        \"\"\"Runs evaluation over training, validation, and test sets.\n\n        Also:\n        - Prints results, saves results to the progress tracker.\n        - Saves the model if the validation score is the best so far\n        - If there is no validation set, the model is always saved.\n\n        Returns whether the trainer should early stop, based on validation metrics history.\n        \"\"\"\n        start_time = time.time()\n        self.callback(lambda c: c.on_eval_start(self, progress_tracker, save_path))\n\n        progress_tracker.checkpoint_number += 1\n        if self.is_coordinator():\n            logger.info(f\"\\nRunning evaluation for step: {progress_tracker.steps}, epoch: {progress_tracker.epoch}\")\n\n        # ================ Eval ================\n        # Run a separate pass over the training data to compute metrics\n        # Appends results to progress_tracker.train_metrics.\n        if self.evaluate_training_set:\n            self.evaluation(\n                training_set, \"train\", progress_tracker.train_metrics, self.eval_batch_size, progress_tracker\n            )\n\n        self.write_eval_summary(\n            summary_writer=train_summary_writer,\n            metrics=progress_tracker.train_metrics,\n            step=progress_tracker.steps,\n        )\n\n        if validation_set is not None:\n            self.callback(lambda c: c.on_validation_start(self, progress_tracker, save_path))\n\n            # eval metrics on validation set\n            self.evaluation(\n                validation_set,\n                VALIDATION,\n                progress_tracker.validation_metrics,\n                self.eval_batch_size,\n                progress_tracker,\n            )\n\n            self.write_eval_summary(\n                summary_writer=validation_summary_writer,\n                metrics=progress_tracker.validation_metrics,\n                step=progress_tracker.steps,\n            )\n\n            self.callback(lambda c: c.on_validation_end(self, progress_tracker, save_path))\n\n        if test_set is not None:\n            self.callback(lambda c: c.on_test_start(self, progress_tracker, save_path))\n\n            # eval metrics on test set\n            self.evaluation(test_set, TEST, progress_tracker.test_metrics, self.eval_batch_size, progress_tracker)\n\n            self.write_eval_summary(\n                summary_writer=test_summary_writer,\n                metrics=progress_tracker.test_metrics,\n                step=progress_tracker.steps,\n            )\n\n            self.callback(lambda c: c.on_test_end(self, progress_tracker, save_path))\n\n        elapsed_time = (time.time() - start_time) * 1000.0\n\n        if self.is_coordinator():\n            logger.info(f\"Evaluation took {time_utils.strdelta(elapsed_time)}\\n\")\n            print_metrics_table(\n                output_features,\n                progress_tracker.train_metrics,\n                progress_tracker.validation_metrics,\n                progress_tracker.test_metrics,\n            )\n\n        # Trigger eval end callback after any model weights save for complete checkpoint\n        self.callback(lambda c: c.on_eval_end(self, progress_tracker, save_path))\n\n        return False\n\n\n@register_llm_trainer(\"finetune\")\nclass FineTuneTrainer(Trainer):\n    @staticmethod\n    def get_schema_cls():\n        return FineTuneTrainerConfig\n\n    def __init__(\n        self,\n        config: FineTuneTrainerConfig,\n        model: LLM,\n        resume: float = False,\n        skip_save_model: bool = False,\n        skip_save_progress: bool = False,\n        skip_save_log: bool = False,\n        callbacks: list = None,\n        report_tqdm_to_ray=False,\n        random_seed: int = default_random_seed,\n        distributed: DistributedStrategy | None = None,\n        device: str | None = None,\n        **kwargs,\n    ):\n        super().__init__(\n            config,\n            model,\n            resume,\n            skip_save_model,\n            skip_save_progress,\n            skip_save_log,\n            callbacks,\n            report_tqdm_to_ray,\n            random_seed,\n            distributed,\n            device,\n            **kwargs,\n        )\n\n    def evaluation(self, dataset, dataset_name, metrics_log, batch_size, progress_tracker):\n        predictor = LlmFineTunePredictor(\n            self.model, batch_size=batch_size, distributed=self.distributed, report_tqdm_to_ray=self.report_tqdm_to_ray\n        )\n        metrics, _, input_target_output_dict = predictor.batch_evaluation(\n            dataset, collect_predictions=False, dataset_name=dataset_name\n        )\n        # Setting collect_predictions=True currently causes an error when doing batch evaluation because the outputs\n        # can be of variable sizes but we try to concatenate them into a single tensor.\n\n        tokenizer = self.dist_model.tokenizer\n\n        # There should only be one key in the dict for LLMs\n        input_key = list(input_target_output_dict[\"inputs\"].keys())[0]\n        num_examples = min(len(input_target_output_dict[\"inputs\"][input_key]), MAX_EVALUATION_EXAMPLES)\n\n        llm_eval_examples = {\"inputs\": [], \"targets\": [], \"outputs\": []}\n        for key in input_target_output_dict[\"inputs\"]:\n            for inp in input_target_output_dict[\"inputs\"][key][:num_examples]:\n                llm_eval_examples[\"inputs\"].append(tokenizer.decode(inp, skip_special_tokens=True))\n\n        for key in input_target_output_dict[\"targets\"]:\n            for tar in input_target_output_dict[\"targets\"][key][:num_examples]:\n                llm_eval_examples[\"targets\"].append(tokenizer.decode(tar, skip_special_tokens=True))\n\n        for key in input_target_output_dict[\"outputs\"]:\n            for out in input_target_output_dict[\"outputs\"][key][:num_examples]:\n                llm_eval_examples[\"outputs\"].append(tokenizer.decode(out, skip_special_tokens=True))\n\n        num_examples_shown = min(len(llm_eval_examples[\"inputs\"]), MAX_EVALUATION_EXAMPLES_SHOWN)\n        for i in range(num_examples_shown):\n            logger.info(f\"Input: {llm_eval_examples['inputs'][i].strip()}\")\n            logger.info(f\"Output: {llm_eval_examples['outputs'][i].strip()}\")\n            logger.info(\"--------------------\")\n\n        progress_tracker.llm_eval_examples = llm_eval_examples\n        return append_metrics(self.model, dataset_name, metrics, metrics_log, progress_tracker)\n\n    def tune_batch_size(\n        self,\n        config: ModelConfigDict,\n        training_set: Dataset,\n        random_seed: int = default_random_seed,\n        max_trials: int = 20,\n        halving_limit: int = 3,\n        snapshot_weights: bool = True,\n        on_best_batch_size_updated: Callable[[int, float, int], None] | None = None,\n        tune_for_training: bool = True,\n        global_max_sequence_length: int | None = None,\n    ) -> int:\n        if global_max_sequence_length is None:\n            global_max_sequence_length = self.model.global_max_sequence_length\n        return super().tune_batch_size(\n            config,\n            training_set,\n            random_seed,\n            max_trials,\n            halving_limit,\n            snapshot_weights,\n            on_best_batch_size_updated,\n            tune_for_training,\n            global_max_sequence_length,\n        )\n\n    def _create_batch_size_evaluator(self) -> BatchSizeEvaluator:\n        return LLMFinetuneTrainerBatchSizeEvaluator(self)\n\n    def _create_predict_batch_size_evaluator(self) -> BatchSizeEvaluator:\n        return LLMFinetunePredictBatchSizeEvaluator(self)\n\n\nclass RemoteLLMTrainer(NoneTrainer):\n    def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs):\n        super().__init__(**kwargs)\n\n        # Only return results from rank 0 to reduce network overhead\n        self.train = self.distributed.return_first(self.train)\n        self.train_online = self.distributed.return_first(self.train_online)\n\n\nclass RemoteLLMFineTuneTrainer(FineTuneTrainer):\n    def __init__(self, gpus=None, gpu_memory_limit=None, allow_parallel_threads=True, **kwargs):\n        super().__init__(**kwargs)\n\n        # Only return results from rank 0 to reduce network overhead\n        self.train = self.distributed.return_first(self.train)\n        self.train_online = self.distributed.return_first(self.train_online)\n"
  },
  {
    "path": "ludwig/types.py",
    "content": "\"\"\"Public API: Common typing for Ludwig dictionary parameters.\"\"\"\n\nfrom typing import Any\n\nFeatureConfigDict = dict[str, Any]\n\"\"\"Dictionary of parameters used to configure an input or output feature.\n\nhttps://ludwig.ai/latest/configuration/features/supported_data_types/\n\"\"\"\n\nModelConfigDict = dict[str, Any]\n\"\"\"Dictionary representation of the ModelConfig object.\n\nhttps://ludwig.ai/latest/configuration/\n\"\"\"\n\nTrainingSetMetadataDict = dict[str, Any]\n\"\"\"Training set metadata, which consists of internal configuration parameters.\"\"\"\n\nPreprocessingConfigDict = dict[str, Any]\n\"\"\"Dictionary of parameters used to configure preprocessing.\n\nMay be type-defaults global preprocessing or feature-specific preprocessing.\nhttps://ludwig.ai/latest/configuration/preprocessing/\n\"\"\"\n\nHyperoptConfigDict = dict[str, Any]\n\"\"\"Dictionary of parameters used to configure hyperopt.\n\nhttps://ludwig.ai/latest/configuration/hyperparameter_optimization/\n\"\"\"\n\nTrainerConfigDict = dict[str, Any]\n\"\"\"Dictionary of parameters used to configure training.\n\nhttps://ludwig.ai/latest/configuration/trainer/\n\"\"\"\n\nFeatureTypeDefaultsDict = dict[str, FeatureConfigDict]\n\"\"\"Dictionary of type to parameters that configure the defaults for that feature type.\n\nhttps://ludwig.ai/latest/configuration/defaults/\n\"\"\"\n\nFeatureMetadataDict = dict[str, Any]\n\"\"\"Metadata for a specific feature like idx2str.\"\"\"\n\nFeaturePostProcessingOutputDict = dict[str, Any]\n\"\"\"Output from feature post-processing.\"\"\"\n"
  },
  {
    "path": "ludwig/upload.py",
    "content": "import argparse\nimport logging\nimport os\nimport sys\n\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.utils.print_utils import get_logging_level_registry\nfrom ludwig.utils.upload_utils import HuggingFaceHub, Predibase\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_upload_registry():\n    return {\n        \"hf_hub\": HuggingFaceHub,\n        \"predibase\": Predibase,\n    }\n\n\ndef upload_cli(\n    service: str,\n    repo_id: str,\n    model_path: str,\n    repo_type: str = \"model\",\n    private: bool = False,\n    commit_message: str = \"Upload trained [Ludwig](https://ludwig.ai/latest/) model weights\",\n    commit_description: str | None = None,\n    dataset_file: str | None = None,\n    dataset_name: str | None = None,\n    **kwargs,\n) -> None:\n    \"\"\"Create an empty repo on the HuggingFace Hub and upload trained model artifacts to that repo.\n\n    Args:\n        service (`str`):\n            Name of the hosted model service to push the trained artifacts to.\n            Currently, this only supports `hf_hub` and `predibase`.\n        repo_id (`str`):\n            A namespace (user or an organization) and a repo name separated\n            by a `/`.\n        model_path (`str`):\n            The path of the saved model. This is the parent-folder of the folder\n            where the 'model_weights' folder and the 'model_hyperparameters.json' file\n            are stored.\n        private (`bool`, *optional*, defaults to `False`):\n            Whether the model repo should be private.\n        repo_type (`str`, *optional*):\n            Set to `\"dataset\"` or `\"space\"` if uploading to a dataset or\n            space, `None` or `\"model\"` if uploading to a model. Default is\n            `None`.\n        commit_message (`str`, *optional*):\n            The summary / title / first line of the generated commit. Defaults to:\n            `f\"Upload {path_in_repo} with huggingface_hub\"`\n        commit_description (`str` *optional*):\n            The description of the generated commit\n        dataset_file (`str`, *optional*):\n            The path to the dataset file. Required if `service` is set to\n            `\"predibase\"` for new model repos.\n        dataset_name (`str`, *optional*):\n            The name of the dataset. Used by the `service`\n            `\"predibase\"`.\n    \"\"\"\n    model_service = get_upload_registry().get(service, \"hf_hub\")\n    hub: HuggingFaceHub = model_service()\n    if os.path.exists(os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)) and os.path.exists(\n        os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)\n    ):\n        experiment_path = model_path\n    elif os.path.exists(os.path.join(model_path, MODEL_WEIGHTS_FILE_NAME)) and os.path.exists(\n        os.path.join(model_path, MODEL_HYPERPARAMETERS_FILE_NAME)\n    ):\n        experiment_path = os.path.normpath(os.path.join(model_path, \"..\"))\n    else:\n        raise ValueError(\n            f\"Can't find 'model_weights' and '{MODEL_HYPERPARAMETERS_FILE_NAME}' either at \"\n            f\"'{model_path}' or at '{model_path}/model'\"\n        )\n    hub.upload(\n        repo_id=repo_id,\n        model_path=experiment_path,\n        repo_type=repo_type,\n        private=private,\n        commit_message=commit_message,\n        commit_description=commit_description,\n        dataset_file=dataset_file,\n        dataset_name=dataset_name,\n    )\n\n\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script pushes a trained model to a hosted model repository service\",\n        prog=\"ludwig upload\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    # ---------------\n    # Required parameters\n    # ---------------\n    parser.add_argument(\n        \"service\",\n        help=\"Name of the model repository service.\",\n        default=\"hf_hub\",\n        choices=[\"hf_hub\", \"predibase\"],\n    )\n\n    parser.add_argument(\n        \"-r\",\n        \"--repo_id\",\n        help=\"Name of the repo. This will be created if it doesn't exist. Format: username/repo_name\",\n        required=True,\n    )\n\n    parser.add_argument(\"-m\", \"--model_path\", help=\"Path of the trained model on disk\", required=True)\n\n    # ---------------\n    # Optional parameters\n    # ---------------\n    parser.add_argument(\"-p\", \"--private\", help=\"Make the repo private\", default=False, choices=[True, False])\n\n    parser.add_argument(\n        \"-t\", \"--repo_type\", help=\"Type of repo\", default=\"model\", choices=[\"model\", \"space\", \"dataset\"]\n    )\n\n    parser.add_argument(\n        \"-c\",\n        \"--commit_message\",\n        help=\"The summary / title / first line of the generated commit.\",\n        default=\"Upload trained [Ludwig](https://ludwig.ai/latest/) model weights\",\n    )\n\n    parser.add_argument(\"-d\", \"--commit_description\", help=\"The description of the generated commit\", default=None)\n\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"The level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    parser.add_argument(\"-df\", \"--dataset_file\", help=\"The location of the dataset file\", default=None)\n    parser.add_argument(\n        \"-dn\", \"--dataset_name\", help=\"(Optional) The name of the dataset in the Provider\", default=None\n    )\n\n    args = parser.parse_args(sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.upload\")\n\n    upload_cli(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "ludwig/utils/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/utils/algorithms_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom ludwig.constants import TIED\n\n\ndef topological_sort(graph_unsorted):\n    \"\"\"Repeatedly go through all of the nodes in the graph, moving each of the nodes that has all its edges\n    resolved, onto a sequence that forms our sorted graph.\n\n    A node has all of its edges resolved and can be moved once all the nodes its edges point to, have been moved from\n    the unsorted graph onto the sorted one.\n    \"\"\"\n\n    # This is the list we'll return, that stores each node/edges pair\n    # in topological order.\n    graph_sorted = []\n\n    # Convert the unsorted graph into a hash table. This gives us\n    # constant-time lookup for checking if edges are unresolved, and\n    # for removing nodes from the unsorted graph.\n    graph_unsorted = dict(graph_unsorted)\n\n    # Run until the unsorted graph is empty.\n    while graph_unsorted:\n        # Go through each of the node/edges pairs in the unsorted\n        # graph. If a set of edges does not contain any nodes that\n        # haven't been resolved, that is, that are still in the\n        # unsorted graph, remove the pair from the unsorted graph,\n        # and append it to the sorted graph. Note here that by using\n        # using the items() method for iterating, a copy of the\n        # unsorted graph is used, allowing us to modify the unsorted\n        # graph as we move through it. We also keep a flag for\n        # checking that that graph is acyclic, which is true if any\n        # nodes are resolved during each pass through the graph. If\n        # not, we need to bail out as the graph therefore can't be\n        # sorted.\n        acyclic = False\n        for node, edges in list(graph_unsorted.items()):\n            if edges is None:\n                edges = []\n            for edge in edges:\n                if edge in graph_unsorted:\n                    break\n            else:\n                acyclic = True\n                del graph_unsorted[node]\n                graph_sorted.append((node, edges))\n\n        if not acyclic:\n            # Uh oh, we've passed through all the unsorted nodes and\n            # weren't able to resolve any of them, which means there\n            # are nodes with cyclic edges that will never be resolved,\n            # so we bail out with an error.\n            raise RuntimeError(\"A cyclic dependency occurred\")\n\n    return graph_sorted\n\n\ndef topological_sort_feature_dependencies(features):\n    # topological sorting of output features for resolving dependencies\n    dependencies_graph = {}\n    output_features_dict = {}\n    for feature in features:\n        dependencies = []\n        if \"dependencies\" in feature:\n            dependencies.extend(feature[\"dependencies\"])\n        if TIED in feature:\n            dependencies.append(feature[TIED])\n        dependencies_graph[feature[\"name\"]] = dependencies\n        output_features_dict[feature[\"name\"]] = feature\n    return [output_features_dict[node[0]] for node in topological_sort(dependencies_graph)]\n"
  },
  {
    "path": "ludwig/utils/audio_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport functools\nimport logging\nfrom io import BytesIO\nfrom typing import Any\n\nimport torch\nimport torch.nn.functional as F\nimport torchaudio\nfrom packaging import version\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DEFAULT_AUDIO_TENSOR_LENGTH\nfrom ludwig.utils.types import TorchAudioTuple\n\nlogger = logging.getLogger(__name__)\n\n# https://github.com/pytorch/audio/blob/main/torchaudio/csrc/sox/types.cpp\nAUDIO_EXTENSIONS = (\".wav\", \".amb\", \".mp3\", \".ogg\", \".vorbis\", \".flac\", \".opus\", \".sphere\")\n\n\n_TORCH_AUDIO_210 = version.parse(torchaudio.__version__) >= version.parse(\"2.1.0\")\n_TORCH_AUDIO_201 = version.parse(torchaudio.__version__) >= version.parse(\"2.0.1\")\n\n\n@DeveloperAPI\ndef is_torch_audio_tuple(audio: Any) -> bool:\n    if isinstance(audio, tuple):\n        if len(audio) == 2 and isinstance(audio[0], torch.Tensor) and isinstance(audio[1], int):\n            return True\n    return False\n\n\n@DeveloperAPI\ndef get_default_audio(audio_lst: list[TorchAudioTuple]) -> TorchAudioTuple:\n    if not audio_lst:\n        # Return a silent audio tensor as default when no valid audio is available\n        default_audio_tensor = torch.zeros(1, DEFAULT_AUDIO_TENSOR_LENGTH)\n        return default_audio_tensor, 16000\n\n    sampling_rates = [audio[1] for audio in audio_lst]\n    tensor_list = [audio[0] for audio in audio_lst]\n\n    for i, tensor in enumerate(tensor_list):\n        if tensor.shape[1] > DEFAULT_AUDIO_TENSOR_LENGTH:\n            tensor_list[i] = tensor[:, :DEFAULT_AUDIO_TENSOR_LENGTH]\n        else:\n            pad_size = DEFAULT_AUDIO_TENSOR_LENGTH - tensor.shape[1]\n            tensor_list[i] = F.pad(tensor, (0, pad_size))\n    default_audio_tensor = torch.mean(torch.stack(tensor_list), dim=0)\n    default_sampling_rate = calculate_mean(sum(sampling_rates), len(sampling_rates))\n\n    return default_audio_tensor, default_sampling_rate\n\n\n@DeveloperAPI\ndef read_audio_from_path(path: str) -> TorchAudioTuple | None:\n    \"\"\"Reads audio from path.\n\n    Useful for reading from a small number of paths. For more intensive reads, use backend.read_binary_files instead.\n    \"\"\"\n    try:\n        if _TORCH_AUDIO_210:\n            return torchaudio.load(path, backend=\"sox\")\n        elif _TORCH_AUDIO_201:\n            return torchaudio.backend.sox_io_backend.load(path)\n        else:\n            return torchaudio.backend.sox_backend.load(path)\n    except Exception as e:\n        logger.warning(e)\n        return None\n\n\n@DeveloperAPI\n@functools.lru_cache(maxsize=32)\ndef read_audio_from_bytes_obj(bytes_obj: bytes) -> TorchAudioTuple | None:\n    try:\n        f = BytesIO(bytes_obj)\n        return torchaudio.load(f)\n    except Exception as e:\n        logger.warning(e)\n        return None\n\n\ndef _pre_emphasize_data(data: torch.Tensor, emphasize_value: float = 0.97):\n    # Increase precision in order to achieve parity with scipy.signal.lfilter implementation\n    filter_window = torch.tensor([1.0, -emphasize_value], dtype=torch.float64, device=data.device)\n    a_coeffs = torch.tensor([1, 0], dtype=torch.float64, device=data.device)\n    pre_emphasized_data = torchaudio.functional.lfilter(\n        data.to(dtype=torch.float64),\n        a_coeffs,\n        filter_window,\n        clamp=False,\n    ).to(torch.float32)\n    return pre_emphasized_data\n\n\n@DeveloperAPI\ndef get_length_in_samp(sampling_rate_in_hz: float | int, length_in_s: float | int) -> int:\n    return int(sampling_rate_in_hz * length_in_s)\n\n\n@DeveloperAPI\ndef get_group_delay(\n    raw_data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n):\n    X_stft_transform = _get_stft(\n        raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type\n    )\n    Y_stft_transform = _get_stft(\n        raw_data,\n        sampling_rate_in_hz,\n        window_length_in_s,\n        window_shift_in_s,\n        num_fft_points,\n        window_type=window_type,\n        data_transformation=\"group_delay\",\n    )\n    X_stft_transform_real = torch.real(X_stft_transform)\n    X_stft_transform_imag = torch.imag(X_stft_transform)\n    Y_stft_transform_real = torch.real(Y_stft_transform)\n    Y_stft_transform_imag = torch.imag(Y_stft_transform)\n    nominator = torch.multiply(X_stft_transform_real, Y_stft_transform_real) + torch.multiply(\n        X_stft_transform_imag, Y_stft_transform_imag\n    )\n    denominator = torch.square(torch.abs(X_stft_transform))\n    group_delay = torch.divide(nominator, denominator + 1e-10)\n    assert not torch.isnan(group_delay).any(), \"There are NaN values in group delay\"\n    return torch.transpose(group_delay, 0, 1)\n\n\n@DeveloperAPI\ndef get_phase_stft_magnitude(\n    raw_data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n) -> torch.Tensor:\n    stft = _get_stft(\n        raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type\n    )\n    abs_stft = torch.abs(stft)\n    phase = torch.angle(stft)\n    stft_phase = torch.cat([phase, abs_stft], dim=1)\n    return torch.transpose(stft_phase, 0, 1)\n\n\n@DeveloperAPI\ndef get_stft_magnitude(\n    raw_data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n):\n    stft = _get_stft(\n        raw_data, sampling_rate_in_hz, window_length_in_s, window_shift_in_s, num_fft_points, window_type=window_type\n    )\n    stft_magnitude = torch.abs(stft)\n    return torch.transpose(stft_magnitude, 0, 1)\n\n\n################################################################################\n# The following code for FBank is adapted from jameslyons/python_speech_features\n# MIT licensed implementation\n# https://github.com/jameslyons/python_speech_features/blob/40c590269b57c64a8c1f1ddaaff2162008d1850c/python_speech_features/base.py#L84################################################################################\n################################################################################\n@DeveloperAPI\ndef get_fbank(\n    raw_data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n    num_filter_bands: int,\n) -> torch.Tensor:\n    stft = _get_stft(\n        raw_data,\n        sampling_rate_in_hz,\n        window_length_in_s,\n        window_shift_in_s,\n        num_fft_points,\n        window_type=window_type,\n        zero_mean_offset=True,\n    )\n    stft_power = torch.abs(stft) ** 2\n    upper_limit_freq = int(sampling_rate_in_hz / 2)\n    upper_limit_mel = _convert_hz_to_mel(upper_limit_freq)\n    lower_limit_mel = 0\n    list_mel_points = torch.linspace(lower_limit_mel, upper_limit_mel, num_filter_bands + 2, device=raw_data.device)\n    mel_fbank_matrix = _get_mel_fbank_matrix(list_mel_points, num_filter_bands, num_fft_points, sampling_rate_in_hz)\n    mel_fbank_feature = torch.matmul(stft_power, torch.transpose(mel_fbank_matrix, 0, 1))\n    log_mel_fbank_feature = torch.log(mel_fbank_feature + 1.0e-10)\n    return torch.transpose(log_mel_fbank_feature, 0, 1)\n\n\ndef _get_mel_fbank_matrix(\n    list_mel_points: torch.Tensor, num_filter_bands: int, num_fft_points: int, sampling_rate_in_hz: int\n) -> torch.Tensor:\n    num_ess_fft_points = get_non_symmetric_length(num_fft_points)\n    freq_scale = (num_fft_points + 1) / sampling_rate_in_hz\n    freq_bins_on_mel_scale = torch.floor(freq_scale * _convert_mel_to_hz(list_mel_points))\n    mel_scaled_fbank = torch.zeros(\n        (num_filter_bands, num_ess_fft_points), dtype=torch.float32, device=list_mel_points.device\n    )\n    for filt_idx in range(num_filter_bands):\n        start_bin_freq = freq_bins_on_mel_scale[filt_idx]\n        middle_bin_freq = freq_bins_on_mel_scale[filt_idx + 1]\n        end_bin_freq = freq_bins_on_mel_scale[filt_idx + 2]\n        mel_scaled_fbank[filt_idx] = _create_triangular_filter(\n            start_bin_freq, middle_bin_freq, end_bin_freq, num_ess_fft_points\n        )\n    return mel_scaled_fbank\n\n\ndef _create_triangular_filter(\n    start_bin_freq: torch.Tensor, middle_bin_freq: torch.Tensor, end_bin_freq: torch.Tensor, num_ess_fft_points: int\n):\n    filter_window = torch.zeros(num_ess_fft_points, dtype=torch.float32, device=start_bin_freq.device)\n    filt_support_begin = middle_bin_freq - start_bin_freq\n    filt_support_end = end_bin_freq - middle_bin_freq\n    for freq in range(int(start_bin_freq), int(middle_bin_freq)):\n        filter_window[freq] = (freq - start_bin_freq) / filt_support_begin\n    for freq in range(int(middle_bin_freq), int(end_bin_freq)):\n        filter_window[freq] = (end_bin_freq - freq) / filt_support_end\n    return filter_window\n\n\ndef _convert_hz_to_mel(hz: int) -> float:\n    return float(2595.0 * torch.log10(torch.tensor(1 + hz / 700.0)))\n\n\ndef _convert_mel_to_hz(mel):\n    return 700.0 * (10 ** (mel / 2595.0) - 1)\n\n\ndef _get_stft(\n    raw_data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n    data_transformation: str | None = None,\n    zero_mean_offset: bool = False,\n) -> torch.Tensor:\n    pre_emphasized_data = _pre_emphasize_data(raw_data)\n    stft = _short_time_fourier_transform(\n        pre_emphasized_data,\n        sampling_rate_in_hz,\n        window_length_in_s,\n        window_shift_in_s,\n        num_fft_points,\n        window_type,\n        data_transformation,\n        zero_mean_offset,\n    )\n    non_symmetric_stft = get_non_symmetric_data(stft)\n    return non_symmetric_stft\n\n\ndef _short_time_fourier_transform(\n    data: torch.Tensor,\n    sampling_rate_in_hz: int,\n    window_length_in_s: float,\n    window_shift_in_s: float,\n    num_fft_points: int,\n    window_type: str,\n    data_transformation: str | None = None,\n    zero_mean_offset: bool = False,\n) -> torch.Tensor:\n    window_length_in_samp: int = get_length_in_samp(window_length_in_s, sampling_rate_in_hz)\n    window_shift_in_samp: int = get_length_in_samp(window_shift_in_s, sampling_rate_in_hz)\n    preprocessed_data_matrix = _preprocess_to_padded_matrix(\n        data[0], window_length_in_samp, window_shift_in_samp, zero_mean_offset=zero_mean_offset\n    )\n    weighted_data_matrix = _weight_data_matrix(\n        preprocessed_data_matrix, window_type, data_transformation=data_transformation\n    )\n    fft = torch.fft.fft(weighted_data_matrix, n=num_fft_points)\n    return fft\n\n\ndef _preprocess_to_padded_matrix(\n    data: torch.Tensor, window_length_in_samp: int, window_shift_in_samp: int, zero_mean_offset: bool = False\n) -> torch.Tensor:\n    num_input = data.shape[0]\n    num_output = get_num_output_padded_to_fit_input(num_input, window_length_in_samp, window_shift_in_samp)\n    zero_padded_matrix = torch.zeros((num_output, window_length_in_samp), dtype=torch.float32, device=data.device)\n    for num_output_idx in range(num_output):\n        start_idx = window_shift_in_samp * num_output_idx\n        is_last_output = num_output_idx == num_output - 1\n        end_idx = start_idx + window_length_in_samp if not is_last_output else num_input\n        end_padded_idx = window_length_in_samp if not is_last_output else end_idx - start_idx\n        window_data = data[start_idx:end_idx]\n        if zero_mean_offset:\n            window_data = window_data - torch.mean(window_data)\n        zero_padded_matrix[num_output_idx, :end_padded_idx] = window_data\n    return zero_padded_matrix\n\n\n@DeveloperAPI\ndef get_num_output_padded_to_fit_input(num_input: int, window_length_in_samp: int, window_shift_in_samp: int) -> int:\n    num_output_valid = torch.tensor((num_input - window_length_in_samp) / window_shift_in_samp + 1)\n    return int(torch.ceil(num_output_valid))\n\n\n@DeveloperAPI\ndef get_window(window_type: str, window_length_in_samp: int, device: torch.device | None = None) -> torch.Tensor:\n    # Increase precision in order to achieve parity with scipy.signal.windows.get_window implementation\n    if window_type == \"bartlett\":\n        return torch.bartlett_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to(\n            torch.float32\n        )\n    elif window_type == \"blackman\":\n        return torch.blackman_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to(\n            torch.float32\n        )\n    elif window_type == \"hamming\":\n        return torch.hamming_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to(\n            torch.float32\n        )\n    elif window_type == \"hann\":\n        return torch.hann_window(window_length_in_samp, periodic=False, dtype=torch.float64, device=device).to(\n            torch.float32\n        )\n    else:\n        raise ValueError(f\"Unknown window type: {window_type}\")\n\n\n@DeveloperAPI\ndef is_audio_score(src_path):\n    # Used for AutoML\n    return int(isinstance(src_path, str) and src_path.lower().endswith(AUDIO_EXTENSIONS))\n\n\ndef _weight_data_matrix(\n    data_matrix: torch.Tensor, window_type: str, data_transformation: str | None = None\n) -> torch.Tensor:\n    window_length_in_samp = data_matrix[0].shape[0]\n    window = get_window(window_type, window_length_in_samp, device=data_matrix.device)\n    if data_transformation is not None and data_transformation == \"group_delay\":\n        window *= torch.arange(window_length_in_samp, device=data_matrix.device).float()\n    return data_matrix * window\n\n\n@DeveloperAPI\ndef get_non_symmetric_length(symmetric_length: int) -> int:\n    return int(symmetric_length / 2) + 1\n\n\n@DeveloperAPI\ndef get_non_symmetric_data(data: torch.Tensor) -> torch.Tensor:\n    num_fft_points = data.shape[-1]\n    num_ess_fft_points = get_non_symmetric_length(num_fft_points)\n    return data[:, :num_ess_fft_points]\n\n\n@DeveloperAPI\ndef get_max_length_stft_based(length_in_samp, window_length_in_s, window_shift_in_s, sampling_rate_in_hz):\n    window_length_in_samp = get_length_in_samp(window_length_in_s, sampling_rate_in_hz)\n    window_shift_in_samp = get_length_in_samp(window_shift_in_s, sampling_rate_in_hz)\n    return get_num_output_padded_to_fit_input(length_in_samp, window_length_in_samp, window_shift_in_samp)\n\n\n@DeveloperAPI\ndef calculate_incr_var(var_prev, mean_prev, mean, length):\n    return var_prev + (length - mean_prev) * (length - mean)\n\n\n@DeveloperAPI\ndef calculate_incr_mean(count, mean, length):\n    return mean + (length - mean) / float(count)\n\n\n@DeveloperAPI\ndef calculate_var(sum1, sum2, count):\n    return (sum2 - ((sum1 * sum1) / float(count))) / float(count - 1) if count > 1 else 0.0\n\n\n@DeveloperAPI\ndef calculate_mean(sum1, count):\n    return sum1 / float(count)\n"
  },
  {
    "path": "ludwig/utils/augmentation_utils.py",
    "content": "from ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.registry import Registry\n\n###\n# Registry for augmentation operations\n# Each augmentation operation is registered with the feature type it is applicable to\n# and the name of the operation.\n###\n_augmentation_op_registry = Registry()\n\n\n@DeveloperAPI\ndef get_augmentation_op_registry() -> Registry:\n    return _augmentation_op_registry\n\n\n@DeveloperAPI\ndef register_augmentation_op(name: str, features: str | list[str]):\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for feature in features:\n            augmentation_op_registry = get_augmentation_op_registry().get(feature, {})\n            augmentation_op_registry[name] = cls\n            get_augmentation_op_registry()[feature] = augmentation_op_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_augmentation_op(feature_type: str, op_name: str):\n    return get_augmentation_op_registry()[feature_type][op_name]\n\n\nclass AugmentationPipelines:\n    \"\"\"Container holding augmentation pipelines defined in the model.\"\"\"\n\n    def __init__(self, augmentation_pipelines: dict):\n        self.augmentation_pipelines = augmentation_pipelines\n\n    def __getitem__(self, key):\n        return self.augmentation_pipelines[key]\n\n    def __contains__(self, key):\n        return key in self.augmentation_pipelines\n\n    def __len__(self):\n        return len(self.augmentation_pipelines)\n\n    def __iter__(self):\n        return self.augmentation_pipelines.__iter__()\n\n    def items(self):\n        return self.augmentation_pipelines.items()\n"
  },
  {
    "path": "ludwig/utils/automl/__init__.py",
    "content": ""
  },
  {
    "path": "ludwig/utils/automl/data_source.py",
    "content": "from abc import ABC, abstractmethod\n\nimport dask.dataframe as dd\nimport pandas as pd\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.audio_utils import is_audio_score\nfrom ludwig.utils.automl.utils import avg_num_tokens\nfrom ludwig.utils.image_utils import is_image_score\nfrom ludwig.utils.misc_utils import memoized_method\nfrom ludwig.utils.types import DataFrame\n\n\n@DeveloperAPI\nclass DataSource(ABC):\n    @property\n    @abstractmethod\n    def columns(self) -> list[str]:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_dtype(self, column: str) -> str:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_distinct_values(self, column: str, max_values_to_return: int) -> tuple[int, list[str], float]:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_nonnull_values(self, column: str) -> int:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def get_avg_num_tokens(self, column: str) -> int:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def is_string_type(self, dtype: str) -> bool:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def size_bytes(self) -> int:\n        raise NotImplementedError()\n\n    @abstractmethod\n    def __len__(self) -> int:\n        raise NotImplementedError()\n\n\n@DeveloperAPI\nclass DataframeSourceMixin:\n    df: DataFrame\n\n    @property\n    def columns(self) -> list[str]:\n        return self.df.columns\n\n    def get_dtype(self, column: str) -> str:\n        return self.df[column].dtype.name\n\n    def get_distinct_values(self, column, max_values_to_return: int) -> tuple[int, list[str], float]:\n        unique_values = self.df[column].dropna().unique()\n        num_unique_values = len(unique_values)\n        unique_values_counts = self.df[column].value_counts()\n        if len(unique_values_counts) != 0:\n            unique_majority_values = unique_values_counts[unique_values_counts.idxmax()]\n            unique_minority_values = unique_values_counts[unique_values_counts.idxmin()]\n            unique_values_balance = unique_minority_values / unique_majority_values\n        else:\n            unique_values_balance = 1.0\n        return num_unique_values, unique_values[:max_values_to_return], unique_values_balance\n\n    def get_nonnull_values(self, column: str) -> int:\n        return len(self.df[column].notnull())\n\n    def get_image_values(self, column: str, sample_size: int = 10) -> int:\n        return int(sum(is_image_score(x) for x in self.df[column].head(sample_size)))\n\n    def get_audio_values(self, column: str, sample_size: int = 10) -> int:\n        return int(sum(is_audio_score(x) for x in self.df[column].head(sample_size)))\n\n    def get_avg_num_tokens(self, column: str) -> int:\n        return avg_num_tokens(self.df[column])\n\n    def is_string_type(self, dtype: str) -> bool:\n        return dtype in [\"str\", \"string\", \"object\"]\n\n    def size_bytes(self) -> int:\n        return sum(self.df.memory_usage(deep=True))\n\n    def __len__(self) -> int:\n        return len(self.df)\n\n\n@DeveloperAPI\nclass DataframeSource(DataframeSourceMixin, DataSource):\n    def __init__(self, df):\n        self.df = df\n\n\n@DeveloperAPI\nclass DaskDataSource(DataframeSource):\n    @memoized_method(maxsize=1)\n    def get_sample(self) -> pd.DataFrame:\n        # TODO: uniform random sample\n        return self.df.head(10000)\n\n    @property\n    def sample(self) -> pd.DataFrame:\n        return self.get_sample()\n\n    def get_distinct_values(self, column, max_values_to_return) -> tuple[int, list[str], float]:\n        unique_values = self.df[column].drop_duplicates().dropna().persist()\n        num_unique_values = len(unique_values)\n\n        # TODO(travis): implement imbalance ratio\n        imbalance_ratio = 1.0\n        return num_unique_values, unique_values.head(max_values_to_return), imbalance_ratio\n\n    def get_nonnull_values(self, column) -> int:\n        return self.df[column].notnull().sum().compute()\n\n    def get_image_values(self, column: str, sample_size: int = 10) -> int:\n        return int(sum(is_image_score(x) for x in self.sample[column].head(sample_size)))\n\n    def get_audio_values(self, column: str, sample_size: int = 10) -> int:\n        return int(sum(is_audio_score(x) for x in self.sample[column].head(sample_size)))\n\n    def get_avg_num_tokens(self, column) -> int:\n        return avg_num_tokens(self.sample[column])\n\n\n@DeveloperAPI\ndef wrap_data_source(df: DataFrame) -> DataSource:\n    if isinstance(df, dd.DataFrame):\n        return DaskDataSource(df)\n    return DataframeSource(df)\n"
  },
  {
    "path": "ludwig/utils/automl/field_info.py",
    "content": "from dataclasses import dataclass\n\nfrom dataclasses_json import dataclass_json, LetterCase\n\nfrom ludwig.api_annotations import DeveloperAPI\n\n\n@DeveloperAPI\n@dataclass_json(letter_case=LetterCase.CAMEL)\n@dataclass\nclass FieldInfo:\n    name: str\n    dtype: str\n    key: str = None\n    distinct_values: list = None\n    distinct_values_balance: float = 1.0\n    num_distinct_values: int = 0\n    nonnull_values: int = 0\n    image_values: int = 0\n    audio_values: int = 0\n    avg_words: int = None\n\n\n@DeveloperAPI\n@dataclass_json(letter_case=LetterCase.CAMEL)\n@dataclass\nclass FieldConfig:\n    name: str\n    column: str\n    type: str\n\n\n@DeveloperAPI\n@dataclass_json(letter_case=LetterCase.CAMEL)\n@dataclass\nclass FieldMetadata:\n    name: str\n    config: FieldConfig\n    excluded: bool\n    mode: str\n    missing_values: float\n    imbalance_ratio: float\n"
  },
  {
    "path": "ludwig/utils/automl/ray_utils.py",
    "content": "import os\n\nfrom ludwig.backend.ray import initialize_ray\n\ntry:\n    import ray\nexcept ImportError:\n    raise ImportError(\" ray is not installed. \" \"In order to use auto_train please run \" \"pip install ludwig[ray]\")\n\n\ndef _ray_init():\n    if ray.is_initialized():\n        return\n\n    # Forcibly terminate trial requested to stop after this amount of time passes\n    os.environ.setdefault(\"TUNE_FORCE_TRIAL_CLEANUP_S\", \"120\")\n\n    initialize_ray()\n"
  },
  {
    "path": "ludwig/utils/automl/type_inference.py",
    "content": "import logging\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUDIO, BINARY, CATEGORY, DATE, IMAGE, NUMBER, TEXT\nfrom ludwig.utils import strings_utils\nfrom ludwig.utils.automl.field_info import FieldInfo\n\n# For a given feature, the highest percentage of distinct values out of the total number of rows that we might still\n# assign the CATEGORY type.\nCATEGORY_TYPE_DISTINCT_VALUE_PERCENTAGE_CUTOFF = 0.5\n\n# Consider the field a valid text field if it has at least 5 average words. Fewer than this and it may be a cateogry\n# or an ID field (like a name or place) of some kind.\nTEXT_AVG_WORDS_CUTOFF = 5\n\n\n@DeveloperAPI\ndef infer_type(field: FieldInfo, missing_value_percent: float, row_count: int) -> str:\n    \"\"\"Perform type inference on field.\n\n    # Inputs\n    :param field: (FieldInfo) object describing field\n    :param missing_value_percent: (float) percent of missing values in the column\n    :param row_count: (int) total number of entries in original dataset  # Return\n    :return: (str) feature type\n    \"\"\"\n    if field.dtype == DATE or field.dtype.startswith(\"datetime\"):\n        return DATE\n\n    num_distinct_values = field.num_distinct_values\n    distinct_values = field.distinct_values\n\n    if num_distinct_values <= 1:\n        return CATEGORY\n\n    if num_distinct_values == 2 and missing_value_percent == 0:\n        # Check that all distinct values are conventional bools.\n        if strings_utils.are_conventional_bools(distinct_values):\n            return BINARY\n\n    if field.image_values >= 3:\n        return IMAGE\n\n    if field.audio_values >= 3:\n        return AUDIO\n\n    if strings_utils.are_all_datetimes(distinct_values):\n        return DATE\n\n    # Use CATEGORY if:\n    # - The number of distinct values is significantly less than the total number of examples.\n    # - The distinct values are not all numbers.\n    # - The distinct values are all numbers but comprise of a perfectly sequential list of integers that suggests the\n    #   values represent categories.\n    valid_row_count = row_count * (1.0 - missing_value_percent)\n    if num_distinct_values < valid_row_count * CATEGORY_TYPE_DISTINCT_VALUE_PERCENTAGE_CUTOFF and (\n        (not strings_utils.are_all_numbers(distinct_values)) or strings_utils.are_sequential_integers(distinct_values)\n    ):\n        return CATEGORY\n\n    # Use NUMBER if all of the distinct values are numbers.\n    if strings_utils.are_all_numbers(distinct_values):\n        return NUMBER\n\n    # TODO (ASN): add other modalities (image, etc. )\n    # Fallback to TEXT.\n    return TEXT\n\n\n@DeveloperAPI\ndef should_exclude(\n    idx: int, field: FieldInfo, dtype: str, column_count: int, row_count: int, targets: set[str]\n) -> bool:\n    if field.key == \"PRI\":\n        logging.info(f\"Exclude {field.name} ({dtype}): primary key\")\n        return True\n\n    if field.name in targets:\n        return False\n\n    if field.num_distinct_values <= 1:\n        logging.info(f\"Exclude {field.name} ({dtype}): less than 2 distinct values\")\n        return True\n\n    distinct_value_percent = float(field.num_distinct_values) / row_count\n    if distinct_value_percent == 1.0:\n        upper_name = field.name.upper()\n        if (\n            (idx == 0 and \"INDEX\" in upper_name and dtype == NUMBER)\n            or upper_name.endswith(\"ID\")\n            or upper_name.startswith(\"ID\")\n        ):\n            logging.info(f\"Exclude {field.name} ({dtype}): unique ID column\")\n            return True\n\n    # For TEXT fields, we only want to use them if they appear \"interesting\", otherwise we would rather exclude\n    # them and treat the problem as a tabular problem\n    if column_count > 3 and dtype == TEXT and (field.avg_words or 0) < TEXT_AVG_WORDS_CUTOFF:\n        logging.info(f\"Exclude {field.name} ({dtype}): too few average words\")\n        return True\n\n    return False\n"
  },
  {
    "path": "ludwig/utils/automl/utils.py",
    "content": "import bisect\nimport logging\n\nfrom numpy import nan_to_num\nfrom pandas import Series\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    BINARY,\n    CATEGORY,\n    COMBINER,\n    CONFIG,\n    HYPEROPT,\n    IMBALANCE_DETECTION_RATIO,\n    NAME,\n    NUMBER,\n    PARAMETERS,\n    SEARCH_ALG,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.features.feature_registries import get_output_type_registry\nfrom ludwig.modules.metric_registry import get_metric_objective\nfrom ludwig.schema.combiners.utils import get_combiner_jsonschema\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\ndef avg_num_tokens_decoder(x):\n    if x is None:\n        return None\n    if type(x) is bytes:\n        return x.decode(\"utf-8\")\n    return str(x)\n\n\n@DeveloperAPI\ndef avg_num_tokens(field: Series) -> int:\n    logger.info(f\"Calculating average number tokens for field {field.name} using sample of 100 rows.\")\n    field_sample = field.head(100).apply(avg_num_tokens_decoder)\n\n    unique_entries = field_sample.unique()\n    avg_words = round(nan_to_num(Series(unique_entries).str.split().str.len().mean()))\n    return avg_words\n\n\n@DeveloperAPI\ndef get_model_type(config: dict) -> str:\n    if (\n        \"input_features\" in config\n        and len(config[\"input_features\"]) == 1\n        and \"type\" in config[\"input_features\"][0]\n        and config[\"input_features\"][0][\"type\"] == \"text\"\n    ):\n        model_type = \"text\"\n    elif COMBINER in config and TYPE in config[COMBINER]:\n        model_type = config[COMBINER][TYPE]\n    else:\n        default_combiner_type = get_combiner_jsonschema()[\"properties\"][\"type\"][\"default\"]\n        model_type = default_combiner_type\n    return model_type\n\n\n# ref_configs comes from a file storing the config for a high-performing model per reference dataset.\n# If the automl model type matches that of any reference models, set the initial point_to_evaluate\n# in the automl hyperparameter search to the config of the reference model with the closest-matching\n# input number columns ratio.  This model config \"transfer learning\" can improve the automl search.\ndef _add_transfer_config(base_config: dict, ref_configs: dict) -> dict:\n    base_model_type = base_config[COMBINER][TYPE]\n    base_model_numeric_ratio = _get_ratio_numeric_input_features(base_config[\"input_features\"])\n    min_numeric_ratio_distance = 1.0\n    min_dataset = None\n\n    for dataset in ref_configs[\"datasets\"]:\n        dataset_config = dataset[CONFIG]\n        if base_model_type == dataset_config[COMBINER][TYPE]:\n            dataset_numeric_ratio = _get_ratio_numeric_input_features(dataset_config[\"input_features\"])\n            ratio_distance = abs(base_model_numeric_ratio - dataset_numeric_ratio)\n            if ratio_distance <= min_numeric_ratio_distance:\n                min_numeric_ratio_distance = ratio_distance\n                min_dataset = dataset\n\n    if min_dataset is not None:\n        logger.info(\"Transfer config from dataset {}\".format(min_dataset[\"name\"]))\n        min_dataset_config = min_dataset[CONFIG]\n        hyperopt_params = base_config[HYPEROPT][PARAMETERS]\n        point_to_evaluate = {}\n        _add_option_to_evaluate(point_to_evaluate, min_dataset_config, hyperopt_params, COMBINER)\n        _add_option_to_evaluate(point_to_evaluate, min_dataset_config, hyperopt_params, TRAINER)\n        base_config[HYPEROPT][SEARCH_ALG][\"points_to_evaluate\"] = [point_to_evaluate]\n    return base_config\n\n\ndef _get_ratio_numeric_input_features(input_features: dict) -> float:\n    num_input_features = len(input_features)\n    num_numeric_input = 0\n    for input_feature in input_features:\n        if input_feature[TYPE] == NUMBER:\n            num_numeric_input = num_numeric_input + 1\n    return num_numeric_input / num_input_features\n\n\n# Update point_to_evaluate w/option value from dataset_config for options in hyperopt_params.\n# Also, add option value to associated categories list if it is not already included.\ndef _add_option_to_evaluate(\n    point_to_evaluate: dict, dataset_config: dict, hyperopt_params: dict, option_type: str\n) -> dict:\n    options = dataset_config[option_type]\n    for option in options.keys():\n        option_param = option_type + \".\" + option\n        if option_param in hyperopt_params.keys():\n            option_val = options[option]\n            point_to_evaluate[option_param] = option_val\n            if option_val not in hyperopt_params[option_param][\"categories\"]:\n                bisect.insort(hyperopt_params[option_param][\"categories\"], option_val)\n    return point_to_evaluate\n\n\n@DeveloperAPI\ndef set_output_feature_metric(base_config):\n    \"\"\"If single output feature, set trainer and hyperopt metric and goal for that feature if not set.\"\"\"\n    if len(base_config[\"output_features\"]) != 1:\n        # If multiple output features, ludwig uses the goal of minimizing combined loss;\n        # this could be revisited/refined in the future.\n        return base_config\n    output_name = base_config[\"output_features\"][0][NAME]\n    output_type = base_config[\"output_features\"][0][TYPE]\n    output_metric = get_output_type_registry()[output_type].get_schema_cls().default_validation_metric\n    output_goal = get_metric_objective(output_metric)\n    if \"validation_field\" not in base_config[TRAINER] and \"validation_metric\" not in base_config[TRAINER]:\n        base_config[TRAINER][\"validation_field\"] = output_name\n        base_config[TRAINER][\"validation_metric\"] = output_metric\n    if (\n        \"output_feature\" not in base_config[HYPEROPT]\n        and \"metric\" not in base_config[HYPEROPT]\n        and \"goal\" not in base_config[HYPEROPT]\n    ):\n        base_config[HYPEROPT][\"output_feature\"] = output_name\n        base_config[HYPEROPT][\"metric\"] = output_metric\n        base_config[HYPEROPT][\"goal\"] = output_goal\n    return base_config\n\n\n@DeveloperAPI\ndef has_imbalanced_output(base_config, features_metadata) -> bool:\n    \"\"\"Check binary and category output feature(s) for imbalance, i.e., low minority/majority instance count\n    ratio.\"\"\"\n    imbalanced_output = False\n    for output_feature in base_config[\"output_features\"]:\n        if output_feature[TYPE] == BINARY or output_feature[TYPE] == CATEGORY:\n            for feature_metadata in features_metadata:\n                if output_feature[NAME] == feature_metadata.name:\n                    if feature_metadata.imbalance_ratio < IMBALANCE_DETECTION_RATIO:\n                        logger.info(\n                            f\"Imbalance in {output_feature[NAME]}: minority/majority={feature_metadata.imbalance_ratio}\"\n                        )\n                        imbalanced_output = True\n                    break\n    return imbalanced_output\n"
  },
  {
    "path": "ludwig/utils/backward_compatibility.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport logging\nimport warnings\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BIAS,\n    CLASS_WEIGHTS,\n    COLUMN,\n    CONV_BIAS,\n    CONV_USE_BIAS,\n    DECODER,\n    DEFAULT_BIAS,\n    DEFAULT_USE_BIAS,\n    DEFAULTS,\n    ENCODER,\n    EVAL_BATCH_SIZE,\n    EXECUTOR,\n    FORCE_SPLIT,\n    HEIGHT,\n    HYPEROPT,\n    IMAGE,\n    INPUT_FEATURES,\n    LOSS,\n    MISSING_VALUE_STRATEGY,\n    MODEL_ECD,\n    NAME,\n    NUM_SAMPLES,\n    NUMBER,\n    OUTPUT_FEATURES,\n    PARAMETERS,\n    PREPROCESSING,\n    PROBABILITIES,\n    RANDOM,\n    RAY,\n    SAMPLER,\n    SCHEDULER,\n    SEARCH_ALG,\n    SEQUENCE,\n    SPLIT,\n    SPLIT_PROBABILITIES,\n    STRATIFY,\n    TEXT,\n    TIMESERIES,\n    TRAINER,\n    TRAINING,\n    TYPE,\n    USE_BIAS,\n    WIDTH,\n)\nfrom ludwig.features.feature_registries import get_base_type_registry, get_input_type_registry, get_output_type_registry\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.schema.encoders.utils import get_encoder_cls\nfrom ludwig.types import (\n    FeatureConfigDict,\n    FeatureTypeDefaultsDict,\n    HyperoptConfigDict,\n    ModelConfigDict,\n    PreprocessingConfigDict,\n    TrainerConfigDict,\n    TrainingSetMetadataDict,\n)\nfrom ludwig.utils.metric_utils import TrainerMetric\nfrom ludwig.utils.misc_utils import get_from_registry, merge_dict\nfrom ludwig.utils.version_transformation import VersionTransformation, VersionTransformationRegistry\n\nconfig_transformation_registry = VersionTransformationRegistry()\n\n\n@DeveloperAPI\ndef register_config_transformation(version: str, prefixes: str | list[str] = []) -> Callable:\n    \"\"\"This decorator registers a transformation function for a config version. Version is the first version which\n    requires the transform. For example, since \"training\" is renamed to \"trainer\" in 0.5, this change should be\n    registered with 0.5.  from_version < version <= to_version.\n\n    Args:\n        version: The version to register this transformation with. The earliest ludwig version which requires this\n                 transformation.\n        prefixes: A list of keypath prefixes to apply this transformation to. If not specified, transforms the entire\n                  config dict. If a prefix indicates a list, i.e. \"input_features\", the transformation is applied to\n                  each element of the list (each input feature).\n    \"\"\"\n    if isinstance(prefixes, str):\n        prefixes = [prefixes]\n\n    def wrap(fn: Callable[[dict], dict]):\n        config_transformation_registry.register(VersionTransformation(transform=fn, version=version, prefixes=prefixes))\n        return fn\n\n    return wrap\n\n\n@DeveloperAPI\ndef upgrade_config_dict_to_latest_version(config: ModelConfigDict) -> ModelConfigDict:\n    \"\"\"Updates config from an older version of Ludwig to the current version. If config does not have a\n    \"ludwig_version\" key, all updates are applied.\n\n    Args:\n        config: A config saved by an older version of Ludwig.\n\n    Returns A new copy of config, upgraded to the current Ludwig version. Returns config if config has no\n            \"ludwig_version\".\n    \"\"\"\n    return config_transformation_registry.update_config(\n        config, from_version=config.get(\"ludwig_version\", \"0.0\"), to_version=LUDWIG_VERSION\n    )\n\n\ndef upgrade_model_progress(model_progress: dict) -> dict:\n    \"\"\"Updates model progress info to be compatible with latest ProgressTracker implementation.\n\n    Notably, we convert epoch-based stats to their step-based equivalents and reformat metrics into `TrainerMetric`\n    tuples.\n    \"\"\"\n    ret = copy.deepcopy(model_progress)\n\n    if \"last_improvement_epoch\" in ret:\n        ret[\"last_improvement_steps\"] = ret[\"last_improvement_epoch\"] * ret[\"batch_size\"]\n        del ret[\"last_improvement_epoch\"]\n\n    if \"last_learning_rate_reduction_epoch\" in ret:\n        ret[\"last_learning_rate_reduction_steps\"] = ret[\"last_learning_rate_reduction_epoch\"] * ret[\"batch_size\"]\n        del ret[\"last_learning_rate_reduction_epoch\"]\n\n    if \"last_increase_batch_size_epoch\" in ret:\n        ret[\"last_increase_batch_size_steps\"] = ret[\"last_increase_batch_size_epoch\"] * ret[\"batch_size\"]\n        del ret[\"last_increase_batch_size_epoch\"]\n\n    if \"vali_metrics\" in ret:\n        ret[\"validation_metrics\"] = ret[\"vali_metrics\"]\n        del ret[\"vali_metrics\"]\n\n    for metric_group in (\"train_metrics\", \"test_metrics\", \"validation_metrics\"):\n        if metric_group not in ret:\n            continue\n        for tgt in ret[metric_group]:\n            for metric in ret[metric_group][tgt]:\n                if len(ret[metric_group][tgt][metric]) == 0 or isinstance(\n                    ret[metric_group][tgt][metric][0], (tuple, list)\n                ):\n                    continue\n\n                ret[metric_group][tgt][metric] = [\n                    TrainerMetric(i + 1, (i + 1) * ret[\"batch_size\"], val)\n                    for i, val in enumerate(ret[metric_group][tgt][metric])\n                ]\n\n    if \"tune_checkpoint_num\" not in ret:\n        ret[\"tune_checkpoint_num\"] = 0\n\n    # Upgrades related to extending progress tracker with explicit bests.\n    if \"checkpoint_number\" not in ret:\n        ret[\"checkpoint_number\"] = 0\n\n    if \"best_eval_metric_steps\" not in ret:\n        ret[\"best_eval_metric_steps\"] = 0\n\n    if \"best_eval_metric_epoch\" not in ret:\n        ret[\"best_eval_metric_epoch\"] = 0\n\n    if \"best_eval_metric_checkpoint_number\" not in ret:\n        ret[\"best_eval_metric_checkpoint_number\"] = 0\n\n    if \"best_eval_train_metrics\" not in ret:\n        ret[\"best_eval_train_metrics\"] = {}\n\n    if \"best_eval_validation_metrics\" not in ret:\n        ret[\"best_eval_validation_metrics\"] = {}\n\n    if \"best_eval_test_metrics\" not in ret:\n        ret[\"best_eval_test_metrics\"] = {}\n\n    if \"best_eval_metric\" in ret:\n        ret[\"best_eval_metric_value\"] = ret[\"best_eval_metric\"]\n        del ret[\"best_eval_metric\"]\n\n    if \"last_improvement\" in ret:\n        del ret[\"last_improvement\"]\n\n    # Delete learning-rate related fields removed in https://github.com/ludwig-ai/ludwig/pull/2877.\n    if \"best_reduce_learning_rate_eval_metric\" in ret:\n        del ret[\"best_reduce_learning_rate_eval_metric\"]\n\n    if \"last_reduce_learning_rate_eval_metric_improvement\" in ret:\n        del ret[\"last_reduce_learning_rate_eval_metric_improvement\"]\n\n    return ret\n\n\ndef _traverse_dicts(config: Any, f: Callable[[dict], None]):\n    \"\"\"Recursively applies function f to every dictionary contained in config.\n\n    f should in-place modify the config dict. f will be called on leaves first, root last.\n    \"\"\"\n    if isinstance(config, dict):\n        for k, v in config.items():\n            _traverse_dicts(v, f)\n        f(config)\n    elif isinstance(config, list):\n        for v in config:\n            _traverse_dicts(v, f)\n\n\n@register_config_transformation(\"0.6\", \"backend\")\ndef _update_backend_cache_credentials(backend: dict[str, Any]) -> dict[str, Any]:\n    if \"cache_credentials\" in backend:\n        credentials = backend.get(\"credentials\", {})\n        if \"cache\" in credentials:\n            warnings.warn(\"`cache` already found in `backend.credentials`, ignoring `cache_credentials`\")\n        else:\n            warnings.warn(\n                \"`backend.cache_credentials` has been renamed `backend.credentials.cache`\", DeprecationWarning\n            )\n            credentials[\"cache\"] = backend.pop(\"cache_credentials\")\n        backend[\"credentials\"] = credentials\n    return backend\n\n\n@register_config_transformation(\"0.6\", [\"output_features\"])\ndef update_class_weights_in_features(feature: FeatureConfigDict) -> FeatureConfigDict:\n    if LOSS in feature:\n        class_weights = feature[LOSS].get(CLASS_WEIGHTS, None)\n        if not isinstance(class_weights, (list, dict)):\n            class_weights = None\n        feature[LOSS][CLASS_WEIGHTS] = class_weights\n\n    return feature\n\n\n@register_config_transformation(\"0.4\")\ndef _update_level_metadata(config: ModelConfigDict) -> ModelConfigDict:\n    # Replace parameters represented as keys with params represented as values.\n    # Precedence is defined by first in the dictionary order, so if multiple\n    # provided keys map to the same value, the one that appears earlier in this\n    # dictionary will take priority.\n    drop_params = {\n        \"sequence_length_limit\": \"max_sequence_length\",\n        \"word_most_common\": \"most_common\",\n        \"word_sequence_length_limit\": \"max_sequence_length\",\n        \"word_tokenizer\": \"tokenizer\",\n        \"word_vocab_file\": \"vocab_file\",\n        \"char_most_common\": \"most_common\",\n        \"char_sequence_length_limit\": \"max_sequence_length\",\n        \"char_tokenizer\": \"tokenizer\",\n        \"char_vocab_file\": \"vocab_file\",\n    }\n\n    def upgrade_params(params):\n        for key, value in drop_params.items():\n            if key in params:\n                if value in params:\n                    warnings.warn(\n                        f\"Removing deprecated config preprocessing parameter {key} as new param {value} already \"\n                        f\"present in the config\",\n                        DeprecationWarning,\n                    )\n                else:\n                    warnings.warn(\n                        f\"Renaming deprecated config preprocessing parameter {key} to {value}\",\n                        DeprecationWarning,\n                    )\n                    params[value] = params[key]\n                del params[key]\n\n    sequence_types = [SEQUENCE, TEXT, AUDIO, TIMESERIES]\n    for dtype in sequence_types:\n        params = config.get(PREPROCESSING, {}).get(dtype, {})\n        upgrade_params(params)\n\n    for feature in config[INPUT_FEATURES]:\n        if feature.get(TYPE) not in sequence_types:\n            continue\n        params = feature.get(PREPROCESSING, {})\n        upgrade_params(params)\n\n    return config\n\n\n@register_config_transformation(\"0.5\")\ndef rename_training_to_trainer(config: ModelConfigDict) -> ModelConfigDict:\n    if TRAINING in config:\n        warnings.warn(\n            'Config section \"training\" renamed to \"trainer\" and will be removed in a future version', DeprecationWarning\n        )\n        config[TRAINER] = config[TRAINING]\n        del config[TRAINING]\n    return config\n\n\n@register_config_transformation(\"0.5\", [\"input_features\", \"output_features\"])\ndef _upgrade_use_bias_in_features(feature: FeatureConfigDict) -> FeatureConfigDict:\n    def upgrade_use_bias(config):\n        if BIAS in config:\n            warnings.warn(\n                'Parameter \"bias\" renamed to \"use_bias\" and will be removed in a future version', DeprecationWarning\n            )\n            config[USE_BIAS] = config[BIAS]\n            del config[BIAS]\n        if CONV_BIAS in config:\n            warnings.warn(\n                'Parameter \"conv_bias\" renamed to \"conv_use_bias\" and will be removed in a future version',\n                DeprecationWarning,\n            )\n            config[CONV_USE_BIAS] = config[CONV_BIAS]\n            del config[CONV_BIAS]\n        if DEFAULT_BIAS in config:\n            warnings.warn(\n                'Parameter \"default_bias\" renamed to \"default_use_bias\" and will be removed in a future version',\n                DeprecationWarning,\n            )\n            config[DEFAULT_USE_BIAS] = config[DEFAULT_BIAS]\n            del config[DEFAULT_BIAS]\n\n    _traverse_dicts(feature, upgrade_use_bias)\n    return feature\n\n\n@register_config_transformation(\"0.5\", [\"input_features\", \"output_features\"])\ndef _upgrade_feature(feature: FeatureConfigDict) -> FeatureConfigDict:\n    \"\"\"Upgrades feature config (in-place)\"\"\"\n    if feature.get(TYPE) == \"numerical\":\n        warnings.warn(\n            'Feature type \"numerical\" renamed to \"number\" and will be removed in a future version', DeprecationWarning\n        )\n        feature[TYPE] = NUMBER\n    if feature.get(TYPE) == AUDIO:\n        if PREPROCESSING in feature:\n            feature[PREPROCESSING] = upgrade_audio_preprocessing(feature[PREPROCESSING])\n        warnings.warn(\n            \"Parameters specified at the `audio_feature` parameter level have been unnested and should now \"\n            \"be specified at the preprocessing level. Support for `audio_feature` will be removed in a future version\",\n            DeprecationWarning,\n        )\n    return feature\n\n\ndef upgrade_audio_preprocessing(preproc_dict: PreprocessingConfigDict) -> PreprocessingConfigDict:\n    if \"audio_feature\" in preproc_dict:\n        for k, v in preproc_dict[\"audio_feature\"].items():\n            preproc_dict[k] = v\n        del preproc_dict[\"audio_feature\"]\n    return preproc_dict\n\n\n@register_config_transformation(\"0.6\", [\"input_features\"])\ndef _upgrade_encoder_params(feature: FeatureConfigDict) -> FeatureConfigDict:\n    return _upgrade_encoder_decoder_params(feature, True)\n\n\n@register_config_transformation(\"0.6\", [\"output_features\"])\ndef _upgrade_decoder_params(feature: FeatureConfigDict) -> FeatureConfigDict:\n    return _upgrade_encoder_decoder_params(feature, False)\n\n\ndef _upgrade_encoder_decoder_params(feature: FeatureConfigDict, input_feature: bool) -> FeatureConfigDict:\n    \"\"\"\n    This function nests un-nested encoder/decoder parameters to conform with the new config structure for 0.6\n    Args:\n        feature (Dict): Feature to nest encoder/decoder params for.\n        input_feature (Bool): Whether this feature is an input feature or not.\n    \"\"\"\n    if TYPE not in feature:\n        return feature\n\n    try:\n        if input_feature:\n            module_type = ENCODER\n            feature_cls = get_from_registry(feature[TYPE], get_input_type_registry())\n        else:\n            module_type = DECODER\n            feature_cls = get_from_registry(feature[TYPE], get_output_type_registry())\n    except ValueError:\n        logging.exception(\"Failed to obtain encoder / decoder from registry\")\n        return feature\n\n    feature_schema_cls = feature_cls.get_schema_cls()\n    feature_keys = feature_schema_cls.get_valid_field_names()\n\n    # These keys have been renamed from the form below to `fc_<key>` in the new config\n    fc_layer_keys = [\n        \"fc_layers\",\n        \"output_size\",\n        \"use_bias\",\n        \"weights_initializer\",\n        \"bias_initializer\",\n        \"norm\",\n        \"norm_params\",\n        \"activation\",\n        \"dropout\",\n    ]\n\n    module = feature.get(module_type, {})\n\n    warn = False\n    if isinstance(module, str):\n        module = {TYPE: module}\n        feature[module_type] = module\n        warn = True\n\n    nested_params = []\n    for k, v in feature.items():\n        if k not in feature_keys:\n            module[k] = v\n            if k in fc_layer_keys and module_type == DECODER:\n                module[f\"fc_{k}\"] = v\n            nested_params.append(k)\n            warn = True\n\n    if module:\n        if module_type in feature:\n            feature[module_type].update(module)\n        else:\n            feature[module_type] = module\n\n    for k in nested_params:\n        del feature[k]\n\n    if warn:\n        warnings.warn(\n            f\"{module_type} specific parameters should now be nested within a dictionary under the '{module_type}' \"\n            f\"parameter. Support for un-nested {module_type} specific parameters will be removed in a future version\",\n            DeprecationWarning,\n        )\n    return feature\n\n\n@register_config_transformation(\"0.5\", [\"hyperopt\"])\ndef _upgrade_hyperopt(hyperopt: HyperoptConfigDict) -> HyperoptConfigDict:\n    \"\"\"Upgrades hyperopt config (in-place)\"\"\"\n    # check for use of legacy \"training\" reference, if any found convert to \"trainer\"\n    if PARAMETERS in hyperopt:\n        hparams = hyperopt[PARAMETERS]\n        for k, v in list(hparams.items()):\n            substr = \"training.\"\n            if k.startswith(substr):\n                warnings.warn(\n                    'Config section \"training\" renamed to \"trainer\" and will be removed in a future version',\n                    DeprecationWarning,\n                )\n                hparams[\"trainer.\" + k[len(substr) :]] = v\n                del hparams[k]\n\n    # check for legacy parameters in \"executor\"\n    if EXECUTOR in hyperopt:\n        hpexecutor = hyperopt[EXECUTOR]\n        executor_type = hpexecutor.get(TYPE, None)\n        if executor_type is not None and executor_type != RAY:\n            warnings.warn(\n                f'executor type \"{executor_type}\" not supported, converted to \"ray\" will be flagged as error '\n                \"in a future version\",\n                DeprecationWarning,\n            )\n            hpexecutor[TYPE] = RAY\n\n        # if search_alg not at top level and is present in executor, promote to top level\n        if SEARCH_ALG in hpexecutor:\n            # promote only if not in top-level, otherwise use current top-level\n            if SEARCH_ALG not in hyperopt:\n                hyperopt[SEARCH_ALG] = hpexecutor[SEARCH_ALG]\n                if isinstance(hyperopt[SEARCH_ALG], str):\n                    hyperopt[SEARCH_ALG] = {TYPE: hyperopt[SEARCH_ALG]}\n            del hpexecutor[SEARCH_ALG]\n    else:\n        warnings.warn(\n            'Missing \"executor\" section, adding \"ray\" executor will be flagged as error in a future version',\n            DeprecationWarning,\n        )\n        hyperopt[EXECUTOR] = {TYPE: RAY}\n\n    # check for legacy \"sampler\" section\n    if SAMPLER in hyperopt:\n        warnings.warn(\n            f'\"{SAMPLER}\" is no longer supported, converted to \"{SEARCH_ALG}\". \"{SAMPLER}\" will be flagged as '\n            \"error in a future version\",\n            DeprecationWarning,\n        )\n        if SEARCH_ALG in hyperopt[SAMPLER]:\n            if SEARCH_ALG not in hyperopt:\n                hyperopt[SEARCH_ALG] = hyperopt[SAMPLER][SEARCH_ALG]\n                if isinstance(hyperopt[SEARCH_ALG], str):\n                    hyperopt[SEARCH_ALG] = {TYPE: hyperopt[SEARCH_ALG]}\n                warnings.warn('Moved \"search_alg\" to hyperopt config top-level', DeprecationWarning)\n\n        # if num_samples or scheduler exist in SAMPLER move to EXECUTOR Section\n        if NUM_SAMPLES in hyperopt[SAMPLER] and NUM_SAMPLES not in hyperopt[EXECUTOR]:\n            hyperopt[EXECUTOR][NUM_SAMPLES] = hyperopt[SAMPLER][NUM_SAMPLES]\n            warnings.warn('Moved \"num_samples\" from \"sampler\" to \"executor\"', DeprecationWarning)\n\n        if SCHEDULER in hyperopt[SAMPLER] and SCHEDULER not in hyperopt[EXECUTOR]:\n            hyperopt[EXECUTOR][SCHEDULER] = hyperopt[SAMPLER][SCHEDULER]\n            warnings.warn('Moved \"scheduler\" from \"sampler\" to \"executor\"', DeprecationWarning)\n\n        if SCHEDULER in hyperopt[EXECUTOR] and len(hyperopt[EXECUTOR][SCHEDULER].keys()) == 0:\n            del hyperopt[EXECUTOR][SCHEDULER]\n\n        # remove legacy section\n        del hyperopt[SAMPLER]\n\n    if SEARCH_ALG not in hyperopt:\n        # make top-level as search_alg, if missing put in default value\n        hyperopt[SEARCH_ALG] = {TYPE: \"variant_generator\"}\n        warnings.warn(\n            'Missing \"search_alg\" at hyperopt top-level, adding in default value, will be flagged as error '\n            \"in a future version\",\n            DeprecationWarning,\n        )\n    return hyperopt\n\n\n@register_config_transformation(\"0.5\", [\"trainer\"])\ndef _upgrade_trainer(trainer: TrainerConfigDict) -> TrainerConfigDict:\n    \"\"\"Upgrades trainer config (in-place)\"\"\"\n    eval_batch_size = trainer.get(EVAL_BATCH_SIZE)\n    if eval_batch_size == 0:\n        warnings.warn(\n            \"`trainer.eval_batch_size` value `0` changed to `None`, will be unsupported in a future version\",\n            DeprecationWarning,\n        )\n        trainer[EVAL_BATCH_SIZE] = None\n    return trainer\n\n\n@register_config_transformation(\"0.5\")\ndef _upgrade_preprocessing_defaults(config: ModelConfigDict) -> ModelConfigDict:\n    \"\"\"Move feature-specific preprocessing parameters into defaults in config (in-place)\"\"\"\n    type_specific_preprocessing_params = dict()\n\n    # If preprocessing section specified and it contains feature specific preprocessing parameters,\n    # make a copy and delete it from the preprocessing section\n    for parameter in list(config.get(PREPROCESSING, {})):\n        if parameter in get_base_type_registry():\n            warnings.warn(\n                f\"Moving preprocessing configuration for `{parameter}` feature type from `preprocessing` section\"\n                \" to `defaults` section in Ludwig config. This will be unsupported in a future version.\",\n                DeprecationWarning,\n            )\n            type_specific_preprocessing_params[parameter] = config[PREPROCESSING].pop(parameter)\n\n        if parameter == \"numerical\":\n            warnings.warn(\n                f\"Moving preprocessing configuration for `{parameter}` feature type from `preprocessing` section\"\n                \" to `defaults` section in Ludwig config. This will be unsupported in a future version.\",\n                DeprecationWarning,\n            )\n            type_specific_preprocessing_params[NUMBER] = config[PREPROCESSING].pop(parameter)\n\n    # Delete empty preprocessing section if no other preprocessing parameters specified\n    if PREPROCESSING in config and not config[PREPROCESSING]:\n        del config[PREPROCESSING]\n\n    # Update defaults with the default feature specific preprocessing parameters\n    defaults = config.get(DEFAULTS, {})\n    for feature_type, preprocessing_param in type_specific_preprocessing_params.items():\n        if PREPROCESSING in preprocessing_param:\n            preprocessing_param = preprocessing_param[PREPROCESSING]\n\n        if feature_type == AUDIO:\n            preprocessing_param = upgrade_audio_preprocessing(preprocessing_param)\n\n        # If defaults was empty, then create a new key with feature type\n        if feature_type not in defaults:\n            defaults[feature_type] = {PREPROCESSING: preprocessing_param}\n        # Feature type exists but preprocessing hasn't be specified\n        elif PREPROCESSING not in defaults[feature_type]:\n            defaults[feature_type][PREPROCESSING] = preprocessing_param\n        # Update default feature specific preprocessing with parameters from config\n        else:\n            defaults[feature_type][PREPROCESSING].update(\n                merge_dict(defaults[feature_type][PREPROCESSING], preprocessing_param)\n            )\n\n    if defaults:\n        config[DEFAULTS] = defaults\n\n    return config\n\n\n@register_config_transformation(\"0.5\", \"preprocessing\")\ndef _upgrade_preprocessing_split(preprocessing: PreprocessingConfigDict) -> PreprocessingConfigDict:\n    \"\"\"Upgrade split related parameters in preprocessing.\"\"\"\n    split_params = {}\n\n    force_split = preprocessing.pop(FORCE_SPLIT, None)\n    split_probabilities = preprocessing.pop(SPLIT_PROBABILITIES, None)\n    stratify = preprocessing.pop(STRATIFY, None)\n\n    if split_probabilities is not None:\n        split_params[PROBABILITIES] = split_probabilities\n        warnings.warn(\n            \"`preprocessing.split_probabilities` has been replaced by `preprocessing.split.probabilities`, \"\n            \"will be flagged as error in a future version\",\n            DeprecationWarning,\n        )\n\n    if stratify is not None:\n        split_params[TYPE] = STRATIFY\n        split_params[COLUMN] = stratify\n        warnings.warn(\n            \"`preprocessing.stratify` has been replaced by `preprocessing.split.column` \"\n            'when setting `preprocessing.split.type` to \"stratify\", '\n            \"will be flagged as error in a future version\",\n            DeprecationWarning,\n        )\n\n    if force_split is not None:\n        warnings.warn(\n            \"`preprocessing.force_split` has been replaced by `preprocessing.split.type`, \"\n            \"will be flagged as error in a future version\",\n            DeprecationWarning,\n        )\n\n        if TYPE not in split_params:\n            split_params[TYPE] = RANDOM\n\n    if split_params:\n        preprocessing[SPLIT] = split_params\n\n    if AUDIO in preprocessing:\n        if \"audio_feature\" in preprocessing[AUDIO]:\n            for k, v in preprocessing[AUDIO][\"audio_feature\"].items():\n                preprocessing[AUDIO][k] = v\n            del preprocessing[AUDIO][\"audio_feature\"]\n        warnings.warn(\n            \"Parameters specified at the `audio_feature` parameter level have been unnested and should now \"\n            \"be specified at the preprocessing level. Support for `audio_feature` will be removed in a future version\",\n            DeprecationWarning,\n        )\n    return preprocessing\n\n\n@register_config_transformation(\"0.5\")\ndef update_training(config: ModelConfigDict) -> ModelConfigDict:\n    if TRAINING in config:\n        warnings.warn(\n            'Config section \"training\" renamed to \"trainer\" and will be removed in a future version', DeprecationWarning\n        )\n        config[TRAINER] = config[TRAINING]\n        del config[TRAINING]\n    return config\n\n\n@register_config_transformation(\"0.6\")\ndef upgrade_missing_value_strategy(config: ModelConfigDict) -> ModelConfigDict:\n    for input_feature in config.get(INPUT_FEATURES, []):\n        if _is_old_missing_value_strategy(input_feature):\n            _update_old_missing_value_strategy(input_feature)\n\n    for output_feature in config.get(OUTPUT_FEATURES, []):\n        if _is_old_missing_value_strategy(output_feature):\n            _update_old_missing_value_strategy(output_feature)\n\n    for feature, feature_defaults in config.get(DEFAULTS, {}).items():\n        if _is_old_missing_value_strategy(feature_defaults):\n            _update_old_missing_value_strategy(config.get(DEFAULTS).get(feature))\n\n    return config\n\n\n@register_config_transformation(\"0.6\", [\"trainer\"])\ndef _upgrade_max_batch_size(trainer: TrainerConfigDict) -> TrainerConfigDict:\n    if \"increase_batch_size_on_plateau_max\" in trainer:\n        warnings.warn(\n            'Config param \"increase_batch_size_on_plateau_max\" renamed to \"max_batch_size\" and will be '\n            \"removed in a future version\",\n            DeprecationWarning,\n        )\n        increase_batch_size_on_plateau_max_val = trainer.pop(\"increase_batch_size_on_plateau_max\")\n        if \"max_batch_size\" in trainer:\n            warnings.warn('\"max_batch_size\" config param already set. Discarding \"increase_batch_size_on_plateau_max\".')\n        else:\n            warnings.warn(\n                f'Setting \"max_batch_size\" config param to \"increase_batch_size_on_plateau_max\" value '\n                f'({increase_batch_size_on_plateau_max_val}) and discarding \"increase_batch_size_on_plateau_max\"'\n            )\n            trainer[\"max_batch_size\"] = increase_batch_size_on_plateau_max_val\n    return trainer\n\n\n@register_config_transformation(\"0.6\")\ndef remove_trainer_type(config: ModelConfigDict) -> ModelConfigDict:\n    # LLM Model types support different trainer types\n    if config.get(\"model_type\", None) == \"llm\":\n        return config\n\n    if TYPE in config.get(\"trainer\", {}):\n        warnings.warn(\n            \"Config param `type` has been removed from the trainer. The trainer type is determined by the top level \"\n            \" `model_type` parameter. Support for the `type` params in trainer will be removed in a future version\",\n            DeprecationWarning,\n        )\n        del config[\"trainer\"][TYPE]\n\n    return config\n\n\n@register_config_transformation(\"0.7\", [\"trainer\"])\ndef learning_rate_scheduler(trainer: TrainerConfigDict) -> TrainerConfigDict:\n    key_mapping = {\n        \"reduce_learning_rate_on_plateau\": \"reduce_on_plateau\",\n        \"reduce_learning_rate_on_plateau_patience\": \"reduce_on_plateau_patience\",\n        \"reduce_learning_rate_on_plateau_rate\": \"reduce_on_plateau_rate\",\n        \"reduce_learning_rate_eval_metric\": \"reduce_eval_metric\",\n        \"reduce_learning_rate_eval_split\": \"reduce_eval_split\",\n        \"decay\": \"decay\",\n        \"decay_steps\": \"decay_steps\",\n        \"decay_rate\": \"decay_rate\",\n        \"staircase\": \"staircase\",\n        \"learning_rate_warmup_epochs\": \"warmup_evaluations\",\n    }\n\n    lr_scheduler = trainer.get(\"learning_rate_scheduler\", {})\n    for old_key, new_key in key_mapping.items():\n        if old_key in trainer:\n            warnings.warn(\n                f\"Config param `trainer.{old_key}` has been moved to `trainer.learning_rate_scheduler.{new_key}`.\",\n                DeprecationWarning,\n            )\n            if new_key in lr_scheduler:\n                warnings.warn(\n                    f\"`trainer.learning_rate_scheduler.{new_key}` config param already set. \"\n                    f\"Discarding `trainer.{old_key}`.\"\n                )\n            else:\n                value = trainer[old_key]\n                if old_key == \"decay\" and isinstance(value, bool):\n                    # Decay has changed from a bool to an optional enum\n                    lr_scheduler[new_key] = \"exponential\" if value else None\n                elif old_key == \"reduce_learning_rate_on_plateau\":\n                    lr_scheduler[new_key] = int(value)\n                else:\n                    lr_scheduler[new_key] = value\n            del trainer[old_key]\n\n    if lr_scheduler:\n        trainer[\"learning_rate_scheduler\"] = lr_scheduler\n\n    return trainer\n\n\n@register_config_transformation(\"0.7\", [\"input_features\"])\ndef _upgrade_legacy_image_encoders(feature: FeatureConfigDict) -> FeatureConfigDict:\n    if feature.get(TYPE) != IMAGE:\n        return feature\n\n    encoder_mapping = {\n        \"resnet\": \"_resnet_legacy\",\n        \"vit\": \"_vit_legacy\",\n    }\n\n    encoder = feature.get(ENCODER, {})\n    encoder_type = encoder.get(TYPE)\n    if encoder_type not in encoder_mapping:\n        return feature\n\n    # For this version of Ludwig, only ECD supported these encoders.\n    new_encoder_cls = get_encoder_cls(MODEL_ECD, feature[TYPE], encoder_type)\n    new_encoder_fields = new_encoder_cls.get_valid_field_names()\n\n    legacy_encoder_cls = get_encoder_cls(MODEL_ECD, feature[TYPE], encoder_mapping[encoder_type])\n    legacy_encoder_fields = legacy_encoder_cls.get_valid_field_names()\n\n    user_fields = set(encoder.keys())\n    user_fields.remove(TYPE)\n\n    removed_fields = legacy_encoder_fields.difference(new_encoder_fields)\n    added_fields = new_encoder_fields.difference(legacy_encoder_fields)\n\n    user_legacy_fields = user_fields.intersection(removed_fields)\n    user_new_fields = user_fields.intersection(added_fields)\n    if len(user_legacy_fields) > 0:\n        if len(user_new_fields) > 0:\n            raise ValueError(\n                f\"Intended encoder type is ambiguous. \"\n                f\"Provided encoder fields matching encoder '{encoder_type}' {user_new_fields} and \"\n                f\"legacy encoder '{encoder_mapping[encoder_type]}' {user_legacy_fields}. \"\n                f\"Please remove features unique to one of these encoder types from your configuration.\"\n            )\n\n        warnings.warn(\n            f\"Encoder '{encoder_type}' with params '{user_legacy_fields}' has been renamed to \"\n            f\"'{encoder_mapping[encoder_type]}'. Please upgrade your config to use the new '{encoder_type}' as \"\n            f\"support for '{encoder_mapping[encoder_type]}' is not guaranteed in future versions.\",\n            DeprecationWarning,\n        )\n\n        # User provided legacy fields and no new fields, so we assume they intended to use the legacy encoder\n        encoder[TYPE] = encoder_mapping[encoder_type]\n\n    return feature\n\n\n@register_config_transformation(\"0.7\")\ndef upgrade_missing_hyperopt(config: ModelConfigDict) -> ModelConfigDict:\n    hyperopt = config.get(HYPEROPT)\n    if hyperopt == {}:\n        # This is a deprecated form of providing a missing hyperopt section, as it violates the schema definition\n        warnings.warn(\n            \"Config section `hyperopt: {}` is deprecated, please set `hyperopt: null` to disable hyperopt.\",\n            DeprecationWarning,\n        )\n        del config[HYPEROPT]\n    return config\n\n\n@register_config_transformation(\"0.7\", \"defaults\")\ndef remove_extra_type_param_in_defaults_config(defaults: FeatureTypeDefaultsDict) -> FeatureTypeDefaultsDict:\n    \"\"\"Fixes a bug introduced before 0.7.3.\n\n    [1] and subsequent refactors accidentally introduced a bug where a `type` param was added to every feature in the\n    defaults config. It was removed by [2], but made it into one of the patch releases. This transformation removes that\n    `type` param from each section of the defaults config if it exists.\n\n    [1]: https://github.com/ludwig-ai/ludwig/pull/3223\n    [2]: https://github.com/ludwig-ai/ludwig/pull/3258\n    \"\"\"\n    defaults_copy = copy.deepcopy(defaults)\n    for feature_type, feature_config in defaults.items():\n        if TYPE in feature_config:\n            del defaults_copy[feature_type][TYPE]\n    return defaults_copy\n\n\ndef upgrade_metadata(metadata: TrainingSetMetadataDict) -> TrainingSetMetadataDict:\n    # TODO(travis): stopgap solution, we should make it so we don't need to do this\n    # by decoupling config and metadata\n    metadata = copy.deepcopy(metadata)\n    _upgrade_metadata_missing_values(metadata)\n    return metadata\n\n\ndef _upgrade_metadata_missing_values(metadata: TrainingSetMetadataDict):\n    for k, v in metadata.items():\n        if isinstance(v, dict) and _is_old_missing_value_strategy(v):\n            _update_old_missing_value_strategy(v)\n        elif isinstance(v, dict) and _is_image_feature(v):\n            _update_old_image_preprocessing(v)\n\n\ndef _update_old_missing_value_strategy(feature_config: FeatureConfigDict):\n    missing_value_strategy = feature_config.get(PREPROCESSING).get(MISSING_VALUE_STRATEGY)\n    replacement_strategy = \"bfill\" if missing_value_strategy == \"backfill\" else \"ffill\"\n    feature_name = feature_config.get(NAME)\n    warnings.warn(\n        f\"Using `{replacement_strategy}` instead of `{missing_value_strategy}` as the missing value strategy\"\n        f\" for `{feature_name}`. These are identical. `{missing_value_strategy}` will be removed in a future version\",\n        DeprecationWarning,\n    )\n    feature_config[PREPROCESSING].update({MISSING_VALUE_STRATEGY: replacement_strategy})\n\n\ndef _is_old_missing_value_strategy(feature_config: FeatureConfigDict):\n    if PREPROCESSING not in feature_config:\n        return False\n    missing_value_strategy = feature_config.get(PREPROCESSING).get(MISSING_VALUE_STRATEGY, None)\n    if not missing_value_strategy or missing_value_strategy not in (\"backfill\", \"pad\"):\n        return False\n    return True\n\n\ndef _is_image_feature(feature_config: FeatureConfigDict):\n    preproc = feature_config.get(PREPROCESSING, {})\n    return HEIGHT in preproc and WIDTH in preproc\n\n\ndef _update_old_image_preprocessing(feature_config: FeatureConfigDict):\n    preprocessing = feature_config.get(PREPROCESSING)\n    if not preprocessing:\n        return\n    preprocessing[\"standardize_image\"] = preprocessing.get(\"standardize_image\")\n"
  },
  {
    "path": "ludwig/utils/batch_size_tuner.py",
    "content": "import gc\nimport logging\nimport statistics\nimport time\nfrom abc import ABC\n\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import MAX_BATCH_SIZE_DATASET_FRACTION, MIN_POSSIBLE_BATCH_SIZE\n\nlogger = logging.getLogger(__name__)\n\nTOTAL_STEPS = 5\n\n\n@DeveloperAPI\nclass BatchSizeEvaluator(ABC):\n    def select_best_batch_size(\n        self,\n        dataset_len: int,\n        max_batch_size: int | None = None,\n        max_trials: int = 20,\n        is_coordinator: bool | None = True,\n        global_max_sequence_length: int | None = None,\n    ) -> int:\n        \"\"\"Returns optimal batch size as measured by throughput (samples / sec).\"\"\"\n        logger.info(\"Tuning batch size...\")\n\n        max_batch_size = max_batch_size or dataset_len\n\n        def _is_valid_batch_size(batch_size):\n            # make sure that batch size is valid (e.g. less than 20% of ds size and max_batch_size)\n            is_smaller_than_training_set = batch_size <= MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len\n            is_under_max_batch_size = batch_size <= max_batch_size\n            is_valid = is_smaller_than_training_set and is_under_max_batch_size\n            if not is_valid and is_coordinator:\n                logger.info(\n                    f\"Batch size {batch_size} is invalid, must be less than or equal to \"\n                    f\"{MAX_BATCH_SIZE_DATASET_FRACTION * 100}% dataset size \"\n                    f\"({int(MAX_BATCH_SIZE_DATASET_FRACTION * dataset_len)} samples \"\n                    f\"of {dataset_len}) and less than or equal to max batch size {max_batch_size}\"\n                )\n            return is_valid\n\n        batch_size = MIN_POSSIBLE_BATCH_SIZE\n        best_samples_per_sec = 0\n        best_batch_size = None\n        count = 0\n        while count < max_trials and _is_valid_batch_size(batch_size):\n            if is_coordinator:\n                logger.info(f\"Exploring batch_size={batch_size}\")\n            gc.collect()\n\n            try:\n                samples_per_sec = self.evaluate(\n                    batch_size, total_steps=TOTAL_STEPS, global_max_sequence_length=global_max_sequence_length\n                )\n                if is_coordinator:\n                    logger.info(f\"Throughput at batch_size={batch_size}: {samples_per_sec:.5f} samples/s\")\n                if samples_per_sec < best_samples_per_sec:\n                    # We assume that once the throughput starts degrading, it won't go up again\n                    if is_coordinator:\n                        logger.info(f\"Throughput decrease at batch_size={batch_size}\")\n                    break\n\n                best_samples_per_sec = samples_per_sec\n                best_batch_size = batch_size\n                count += 1\n\n                # double batch size\n                batch_size *= 2\n            except RuntimeError as e:\n                # PyTorch only generates Runtime errors for CUDA OOM.\n                gc.collect()\n                if \"CUDA out of memory\" in str(e) or isinstance(e, torch.cuda.OutOfMemoryError):\n                    if is_coordinator:\n                        logger.info(f\"OOM at batch_size={batch_size}\")\n                else:\n                    # Not a CUDA error\n                    raise\n                break\n\n        # Ensure that some batch size is found.\n        # `best_batch_size` can be None if the first batch size is invalid.\n        if best_batch_size is None:\n            if is_coordinator:\n                logger.info(f\"Could not tune batch size, using minimum batch size of {MIN_POSSIBLE_BATCH_SIZE}\")\n            best_batch_size = MIN_POSSIBLE_BATCH_SIZE\n\n        if is_coordinator:\n            logger.info(f\"Selected batch_size={best_batch_size}\")\n        return best_batch_size\n\n    def evaluate(self, batch_size: int, total_steps: int = 5, global_max_sequence_length: int | None = None) -> float:\n        \"\"\"Evaluates throughput of the given batch size.\n\n        Return:\n            Median throughput in samples / sec.\n        \"\"\"\n        durations = []\n        for _ in range(total_steps):\n            self.reset()\n            start_ts = time.time()\n            self.step(batch_size, global_max_sequence_length=global_max_sequence_length)\n            durations.append(time.time() - start_ts)\n\n        med_duration_s = statistics.median(durations)\n        if med_duration_s == 0.0:\n            return float(\"inf\")\n\n        return batch_size / med_duration_s\n\n    def reset(self):\n        \"\"\"Called at the beginning of each evaluation step.\"\"\"\n\n    def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n        \"\"\"Called each step to evaluate the given batch size.\"\"\"\n        raise NotImplementedError(\"`step` must be implemented by concrete evaluator.\")\n\n\nclass BaseLLMBatchSizeEvaluator(BatchSizeEvaluator):\n    \"\"\"Base class for batch size evaluators for LLM models.\"\"\"\n\n    def __init__(self, trainer):\n        self.trainer = trainer\n        self.input_feature_name, self.input_feature = list(trainer.model.input_features.items())[0]\n        self.output_feature_name, self.output_feature = list(trainer.model.output_features.items())[0]\n\n        # Get the length of the longest input sequence from the training data\n        self.input_msl = self.input_feature.input_shape[0]\n        if trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length:\n            self.input_msl = trainer.model.config_obj.input_features[0].preprocessing.max_sequence_length\n\n        # Get the length of the longest output sequence from the training data\n        self.output_msl = self.output_feature.output_shape[0]\n        if trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length:\n            self.output_msl = trainer.model.config_obj.output_features[0].preprocessing.max_sequence_length\n\n        # This is useful to create the synthetic input and target data which will be a\n        # random sequence of integers between 0 and vocab_size\n        self.vocab_size = len(trainer.model.config_obj.input_features[0].encoder.vocab)\n\n    def reset(self):\n        self.trainer.model.reset_metrics()\n        self.trainer.optimizer.zero_grad()\n\n    def step(self, batch_size: int, global_max_sequence_length: int | None = None):\n        if global_max_sequence_length and self.input_msl + self.output_msl > global_max_sequence_length:\n            # In this case, we just need to make sure that the length of the synthetic data exceeds\n            # max_sequence_length by at most a small amount\n            self.input_msl = global_max_sequence_length // 2 + 1\n            self.output_msl = global_max_sequence_length // 2 + 1\n\n        inputs = {\n            self.input_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.input_msl))\n            .to(self.input_feature.input_dtype)\n            .to(self.trainer.device)\n        }\n        targets = {\n            self.output_feature_name: torch.randint(0, self.vocab_size, size=(batch_size, self.output_msl))\n            .to(self.output_feature.get_output_dtype())\n            .to(self.trainer.device)\n        }\n\n        self.perform_step(inputs, targets)\n\n    def perform_step(self, inputs, targets):\n        raise NotImplementedError(\"perform_step method must be implemented in subclasses\")\n\n\nclass LLMFinetuneTrainerBatchSizeEvaluator(BaseLLMBatchSizeEvaluator):\n    \"\"\"Batch size evaluator for training batch size for LLM finetuning.\"\"\"\n\n    def perform_step(self, inputs, targets):\n        self.trainer.train_step(inputs, targets)\n\n\nclass LLMFinetunePredictBatchSizeEvaluator(BaseLLMBatchSizeEvaluator):\n    \"\"\"Batch size evaluator for prediction/evaluation batch size for LLM finetuning.\"\"\"\n\n    def perform_step(self, inputs, targets):\n        with torch.no_grad():\n            self.trainer.dist_model((inputs, targets))\n"
  },
  {
    "path": "ludwig/utils/calibration.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport logging\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass\n\nimport numpy as np\nimport torch\nimport torch.nn as nn\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import BINARY, CATEGORY\nfrom ludwig.utils.registry import DEFAULT_KEYS, Registry\n\nlogger = logging.getLogger(__name__)\n\ncalibration_registry = Registry()\n\n\n@DeveloperAPI\ndef register_calibration(name: str, features: str | list[str], default=False):\n    \"\"\"Registers a calibration implementation for a list of features.\"\"\"\n    if isinstance(features, str):\n        features = [features]\n\n    def wrap(cls):\n        for feature in features:\n            feature_registry = calibration_registry.get(feature, {})\n            feature_registry[name] = cls\n            if default:\n                for key in DEFAULT_KEYS:\n                    feature_registry[key] = cls\n            calibration_registry[feature] = feature_registry\n        return cls\n\n    return wrap\n\n\n@DeveloperAPI\ndef get_calibration_cls(feature: str, calibration_method: str) -> type[\"CalibrationModule\"]:\n    \"\"\"Get calibration class for specified feature type and calibration method.\"\"\"\n    if not calibration_method:\n        return None\n    if feature in calibration_registry:\n        if calibration_method in calibration_registry[feature]:\n            return calibration_registry[feature][calibration_method]\n        else:\n            raise ValueError(f\"Calibration method {calibration_method} not supported for {feature} output features\")\n    else:\n        raise ValueError(f\"Calibration not yet supported for {feature} output features\")\n    return None\n\n\n@DeveloperAPI\nclass ECELoss(nn.Module):\n    \"\"\"Calculates the Expected Calibration Error of a model.\n\n    The input to this loss is the logits of a model, NOT the softmax scores.\n    This divides the confidence outputs into equally-sized interval bins.\n    In each bin, we compute the confidence gap:\n\n        bin_gap = | avg_confidence_in_bin - accuracy_in_bin |\n\n    We then return an average of the gaps, weighted by the number of samples in each bin.\n\n    References:\n        Naeini, Mahdi Pakdaman, Gregory F. Cooper, and Milos Hauskrecht\n        \"Obtaining Well Calibrated Probabilities Using Bayesian Binning.\" AAAI. 2015.\n\n        Chuan Guo, Geoff Pleiss, Yu Sun, Kilian Q. Weinberger\n        \"On Calibration of Modern Neural Networks.\" PMLR 2017.\n    \"\"\"\n\n    def __init__(self, n_bins: int = 15):\n        \"\"\"n_bins (int): number of confidence interval bins.\"\"\"\n        super().__init__()\n        bin_boundaries = torch.linspace(0, 1, n_bins + 1)\n        self.bin_lowers = bin_boundaries[:-1]\n        self.bin_uppers = bin_boundaries[1:]\n\n    def forward(self, logits: torch.Tensor, one_hot_labels: torch.Tensor) -> torch.Tensor:\n        softmaxes = nn.functional.softmax(logits, dim=1)\n        confidences, predictions = torch.max(softmaxes, 1)\n        labels = torch.argmax(one_hot_labels, 1)\n        accuracies = predictions.eq(labels)\n        ece = torch.zeros(1, device=logits.device)\n        for bin_lower, bin_upper in zip(self.bin_lowers, self.bin_uppers):\n            # Calculates |confidence - accuracy| in each bin\n            in_bin = confidences.gt(bin_lower.item()) * confidences.le(bin_upper.item())\n            prop_in_bin = in_bin.float().mean()\n            if prop_in_bin.item() > 0:\n                accuracy_in_bin = accuracies[in_bin].float().mean()\n                avg_confidence_in_bin = confidences[in_bin].mean()\n                ece += torch.abs(avg_confidence_in_bin - accuracy_in_bin) * prop_in_bin\n        return ece\n\n\n@DeveloperAPI\n@dataclass\nclass CalibrationResult:\n    \"\"\"Tracks results of probability calibration.\"\"\"\n\n    before_calibration_nll: float\n    before_calibration_ece: float\n    after_calibration_nll: float\n    after_calibration_ece: float\n\n\n@DeveloperAPI\nclass CalibrationModule(nn.Module, ABC):\n    @abstractmethod\n    def train_calibration(\n        self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray\n    ) -> CalibrationResult:\n        \"\"\"Calibrate output probabilities using logits and labels from validation set.\"\"\"\n        return NotImplementedError()\n\n\n@DeveloperAPI\n@register_calibration(\"temperature_scaling\", [BINARY, CATEGORY], default=True)\nclass TemperatureScaling(CalibrationModule):\n    \"\"\"Implements temperature scaling of logits. Based on results from \"On Calibration of Modern Neural Networks\":\n    https://arxiv.org/abs/1706.04599. Temperature scaling scales all logits by the same constant factor. Though it\n    may modify output probabilities it will never change argmax or categorical top-n predictions. In the case of\n    binary classification with a threshold, however, calibration may change predictions.\n\n    Implementation inspired by https://github.com/gpleiss/temperature_scaling\n\n    Args:\n        num_classes: The number of classes. Must be 2 if binary is True.\n        binary: If binary is true, logits is expected to be a 1-dimensional array. If false, logits is a 2-dimensional\n                array of shape (num_examples, num_classes).\n    \"\"\"\n\n    def __init__(self, num_classes: int = 2, binary: bool = False):\n        super().__init__()\n        self.num_classes = 2 if binary else num_classes\n        self.binary = binary\n        self.device = \"cuda\" if torch.cuda.is_available() and torch.cuda.device_count() > 0 else \"cpu\"\n        self.temperature = nn.Parameter(torch.ones(1), requires_grad=False).to(self.device)\n\n    def train_calibration(\n        self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray\n    ) -> CalibrationResult:\n        logits = torch.as_tensor(logits, dtype=torch.float32, device=self.device)\n        labels = torch.as_tensor(labels, dtype=torch.int64, device=self.device)\n        one_hot_labels = nn.functional.one_hot(labels, self.num_classes).float()\n        if self.binary:\n            # Treat binary classification as multi-class with 2 classes to re-use code.\n            # The math works out the same: softmax([0, a])[1] == sigmoid(a)\n            logits = torch.stack([torch.zeros_like(logits), logits], axis=-1)\n        nll_criterion = nn.CrossEntropyLoss().to(self.device)\n        ece_criterion = ECELoss().to(self.device)\n        # Saves the original temperature parameter, in case something goes wrong in optimization.\n        original_temperature = self.temperature.clone().detach()\n        self.temperature.requires_grad = True\n        # Calculate NLL and ECE before temperature scaling\n        before_calibration_nll = nll_criterion(logits, one_hot_labels).item()\n        before_calibration_ece = ece_criterion(logits, one_hot_labels).item()\n        logger.info(\n            \"Before temperature scaling:\\n\"\n            \"    Negative log-likelihood: %.3f\\n\"\n            \"    Expected Calibration Error: %.3f\" % (before_calibration_nll, before_calibration_ece)\n        )\n\n        # Optimizes the temperature to minimize NLL\n        optimizer = torch.optim.LBFGS([self.temperature], lr=0.01, max_iter=50, line_search_fn=\"strong_wolfe\")\n\n        def eval():\n            optimizer.zero_grad()\n            loss = nll_criterion(self.scale_logits(logits), one_hot_labels)\n            loss.backward()\n            return loss\n\n        optimizer.step(eval)\n\n        # Calculate NLL and ECE after temperature scaling\n        after_calibration_nll = nll_criterion(self.scale_logits(logits), one_hot_labels).item()\n        after_calibration_ece = ece_criterion(self.scale_logits(logits), one_hot_labels).item()\n        logger.info(\"Optimal temperature: %.3f\" % self.temperature.item())\n        logger.info(\n            \"After temperature scaling:\\n\"\n            \"    Negative log-likelihood: %.3f\\n\"\n            \"    Expected Calibration Error: %.3f\" % (after_calibration_nll, after_calibration_ece)\n        )\n        self.temperature.requires_grad = False\n        # This should never happen, but if expected calibration error is higher after optimizing temperature, revert.\n        if after_calibration_ece > before_calibration_ece:\n            logger.warning(\n                \"Expected calibration error higher after scaling, \"\n                \"reverting to temperature=%.3f.\" % original_temperature.item()\n            )\n            with torch.no_grad():\n                self.temperature.data = original_temperature.data\n        return CalibrationResult(\n            before_calibration_nll, before_calibration_ece, after_calibration_nll, after_calibration_ece\n        )\n\n    def scale_logits(self, logits: torch.Tensor) -> torch.Tensor:\n        return torch.div(logits, self.temperature)\n\n    def forward(self, logits: torch.Tensor) -> torch.Tensor:\n        \"\"\"Converts logits to probabilities.\"\"\"\n        scaled_logits = self.scale_logits(logits)\n        if self.binary:\n            return torch.sigmoid(scaled_logits)\n        else:\n            return torch.softmax(scaled_logits, -1)\n\n\n@DeveloperAPI\n@register_calibration(\"matrix_scaling\", CATEGORY, default=False)\nclass MatrixScaling(CalibrationModule):\n    \"\"\"Implements matrix scaling of logits, as described in Beyond temperature scaling: Obtaining well-calibrated\n    multiclass probabilities with Dirichlet calibration https://arxiv.org/abs/1910.12656.\n\n    Unlike temperature scaling which has only one free parameter, matrix scaling has n_classes x (n_classes + 1)\n    parameters. Use this only with a large validation set, as matrix scaling has a tendency to overfit small datasets.\n    Also, unlike temperature scaling, matrix scaling can change the argmax or top-n predictions.\n\n    NOTE: Matrix Scaling is not exposed in the UI or config yet, though it may be in a future release after testing.\n\n    Args:\n    num_classes: The number of classes.\n    off_diagonal_l2: The regularization weight for off-diagonal matrix entries.\n    mu: The regularization weight for bias vector. Defaults to off_diagonal_l2 if not specified.\n    \"\"\"\n\n    def __init__(self, num_classes: int = 2, off_diagonal_l2: float = 0.01, mu: float = None):\n        super().__init__()\n        self.num_classes = num_classes\n        self.device = \"cuda\" if torch.cuda.is_available() and torch.cuda.device_count() > 0 else \"cpu\"\n        self.w = nn.Parameter(torch.eye(self.num_classes), requires_grad=False).to(self.device)\n        self.b = nn.Parameter(torch.zeros(self.num_classes), requires_grad=False).to(self.device)\n        self.off_diagonal_l2 = off_diagonal_l2\n        self.mu = off_diagonal_l2 if mu is None else mu\n\n    def train_calibration(\n        self, logits: torch.Tensor | np.ndarray, labels: torch.Tensor | np.ndarray\n    ) -> CalibrationResult:\n        logits = torch.as_tensor(logits, dtype=torch.float32, device=self.device)\n        labels = torch.as_tensor(labels, dtype=torch.int64, device=self.device)\n        one_hot_labels = nn.functional.one_hot(labels, self.num_classes).float()\n        nll_criterion = nn.CrossEntropyLoss().to(self.device)\n        ece_criterion = ECELoss().to(self.device)\n        self.w.requires_grad = True\n        self.b.requires_grad = True\n        # Calculate NLL and ECE before temperature scaling\n        before_calibration_nll = nll_criterion(logits, one_hot_labels).item()\n        before_calibration_ece = ece_criterion(logits, one_hot_labels).item()\n        logger.info(\n            \"Before matrix scaling:\\n\"\n            \"    Negative log-likelihood: %.3f\\n\"\n            \"    Expected Calibration Error: %.3f\" % (before_calibration_nll, before_calibration_ece)\n        )\n\n        # Optimizes the linear transform to minimize NLL\n        optimizer = torch.optim.LBFGS([self.w, self.b], lr=0.001, max_iter=200, line_search_fn=\"strong_wolfe\")\n\n        def eval():\n            optimizer.zero_grad()\n            loss = nll_criterion(self.scale_logits(logits), one_hot_labels) + self.regularization_terms()\n            loss.backward()\n            return loss\n\n        optimizer.step(eval)\n\n        # Calculate NLL and ECE after matrix scaling\n        after_calibration_nll = nll_criterion(self.scale_logits(logits), one_hot_labels).item()\n        after_calibration_ece = ece_criterion(self.scale_logits(logits), one_hot_labels).item()\n        logger.info(\n            \"After matrix scaling:\\n\"\n            \"    Negative log-likelihood: %.3f\\n\"\n            \"    Expected Calibration Error: %.3f\" % (after_calibration_nll, after_calibration_ece)\n        )\n        self.w.requires_grad = False\n        self.b.requires_grad = False\n        # This should never happen, but if expected calibration error is higher after optimizing matrix, revert.\n        if after_calibration_ece > before_calibration_ece:\n            logger.warning(\"Expected calibration error higher after matrix scaling, reverting to identity.\")\n            with torch.no_grad():\n                self.w.data = torch.eye(self.num_classes)\n                self.b.data = torch.zeros(self.num_classes)\n        return CalibrationResult(\n            before_calibration_nll, before_calibration_ece, after_calibration_nll, after_calibration_ece\n        )\n\n    def regularization_terms(self) -> torch.Tensor:\n        \"\"\"Off-Diagonal and Intercept Regularisation (ODIR).\n\n        Described in \"Beyond temperature scaling: Obtaining well-calibrated multiclass probabilities with Dirichlet\n        calibration\"\n        https://proceedings.neurips.cc/paper/2019/file/8ca01ea920679a0fe3728441494041b9-Paper.pdf\n        \"\"\"\n        off_diagonal_entries = torch.masked_select(\n            self.w, ~torch.eye(self.num_classes, dtype=bool, device=self.w.device)\n        )\n        weight_matrix_loss = self.off_diagonal_l2 * torch.linalg.vector_norm(off_diagonal_entries)\n        bias_vector_loss = self.mu * torch.linalg.vector_norm(self.b, 2)\n        return bias_vector_loss + weight_matrix_loss\n\n    def scale_logits(self, logits: torch.Tensor) -> torch.Tensor:\n        return torch.matmul(self.w, logits.T).T + self.b\n\n    def forward(self, logits: torch.Tensor) -> torch.Tensor:\n        \"\"\"Converts logits to probabilities.\"\"\"\n        return torch.softmax(self.scale_logits(logits), -1)\n"
  },
  {
    "path": "ludwig/utils/carton_utils.py",
    "content": "import asyncio\nimport importlib.util\nimport logging\nimport os\nimport shutil\nimport sys\nimport tempfile\nimport traceback\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import NAME\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.fs_utils import open_file\n\nlogger = logging.getLogger(__name__)\n\n\nINFERENCE_MODULE_TEMPLATE = \"\"\"\nfrom typing import Any, Dict, List, Tuple, Union\nimport torch\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nclass GeneratedInferenceModule(torch.nn.Module):\n    def __init__(self, inference_module):\n        super().__init__()\n        self.inference_module = inference_module\n\n    def forward(self, inputs: Dict[str, Any]):\n        retyped_inputs: Dict[str, TorchscriptPreprocessingInput] = {{}}\n        for k, v in inputs.items():\n            assert isinstance(v, TorchscriptPreprocessingInput)\n            retyped_inputs[k] = v\n\n        results = self.inference_module(retyped_inputs)\n        return {output_dicts}\n\"\"\"\n\n\ndef _get_output_dicts(config: ModelConfigDict) -> str:\n    results = []\n    for feature in config[\"output_features\"]:\n        name = feature[NAME]\n        results.append(f'\"{name}\": results[\"{name}\"][\"predictions\"]')\n    return \"{\" + \", \".join(results) + \"}\"\n\n\n@DeveloperAPI\ndef generate_carton_torchscript(model: LudwigModel):\n    config = model.config\n    inference_module = model.to_torchscript()\n    with tempfile.TemporaryDirectory() as tmpdir:\n        ts_path = os.path.join(tmpdir, \"generated.py\")\n        with open_file(ts_path, \"w\") as f:\n            f.write(\n                INFERENCE_MODULE_TEMPLATE.format(\n                    output_dicts=_get_output_dicts(config),\n                )\n            )\n\n        spec = importlib.util.spec_from_file_location(\"generated.ts\", ts_path)\n        gen_ts = importlib.util.module_from_spec(spec)\n        spec.loader.exec_module(gen_ts)\n\n        gen_module = gen_ts.GeneratedInferenceModule(inference_module)\n        scripted_module = torch.jit.script(gen_module)\n    return scripted_module\n\n\ndef _get_input_spec(model: LudwigModel) -> list[dict[str, Any]]:\n    from cartonml import TensorSpec\n\n    spec = []\n    for feature_name, feature in model.model.input_features.items():\n        metadata = model.training_set_metadata[feature_name]\n        spec.append(\n            TensorSpec(\n                name=feature.feature_name, dtype=feature.get_preproc_input_dtype(metadata), shape=(\"batch_size\",)\n            )\n        )\n    return spec\n\n\ndef _get_output_spec(model: LudwigModel) -> list[dict[str, Any]]:\n    from cartonml import TensorSpec\n\n    spec = []\n    for feature_name, feature in model.model.output_features.items():\n        metadata = model.training_set_metadata[feature_name]\n        spec.append(\n            TensorSpec(\n                name=feature.feature_name, dtype=feature.get_postproc_output_dtype(metadata), shape=(\"batch_size\",)\n            )\n        )\n    return spec\n\n\n@DeveloperAPI\ndef export_carton(model: LudwigModel, carton_path: str, carton_model_name=\"ludwig_model\"):\n    try:\n        import cartonml as carton\n    except ImportError:\n        raise RuntimeError('The \"cartonml-nightly\" package is not installed in your environment.')\n\n    # Generate a torchscript model\n    model_ts = generate_carton_torchscript(model)\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        # Save the model to a temp dir\n        input_model_path: str = os.path.join(tmpdir, \"model.pt\")\n        torch.jit.save(model_ts, input_model_path)\n\n        # carton.pack is an async function so we run it and wait until it's complete\n        # See https://pyo3.rs/v0.20.0/ecosystem/async-await#a-note-about-asynciorun for why we wrap it\n        # in another function\n        async def pack() -> str:\n            try:\n                return await carton.pack(\n                    path=input_model_path,\n                    runner_name=\"torchscript\",\n                    # Any 2.x.x version is okay\n                    # TODO: improve this\n                    required_framework_version=\"=2\",\n                    model_name=carton_model_name,\n                    inputs=_get_input_spec(model),\n                    outputs=_get_output_spec(model),\n                )\n            except Exception as e:\n                exception_message: str = 'An Exception inside \"pack()\" occurred.\\n'\n                exception_traceback: str = traceback.format_exc()\n                exception_message += f'{type(e).__name__}: \"{str(e)}\".  Traceback: \"{exception_traceback}\".'\n                sys.stderr.write(exception_message)\n                sys.stderr.flush()\n                raise ValueError(exception_message) from e  # Re-raise error for calling function to handle.\n\n        try:\n            tmp_out_path: str = asyncio.run(pack())\n            # Move it to the output path\n            shutil.move(tmp_out_path, carton_path)\n        except Exception as e:\n            exception_message: str = 'An Exception inside \"export_carton()\" occurred.\\n'\n            exception_traceback: str = traceback.format_exc()\n            exception_message += f'{type(e).__name__}: \"{str(e)}\".  Traceback: \"{exception_traceback}\".'\n            sys.stderr.write(exception_message)\n            sys.stderr.flush()\n            raise SystemExit(exception_message) from e  # Make sure error is fatal.\n"
  },
  {
    "path": "ludwig/utils/checkpoint_utils.py",
    "content": "\"\"\"Implements similar functionality as tf.train.Checkpoint and tf.train.CheckpointManager.\n\nhttps://gist.github.com/kevinzakka/5d345421f7abefd5dbaf6a77f829e70a.\n\"\"\"\n\nimport errno\nimport logging\nimport os\nimport re\nimport shutil\nimport signal\nimport tempfile\nimport uuid\nfrom abc import ABC, abstractmethod\nfrom collections.abc import Mapping\nfrom glob import glob\nfrom typing import Any, TYPE_CHECKING\n\nimport torch\nfrom torch.optim import Optimizer\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.globals import MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.modules.lr_scheduler import LRScheduler\n\nif TYPE_CHECKING:\n    from ludwig.distributed.base import DistributedStrategy\n    from ludwig.models.base import BaseModel\n\n\nlogger = logging.getLogger(__name__)\nLATEST = \"latest\"\nBEST = \"best\"\n\n\n@DeveloperAPI\ndef mkdir(s):\n    \"\"\"Create a directory if it doesn't already exist.\"\"\"\n    if not os.path.exists(s):\n        os.makedirs(s)\n\n\n@DeveloperAPI\ndef get_files(d, pattern, sort=True):\n    \"\"\"Return a list of files in a given directory.\n\n    Args:\n      d (str): The path to the directory.\n      pattern (str): The wildcard to filter files with.\n      sort (bool): Whether to sort the returned list. Assumes filenames contain a number value to sort by (tmp-001).\n    \"\"\"\n    files = glob(os.path.join(d, pattern))\n    files = [f for f in files if os.path.isfile(f)]\n    if sort:\n\n        def filter_numeric(s):\n            return re.sub(\"[^0-9]\", \"\", s)\n\n        files.sort(key=lambda x: int(filter_numeric(os.path.basename(x).split(\".\")[0])))\n    return files\n\n\n@DeveloperAPI\ndef get_latest_checkpoint_path(directory: str) -> str:\n    latest_path = os.path.join(directory, f\"{LATEST}.ckpt\")\n    if os.path.exists(latest_path):\n        return latest_path\n\n    # Legacy codepath for checkpoints saved by global step number\n    ckpts = get_files(directory, \"*.ckpt\")\n    if ckpts:\n        return ckpts[-1]\n\n    return None\n\n\n@DeveloperAPI\nclass Checkpoint(ABC):\n    \"\"\"Save and restore model and optimizer states.\"\"\"\n\n    def __init__(\n        self,\n        distributed: \"DistributedStrategy\",\n        model: \"BaseModel\",\n        optimizer: Optimizer | None = None,\n        scheduler: LRScheduler | None = None,\n    ):\n        \"\"\"Constructor.\"\"\"\n        self.distributed = distributed\n        self.model = model\n        self.optimizer = optimizer\n        self.scheduler = scheduler\n        self.global_step = 0\n\n    def prepare(self, directory: str):\n        # create checkpoint directory if it doesn't\n        # already exist\n        mkdir(directory)\n\n    @abstractmethod\n    def load(self, save_path: str, device: torch.device | None = None) -> bool:\n        pass\n\n    @abstractmethod\n    def get_state_for_inference(self, save_path: str, device: torch.device | None = None) -> Mapping[str, Any]:\n        pass\n\n    @abstractmethod\n    def save(self, save_path: str, global_step: int):\n        pass\n\n    def _get_global_step(self, state: dict[str, Any], save_path: str) -> int:\n        global_step = state.get(\"global_step\")\n        if global_step is None:\n            # Legacy step detection for older checkpoint format which encoded the\n            # step number in the checkpoint filename.\n            return int(os.path.basename(save_path).split(\".\")[0])\n        return global_step\n\n\n@DeveloperAPI\nclass MultiNodeCheckpoint(Checkpoint):\n    def prepare(self, directory: str):\n        if self.is_local_rank_0():\n            super().prepare(directory)\n        self.distributed.barrier()\n\n    def load(self, save_path: str, device: torch.device | None = None) -> bool:\n        \"\"\"Load state from a saved checkpoint.\n\n        Args:\n          save_path (str): The filepath to the saved checkpoint.\n          device (torch.device): The device on which to\n            load the state.\n\n        Returns:\n          True if the checkpoint was sucessfully loaded, False if the checkpoint file\n            could not be found.\n        \"\"\"\n        try:\n            state = torch.load(save_path, map_location=device)\n            try:\n                self.global_step = self._get_global_step(state, save_path)\n                _, unexpected_keys = self.model.load_state_dict(state[MODEL_WEIGHTS_FILE_NAME], strict=False)\n                assert unexpected_keys == [], f\"Unexpected keys found in state dict: {unexpected_keys}\"\n                if self.optimizer is not None:\n                    self.optimizer.load_state_dict(state[\"optim_state\"])\n                if self.scheduler is not None and \"scheduler_state\" in state:\n                    self.scheduler.load_state_dict(state[\"scheduler_state\"])\n                logger.info(f\"Successfully loaded model weights from {save_path}.\")\n                return True\n            except Exception as e:\n                # there was an issue loading the state which means\n                # either the model definition and saved weights\n                # do not agree or they were not saved in the first\n                # place.\n                # since this is a severe issue, we raise an error\n                # rather than allowing the program to proceed.\n                raise e\n        except FileNotFoundError as e:\n            logger.error(e)\n            return False\n\n    def get_state_for_inference(self, save_path: str, device: torch.device | None = None) -> Mapping[str, Any]:\n        state = torch.load(save_path, map_location=device)\n        return state[MODEL_WEIGHTS_FILE_NAME]\n\n    def save(self, save_path: str, global_step: int):\n        \"\"\"Save a state to disk.\n\n        Modified from brentyi/fannypack.\n\n        Args:\n          save_path (str): The name of the checkpoint to save.\n          global_step (int): The iteration number which will be used\n             to name the checkpoint.\n        \"\"\"\n        if self.is_local_rank_0():\n            state = {\n                \"global_step\": global_step,\n                MODEL_WEIGHTS_FILE_NAME: self.get_model_state_dict(),\n            }\n            if self.optimizer is not None:\n                state[\"optim_state\"] = self.optimizer.state_dict()\n            if self.scheduler is not None:\n                state[\"scheduler_state\"] = self.scheduler.state_dict()\n\n            # ignore ctrl+c while saving\n            try:\n                orig_handler = signal.getsignal(signal.SIGINT)\n                signal.signal(signal.SIGINT, lambda _sig, _frame: None)\n            except ValueError:\n                # signal throws a ValueError if we're not in the main thread\n                orig_handler = None\n\n            try:\n                # atomic save\n                with tempfile.TemporaryDirectory() as tmpdir:\n                    # Save to a temporary directory outside of the checkpoint dir so\n                    # async processes do not try and copy a partially-written checkpoint.\n                    # See Ray Tune and MLFlow for examples of background processes that\n                    # are affected by this.\n                    tmp_path = os.path.join(tmpdir, \"temp.ckpt\")\n                    torch.save(state, tmp_path)\n\n                    self.safe_move_file(tmp_path, save_path)\n                    logger.debug(f\"Saved checkpoint at {save_path}.\")\n            finally:\n                # restore SIGINT handler\n                if orig_handler is not None:\n                    signal.signal(signal.SIGINT, orig_handler)\n        self.distributed.barrier()\n\n    def get_model_state_dict(self) -> dict[str, Any]:\n        state = self.model.state_dict()\n\n        # Remove frozen parameter weights from state_dict for adapters and pretrained models\n        for n, p in self.model.named_parameters():\n            if n in state and not p.requires_grad:\n                del state[n]\n\n        return state\n\n    def is_local_rank_0(self) -> bool:\n        return self.distributed.local_rank() == 0\n\n    def safe_move_file(self, src: str, dst: str):\n        \"\"\"Move a file from one directory to another, possibly across filesystems.\n\n        This implementation specifically addresses the following issue with distributed training:\n\n        1. The `save_path` is a directory local to the node, in which case every node should write\n           checkpoints separately.\n        2. The `save_path` is a remote / global filesystem like NFS, in which case only the coordinator\n           should write checkpoints.\n        \"\"\"\n        try:\n            os.replace(src, dst)\n        except OSError as err:\n            if err.errno == errno.EXDEV:\n                # Tried to move to an external filesystem. This means we should only run this on the coordinator\n                if not self.distributed.is_coordinator():\n                    logger.info(\n                        f\"Skipping writing checkpoint from rank {self.distributed.rank()} as it is not the coordinator \"\n                        f\"and the destination filesystem is remote.\"\n                    )\n                    return\n\n                # Generate a unique ID, and copy `<src>` to the target directory with a temporary name `<dst>.<ID>.tmp`.\n                # Because we're copying across a filesystem boundary, this initial copy may not be atomic.  We insert a\n                # random UUID so if different processes are copying into `<dst>`, they don't overlap in their tmp\n                # copies.\n                copy_id = uuid.uuid4()\n                tmp_dst = f\"{dst}.{copy_id}.tmp\"\n                shutil.copyfile(src, tmp_dst)\n\n                # Atomic replace file onto the new name, and clean up original source file.\n                os.replace(tmp_dst, dst)\n                os.unlink(src)\n            else:\n                raise\n\n\n@DeveloperAPI\nclass CheckpointManager:\n    \"\"\"A model and optimizer checkpoint manager.\"\"\"\n\n    def __init__(self, checkpoint: Checkpoint, directory: str, device: torch.device):\n        \"\"\"Constructor.\n\n        Args:\n          checkpoint (Checkpoint): An instance of `Checkpoint`.\n          directory (str): The directory in which checkpoints will be saved.\n          device (torch.device): The computing device on which to restore\n            checkpoints.\n        \"\"\"\n        self.checkpoint = checkpoint\n        self.directory = directory\n        self.device = device\n        self.latest_checkpoint = None\n        self.checkpoint.prepare(self.directory)\n\n    def restore_or_initialize(self) -> int:\n        \"\"\"Restore items in checkpoint from the latest checkpoint file.\n\n        Returns:\n          The global iteration step. This is parsed from the latest\n            checkpoint file if one is found, else 0 is returned.\n        \"\"\"\n        last_ckpt = get_latest_checkpoint_path(self.directory)\n        if last_ckpt:\n            status = self.checkpoint.load(last_ckpt, self.device)\n            if not status:\n                logger.warning(\"Could not restore latest checkpoint file.\")\n                return 0\n            self.latest_checkpoint = last_ckpt\n            return self.checkpoint.global_step\n        return 0\n\n    def save(self, global_step: int, tag: str = LATEST):\n        \"\"\"Create a new checkpoint.\n\n        Args:\n           global_step (int): The iteration number which will be used\n             to name the checkpoint.\n        \"\"\"\n        save_path = os.path.join(self.directory, f\"{tag}.ckpt\")\n        self.checkpoint.save(save_path, global_step)\n        self.latest_checkpoint = save_path\n\n    def save_best(self, global_step: int):\n        self.save(global_step, BEST)\n\n    def load(self, tag: str = LATEST):\n        \"\"\"Load a checkpoint.\n\n        Args:\n          tag (str): The tag of the checkpoint to load.\n        \"\"\"\n        save_path = os.path.join(self.directory, f\"{tag}.ckpt\")\n        self.checkpoint.load(save_path, self.device)\n\n    def get_best_checkpoint_state_for_inference(self, device: torch.device) -> tuple[Mapping[str, Any], None]:\n        save_path = os.path.join(self.directory, f\"{BEST}.ckpt\")\n        try:\n            return self.checkpoint.get_state_for_inference(save_path, device)\n        except Exception:\n            # This exception may be hit if the best checkpoint does not exist. This can happen if the model runs into\n            # NaN loss because of NaN or inf values in the weights before the first checkpoint is saved. In this case,\n            logger.error(f\"Could not load best checkpoint state from {save_path}. Best checkpoint may not exist.\")\n            return None\n\n    def close(self):\n        pass\n\n    @staticmethod\n    def load_latest_checkpoint(checkpoint: Checkpoint, directory: str, device: torch.device):\n        last_ckpt = get_latest_checkpoint_path(directory)\n        if last_ckpt:\n            checkpoint.load(last_ckpt, device)\n        else:\n            raise FileNotFoundError(f\"No checkpoints found in {directory}.\")\n"
  },
  {
    "path": "ludwig/utils/config_utils.py",
    "content": "from typing import Any\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    DECODER,\n    ENCODER,\n    IMAGE,\n    INPUT_FEATURES,\n    MODEL_ECD,\n    MODEL_LLM,\n    MODEL_TYPE,\n    PREPROCESSING,\n    SEQUENCE,\n    TEXT,\n    TIMESERIES,\n    TYPE,\n)\nfrom ludwig.features.feature_registries import get_input_type_registry\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.types import FeatureConfigDict, FeatureTypeDefaultsDict, PreprocessingConfigDict\n\n\n@DeveloperAPI\ndef get_feature_type_parameter_values_from_section(\n    config: ModelConfig, features_section: str, feature_type: str, parameter_name: str\n) -> set:\n    \"\"\"Returns the set of all parameter values used for the given features_section, feature_type, and\n    parameter_name.\"\"\"\n    parameter_values = set()\n    for feature in config[features_section]:\n        if feature[TYPE] == feature_type:\n            if parameter_name in feature:\n                parameter_values.add(feature[parameter_name])\n            elif parameter_name in feature[ENCODER]:\n                parameter_values.add(feature[ENCODER][parameter_name])\n            elif parameter_name in feature[DECODER]:\n                parameter_values.add(feature[DECODER][parameter_name])\n    return parameter_values\n\n\n@DeveloperAPI\ndef get_defaults_section_for_feature_type(\n    feature_type: str,\n    config_defaults: FeatureTypeDefaultsDict,\n    config_defaults_section: str,\n) -> FeatureConfigDict:\n    \"\"\"Returns a dictionary of all default parameter values specified in the global defaults section for the\n    config_defaults_section of the feature_type.\"\"\"\n\n    if feature_type not in config_defaults:\n        return {}\n\n    if config_defaults_section not in config_defaults[feature_type]:\n        return {}\n\n    return config_defaults[feature_type][config_defaults_section]\n\n\ndef _to_dict(obj) -> dict:\n    \"\"\"Convert a config object or dict to a plain dict.\"\"\"\n    if isinstance(obj, dict):\n        return obj\n    return obj.to_dict()\n\n\ndef get_preprocessing_params(config_obj: ModelConfig) -> PreprocessingConfigDict:\n    \"\"\"Returns a new dictionary that merges preprocessing section of config with type-specific preprocessing\n    parameters from config defaults.\"\"\"\n    preprocessing_params = {}\n    preprocessing_params.update(_to_dict(config_obj.preprocessing))\n    for feat_type in get_input_type_registry().keys():\n        if hasattr(config_obj.defaults, feat_type):\n            feat_defaults = getattr(config_obj.defaults, feat_type)\n            preprocessing = (\n                feat_defaults.preprocessing\n                if not isinstance(feat_defaults, dict)\n                else feat_defaults.get(\"preprocessing\", {})\n            )\n            preprocessing_params[feat_type] = _to_dict(preprocessing)\n    return preprocessing_params\n\n\n@DeveloperAPI\ndef merge_config_preprocessing_with_feature_specific_defaults(\n    config_preprocessing: PreprocessingConfigDict, config_defaults: FeatureTypeDefaultsDict\n) -> PreprocessingConfigDict:\n    \"\"\"Returns a new dictionary that merges preprocessing section of config with type-specific preprocessing\n    parameters from config defaults.\"\"\"\n    preprocessing_params = {}\n    preprocessing_params.update(config_preprocessing)\n    for feature_type in config_defaults:\n        preprocessing_params[feature_type] = config_defaults[feature_type].get(PREPROCESSING, {})\n    return preprocessing_params\n\n\ndef has_trainable_encoder(config: ModelConfig) -> bool:\n    for feature in config.input_features.to_list():\n        encoder = feature.get(\"encoder\", {})\n        if encoder.get(\"trainable\", False):\n            # TODO(travis): we assume here that False is always the default, which may not be true. We should dervice\n            # this from the schema.\n            return True\n\n    return False\n\n\ndef has_unstructured_input_feature(config: ModelConfig) -> bool:\n    for feature in config.input_features.to_list():\n        if feature.get(\"type\", None) in {TEXT, IMAGE, SEQUENCE, TIMESERIES}:\n            return True\n    return False\n\n\ndef has_pretrained_encoder(config: ModelConfig) -> bool:\n    for feature in config.input_features:\n        if feature.encoder.is_pretrained():\n            return True\n    return False\n\n\ndef config_uses_llm(config: dict[str, Any] | ModelConfig) -> bool:\n    \"\"\"Determine if a config uses an LLM.\n\n    Args:\n        config: Ludwig config object or dictionary\n\n    Returns:\n        True if the model type is LLM or if the model uses and LLM encoder, otherwise False.\n    \"\"\"\n    uses_llm = False\n\n    # For a valid config, model_type LLM is automatically True\n    # ECD models need to be checked for at least one LLM text encoder\n    if isinstance(config, ModelConfig):\n        if config.model_type == MODEL_LLM:\n            uses_llm = True\n        else:\n            for feature in config.input_features:\n                if feature.encoder and feature.encoder.type == MODEL_LLM:\n                    uses_llm = True\n                    break\n    elif isinstance(config, dict) and config:\n        if config.get(MODEL_TYPE, MODEL_ECD) == MODEL_LLM:\n            uses_llm = True\n        elif INPUT_FEATURES in config:\n            for feature in config.get(INPUT_FEATURES, []):\n                if feature.get(ENCODER, {}).get(TYPE) == MODEL_LLM:\n                    uses_llm = True\n                    break\n        else:\n            raise ValueError(\n                \"Invalid config cannot be checked for LLM usage because it has no input features.\" f\"Config: {config}\"\n            )\n    else:\n        raise ValueError(f\"Invalid config cannot be checked for LLM usage. Config: {config}\")\n\n    return uses_llm\n\n\ndef get_quantization(config: dict[str, Any] | ModelConfig) -> list[int | None]:\n    \"\"\"Get the quantization specified in a config at any level.\n\n    Args:\n        config: Ludwig config object or dictionary\n\n    Returns:\n        For LLM models, the value of quantization.bits or None if it is not specified.\n        For ECD models, the list of values of quantization.bits for each encoder. If the encoder does not\n        support quantization or no quantization config is specified, the list entry is None.\n    \"\"\"\n    if isinstance(config, ModelConfig):\n        if config.model_type == MODEL_LLM:\n            return [config.quantization.bits] if config.quantization else [None]\n        else:\n            quantization_bits = []\n            for feature in config.input_features:\n                try:\n                    quantization = feature.encoder.quantization.bits\n                except AttributeError:\n                    quantization = None\n                quantization_bits.append(quantization)\n            return quantization_bits\n    elif isinstance(config, dict) and config:\n        if config.get(MODEL_TYPE, MODEL_ECD) == MODEL_LLM:\n            return [config.get(\"quantization\", {}).get(\"bits\")]\n        elif INPUT_FEATURES in config:\n            quantization_bits = []\n            for feature in config.get(INPUT_FEATURES, []):\n                quantization_bits.append(feature.get(ENCODER, {}).get(\"quantization\", {}).get(\"bits\"))\n            return quantization_bits\n        else:\n            raise ValueError(\n                \"Invalid config cannot be checked for quantization because it has no input features.\"\n                f\"Config: {config}\"\n            )\n    else:\n        raise ValueError(f\"Invalid config cannot be checked for quantization. Config: {config}\")\n"
  },
  {
    "path": "ludwig/utils/data_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport base64\nimport collections.abc\nimport contextlib\nimport csv\nimport dataclasses\nimport functools\nimport hashlib\nimport json\nimport logging\nimport os\nimport os.path\nimport pickle\nimport random\nimport re\nimport tempfile\nimport threading\nfrom itertools import islice\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport pyarrow as pa\nimport yaml\nfrom fsspec.config import conf, set_conf_files\nfrom pandas.errors import ParserError\nfrom sklearn.model_selection import KFold\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import PREPROCESSING, SPLIT\nfrom ludwig.data.cache.types import CacheableDataset\nfrom ludwig.globals import MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, TRAIN_SET_METADATA_FILE_NAME\nfrom ludwig.utils.dataframe_utils import from_numpy_dataset, is_dask_lib, to_numpy_dataset\nfrom ludwig.utils.fs_utils import download_h5, has_remote_protocol, open_file, upload_h5\nfrom ludwig.utils.math_utils import cumsum\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.types import DataFrame\n\ntry:\n    import dask\n    import dask.dataframe as dd\n\n    DASK_DF_FORMATS = {dd.DataFrame}\nexcept ImportError:\n    DASK_DF_FORMATS = set()\n    dd = None\n\nlogger = logging.getLogger(__name__)\n\nDATASET_SPLIT_URL = \"dataset_{}_fp\"\nDATA_PROCESSED_CACHE_DIR = \"data_processed_cache_dir\"\nDATA_TRAIN_HDF5_FP = \"data_train_hdf5_fp\"\n\nDATA_TRAIN_PARQUET_FP = \"data_train_parquet_fp\"\nDATA_VALIDATION_PARQUET_FP = \"data_validation_parquet_fp\"\nDATA_TEST_PARQUET_FP = \"data_test_parquet_fp\"\n\nHDF5_COLUMNS_KEY = \"columns\"\nDICT_FORMATS = {\"dict\", \"dictionary\", dict}\nDATAFRAME_FORMATS = {\"dataframe\", \"df\", pd.DataFrame} | DASK_DF_FORMATS\nCSV_FORMATS = {\"csv\"}\nTSV_FORMATS = {\"tsv\"}\nJSON_FORMATS = {\"json\"}\nJSONL_FORMATS = {\"jsonl\"}\nEXCEL_FORMATS = {\"excel\"}\nPARQUET_FORMATS = {\"parquet\"}\nPICKLE_FORMATS = {\"pickle\"}\nFEATHER_FORMATS = {\"feather\"}\nFWF_FORMATS = {\"fwf\"}\nHTML_FORMATS = {\"html\"}\nORC_FORMATS = {\"orc\"}\nSAS_FORMATS = {\"sas\"}\nSPSS_FORMATS = {\"spss\"}\nSTATA_FORMATS = {\"stata\"}\nHDF5_FORMATS = {\"hdf5\", \"h5\"}\nCACHEABLE_FORMATS = set.union(\n    *(\n        CSV_FORMATS,\n        TSV_FORMATS,\n        JSON_FORMATS,\n        JSONL_FORMATS,\n        EXCEL_FORMATS,\n        PARQUET_FORMATS,\n        PICKLE_FORMATS,\n        FEATHER_FORMATS,\n        FWF_FORMATS,\n        HTML_FORMATS,\n        ORC_FORMATS,\n        SAS_FORMATS,\n        SPSS_FORMATS,\n        STATA_FORMATS,\n        DATAFRAME_FORMATS,\n    )\n)\n\nPANDAS_DF = pd\n\n\n# Lock over the entire interpreter as we can only have one set\n# of credentials scoped to the interpreter at once.\nGLOBAL_CRED_LOCK = threading.Lock()\n\n\n@DeveloperAPI\ndef get_parquet_filename(n: int):\n    \"\"\"Left pads the partition number with zeros to preserve order in downstream reads.\n\n    Downstream reads use the filename to determine the lexical order of the partitions.\n    \"\"\"\n    return f\"part.{str(n).zfill(8)}.parquet\"\n\n\n@DeveloperAPI\ndef get_split_path(dataset_fp):\n    return os.path.splitext(dataset_fp)[0] + \".split.parquet\"\n\n\n@DeveloperAPI\ndef get_abs_path(src_path, file_path):\n    if has_remote_protocol(file_path):\n        return file_path\n    elif src_path is not None:\n        return os.path.join(src_path, file_path)\n    else:\n        return file_path\n\n\n@DeveloperAPI\ndef load_csv(data_fp):\n    with open_file(data_fp, \"rb\") as f:\n        data = list(csv.reader(f))\n    return data\n\n\n# Decorator used to encourage Dask on Ray to spread out data loading across workers\n@DeveloperAPI\ndef spread(fn):\n    def wrapped_fn(*args, **kwargs):\n        if dd is None or not hasattr(dask, \"annotate\"):\n            return fn(*args, **kwargs)\n\n        with dask.annotate(ray_remote_args=dict(scheduling_strategy=\"SPREAD\")):\n            return fn(*args, **kwargs)\n\n    return wrapped_fn\n\n\ndef read_nrows_via_chunksize(fp, read_fn, **kwargs):\n    chunksize = kwargs.pop(\"nrows\", None)\n    ret = read_fn(fp, chunksize=chunksize, **kwargs)\n\n    if isinstance(ret, collections.abc.Iterator):\n        return next(ret)\n\n    return ret\n\n\n@DeveloperAPI\n@spread\ndef read_xsv(data_fp, df_lib=PANDAS_DF, separator=\",\", header=0, nrows=None, skiprows=None, dtype=object, **kwargs):\n    \"\"\"Helper method to read a csv file. Wraps around pd.read_csv to handle some exceptions. Can extend to cover\n    cases as necessary.\n\n    :param data_fp: path to the xsv file\n    :param df_lib: DataFrame library used to read in the CSV\n    :param separator: defaults separator to use for splitting\n    :param header: header argument for pandas to read the csv\n    :param nrows: number of rows to read from the csv, None means all\n    :param skiprows: number of rows to skip from the csv, None means no skips\n    :param dtype: dtype to use for columns. Defaults to object to disable type inference.\n    :return: Pandas dataframe with the data\n    \"\"\"\n    with open_file(data_fp, \"r\", encoding=\"utf8\") as csvfile:\n        try:\n            dialect = csv.Sniffer().sniff(csvfile.read(1024 * 100), delimiters=[\",\", \"\\t\", \"|\"])\n            separator = dialect.delimiter\n        except csv.Error:\n            # Could not conclude the delimiter, defaulting to user provided\n            pass\n\n    # NOTE: by default we read all XSV columns in as dtype=object, bypassing all type inference. This is to avoid silent\n    # issues related to incorrect type inference (e.g. NaNs in bool columns). Convert data to correct types after\n    # reading in.\n    kwargs = dict(sep=separator, header=header, skiprows=skiprows, dtype=dtype, **kwargs)\n\n    if nrows is not None:\n        kwargs[\"nrows\"] = nrows\n\n    try:\n        df = df_lib.read_csv(data_fp, **kwargs)\n    except ParserError:\n        logger.warning(\"Failed to parse the CSV with pandas default way, trying \\\\ as escape character.\")\n        df = df_lib.read_csv(data_fp, escapechar=\"\\\\\", **kwargs)\n\n    return df\n\n\nread_csv = functools.partial(read_xsv, separator=\",\")\nread_tsv = functools.partial(read_xsv, separator=\"\\t\")\n\n\n@DeveloperAPI\n@spread\ndef read_json(data_fp, df_lib, normalize=False, **kwargs):\n    # Not supported unless lines=True\n    kwargs.pop(\"nrows\", None)\n\n    if normalize:\n        return df_lib.json_normalize(load_json(data_fp))\n    else:\n        return df_lib.read_json(data_fp, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_jsonl(data_fp, df_lib, **kwargs):\n    return df_lib.read_json(data_fp, lines=True, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_excel(data_fp, df_lib, **kwargs):\n    fp_split = os.path.splitext(data_fp)\n    if fp_split[1] == \".xls\":\n        excel_engine = \"xlrd\"\n    else:\n        excel_engine = \"openpyxl\"\n\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_excel() since dask backend does not support it\")\n        return dd.from_pandas(pd.read_excel(data_fp, engine=excel_engine, **kwargs), npartitions=1)\n    return df_lib.read_excel(data_fp, engine=excel_engine, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_parquet(data_fp, df_lib, nrows=None, **kwargs):\n    if nrows is not None:\n        import pyarrow.parquet as pq\n\n        from ludwig.utils.fs_utils import get_fs_and_path\n\n        fs, _ = get_fs_and_path(data_fp)\n        dataset = pq.ParquetDataset(data_fp, filesystem=fs).fragments[0]\n\n        preview = dataset.head(nrows).to_pandas()\n\n        if is_dask_lib(df_lib):\n            return df_lib.from_pandas(preview, npartitions=1)\n        return preview\n\n    return df_lib.read_parquet(data_fp, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_pickle(data_fp, df_lib, **kwargs):\n    # Chunking is not supported for pickle files:\n    kwargs.pop(\"nrows\", None)\n\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_pickle() since dask backend does not support it\")\n        return dd.from_pandas(pd.read_pickle(data_fp), npartitions=1)\n    return df_lib.read_pickle(data_fp)\n\n\n@DeveloperAPI\n@spread\ndef read_fwf(data_fp, df_lib, **kwargs):\n    return df_lib.read_fwf(data_fp, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_feather(data_fp, df_lib, **kwargs):\n    # Chunking is not supported for feather files:\n    kwargs.pop(\"nrows\", None)\n\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_feather() since dask backend does not support it\")\n        return dd.from_pandas(pd.read_feather(data_fp), npartitions=1)\n    return df_lib.read_feather(data_fp)\n\n\n@DeveloperAPI\n@spread\ndef read_html(data_fp, df_lib, **kwargs):\n    # Chunking is not supported for html files:\n    kwargs.pop(\"nrows\", None)\n\n    # Wrap literal HTML strings in StringIO to avoid pandas FutureWarning\n    from io import StringIO\n\n    if isinstance(data_fp, str) and not os.path.isfile(data_fp):\n        data_fp = StringIO(data_fp)\n\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_html() since dask backend does not support it\")\n        return dd.from_pandas(pd.read_html(data_fp)[0], npartitions=1)\n    return df_lib.read_html(data_fp)[0]\n\n\n@DeveloperAPI\n@spread\ndef read_orc(data_fp, df_lib, **kwargs):\n    # Chunking is not supported for orc files:\n    kwargs.pop(\"nrows\", None)\n\n    return df_lib.read_orc(data_fp, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_sas(data_fp, df_lib, **kwargs):\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_sas() since dask backend does not support it\")\n        return dd.from_pandas(read_nrows_via_chunksize(data_fp, df_lib.read_sas, **kwargs), npartitions=1)\n    return read_nrows_via_chunksize(data_fp, df_lib.read_sas, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_spss(data_fp, df_lib, **kwargs):\n    # Chunking is not supported for spss files:\n    kwargs.pop(\"nrows\", None)\n\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_spss() since dask backend does not support it\")\n        return dd.from_pandas(pd.read_spss(data_fp), npartitions=1)\n    return df_lib.read_spss(data_fp)\n\n\n@DeveloperAPI\n@spread\ndef read_stata(data_fp, df_lib, **kwargs):\n    # https://github.com/dask/dask/issues/9055\n    if is_dask_lib(df_lib):\n        logger.warning(\"Falling back to pd.read_stata() since dask backend does not support it\")\n        return dd.from_pandas(read_nrows_via_chunksize(data_fp, df_lib.read_stata, **kwargs), npartitions=1)\n    return read_nrows_via_chunksize(data_fp, df_lib.read_stata, **kwargs)\n\n\n@DeveloperAPI\n@spread\ndef read_hdf5(data_fp, **_kwargs):\n    return load_hdf5(data_fp, clean_cols=True)\n\n\n@DeveloperAPI\n@spread\ndef read_buffer(buf, fname):\n    \"\"\"Reads data in from a binary buffer by first writing the data to a temporary file, and then processes it\n    based on its format (hdf5, csv, tsv etc).\n\n    Useful if object is a binary buffer coming from streaming data.\n    \"\"\"\n    data_format = figure_data_format_dataset(fname)\n    reader_fn = data_reader_registry[data_format]\n    with tempfile.TemporaryDirectory() as tmpdir:\n        temp_name = os.path.join(tmpdir, \"dataset\")\n        with open(temp_name, \"wb\") as f:\n            f.write(buf.read())\n        return reader_fn(temp_name, pd)\n\n\n@DeveloperAPI\n@spread\ndef read_fname(fname, data_format=None, df_lib=pd, **kwargs):\n    \"\"\"This function reads data from fname using the df_lib data processing library (defaults to pandas).\n\n    Useful if you don't know the file type extension in advance.\n    \"\"\"\n    data_format = data_format or figure_data_format_dataset(fname)\n    reader_fn = data_reader_registry[data_format]\n    return reader_fn(fname, df_lib, **kwargs)\n\n\n@DeveloperAPI\ndef save_csv(data_fp, data):\n    with open_file(data_fp, \"w\", encoding=\"utf-8\") as csv_file:\n        writer = csv.writer(csv_file)\n        for row in data:\n            if not isinstance(row, collections.abc.Iterable) or isinstance(row, str):\n                row = [row]\n            writer.writerow(row)\n\n\n@DeveloperAPI\ndef csv_contains_column(data_fp, column_name):\n    return column_name in read_csv(data_fp, nrows=0)  # only loads header\n\n\n@DeveloperAPI\ndef load_yaml(yaml_fp):\n    with open_file(yaml_fp, \"r\") as f:\n        return yaml.safe_load(f)\n\n\n@DeveloperAPI\ndef load_config_from_str(config):\n    \"\"\"Load the config as either a serialized string or a path to a YAML file.\"\"\"\n    config = yaml.safe_load(config)\n    if isinstance(config, str):\n        # Assume the caller provided a path name\n        with open(config, encoding=\"utf-8\") as f:\n            config = yaml.safe_load(f)\n    return config\n\n\n@DeveloperAPI\ndef load_json(data_fp):\n    with open_file(data_fp, \"r\") as input_file:\n        data = json.load(input_file)\n    return data\n\n\n@DeveloperAPI\ndef save_json(data_fp, data, sort_keys=True, indent=4):\n    with open_file(data_fp, \"w\") as output_file:\n        json.dump(data, output_file, cls=NumpyEncoder, sort_keys=sort_keys, indent=indent)\n\n\n@DeveloperAPI\ndef hash_dict(d: dict, max_length: int | None = 6) -> bytes:\n    \"\"\"Function that maps a dictionary into a unique hash.\n\n    Known limitation: All values and keys of the dict must have an ordering. If not, there's no guarantee to obtain the\n    same hash. For instance, values that are sets will potentially lead to different hashed when run on different\n    machines or in different python sessions. Replacing them with  sorted lists is suggested.\n    \"\"\"\n    s = json.dumps(d, cls=NumpyEncoder, sort_keys=True, ensure_ascii=True)\n    h = hashlib.md5(s.encode())\n    d = h.digest()\n    b = base64.b64encode(d, altchars=b\"__\")\n    return b[:max_length]\n\n\n@DeveloperAPI\ndef to_json_dict(d):\n    \"\"\"Converts Python dict to pure JSON ready format.\"\"\"\n    return json.loads(json.dumps(d, cls=NumpyEncoder))\n\n\n@DeveloperAPI\ndef chunk_dict(data, chunk_size=100):\n    \"\"\"Split large dictionary into chunks.\n\n    Source: https://stackoverflow.com/a/22878842\n    \"\"\"\n    it = iter(data)\n    for _ in range(0, len(data), chunk_size):\n        yield {k: data[k] for k in islice(it, chunk_size)}\n\n\n@DeveloperAPI\ndef flatten_dict(d, parent_key=\"\", sep=\".\"):\n    \"\"\"Based on https://www.geeksforgeeks.org/python-convert-nested-dictionary-into-flattened-dictionary/\"\"\"\n    items = []\n    for k, v in d.items():\n        new_key = parent_key + sep + k if parent_key else k\n\n        if isinstance(v, collections.abc.MutableMapping):\n            items.extend(flatten_dict(v, new_key, sep=sep).items())\n        elif isinstance(v, list):\n            list_mapping = {str(i): item for i, item in enumerate(v)}\n            items.extend(flatten_dict(list_mapping, new_key, sep=sep).items())\n        else:\n            items.append((new_key, v))\n    return dict(items)\n\n\n@DeveloperAPI\ndef save_hdf5(data_fp, data):\n    numpy_dataset = to_numpy_dataset(data)\n    with upload_h5(data_fp) as h5_file:\n        h5_file.create_dataset(HDF5_COLUMNS_KEY, data=np.array(data.columns.values, dtype=\"S\"))\n        for column in data.columns:\n            h5_file.create_dataset(column, data=numpy_dataset[column])\n\n\n@DeveloperAPI\ndef load_hdf5(data_fp, clean_cols: bool = False):\n    with download_h5(data_fp) as hdf5_data:\n        columns = [s.decode(\"utf-8\") for s in hdf5_data[HDF5_COLUMNS_KEY][()].tolist()]\n\n        numpy_dataset = {}\n        for column in columns:\n            # Column names from training hdf5 will be in the form 'Survived_a2fv4'\n            np_col = column.rsplit(\"_\", 1)[0] if clean_cols else column\n            numpy_dataset[np_col] = hdf5_data[column][()]\n\n    return from_numpy_dataset(numpy_dataset)\n\n\n@DeveloperAPI\ndef load_object(object_fp):\n    with open_file(object_fp, \"rb\") as f:\n        return pickle.load(f)\n\n\n@DeveloperAPI\ndef save_object(object_fp, obj):\n    with open_file(object_fp, \"wb\") as f:\n        pickle.dump(obj, f)\n\n\n@DeveloperAPI\ndef load_array(data_fp, dtype=float):\n    list_num = []\n    with open_file(data_fp, \"r\") as input_file:\n        for x in input_file:\n            list_num.append(dtype(x.strip()))\n    return np.array(list_num)\n\n\n@DeveloperAPI\ndef load_matrix(data_fp, dtype=float):\n    list_num = []\n    with open_file(data_fp, \"r\") as input_file:\n        for row in input_file:\n            list_num.append([dtype(elem) for elem in row.strip().split()])\n    return np.squeeze(np.array(list_num))\n\n\n@DeveloperAPI\ndef save_array(data_fp, array):\n    with open_file(data_fp, \"w\") as output_file:\n        for x in np.nditer(array):\n            output_file.write(str(x) + \"\\n\")\n\n\n# TODO(shreya): Confirm types of args\n@DeveloperAPI\ndef load_pretrained_embeddings(embeddings_path: str, vocab: list[str]) -> np.ndarray:\n    \"\"\"Create an embedding matrix of all words in vocab.\"\"\"\n    embeddings, embeddings_size = load_glove(embeddings_path, return_embedding_size=True)\n\n    # calculate an average embedding, to use for initializing missing words\n    avg_embedding = [embeddings[w] for w in vocab if w in embeddings]\n    avg_embedding = sum(avg_embedding) / len(avg_embedding)\n\n    # create the embedding matrix\n    embeddings_vectors = []\n    for word in vocab:\n        if word in embeddings:\n            embeddings_vectors.append(embeddings[word])\n        else:\n            embeddings_vectors.append(avg_embedding + np.random.uniform(-0.01, 0.01, embeddings_size))\n    embeddings_matrix = np.stack(embeddings_vectors)\n\n    # let's help the garbage collector free some memory\n    embeddings = None\n\n    return embeddings_matrix\n\n\n@DeveloperAPI\n@functools.lru_cache(1)\ndef load_glove(file_path: str, return_embedding_size: bool = False) -> dict[str, np.ndarray]:\n    \"\"\"Loads Glove embeddings for each word.\n\n    Returns:\n        Mapping between word and numpy array of size embedding_size as set by\n        first line of file.\n    \"\"\"\n    logger.info(f\"  Loading Glove format file {file_path}\")\n    embeddings = {}\n    embedding_size = 0\n\n    # collect embeddings size assuming the first line is correct\n    with open_file(file_path, \"r\", encoding=\"utf-8\") as f:\n        found_line = False\n        while not found_line:\n            line = f.readline()\n            if line:\n                embedding_size = len(line.split()) - 1\n                found_line = True\n\n    # collect embeddings\n    with open_file(file_path, \"r\", encoding=\"utf-8\") as f:\n        for line_number, line in enumerate(f):\n            if line:\n                try:\n                    split = line.split()\n                    if len(split) != embedding_size + 1:\n                        raise ValueError(\n                            f\"Line {line_number} is of length {len(split)}, \"\n                            f\"while expected length is {embedding_size + 1}.\"\n                        )\n                    word = split[0]\n                    embedding = np.array([float(val) for val in split[-embedding_size:]])\n                    embeddings[word] = embedding\n                except ValueError:\n                    logger.warning(f\"Line {line_number} in the GloVe file {file_path} is malformed, skipping it\")\n    logger.info(f\"  {len(embeddings)} embeddings loaded\")\n\n    if return_embedding_size:\n        return embeddings, embedding_size\n    return embeddings\n\n\n@DeveloperAPI\ndef split_data(split: float, data: list) -> tuple[list, list]:\n    split_length = int(round(split * len(data)))\n    random.shuffle(data)\n    return data[:split_length], data[split_length:]\n\n\n@DeveloperAPI\ndef split_by_slices(slices: list[Any], n: int, probabilities: list[float]) -> list[Any]:\n    splits = []\n    indices = cumsum([int(x * n) for x in probabilities])\n    start = 0\n    for end in indices:\n        splits.append(slices[start:end])\n        start = end\n    return splits\n\n\n@DeveloperAPI\ndef shuffle_unison_inplace(list_of_lists, random_state=None):\n    if list_of_lists:\n        assert all(len(single_list) == len(list_of_lists[0]) for single_list in list_of_lists)\n        if random_state is not None:\n            p = random_state.permutation(len(list_of_lists[0]))\n        else:\n            p = np.random.permutation(len(list_of_lists[0]))\n        return [single_list[p] for single_list in list_of_lists]\n    return None\n\n\n@DeveloperAPI\ndef shuffle_dict_unison_inplace(np_dict, random_state=None):\n    keys = list(np_dict.keys())\n    list_of_lists = list(np_dict.values())\n\n    # shuffle up the list of lists according to previous fct\n    shuffled_list = shuffle_unison_inplace(list_of_lists, random_state)\n\n    recon = {}\n    for ii, dkey in enumerate(keys):\n        recon[dkey] = shuffled_list[ii]\n\n    # we've shuffled the dictionary in place!\n    return recon\n\n\n@DeveloperAPI\ndef split_dataset_ttv(dataset, split):\n    # Obtain distinct splits from the split column. If\n    # a split is not present in this set, then we can skip generating\n    # the dataframe for that split.\n    if dataset[split].dtype != int:\n        dataset[split] = dataset[split].astype(int)\n\n    distinct_values = dataset[split].drop_duplicates()\n    if hasattr(distinct_values, \"compute\"):\n        distinct_values = distinct_values.compute()\n    distinct_values = set(distinct_values.values.tolist())\n\n    training_set = split_dataset(dataset, split, 0) if 0 in distinct_values else None\n    validation_set = split_dataset(dataset, split, 1) if 1 in distinct_values else None\n    test_set = split_dataset(dataset, split, 2) if 2 in distinct_values else None\n    return training_set, test_set, validation_set\n\n\n@DeveloperAPI\ndef split_dataset(dataset, split, value_to_split=0):\n    split_df = dataset[dataset[split] == value_to_split]\n    return split_df\n\n\n@DeveloperAPI\ndef collapse_rare_labels(labels, labels_limit):\n    if labels_limit > 0:\n        labels[labels >= labels_limit] = labels_limit\n    return labels\n\n\n@DeveloperAPI\ndef class_counts(dataset, labels_field):\n    return np.bincount(dataset[labels_field].flatten()).tolist()\n\n\n@DeveloperAPI\ndef load_from_file(file_name, field=None, dtype=int, ground_truth_split=2):\n    \"\"\"Load experiment data from supported file formats.\n\n    Experiment data can be test/train statistics, model predictions, probability, ground truth,  ground truth metadata.\n    :param file_name: Path to file to be loaded\n    :param field: Target Prediction field.\n    :param dtype:\n    :param ground_truth_split: Ground truth split filter where 0 is train 1 is validation and 2 is test split. By\n        default test split is used when loading ground truth from hdf5.\n    :return: Experiment data as array\n    \"\"\"\n    if file_name.endswith(\".hdf5\") and field is not None:\n        dataset = pd.read_hdf(file_name, key=HDF5_COLUMNS_KEY)\n        column = dataset[field]\n        array = column[dataset[SPLIT] == ground_truth_split].values  # ground truth\n    elif file_name.endswith(\".npy\"):\n        array = np.load(file_name)\n    elif file_name.endswith(\".csv\"):\n        array = read_csv(file_name, header=None).values\n    else:\n        array = load_matrix(file_name, dtype)\n    return array\n\n\n@DeveloperAPI\ndef replace_file_extension(file_path, extension):\n    \"\"\"Return a file path for a file with same name but different format. a.csv, json -> a.json a.csv, hdf5 ->\n    a.hdf5.\n\n    :param file_path: original file path\n    :param extension: file extension\n    :return: file path with same name but different format\n    \"\"\"\n    if file_path is None:\n        return None\n    extension = extension.strip()\n    if extension.startswith(\".\"):\n        # Handle the case if the user calls with '.hdf5' instead of 'hdf5'\n        extension = extension[1:]\n\n    return os.path.splitext(file_path)[0] + \".\" + extension\n\n\n@DeveloperAPI\ndef file_exists_with_diff_extension(file_path, extension):\n    return file_path is None or os.path.isfile(replace_file_extension(file_path, extension))\n\n\n@DeveloperAPI\ndef add_sequence_feature_column(df, col_name, seq_length):\n    \"\"\"Adds a new column to the dataframe computed from an existing column. Values in the new column are space-\n    delimited strings composed of preceding values of the same column up to seq_length. For example values of the\n    i-th row of the new column will be a space-delimited string of df[col_name][i-seq_length].\n\n    :param df: input dataframe\n    :param col_name: column name containing sequential data\n    :param seq_length: length of an array of preceeding column values to use\n    \"\"\"\n    if col_name not in df.columns.values:\n        logger.error(f\"{col_name} column does not exist\")\n        return\n\n    new_col_name = col_name + \"_feature\"\n    if new_col_name in df.columns.values:\n        logger.warning(f\"{new_col_name} column already exists, values will be overridden\")\n\n    new_data = [None] * seq_length\n    old_data = np.array(df[col_name])\n\n    for i in range(seq_length, len(df)):\n        new_data.append(\" \".join(str(j) for j in old_data[i - seq_length : i]))\n\n    df[new_col_name] = new_data\n    df[new_col_name] = df[new_col_name].bfill()\n\n\n@DeveloperAPI\ndef override_in_memory_flag(input_features, override_value):\n    num_overrides = 0\n    for feature in input_features:\n        if PREPROCESSING in feature:\n            if \"in_memory\" in feature[PREPROCESSING]:\n                feature[PREPROCESSING][\"in_memory\"] = override_value\n                num_overrides += 1\n    return num_overrides\n\n\n@DeveloperAPI\ndef normalize_numpy(obj):\n    if isinstance(obj, np.integer):\n        return int(obj)\n    elif isinstance(obj, np.floating):\n        return float(obj)\n    elif isinstance(obj, np.ndarray):\n        return normalize_numpy(obj.tolist())\n    elif isinstance(obj, list):\n        return [normalize_numpy(v) for v in obj]\n    else:\n        return obj\n\n\n@DeveloperAPI\nclass NumpyEncoder(json.JSONEncoder):\n    \"\"\"Custom JSON encoder for handling NumPy objects.\n\n    This encoder extends the `json.JSONEncoder` class and provides\n    custom serialization for NumPy objects. It converts NumPy arrays,\n    sets, tuples, integers, floating-point numbers, booleans, and\n    dataclasses to their JSON serializable equivalents.\n\n    Attributes:\n        None\n\n    Methods:\n        default: Overrides the default method of `json.JSONEncoder`\n            to provide custom serialization for NumPy objects.\n\n    Usage:\n        Use this encoder when serializing objects that contain NumPy\n        arrays or other NumPy objects to JSON.\n\n    Example:\n        encoder = NumpyEncoder()\n        json_data = encoder.encode(data)\n    \"\"\"\n\n    def default(self, o):\n        if isinstance(o, (set, tuple)):\n            return list(o)\n        elif isinstance(o, np.bool_):\n            return bool(o)\n        elif isinstance(o, np.integer):\n            return int(o)\n        elif isinstance(o, np.floating):\n            return float(o)\n        elif isinstance(o, np.ndarray):\n            return o.tolist()\n        elif dataclasses.is_dataclass(o):\n            return dataclasses.asdict(o)\n        elif hasattr(o, \"to_dict\"):\n            return o.to_dict()\n        else:\n            return json.JSONEncoder.default(self, o)\n\n\n@DeveloperAPI\ndef generate_kfold_splits(data_df, num_folds, random_state):\n    kf = KFold(n_splits=num_folds, shuffle=True, random_state=random_state)\n    fold_num = 0\n    for train_indices, test_indices in kf.split(data_df):\n        fold_num += 1\n        yield train_indices, test_indices, fold_num\n\n\n@DeveloperAPI\ndef get_path_size(start_path, regex_accept=None, regex_reject=None):\n    total_size = 0\n    pattern_accept = re.compile(regex_accept) if regex_accept else None\n    pattern_reject = re.compile(regex_reject) if regex_reject else None\n\n    for dirpath, _, filenames in os.walk(start_path):\n        for filename in filenames:\n            filepath = os.path.join(dirpath, filename)\n            if not os.path.islink(filepath):\n                accepted = True\n                if pattern_accept:\n                    accepted = accepted and pattern_accept.match(filename)\n                if pattern_reject:\n                    accepted = accepted and not pattern_reject.match(filename)\n                if accepted:\n                    total_size += os.path.getsize(filepath)\n\n    return total_size\n\n\n@DeveloperAPI\ndef clear_data_cache():\n    \"\"\"Clears any cached data objects (e.g., embeddings)\"\"\"\n    load_glove.cache_clear()\n\n\n@DeveloperAPI\ndef figure_data_format_dataset(dataset):\n    if isinstance(dataset, CacheableDataset):\n        return figure_data_format_dataset(dataset.unwrap())\n    elif isinstance(dataset, pd.DataFrame):\n        return pd.DataFrame\n    elif dd and isinstance(dataset, dd.DataFrame):\n        return dd.DataFrame\n    elif isinstance(dataset, dict):\n        return dict\n    elif isinstance(dataset, str):\n        dataset = dataset.strip()\n        if dataset.startswith(\"ludwig://\"):\n            return \"ludwig\"\n        if dataset.startswith(\"hf://\"):\n            return \"hf\"\n\n        dataset = dataset.lower()\n        if dataset.endswith(\".csv\"):\n            return \"csv\"\n        elif dataset.endswith(\".tsv\"):\n            return \"tsv\"\n        elif dataset.endswith(\".json\"):\n            return \"json\"\n        elif dataset.endswith(\".jsonl\"):\n            return \"jsonl\"\n        elif (\n            dataset.endswith(\".xls\")\n            or dataset.endswith(\".xlsx\")\n            or dataset.endswith(\".xlsm\")\n            or dataset.endswith(\".xlsb\")\n            or dataset.endswith(\".odf\")\n            or dataset.endswith(\".ods\")\n            or dataset.endswith(\".odt\")\n        ):\n            return \"excel\"\n        elif dataset.endswith(\".parquet\"):\n            return \"parquet\"\n        elif dataset.endswith(\".pickle\") or dataset.endswith(\".p\"):\n            return \"pickle\"\n        elif dataset.endswith(\".feather\"):\n            return \"feather\"\n        elif dataset.endswith(\".fwf\"):\n            return \"fwf\"\n        elif dataset.endswith(\".html\"):\n            return \"html\"\n        elif dataset.endswith(\".orc\"):\n            return \"orc\"\n        elif dataset.endswith(\".sas\"):\n            return \"sas\"\n        elif dataset.endswith(\".spss\"):\n            return \"spss\"\n        elif dataset.endswith(\".dta\") or dataset.endswith(\".stata\"):\n            return \"stata\"\n        elif dataset.endswith(\".h5\") or dataset.endswith(\".hdf5\"):\n            return \"hdf5\"\n        else:\n            raise ValueError(f\"Dataset path string {dataset} does not contain a valid extension\")\n    else:\n        raise ValueError(f\"Cannot figure out the format of dataset {dataset}\")\n\n\n@DeveloperAPI\ndef figure_data_format(dataset=None, training_set=None, validation_set=None, test_set=None):\n    if dataset is not None:\n        data_format = figure_data_format_dataset(dataset)\n    elif training_set is not None:\n        data_formats = [figure_data_format_dataset(training_set)]\n        if validation_set is not None:\n            data_formats.append(figure_data_format_dataset(validation_set))\n        if test_set is not None:\n            data_formats.append(figure_data_format_dataset(test_set))\n        data_formats_set = set(data_formats)\n        if len(data_formats_set) > 1:\n            error_message = \"Datasets have different formats. Training: \"\n            error_message += str(data_formats[0])\n            if validation_set:\n                error_message = \", Validation: \"\n                error_message += str(data_formats[1])\n            if test_set:\n                error_message = \", Test: \"\n                error_message += str(data_formats[-1])\n            raise ValueError(error_message)\n        else:\n            data_format = next(iter(data_formats_set))\n    else:\n        raise ValueError(\"At least one between dataset and training_set must be not None\")\n    return data_format\n\n\n@DeveloperAPI\ndef is_model_dir(path: str) -> bool:\n    hyperparameters_fn = os.path.join(path, MODEL_HYPERPARAMETERS_FILE_NAME)\n    ts_metadata_fn = os.path.join(path, TRAIN_SET_METADATA_FILE_NAME)\n    is_a_model_dir = False\n    if os.path.isdir(path) and os.path.isfile(hyperparameters_fn) and os.path.isfile(ts_metadata_fn):\n        weights_files_count = 0\n        for file_name in os.listdir(path):\n            if file_name.startswith(MODEL_WEIGHTS_FILE_NAME):\n                weights_files_count += 1\n        if weights_files_count >= 2:\n            is_a_model_dir = True\n    return is_a_model_dir\n\n\n@DeveloperAPI\ndef ndarray2string(parm_array):\n    # convert numpy.ndarray to ludwig custom string format\n    if isinstance(parm_array, np.ndarray):\n        return f\"__ndarray__{json.dumps(parm_array.tolist())}\"\n    else:\n        raise ValueError(f\"Argument must be numpy.ndarray. Instead argument found to be {type(parm_array)}\")\n\n\n@DeveloperAPI\ndef string2ndarray(parm_string):\n    # convert ludwig custom ndarray string to numpy.ndarray\n    if isinstance(parm_string, str) and parm_string[:11] == \"__ndarray__\":\n        return np.array(json.loads(parm_string[11:]))\n    else:\n        raise ValueError(\"Argument must be Ludwig custom string format for numpy.ndarray\")\n\n\n@DeveloperAPI\ndef is_ludwig_ndarray_string(parm_string):\n    # tests if parameter is a Ludwig custom ndarray string\n    return isinstance(parm_string, str) and parm_string[:11] == \"__ndarray__\"\n\n\n@DeveloperAPI\ndef get_pa_dtype(obj: Any):\n    if np.isscalar(obj):\n        return pa.from_numpy_dtype(np.array(obj).dtype)\n    elif isinstance(obj, np.ndarray) or isinstance(obj, list) or isinstance(obj, tuple):\n        return pa.list_(get_pa_dtype(obj[0]))\n    else:\n        raise ValueError(f\"Unsupported type for pyarrow dtype: {type(obj)}\")\n\n\n@DeveloperAPI\ndef get_pa_schema(df: DataFrame):\n    \"\"\"Gets the pyarrow schema associated with a given DataFrame.\n\n    This will fail in very specific conditions worth enumerating:\n    1. If the DataFrame is a Dask DataFrame which has a partition of size 1 and its only sample is a NaN, then the\n        `schema` dict will not contain the associated key. The value in this case will be inferred (likely incorrectly)\n        as a float64 downstream.\n    2. If the DataFrame contains NaNs in some column and the presence of NaNs changes the overall dtype of the column.\n        For example, if a number feature column contains some NaN-like value, then its dtype will be changed by the\n        below `fillna` call from float32 to float64. This will cause `to_parquet` to fail downstream.\n    \"\"\"\n    head = df.head(100)\n\n    schema = {}\n    for k, v in head.items():\n        if sum(v.isna()) > 0:\n            v = v.fillna(np.nan).replace([np.nan], [None])  # Only fill NaNs if they are present\n        v = v.values\n\n        for val in v:\n            if val is not None and k not in schema:\n                schema[k] = get_pa_dtype(val)\n                break\n    return pa.schema(list(schema.items()))\n\n\ndata_reader_registry = {\n    **{fmt: read_csv for fmt in CSV_FORMATS},\n    **{fmt: read_tsv for fmt in TSV_FORMATS},\n    **{fmt: read_json for fmt in JSON_FORMATS},\n    **{fmt: read_jsonl for fmt in JSONL_FORMATS},\n    **{fmt: read_excel for fmt in EXCEL_FORMATS},\n    **{fmt: read_parquet for fmt in PARQUET_FORMATS},\n    **{fmt: read_pickle for fmt in PICKLE_FORMATS},\n    **{fmt: read_fwf for fmt in FWF_FORMATS},\n    **{fmt: read_feather for fmt in FEATHER_FORMATS},\n    **{fmt: read_html for fmt in HTML_FORMATS},\n    **{fmt: read_orc for fmt in ORC_FORMATS},\n    **{fmt: read_sas for fmt in SAS_FORMATS},\n    **{fmt: read_spss for fmt in SPSS_FORMATS},\n    **{fmt: read_stata for fmt in STATA_FORMATS},\n    **{fmt: read_hdf5 for fmt in HDF5_FORMATS},\n}\n\n\n@DeveloperAPI\ndef load_dataset(dataset, data_format=None, df_lib=PANDAS_DF):\n    if not data_format or data_format == \"auto\":\n        data_format = figure_data_format(dataset)\n\n    # use appropriate reader to create dataframe\n    if data_format in DATAFRAME_FORMATS:\n        return dataset\n    elif data_format in DICT_FORMATS:\n        return pd.DataFrame(dataset)\n    elif data_format in CACHEABLE_FORMATS:\n        data_reader = get_from_registry(data_format, data_reader_registry)\n        return data_reader(dataset, df_lib)\n    else:\n        raise ValueError(f\"{data_format} format is not supported\")\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef use_credentials(creds):\n    if creds is None:\n        with contextlib.nullcontext():\n            yield\n            return\n\n    # https://filesystem-spec.readthedocs.io/en/latest/features.html#configuration\n    # This allows us to avoid having to plumb the `storage_options` kwargs through\n    # every remote FS call in Ludwig. This implementation is restricted to one thread\n    # in the process acquiring the lock at once.\n    with GLOBAL_CRED_LOCK:\n        with tempfile.TemporaryDirectory() as tmpdir:\n            fname = os.path.join(tmpdir, \"conf.json\")\n            with open(fname, \"w\", encoding=\"utf-8\") as f:\n                json.dump(creds, f)\n\n            # Backup any existing credentials\n            old_conf = dict(**conf)\n\n            conf.clear()\n            set_conf_files(tmpdir, conf)\n            try:\n                yield\n            finally:\n                # Restore previous credentials\n                with open(fname, \"w\", encoding=\"utf-8\") as f:\n                    json.dump(old_conf, f)\n                conf.clear()\n                set_conf_files(tmpdir, conf)\n\n\ndef get_sanitized_feature_name(feature_name: str) -> str:\n    \"\"\"Replaces non-word characters (anything other than alphanumeric or _) with _.\n\n    Used in model config initialization and sanitize_column_names(), which is called during dataset building.\n    \"\"\"\n    return re.sub(r\"[(){}.:\\\"\\\"\\'\\'\\[\\]]\", \"_\", feature_name)\n\n\ndef sanitize_column_names(df: DataFrame) -> DataFrame:\n    \"\"\"Renames df columns with non-word characters (anything other than alphanumeric or _) to _.\"\"\"\n    safe_column_names = [get_sanitized_feature_name(col) for col in df.columns]\n    return df.rename(columns=dict(zip(df.columns, safe_column_names)))\n"
  },
  {
    "path": "ludwig/utils/dataframe_utils.py",
    "content": "from typing import Optional\n\nimport numpy as np\nimport pandas as pd\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import DASK_MODULE_NAME\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.utils.types import DataFrame\n\n\n@DeveloperAPI\ndef is_dask_lib(df_lib) -> bool:\n    \"\"\"Returns whether the dataframe library is dask.\"\"\"\n    return df_lib.__name__ == DASK_MODULE_NAME\n\n\n@DeveloperAPI\ndef is_dask_backend(backend: Optional[\"Backend\"]) -> bool:  # noqa: F821\n    \"\"\"Returns whether the backend's dataframe is dask.\"\"\"\n    return backend is not None and is_dask_lib(backend.df_engine.df_lib)\n\n\n@DeveloperAPI\ndef is_dask_series_or_df(df: DataFrame, backend: Optional[\"Backend\"]) -> bool:  # noqa: F821\n    if is_dask_backend(backend):\n        import dask.dataframe as dd\n\n        return isinstance(df, dd.Series) or isinstance(df, dd.DataFrame)\n    return False\n\n\n@DeveloperAPI\ndef flatten_df(df: DataFrame, df_engine: DataFrameEngine) -> tuple[DataFrame, dict[str, tuple]]:  # noqa: F821\n    \"\"\"Returns a flattened dataframe with a dictionary of the original shapes, keyed by dataframe columns.\"\"\"\n    # Workaround for: https://issues.apache.org/jira/browse/ARROW-5645\n    column_shapes = {}\n    for c in df.columns:\n        df = df_engine.persist(df)\n        shape = df_engine.compute(\n            df_engine.map_objects(\n                df[c],\n                lambda x: np.array(x).shape,\n            ).max()\n        )\n\n        if len(shape) > 1:\n            column_shapes[c] = shape\n            df[c] = df_engine.map_objects(df[c], lambda x: np.array(x).reshape(-1))\n    return df, column_shapes\n\n\n@DeveloperAPI\ndef unflatten_df(df: DataFrame, column_shapes: dict[str, tuple], df_engine: DataFrameEngine) -> DataFrame:  # noqa: F821\n    \"\"\"Returns an unflattened dataframe, the reverse of flatten_df.\"\"\"\n    for c in df.columns:\n        shape = column_shapes.get(c)\n        if shape:\n            df[c] = df_engine.map_objects(df[c], lambda x: np.array(x).reshape(shape))\n    return df\n\n\n@DeveloperAPI\ndef to_numpy_dataset(df: DataFrame, backend: Optional[\"Backend\"] = None) -> dict[str, np.ndarray]:  # noqa: F821\n    \"\"\"Returns a dictionary of numpy arrays, keyed by the columns of the given dataframe.\"\"\"\n    # Compute Dask DataFrames to pandas first to avoid issues with extension dtypes\n    # (e.g. TensorDtype) that Dask-expr's metadata system cannot handle.\n    if backend and is_dask_backend(backend):\n        df = backend.df_engine.compute(df)\n    dataset = {}\n    for col in df.columns:\n        if len(df.index) != 0:\n            dataset[col] = np.stack(df[col].to_numpy())\n        else:\n            # Dataframe is empty.\n            # Use to_list() directly, as np.stack() requires at least one array to stack.\n            dataset[col] = df[col].to_list()\n    return dataset\n\n\n@DeveloperAPI\ndef from_numpy_dataset(dataset) -> pd.DataFrame:\n    \"\"\"Returns a pandas dataframe from the dataset.\"\"\"\n    col_mapping = {}\n    for k, v in dataset.items():\n        if len(v.shape) > 1:\n            # unstacking, needed for ndarrays of dimension 2 and more\n            (*vals,) = v\n        else:\n            # not unstacking. Needed because otherwise pandas casts types\n            # the way it wants, like converting a list of float32 scalats\n            # to a column of float64\n            vals = v\n        col_mapping[k] = vals\n    return pd.DataFrame.from_dict(col_mapping)\n\n\n@DeveloperAPI\ndef set_index_name(pd_df: pd.DataFrame, name: str) -> pd.DataFrame:\n    pd_df.index.name = name\n    return pd_df\n\n\n@DeveloperAPI\ndef to_batches(df: pd.DataFrame, batch_size: int) -> list[pd.DataFrame]:\n    n_rows = len(df)\n    return [df[i : i + batch_size].copy() for i in range(0, n_rows, batch_size)]\n\n\n@DeveloperAPI\ndef from_batches(batches: list[pd.DataFrame]) -> pd.DataFrame:\n    return pd.concat(batches)\n\n\n@DeveloperAPI\ndef to_scalar_df(df: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"Converts all columns in a pd.DataFrame to be scalar types.\n\n    For object columns of lists, each element of the list is expanded into its own column named {column}_{index}. We\n    assume all object columns are lists of the same length (i.e., tensor format output from preprocessing). It's also\n    important that the relative order of the columns is preserved, to maintain consistency with other conversions like\n    the one for Hummingbird.\n    \"\"\"\n    scalar_df = df\n    column_ordering = []\n    for c, s in df.items():\n        if s.dtype == \"object\":\n            s_list = s.to_list()\n            try:\n                ncols = s_list[0].shape[0]\n                split_cols = [f\"{c}_{k}\" for k in range(ncols)]\n                sdf = pd.DataFrame(s_list, columns=split_cols)\n                scalar_df = pd.concat([scalar_df, sdf], axis=1)\n                column_ordering += split_cols\n            except AttributeError as e:\n                raise ValueError(f\"Expected series of lists, but found {s_list[0]}\") from e\n        else:\n            column_ordering.append(c)\n    return scalar_df[column_ordering]\n"
  },
  {
    "path": "ludwig/utils/dataset_utils.py",
    "content": "import pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nfrom ludwig.api_annotations import PublicAPI\nfrom ludwig.constants import TEST_SPLIT, TRAIN_SPLIT, VALIDATION_SPLIT\nfrom ludwig.data.dataset.base import Dataset\nfrom ludwig.utils.defaults import default_random_seed\n\n\n@PublicAPI\ndef get_repeatable_train_val_test_split(\n    df_input, stratify_colname=\"\", random_seed=default_random_seed, frac_train=0.7, frac_val=0.1, frac_test=0.2\n):\n    \"\"\"Return df_input with split column containing (if possible) non-zero rows in the train, validation, and test\n    data subset categories.\n\n    If the input dataframe does not contain an existing split column or if the\n    number of rows in both the validation and test split is 0 and non-empty\n    stratify_colname specified, return df_input with split column set according\n    to frac_<subset_name> and stratify_colname.\n\n    Else stratify_colname is ignored, and:\n     If the input dataframe contains an existing split column and non-zero row\n      counts for all three split types, return df_input.\n     If the input dataframe contains an existing split column but only one of\n      validation and test split has non-zero row counts, return df_input with\n      missing split getting rows from train split as per frac_<subset_name>.\n\n    Parameters\n    ----------\n    df_input : Pandas dataframe\n        Input dataframe to be split.\n    stratify_colname : str\n        The column used for stratification (if desired); usually the label column.\n    random_seed : int\n        Seed used to get repeatable split.\n    frac_train : float\n    frac_val   : float\n    frac_test  : float\n        The ratios with which to split the dataframe into train, val, and test data;\n        should sum to 1.0.\n\n    Returns\n    -------\n    df_split :\n        Dataframe containing the three splits.\n    \"\"\"\n\n    if frac_train + frac_val + frac_test != 1.0:\n        raise ValueError(f\"fractions {frac_train:f}, {frac_val:f}, {frac_test:f} do not add up to 1.0\")\n    if stratify_colname:\n        do_stratify_split = True\n        if stratify_colname not in df_input.columns:\n            raise ValueError(\"%s is not a column in the dataframe\" % (stratify_colname))\n    else:\n        do_stratify_split = False\n        if \"split\" not in df_input.columns:\n            df_input[\"split\"] = 0  # set up for non-stratified split path\n\n    if \"split\" in df_input.columns:\n        df_train = df_input[df_input[\"split\"] == TRAIN_SPLIT].copy()\n        df_val = df_input[df_input[\"split\"] == VALIDATION_SPLIT].copy()\n        df_test = df_input[df_input[\"split\"] == TEST_SPLIT].copy()\n        if not do_stratify_split or len(df_val) != 0 or len(df_test) != 0:\n            if len(df_val) == 0:\n                df_val = df_train.sample(frac=frac_val, replace=False, random_state=random_seed)\n                df_train = df_train.drop(df_val.index)\n            if len(df_test) == 0:\n                df_test = df_train.sample(frac=frac_test, replace=False, random_state=random_seed)\n                df_train = df_train.drop(df_test.index)\n            do_stratify_split = False\n\n    if do_stratify_split:\n        # Make sure the `stratify_colname` doesn't have any NaNs.\n        df_input = df_input[df_input[stratify_colname].notna()]\n\n        # Split original dataframe into train and temp dataframes.\n        y = df_input[[stratify_colname]]  # Dataframe of just the column on which to stratify.\n        df_train, df_temp, y_train, y_temp = train_test_split(\n            df_input, y, stratify=y, test_size=(1.0 - frac_train), random_state=random_seed\n        )\n        # Split the temp dataframe into val and test dataframes.\n        relative_frac_test = frac_test / (frac_val + frac_test)\n        df_val, df_test, y_val, y_test = train_test_split(\n            df_temp, y_temp, stratify=y_temp, test_size=relative_frac_test, random_state=random_seed\n        )\n\n    assert len(df_input) == len(df_train) + len(df_val) + len(df_test)\n    df_train[\"split\"] = TRAIN_SPLIT\n    df_val[\"split\"] = VALIDATION_SPLIT\n    df_test[\"split\"] = TEST_SPLIT\n    df_split = pd.concat([df_train, df_val, df_test], ignore_index=True)\n    return df_split\n\n\ndef generate_dataset_statistics(\n    training_set: Dataset,\n    validation_set: str | dict | pd.DataFrame | Dataset | None,\n    test_set: str | dict | pd.DataFrame | Dataset | None,\n) -> list[tuple[str, int, int]]:\n    from ludwig.benchmarking.utils import format_memory\n\n    dataset_statistics = [\n        [\"Dataset\", \"Size (Rows)\", \"Size (In Memory)\"],\n        [\"Training\", len(training_set), format_memory(training_set.in_memory_size_bytes)],\n    ]\n    if validation_set is not None:\n        dataset_statistics.append(\n            [\"Validation\", len(validation_set), format_memory(validation_set.in_memory_size_bytes)]\n        )\n    if test_set is not None:\n        dataset_statistics.append([\"Test\", len(test_set), format_memory(test_set.in_memory_size_bytes)])\n    return dataset_statistics\n"
  },
  {
    "path": "ludwig/utils/date_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport time\nfrom datetime import date, datetime, timezone\n\nimport numpy as np\nfrom dateutil.parser import parse, ParserError\n\nfrom ludwig.api_annotations import DeveloperAPI\n\nSCALE_S = np.floor(np.log10(time.time()))\n\n\n@DeveloperAPI\ndef create_vector_from_datetime_obj(datetime_obj):\n    yearday = datetime_obj.toordinal() - date(datetime_obj.year, 1, 1).toordinal() + 1\n\n    midnight = datetime_obj.replace(hour=0, minute=0, second=0, microsecond=0)\n    second_of_day = (datetime_obj - midnight).seconds\n\n    return [\n        datetime_obj.year,\n        datetime_obj.month,\n        datetime_obj.day,\n        datetime_obj.weekday(),\n        yearday,\n        datetime_obj.hour,\n        datetime_obj.minute,\n        datetime_obj.second,\n        second_of_day,\n    ]\n\n\n@DeveloperAPI\ndef parse_datetime(timestamp: float | int | str) -> datetime:\n    \"\"\"Parse a datetime from a string or a numeric timestamp.\n\n    Args:\n        timestamp: A datetime string or numeric timestamp.\n\n    Returns:\n        A datetime representation of `timestamp`.\n    \"\"\"\n    try:\n        dt = parse(timestamp)\n    except (OverflowError, ParserError, TypeError):\n        dt = convert_number_to_datetime(timestamp)\n\n    return dt\n\n\n@DeveloperAPI\ndef convert_number_to_datetime(timestamp: float | int | str) -> datetime:\n    \"\"\"Convert a numeric timestamp to a datetime object.\n\n    `datetime` objects can be created from POSIX timestamps like those returned by `time.time()`.\n\n    Args:\n        timestamp: A numeric timestamp.\n\n    Returns:\n        A datetime representation of `timestamp`.\n\n    Raises:\n        ValueError: Raised if `timestamp` is not a number or not a valid datetime.\n    \"\"\"\n    try:\n        timestamp = float(timestamp)\n    except TypeError:\n        raise ValueError(f\"Provided value {timestamp} is not a valid numeric timestamp\")\n\n    # Determine the unit of the timestamp\n    ts_scale = np.floor(np.log10(timestamp))\n\n    # `datetime.datetime.fromtimestamp` expects a timestamp in seconds. Rescale the timestamp if it is not in seconds.\n    if SCALE_S < ts_scale:\n        delta = ts_scale - SCALE_S\n        timestamp = timestamp / np.power(10, delta)\n\n    # Convert the timestamp to a datetime object. If it is not a valid timestamp, `ValueError` is raised.\n    dt = datetime.fromtimestamp(timestamp, tz=timezone.utc).replace(tzinfo=None)\n    return dt\n"
  },
  {
    "path": "ludwig/utils/defaults.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport copy\nimport logging\n\nimport yaml\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.features.feature_registries import get_input_type_registry\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.schema.preprocessing import PreprocessingConfig\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom ludwig.utils.data_utils import load_config_from_str, load_yaml\nfrom ludwig.utils.fs_utils import open_file\nfrom ludwig.utils.print_utils import print_ludwig\n\nlogger = logging.getLogger(__name__)\n\ndefault_random_seed = 42\n\n# Still needed for preprocessing  TODO(Connor): Refactor ludwig/data/preprocessing to use schema\n# TODO(travis): remove this, make type a protected string for each subclass\ndefault_feature_specific_preprocessing_parameters = {\n    name: preproc_sect.get_schema_cls()(name=\"__tmp__\", type=name).preprocessing.to_dict()\n    for name, preproc_sect in get_input_type_registry().items()\n}\n\ndefault_training_preprocessing_parameters = copy.deepcopy(default_feature_specific_preprocessing_parameters)\ndefault_training_preprocessing_parameters.update(PreprocessingConfig().to_dict())\n\ndefault_prediction_preprocessing_parameters = copy.deepcopy(default_feature_specific_preprocessing_parameters)\n\n\n@DeveloperAPI\ndef render_config(config=None, output=None, **kwargs):\n    upgraded_config = upgrade_config_dict_to_latest_version(config)\n    output_config = ModelConfig.from_dict(upgraded_config).to_dict()\n\n    if output is None:\n        print(yaml.safe_dump(output_config, None, sort_keys=False))\n    else:\n        with open_file(output, \"w\") as f:\n            yaml.safe_dump(output_config, f, sort_keys=False)\n\n\n@DeveloperAPI\ndef cli_render_config(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script renders the full config from a user config.\",\n        prog=\"ludwig render_config\",\n        usage=\"%(prog)s [options]\",\n    )\n    parser.add_argument(\n        \"-c\",\n        \"--config\",\n        type=load_yaml,\n        help=\"Path to the YAML file containing the model configuration\",\n    )\n    parser.add_argument(\n        \"-cs\",\n        \"--config_str\",\n        dest=\"config\",\n        type=load_config_from_str,\n        help=\"JSON or YAML serialized string of the model configuration\",\n    )\n    parser.add_argument(\n        \"-o\",\n        \"--output\",\n        type=str,\n        help=\"output rendered YAML config path\",\n        required=False,\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"render_config\", *sys_argv)\n\n    print_ludwig(\"Render Config\", LUDWIG_VERSION)\n    render_config(**vars(args))\n"
  },
  {
    "path": "ludwig/utils/entmax/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 DeepSPIN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "ludwig/utils/entmax/README.md",
    "content": "# entmax\n\n______________________________________________________________________\n\nThis package provides a pytorch implementation of entmax and entmax losses:\na sparse family of probability mappings and corresponding loss functions,\ngeneralizing softmax / cross-entropy.\n\n*Features:*\n\n- Exact partial-sort algorithms for 1.5-entmax and 2-entmax (sparsemax).\n- A bisection-based algorithm for generic alpha-entmax.\n- Gradients w.r.t. alpha for adaptive, learned sparsity!\n\n*Requirements:* python 3, pytorch >= 1.0 (and pytest for unit tests)\n\n## Example\n\n```python\nimport torch\nfrom torch.nn.functional import softmax\n\nfrom entmax import sparsemax, entmax15\n\nx = torch.tensor([-2, 0, 0.5])\n\nprint(softmax(x, dim=0))\n# tensor([0.0486, 0.3592, 0.5922])\n\nprint(sparsemax(x, dim=0))\n# tensor([0.0000, 0.2500, 0.7500])\n\nprint(entmax15(x, dim=0))\n# tensor([0.0000, 0.3260, 0.6740])\n```\n\nGradients w.r.t. alpha (continued):\n\n```python\nimport torch\nfrom torch.autograd import grad\n\nfrom entmax import entmax_bisect\n\nx = torch.tensor([[-1, 0, 0.5], [1, 2, 3.5]])\n\nalpha = torch.tensor(1.33, requires_grad=True)\n\np = entmax_bisect(x, alpha)\n\nprint(p)\n# tensor([[0.0460, 0.3276, 0.6264],\n#        [0.0026, 0.1012, 0.8963]], grad_fn=<EntmaxBisectFunctionBackward>)\n\nprint(grad(p[0, 0], alpha))\n# (tensor(-0.2562),)\n```\n\n## Installation\n\n```\npip install entmax\n```\n\n## Citations\n\n[Sparse Sequence-to-Sequence Models](https://www.aclweb.org/anthology/P19-1146)\n\n```\n@inproceedings{entmax,\n  author    = {Peters, Ben and Niculae, Vlad and Martins, Andr{\\'e} FT},\n  title     = {Sparse Sequence-to-Sequence Models},\n  booktitle = {Proc. ACL},\n  year      = {2019},\n  url       = {https://www.aclweb.org/anthology/P19-1146}\n}\n```\n\n[Adaptively Sparse Transformers](https://arxiv.org/pdf/1909.00015.pdf)\n\n```\n@inproceedings{correia19adaptively,\n  author    = {Correia, Gon\\c{c}alo M and Niculae, Vlad and Martins, Andr{\\'e} FT},\n  title     = {Adaptively Sparse Transformers},\n  booktitle = {Proc. EMNLP-IJCNLP (to appear)},\n  year      = {2019},\n}\n```\n\nFurther reading:\n\n- Blondel, Martins, and Niculae, 2019. [Learning with Fenchel-Young Losses](https://arxiv.org/abs/1901.02324).\n- Martins and Astudillo, 2016. [From Softmax to Sparsemax: A Sparse Model of Attention and Multi-Label Classification](https://arxiv.org/abs/1602.02068).\n- 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).\n"
  },
  {
    "path": "ludwig/utils/entmax/__init__.py",
    "content": "__version__ = \"1.1.dev0\"\n\nfrom ludwig.utils.entmax.activations import entmax15, Entmax15, sparsemax, Sparsemax\nfrom ludwig.utils.entmax.losses import (\n    entmax15_loss,\n    Entmax15Loss,\n    entmax_bisect_loss,\n    EntmaxBisectLoss,\n    sparsemax_bisect_loss,\n    sparsemax_loss,\n    SparsemaxBisectLoss,\n    SparsemaxLoss,\n)\nfrom ludwig.utils.entmax.root_finding import entmax_bisect, EntmaxBisect, sparsemax_bisect, SparsemaxBisect\n\n__all__ = [\n    \"entmax15\",\n    \"Entmax15\",\n    \"sparsemax\",\n    \"Sparsemax\",\n    \"entmax15_loss\",\n    \"Entmax15Loss\",\n    \"entmax_bisect_loss\",\n    \"EntmaxBisectLoss\",\n    \"sparsemax_bisect_loss\",\n    \"sparsemax_loss\",\n    \"SparsemaxBisectLoss\",\n    \"SparsemaxLoss\",\n    \"entmax_bisect\",\n    \"EntmaxBisect\",\n    \"sparsemax_bisect\",\n    \"SparsemaxBisect\",\n]\n"
  },
  {
    "path": "ludwig/utils/entmax/activations.py",
    "content": "\"\"\"An implementation of entmax (Peters et al., 2019). See https://arxiv.org/pdf/1905.05702 for detailed\ndescription.\n\nThis builds on previous work with sparsemax (Martins & Astudillo, 2016). See https://arxiv.org/pdf/1602.02068.\n\"\"\"\n\n# Author: Ben Peters\n# Author: Vlad Niculae <vlad@vene.ro>\n# License: MIT\n\nimport torch\nimport torch.nn as nn\nfrom torch.autograd import Function\n\n\ndef _make_ix_like(X, dim):\n    d = X.size(dim)\n    rho = torch.arange(1, d + 1, device=X.device, dtype=X.dtype)\n    view = [1] * X.dim()\n    view[0] = -1\n    return rho.view(view).transpose(0, dim)\n\n\ndef _roll_last(X, dim):\n    if dim == -1:\n        return X\n    elif dim < 0:\n        dim = X.dim() - dim\n\n    perm = [i for i in range(X.dim()) if i != dim] + [dim]\n    return X.permute(perm)\n\n\ndef _sparsemax_threshold_and_support(X, dim=-1, k=None):\n    \"\"\"Core computation for sparsemax: optimal threshold and support size.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor to compute thresholds over.\n\n    dim : int\n        The dimension along which to apply sparsemax.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    tau : torch.Tensor like `X`, with all but the `dim` dimension intact\n        the threshold value for each vector\n    support_size : torch LongTensor, shape like `tau`\n        the number of nonzeros in each vector.\n    \"\"\"\n\n    if k is None or k >= X.shape[dim]:  # do full sort\n        topk, _ = torch.sort(X, dim=dim, descending=True)\n    else:\n        topk, _ = torch.topk(X, k=k, dim=dim)\n\n    topk_cumsum = topk.cumsum(dim) - 1\n    rhos = _make_ix_like(topk, dim)\n    support = rhos * topk > topk_cumsum\n\n    support_size = support.sum(dim=dim).unsqueeze(dim)\n    tau = topk_cumsum.gather(dim, support_size - 1)\n    tau /= support_size.to(X.dtype)\n\n    if k is not None and k < X.shape[dim]:\n        unsolved = (support_size == k).squeeze(dim)\n\n        if torch.any(unsolved):\n            in_ = _roll_last(X, dim)[unsolved]\n            tau_, ss_ = _sparsemax_threshold_and_support(in_, dim=-1, k=2 * k)\n            _roll_last(tau, dim)[unsolved] = tau_\n            _roll_last(support_size, dim)[unsolved] = ss_\n\n    return tau, support_size\n\n\ndef _entmax_threshold_and_support(X, dim=-1, k=None):\n    \"\"\"Core computation for 1.5-entmax: optimal threshold and support size.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor to compute thresholds over.\n\n    dim : int\n        The dimension along which to apply 1.5-entmax.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    tau : torch.Tensor like `X`, with all but the `dim` dimension intact\n        the threshold value for each vector\n    support_size : torch LongTensor, shape like `tau`\n        the number of nonzeros in each vector.\n    \"\"\"\n\n    if k is None or k >= X.shape[dim]:  # do full sort\n        Xsrt, _ = torch.sort(X, dim=dim, descending=True)\n    else:\n        Xsrt, _ = torch.topk(X, k=k, dim=dim)\n\n    rho = _make_ix_like(Xsrt, dim)\n    mean = Xsrt.cumsum(dim) / rho\n    mean_sq = (Xsrt**2).cumsum(dim) / rho\n    ss = rho * (mean_sq - mean**2)\n    delta = (1 - ss) / rho\n\n    # NOTE this is not exactly the same as in reference algo\n    # Fortunately it seems the clamped values never wrongly\n    # get selected by tau <= sorted_z. Prove this!\n    delta_nz = torch.clamp(delta, 0)\n    tau = mean - torch.sqrt(delta_nz)\n\n    support_size = (tau <= Xsrt).sum(dim).unsqueeze(dim)\n    tau_star = tau.gather(dim, support_size - 1)\n\n    if k is not None and k < X.shape[dim]:\n        unsolved = (support_size == k).squeeze(dim)\n\n        if torch.any(unsolved):\n            X_ = _roll_last(X, dim)[unsolved]\n            tau_, ss_ = _entmax_threshold_and_support(X_, dim=-1, k=2 * k)\n            _roll_last(tau_star, dim)[unsolved] = tau_\n            _roll_last(support_size, dim)[unsolved] = ss_\n\n    return tau_star, support_size\n\n\nclass SparsemaxFunction(Function):\n    @classmethod\n    def forward(cls, ctx, X, dim=-1, k=None):\n        ctx.dim = dim\n        output, backwards_kwargs = _sparsemax_forward(X, dim, k)\n        ctx.save_for_backward(backwards_kwargs[\"supp_size\"], output)\n        return output\n\n    @classmethod\n    def backward(cls, ctx, grad_output):\n        supp_size, output = ctx.saved_tensors\n        dim = ctx.dim\n        grad_input = grad_output.clone()\n        grad_input[output == 0] = 0\n\n        v_hat = grad_input.sum(dim=dim) / supp_size.to(output.dtype).squeeze(dim)\n        v_hat = v_hat.unsqueeze(dim)\n        grad_input = torch.where(output != 0, grad_input - v_hat, grad_input)\n        return grad_input, None, None\n\n\ndef _sparsemax_forward(X, dim, k):\n    max_val, _ = X.max(dim=dim, keepdim=True)\n    X = X - max_val  # same numerical stability trick as softmax\n    tau, supp_size = _sparsemax_threshold_and_support(X, dim=dim, k=k)\n    output = torch.clamp(X - tau, min=0)\n    return output, {\"supp_size\": supp_size}\n\n\nclass Entmax15Function(Function):\n    @classmethod\n    def forward(cls, ctx, X, dim=0, k=None):\n        ctx.dim = dim\n        Y, _ = _entmax15_forward(X, dim, k)\n        ctx.save_for_backward(Y)\n        return Y\n\n    @classmethod\n    def backward(cls, ctx, dY):\n        (Y,) = ctx.saved_tensors\n        gppr = Y.sqrt()  # = 1 / g'' (Y)\n        dX = dY * gppr\n        q = dX.sum(ctx.dim) / gppr.sum(ctx.dim)\n        q = q.unsqueeze(ctx.dim)\n        dX -= q * gppr\n        return dX, None, None\n\n\ndef _entmax15_forward(X, dim, k):\n    max_val, _ = X.max(dim=dim, keepdim=True)\n    X = X - max_val  # same numerical stability trick as for softmax\n    X = X / 2  # divide by 2 to solve actual Entmax\n\n    tau_star, _ = _entmax_threshold_and_support(X, dim=dim, k=k)\n\n    Y = torch.clamp(X - tau_star, min=0) ** 2\n    return Y, {}\n\n\ndef sparsemax(X, dim=-1, k=None, training=True):\n    \"\"\"sparsemax: normalizing sparse transform (a la softmax).\n\n    Solves the projection:\n\n        min_p ||x - p||_2   s.t.    p >= 0, sum(p) == 1.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor.\n\n    dim : int\n        The dimension along which to apply sparsemax.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    P : torch tensor, same shape as X\n        The projection result, such that P.sum(dim=dim) == 1 elementwise.\n    \"\"\"\n    # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility\n    # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053\n    if not training:\n        output, _ = _sparsemax_forward(X, dim, k)\n        return output\n    return SparsemaxFunction.apply(X, dim, k)\n\n\ndef entmax15(X, dim=-1, k=None, training=True):\n    \"\"\"1.5-entmax: normalizing sparse transform (a la softmax).\n\n    Solves the optimization problem:\n\n        max_p <x, p> - H_1.5(p)    s.t.    p >= 0, sum(p) == 1.\n\n    where H_1.5(p) is the Tsallis alpha-entropy with alpha=1.5.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor.\n\n    dim : int\n        The dimension along which to apply 1.5-entmax.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    P : torch tensor, same shape as X\n        The projection result, such that P.sum(dim=dim) == 1 elementwise.\n    \"\"\"\n    # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility\n    # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053\n    if not training:\n        output, _ = _entmax15_forward(X, dim, k)\n        return output\n    return Entmax15Function.apply(X, dim, k)\n\n\nclass Sparsemax(nn.Module):\n    def __init__(self, dim=-1, k=None):\n        \"\"\"sparsemax: normalizing sparse transform (a la softmax).\n\n        Solves the projection:\n\n            min_p ||x - p||_2   s.t.    p >= 0, sum(p) == 1.\n\n        Parameters\n        ----------\n        dim : int\n            The dimension along which to apply sparsemax.\n\n        k : int or None\n            number of largest elements to partial-sort over. For optimal\n            performance, should be slightly bigger than the expected number of\n            nonzeros in the solution. If the solution is more than k-sparse,\n            this function is recursively called with a 2*k schedule.\n            If `None`, full sorting is performed from the beginning.\n        \"\"\"\n        self.dim = dim\n        self.k = k\n        super().__init__()\n\n    def forward(self, X):\n        return sparsemax(X, dim=self.dim, k=self.k, training=self.training)\n\n\nclass Entmax15(nn.Module):\n    def __init__(self, dim=-1, k=None):\n        \"\"\"1.5-entmax: normalizing sparse transform (a la softmax).\n\n        Solves the optimization problem:\n\n            max_p <x, p> - H_1.5(p)    s.t.    p >= 0, sum(p) == 1.\n\n        where H_1.5(p) is the Tsallis alpha-entropy with alpha=1.5.\n\n        Parameters\n        ----------\n        dim : int\n            The dimension along which to apply 1.5-entmax.\n\n        k : int or None\n            number of largest elements to partial-sort over. For optimal\n            performance, should be slightly bigger than the expected number of\n            nonzeros in the solution. If the solution is more than k-sparse,\n            this function is recursively called with a 2*k schedule.\n            If `None`, full sorting is performed from the beginning.\n        \"\"\"\n        self.dim = dim\n        self.k = k\n        super().__init__()\n\n    def forward(self, X):\n        return entmax15(X, dim=self.dim, k=self.k, training=self.training)\n"
  },
  {
    "path": "ludwig/utils/entmax/losses.py",
    "content": "import torch\nimport torch.nn as nn\nfrom torch.autograd import Function\n\nfrom ludwig.constants import IGNORE_INDEX_TOKEN_ID\nfrom ludwig.utils.entmax.activations import entmax15, sparsemax\nfrom ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect\n\n\nclass _GenericLoss(nn.Module):\n    def __init__(self, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction=\"elementwise_mean\"):\n        assert reduction in [\"elementwise_mean\", \"sum\", \"none\"]\n        self.reduction = reduction\n        self.ignore_index = ignore_index\n        super().__init__()\n\n    def forward(self, X, target):\n        loss = self.loss(X, target)\n        if self.ignore_index >= 0:\n            ignored_positions = target == self.ignore_index\n            size = (target.size(0) - ignored_positions.sum()).item()\n            loss.masked_fill_(ignored_positions, 0.0)\n        else:\n            size = target.size(0)\n        if self.reduction == \"sum\":\n            loss = loss.sum()\n        elif self.reduction == \"elementwise_mean\":\n            if size == 0:\n                # Returns zero loss and zero gradient in the rare case that all row targets are ignored.\n                loss = loss.sum() * 0.0\n            else:\n                loss = loss.sum() / float(size)\n        return loss\n\n\nclass _GenericLossFunction(Function):\n    @classmethod\n    def forward(cls, ctx, X, target, alpha, proj_args):\n        \"\"\"X (FloatTensor): n x num_classes target (LongTensor): n, the indices of the target classes.\"\"\"\n        assert X.shape[0] == target.shape[0]\n\n        p_star = cls.project(X, alpha, **proj_args)\n        loss = cls.omega(p_star, alpha)\n\n        p_star.scatter_add_(1, target.unsqueeze(1), torch.full_like(p_star, -1))\n        loss += torch.einsum(\"ij,ij->i\", p_star, X)\n        ctx.save_for_backward(p_star)\n\n        return loss\n\n    @classmethod\n    def backward(cls, ctx, grad_output):\n        (p_star,) = ctx.saved_tensors\n        grad = grad_output.unsqueeze(1) * p_star\n        ret = (grad,)\n\n        # pad with as many Nones as needed\n        return ret + (None,) * (1 + cls.n_fwd_args)\n\n\nclass SparsemaxLossFunction(_GenericLossFunction):\n    n_fwd_args = 1\n\n    @classmethod\n    def project(cls, X, alpha, k):\n        return sparsemax(X, dim=-1, k=k)\n\n    @classmethod\n    def omega(cls, p_star, alpha):\n        return (1 - (p_star**2).sum(dim=1)) / 2\n\n    @classmethod\n    def forward(cls, ctx, X, target, k=None):\n        return super().forward(ctx, X, target, alpha=2, proj_args=dict(k=k))\n\n\nclass SparsemaxBisectLossFunction(_GenericLossFunction):\n    n_fwd_args = 1\n\n    @classmethod\n    def project(cls, X, alpha, n_iter):\n        return sparsemax_bisect(X, n_iter=n_iter)\n\n    @classmethod\n    def omega(cls, p_star, alpha):\n        return (1 - (p_star**2).sum(dim=1)) / 2\n\n    @classmethod\n    def forward(cls, ctx, X, target, n_iter=50):\n        return super().forward(ctx, X, target, alpha=2, proj_args=dict(n_iter=n_iter))\n\n\nclass Entmax15LossFunction(_GenericLossFunction):\n    n_fwd_args = 1\n\n    @classmethod\n    def project(cls, X, alpha, k=None):\n        return entmax15(X, dim=-1, k=k)\n\n    @classmethod\n    def omega(cls, p_star, alpha):\n        return (1 - (p_star * torch.sqrt(p_star)).sum(dim=1)) / 0.75\n\n    @classmethod\n    def forward(cls, ctx, X, target, k=None):\n        return super().forward(ctx, X, target, alpha=1.5, proj_args=dict(k=k))\n\n\nclass EntmaxBisectLossFunction(_GenericLossFunction):\n    n_fwd_args = 2\n\n    @classmethod\n    def project(cls, X, alpha, n_iter):\n        return entmax_bisect(X, alpha=alpha, n_iter=n_iter, ensure_sum_one=True)\n\n    @classmethod\n    def omega(cls, p_star, alpha):\n        return (1 - (p_star**alpha).sum(dim=1)) / (alpha * (alpha - 1))\n\n    @classmethod\n    def forward(cls, ctx, X, target, alpha=1.5, n_iter=50):\n        return super().forward(ctx, X, target, alpha, proj_args=dict(n_iter=n_iter))\n\n\ndef sparsemax_loss(X, target, k=None):\n    \"\"\"sparsemax loss: sparse alternative to cross-entropy.\n\n    Computed using a partial sorting strategy.\n\n    Parameters\n    ----------\n    X : torch.Tensor, shape=(n_samples, n_classes)\n        The input 2D tensor of predicted scores\n\n    target : torch.LongTensor, shape=(n_samples,)\n        The ground truth labels, 0 <= target < n_classes.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    losses, torch.Tensor, shape=(n_samples,)\n        The loss incurred at each sample.\n    \"\"\"\n    return SparsemaxLossFunction.apply(X, target, k)\n\n\ndef sparsemax_bisect_loss(X, target, n_iter=50):\n    \"\"\"sparsemax loss: sparse alternative to cross-entropy.\n\n    Computed using bisection.\n\n    Parameters\n    ----------\n    X : torch.Tensor, shape=(n_samples, n_classes)\n        The input 2D tensor of predicted scores\n\n    target : torch.LongTensor, shape=(n_samples,)\n        The ground truth labels, 0 <= target < n_classes.\n\n    n_iter : int\n        Number of bisection iterations. For float32, 24 iterations should\n        suffice for machine precision.\n\n    Returns\n    -------\n    losses, torch.Tensor, shape=(n_samples,)\n        The loss incurred at each sample.\n    \"\"\"\n    return SparsemaxBisectLossFunction.apply(X, target, n_iter)\n\n\ndef entmax15_loss(X, target, k=None):\n    \"\"\"1.5-entmax loss: sparse alternative to cross-entropy\n\n    Computed using a partial sorting strategy.\n\n    Parameters\n    ----------\n    X : torch.Tensor, shape=(n_samples, n_classes)\n        The input 2D tensor of predicted scores\n\n    target : torch.LongTensor, shape=(n_samples,)\n        The ground truth labels, 0 <= target < n_classes.\n\n    k : int or None\n        number of largest elements to partial-sort over. For optimal\n        performance, should be slightly bigger than the expected number of\n        nonzeros in the solution. If the solution is more than k-sparse,\n        this function is recursively called with a 2*k schedule.\n        If `None`, full sorting is performed from the beginning.\n\n    Returns\n    -------\n    losses, torch.Tensor, shape=(n_samples,)\n        The loss incurred at each sample.\n    \"\"\"\n    return Entmax15LossFunction.apply(X, target, k)\n\n\ndef entmax_bisect_loss(X, target, alpha=1.5, n_iter=50):\n    \"\"\"alpha-entmax loss: sparse alternative to cross-entropy.\n\n    Computed using bisection, supporting arbitrary alpha > 1.\n\n    Parameters\n    ----------\n    X : torch.Tensor, shape=(n_samples, n_classes)\n        The input 2D tensor of predicted scores\n\n    target : torch.LongTensor, shape=(n_samples,)\n        The ground truth labels, 0 <= target < n_classes.\n\n    alpha : float or torch.Tensor\n        Tensor of alpha parameters (> 1) to use for each row of X. If scalar\n        or python float, the same value is used for all rows. A value of\n        alpha=2 corresponds to sparsemax, and alpha=1 would in theory recover\n        softmax. For numeric reasons, this algorithm does not work with `alpha=1`:\n        if you want softmax, we recommend `torch.nn.softmax`\n\n    n_iter : int\n        Number of bisection iterations. For float32, 24 iterations should\n        suffice for machine precision.\n\n    Returns\n    -------\n    losses, torch.Tensor, shape=(n_samples,)\n        The loss incurred at each sample.\n    \"\"\"\n    return EntmaxBisectLossFunction.apply(X, target, alpha, n_iter)\n\n\nclass SparsemaxBisectLoss(_GenericLoss):\n    def __init__(self, n_iter=50, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction=\"elementwise_mean\"):\n        self.n_iter = n_iter\n        super().__init__(ignore_index, reduction)\n\n    def loss(self, X, target):\n        return sparsemax_bisect_loss(X, target, self.n_iter)\n\n\nclass SparsemaxLoss(_GenericLoss):\n    def __init__(self, k=None, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction=\"elementwise_mean\"):\n        self.k = k\n        super().__init__(ignore_index, reduction)\n\n    def loss(self, X, target):\n        return sparsemax_loss(X, target, self.k)\n\n\nclass EntmaxBisectLoss(_GenericLoss):\n    def __init__(\n        self,\n        alpha=1.5,\n        n_iter=50,\n        ignore_index=IGNORE_INDEX_TOKEN_ID,\n        reduction=\"elementwise_mean\",\n    ):\n        self.alpha = alpha\n        self.n_iter = n_iter\n        super().__init__(ignore_index, reduction)\n\n    def loss(self, X, target):\n        return entmax_bisect_loss(X, target, self.alpha, self.n_iter)\n\n\nclass Entmax15Loss(_GenericLoss):\n    def __init__(self, k=100, ignore_index=IGNORE_INDEX_TOKEN_ID, reduction=\"elementwise_mean\"):\n        self.k = k\n        super().__init__(ignore_index, reduction)\n\n    def loss(self, X, target):\n        return entmax15_loss(X, target, self.k)\n"
  },
  {
    "path": "ludwig/utils/entmax/root_finding.py",
    "content": "\"\"\"Bisection implementation of alpha-entmax (Peters et al., 2019).\n\nBackward pass wrt alpha per (Correia et al., 2019). See https://arxiv.org/pdf/1905.05702 for detailed description.\n\"\"\"\n\n# Author: Goncalo M Correia\n# Author: Ben Peters\n# Author: Vlad Niculae <vlad@vene.ro>\n\nimport torch\nimport torch.nn as nn\nfrom torch.autograd import Function\n\n\nclass EntmaxBisectFunction(Function):\n    @classmethod\n    def _gp(cls, x, alpha):\n        return x ** (alpha - 1)\n\n    @classmethod\n    def _gp_inv(cls, y, alpha):\n        return y ** (1 / (alpha - 1))\n\n    @classmethod\n    def _p(cls, X, alpha):\n        return cls._gp_inv(torch.clamp(X, min=0), alpha)\n\n    @classmethod\n    def forward(cls, ctx, X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True):\n        p_m, backward_kwargs = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls)\n\n        ctx.alpha = backward_kwargs[\"alpha\"]\n        ctx.dim = backward_kwargs[\"dim\"]\n        ctx.save_for_backward(p_m)\n        return p_m\n\n    @classmethod\n    def backward(cls, ctx, dY):\n        (Y,) = ctx.saved_tensors\n\n        gppr = torch.where(Y > 0, Y ** (2 - ctx.alpha), Y.new_zeros(1))\n\n        dX = dY * gppr\n        q = dX.sum(ctx.dim) / gppr.sum(ctx.dim)\n        q = q.unsqueeze(ctx.dim)\n        dX -= q * gppr\n\n        d_alpha = None\n        if ctx.needs_input_grad[1]:\n            # alpha gradient computation\n            # d_alpha = (partial_y / partial_alpha) * dY\n            # NOTE: ensure alpha is not close to 1\n            # since there is an indetermination\n            # batch_size, _ = dY.shape\n\n            # shannon terms\n            S = torch.where(Y > 0, Y * torch.log(Y), Y.new_zeros(1))\n            # shannon entropy\n            ent = S.sum(ctx.dim).unsqueeze(ctx.dim)\n            Y_skewed = gppr / gppr.sum(ctx.dim).unsqueeze(ctx.dim)\n\n            d_alpha = dY * (Y - Y_skewed) / ((ctx.alpha - 1) ** 2)\n            d_alpha -= dY * (S - Y_skewed * ent) / (ctx.alpha - 1)\n            d_alpha = d_alpha.sum(ctx.dim).unsqueeze(ctx.dim)\n\n        return dX, d_alpha, None, None, None\n\n\ndef _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one, cls=EntmaxBisectFunction):\n    if not isinstance(alpha, torch.Tensor):\n        alpha = torch.tensor(alpha, dtype=X.dtype, device=X.device)\n\n    alpha_shape = list(X.shape)\n    alpha_shape[dim] = 1\n    alpha = alpha.expand(*alpha_shape)\n\n    d = X.shape[dim]\n\n    max_val, _ = X.max(dim=dim, keepdim=True)\n    X = X * (alpha - 1)\n    max_val = max_val * (alpha - 1)\n\n    # Note: when alpha < 1, tau_lo > tau_hi. This still works since dm < 0.\n    tau_lo = max_val - cls._gp(1, alpha)\n    tau_hi = max_val - cls._gp(1 / d, alpha)\n\n    f_lo = cls._p(X - tau_lo, alpha).sum(dim) - 1\n\n    dm = tau_hi - tau_lo\n\n    for it in range(n_iter):\n        dm /= 2\n        tau_m = tau_lo + dm\n        p_m = cls._p(X - tau_m, alpha)\n        f_m = p_m.sum(dim) - 1\n\n        mask = (f_m * f_lo >= 0).unsqueeze(dim)\n        tau_lo = torch.where(mask, tau_m, tau_lo)\n\n    if ensure_sum_one:\n        p_m /= p_m.sum(dim=dim).unsqueeze(dim=dim)\n\n    return p_m, {\"alpha\": alpha, \"dim\": dim}\n\n\n# slightly more efficient special case for sparsemax\nclass SparsemaxBisectFunction(EntmaxBisectFunction):\n    @classmethod\n    def _gp(cls, x, alpha):\n        return x\n\n    @classmethod\n    def _gp_inv(cls, y, alpha):\n        return y\n\n    @classmethod\n    def _p(cls, x, alpha):\n        return torch.clamp(x, min=0)\n\n    @classmethod\n    def forward(cls, ctx, X, dim=-1, n_iter=50, ensure_sum_one=True):\n        p_m, backward_kwargs = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one)\n\n        ctx.alpha = backward_kwargs[\"alpha\"]\n        ctx.dim = backward_kwargs[\"dim\"]\n        ctx.save_for_backward(p_m)\n        return p_m\n\n    @classmethod\n    def backward(cls, ctx, dY):\n        (Y,) = ctx.saved_tensors\n        gppr = (Y > 0).to(dtype=dY.dtype)\n        dX = dY * gppr\n        q = dX.sum(ctx.dim) / gppr.sum(ctx.dim)\n        q = q.unsqueeze(ctx.dim)\n        dX -= q * gppr\n        return dX, None, None, None\n\n\ndef _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one):\n    return _entmax_bisect_forward(X, alpha=2, dim=dim, n_iter=50, ensure_sum_one=True, cls=SparsemaxBisectFunction)\n\n\ndef entmax_bisect(X, alpha=1.5, dim=-1, n_iter=50, ensure_sum_one=True, training=True):\n    \"\"\"alpha-entmax: normalizing sparse transform (a la softmax).\n\n    Solves the optimization problem:\n\n        max_p <x, p> - H_a(p)    s.t.    p >= 0, sum(p) == 1.\n\n    where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1,\n    using a bisection (root finding, binary search) algorithm.\n\n    This function is differentiable with respect to both X and alpha.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor.\n\n    alpha : float or torch.Tensor\n        Tensor of alpha parameters (> 1) to use. If scalar\n        or python float, the same value is used for all rows, otherwise,\n        it must have shape (or be expandable to)\n        alpha.shape[j] == (X.shape[j] if j != dim else 1)\n        A value of alpha=2 corresponds to sparsemax, and alpha=1 would in theory recover\n        softmax. For numeric reasons, this algorithm does not work with `alpha=1`: if you\n        want softmax, we recommend `torch.nn.softmax`.\n\n    dim : int\n        The dimension along which to apply alpha-entmax.\n\n    n_iter : int\n        Number of bisection iterations. For float32, 24 iterations should\n        suffice for machine precision.\n\n    ensure_sum_one : bool,\n        Whether to divide the result by its sum. If false, the result might\n        sum to close but not exactly 1, which might cause downstream problems.\n\n    Returns\n    -------\n    P : torch tensor, same shape as X\n        The projection result, such that P.sum(dim=dim) == 1 elementwise.\n    \"\"\"\n    # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility\n    # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053\n    if not training:\n        output, _ = _entmax_bisect_forward(X, alpha, dim, n_iter, ensure_sum_one)\n        return output\n    return EntmaxBisectFunction.apply(X, alpha, dim, n_iter, ensure_sum_one)\n\n\ndef sparsemax_bisect(X, dim=-1, n_iter=50, ensure_sum_one=True, training=True):\n    \"\"\"sparsemax: normalizing sparse transform (a la softmax), via bisection.\n\n    Solves the projection:\n\n        min_p ||x - p||_2   s.t.    p >= 0, sum(p) == 1.\n\n    Parameters\n    ----------\n    X : torch.Tensor\n        The input tensor.\n\n    dim : int\n        The dimension along which to apply sparsemax.\n\n    n_iter : int\n        Number of bisection iterations. For float32, 24 iterations should\n        suffice for machine precision.\n\n    ensure_sum_one : bool,\n        Whether to divide the result by its sum. If false, the result might\n        sum to close but not exactly 1, which might cause downstream problems.\n\n    Note: This function does not yet support normalizing along anything except\n    the last dimension. Please use transposing and views to achieve more\n    general behavior.\n\n    Returns\n    -------\n    P : torch tensor, same shape as X\n        The projection result, such that P.sum(dim=dim) == 1 elementwise.\n    \"\"\"\n    # Avoids call to custom autograd.Function during eval to ensure torchscript compatibility\n    # custom autograd.Function is not scriptable: https://github.com/pytorch/pytorch/issues/22329#issuecomment-506608053\n    if not training:\n        output, _ = _sparsemax_bisect_forward(X, dim, n_iter, ensure_sum_one)\n        return output\n    return SparsemaxBisectFunction.apply(X, dim, n_iter, ensure_sum_one)\n\n\nclass SparsemaxBisect(nn.Module):\n    def __init__(self, dim=-1, n_iter=None):\n        \"\"\"sparsemax: normalizing sparse transform (a la softmax) via bisection\n\n        Solves the projection:\n\n            min_p ||x - p||_2   s.t.    p >= 0, sum(p) == 1.\n\n        Parameters\n        ----------\n        dim : int\n            The dimension along which to apply sparsemax.\n\n        n_iter : int\n            Number of bisection iterations. For float32, 24 iterations should\n            suffice for machine precision.\n        \"\"\"\n        self.dim = dim\n        self.n_iter = n_iter\n        super().__init__()\n\n    def forward(self, X):\n        return sparsemax_bisect(X, dim=self.dim, n_iter=self.n_iter, training=self.training)\n\n\nclass EntmaxBisect(nn.Module):\n    def __init__(self, alpha=1.5, dim=-1, n_iter=50):\n        \"\"\"alpha-entmax: normalizing sparse map (a la softmax) via bisection.\n\n        Solves the optimization problem:\n\n            max_p <x, p> - H_a(p)    s.t.    p >= 0, sum(p) == 1.\n\n        where H_a(p) is the Tsallis alpha-entropy with custom alpha >= 1,\n        using a bisection (root finding, binary search) algorithm.\n\n        Parameters\n        ----------\n        alpha : float or torch.Tensor\n            Tensor of alpha parameters (> 1) to use. If scalar\n            or python float, the same value is used for all rows, otherwise,\n            it must have shape (or be expandable to)\n            alpha.shape[j] == (X.shape[j] if j != dim else 1)\n            A value of alpha=2 corresponds to sparsemax; and alpha=1 would in theory recover\n            softmax. For numeric reasons, this algorithm does not work with `alpha=1`; if you\n            want softmax, we recommend `torch.nn.softmax`.\n\n        dim : int\n            The dimension along which to apply alpha-entmax.\n\n        n_iter : int\n            Number of bisection iterations. For float32, 24 iterations should\n            suffice for machine precision.\n\n        \"\"\"\n        super().__init__()\n        self.dim = dim\n        self.n_iter = n_iter\n        if isinstance(alpha, torch.Tensor):\n            self.register_buffer(\"alpha\", alpha)\n        else:\n            self.alpha = alpha\n\n    def forward(self, X):\n        return entmax_bisect(X, alpha=self.alpha, dim=self.dim, n_iter=self.n_iter, training=self.training)\n"
  },
  {
    "path": "ludwig/utils/error_handling_utils.py",
    "content": "import logging\nfrom functools import partial\n\nfrom retry.api import retry, retry_call\n\nimport ludwig.constants as const\n\nlogger = logging.getLogger(__name__)\n\n\ndefault_retry_call = partial(\n    retry_call, tries=const.TRIES, backoff=const.BACKOFF, delay=const.DELAY, jitter=const.JITTER, logger=logger\n)\n\n\ndefault_retry = partial(\n    retry, tries=const.TRIES, backoff=const.BACKOFF, delay=const.DELAY, jitter=const.JITTER, logger=logger\n)\n"
  },
  {
    "path": "ludwig/utils/eval_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom collections import OrderedDict\n\nimport numpy as np\nfrom sklearn import metrics\nfrom sklearn.metrics import confusion_matrix\n\nlogger = logging.getLogger(__name__)\n\n\nclass ConfusionMatrix:\n    def __init__(self, conditions, predictions, labels=None, sample_weight=None):\n        # assert (len(predictions) == len(conditions))\n        min_length = min(len(predictions), len(conditions))\n        self.predictions = predictions[:min_length]\n        self.conditions = conditions[:min_length]\n\n        if labels is not None:\n            self.label2idx = {label: idx for idx, label in enumerate(labels)}\n            self.idx2label = {idx: label for idx, label in enumerate(labels)}\n            labels = list(range(len(labels)))\n        else:\n            self.label2idx = {\n                str(label): idx for idx, label in enumerate(np.unique([self.predictions, self.conditions]))\n            }\n            self.idx2label = {\n                idx: str(label) for idx, label in enumerate(np.unique([self.predictions, self.conditions]))\n            }\n        self.cm = confusion_matrix(self.conditions, self.predictions, labels=labels, sample_weight=sample_weight)\n\n        # if labels is not None:\n        #     self.labels_dict = {label: idx for idx, label in enumerate(labels)}\n        # else:\n        #     if conditions.dtype.char == 'S':  # it's an array of strings\n        #         self.labels_dict = {str(label): idx for idx, label in\n        #                             enumerate(np.unique([predictions, conditions]))}\n        #     else:  # number\n        #         max_label = np.concatenate([predictions, conditions]).max()\n        #         self.labels_dict = {str(i): i for i in range(max_label + 1)}\n        #         labels = [str(i) for i in range(max_label + 1)]\n        # self.cm = confusion_matrix(conditions, predictions, labels, sample_weight)\n\n        self.sum_predictions = np.sum(self.cm, axis=0)\n        self.sum_conditions = np.sum(self.cm, axis=1)\n        self.all = np.sum(self.cm)\n\n    def label_to_idx(self, label):\n        return self.label2idx[label]\n\n    def true_positives(self, idx):\n        return self.cm[idx, idx]\n\n    def true_negatives(self, idx):\n        return self.all - self.sum_predictions[idx] - self.sum_conditions[idx] + self.true_positives(idx)\n\n    def false_positives(self, idx):\n        return self.sum_predictions[idx] - self.true_positives(idx)\n\n    def false_negatives(self, idx):\n        return self.sum_conditions[idx] - self.true_positives(idx)\n\n    def true_positive_rate(self, idx):\n        nom = self.true_positives(idx)\n        den = self.sum_conditions[idx]\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def true_negative_rate(self, idx):\n        nom = tn = self.true_negatives(idx)\n        den = tn + self.false_positives(idx)\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def positive_predictive_value(self, idx):\n        nom = self.true_positives(idx)\n        den = self.sum_predictions[idx]\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def negative_predictive_value(self, idx):\n        nom = tn = self.true_negatives(idx)\n        den = tn + self.false_negatives(idx)\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def false_negative_rate(self, idx):\n        return 1.0 - self.true_positive_rate(idx)\n\n    def false_positive_rate(self, idx):\n        return 1.0 - self.true_negative_rate(idx)\n\n    def false_discovery_rate(self, idx):\n        return 1.0 - self.positive_predictive_value(idx)\n\n    def false_omission_rate(self, idx):\n        return 1.0 - self.negative_predictive_value(idx)\n\n    def accuracy(self, idx):\n        nom = self.true_positives(idx) + self.true_negatives(idx)\n        den = self.all\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def precision(self, idx):\n        return self.positive_predictive_value(idx)\n\n    def recall(self, idx):\n        return self.true_positive_rate(idx)\n\n    def fbeta_score(self, beta, idx):\n        beta_2 = np.power(beta, 2)\n        precision = self.precision(idx)\n        recall = self.recall(idx)\n        nom = (1 + beta_2) * precision * recall\n        den = (beta_2 * precision) + recall\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def f1_score(self, idx):\n        return self.fbeta_score(1, idx)\n\n    def sensitivity(self, idx):\n        return self.true_positive_rate(idx)\n\n    def specificity(self, idx):\n        return self.true_negative_rate(idx)\n\n    def hit_rate(self, idx):\n        return self.true_positive_rate(idx)\n\n    def miss_rate(self, idx):\n        return self.false_negative_rate(idx)\n\n    def fall_out(self, idx):\n        return self.false_positive_rate(idx)\n\n    def matthews_correlation_coefficient(self, idx):\n        tp = self.true_positives(idx)\n        tn = self.true_negatives(idx)\n        fp = self.false_positives(idx)\n        fn = self.false_negatives(idx)\n        nom = tp * tn - fp * fn\n        den = np.sqrt((tp + fp) * (tp + fn) * (tn + fp) * (tn + fn))\n        if den == 0 or den == np.nan:\n            return 0\n        else:\n            return nom / den\n\n    def informedness(self, idx):\n        return self.true_positive_rate(idx) + self.true_negative_rate(idx) - 1\n\n    def markedness(self, idx):\n        return self.positive_predictive_value(idx) + self.negative_predictive_value(idx) - 1\n\n    def token_accuracy(self):\n        return metrics.accuracy_score(self.conditions, self.predictions)\n\n    def avg_precision(self, average=\"macro\"):\n        return metrics.precision_score(self.conditions, self.predictions, average=average)\n\n    def avg_recall(self, average=\"macro\"):\n        return metrics.recall_score(self.conditions, self.predictions, average=average)\n\n    def avg_f1_score(self, average=\"macro\"):\n        return metrics.f1_score(self.conditions, self.predictions, average=average)\n\n    def avg_fbeta_score(self, beta, average=\"macro\"):\n        return metrics.fbeta_score(self.conditions, self.predictions, beta=beta, average=average)\n\n    def kappa_score(self):\n        return metrics.cohen_kappa_score(self.conditions, self.predictions)\n\n    def class_stats(self, idx):\n        return {\n            \"true_positives\": self.true_positives(idx),\n            \"true_negatives\": self.true_negatives(idx),\n            \"false_positives\": self.false_positives(idx),\n            \"false_negatives\": self.false_negatives(idx),\n            \"true_positive_rate\": self.true_positive_rate(idx),\n            \"true_negative_rate\": self.true_negative_rate(idx),\n            \"positive_predictive_value\": self.positive_predictive_value(idx),\n            \"negative_predictive_value\": self.negative_predictive_value(idx),\n            \"false_negative_rate\": self.false_negative_rate(idx),\n            \"false_positive_rate\": self.false_positive_rate(idx),\n            \"false_discovery_rate\": self.false_discovery_rate(idx),\n            \"false_omission_rate\": self.false_omission_rate(idx),\n            \"accuracy\": self.accuracy(idx),\n            \"precision\": self.precision(idx),\n            \"recall\": self.recall(idx),\n            \"f1_score\": self.f1_score(idx),\n            \"sensitivity\": self.sensitivity(idx),\n            \"specificity\": self.specificity(idx),\n            \"hit_rate\": self.hit_rate(idx),\n            \"miss_rate\": self.miss_rate(idx),\n            \"fall_out\": self.fall_out(idx),\n            \"matthews_correlation_coefficient\": self.matthews_correlation_coefficient(idx),\n            \"informedness\": self.informedness(idx),\n            \"markedness\": self.markedness(idx),\n        }\n\n    def per_class_stats(self):\n        stats = OrderedDict()\n        for idx in sorted(self.idx2label.keys()):\n            stats[self.idx2label[idx]] = self.class_stats(idx)\n        return stats\n\n    def stats(self):\n        return {\n            \"token_accuracy\": self.token_accuracy(),\n            \"avg_precision_macro\": self.avg_precision(average=\"macro\"),\n            \"avg_recall_macro\": self.avg_recall(average=\"macro\"),\n            \"avg_f1_score_macro\": self.avg_f1_score(average=\"macro\"),\n            \"avg_precision_micro\": self.avg_precision(average=\"micro\"),\n            \"avg_recall_micro\": self.avg_recall(average=\"micro\"),\n            \"avg_f1_score_micro\": self.avg_f1_score(average=\"micro\"),\n            \"avg_precision_weighted\": self.avg_precision(average=\"micro\"),\n            \"avg_recall_weighted\": self.avg_recall(average=\"micro\"),\n            \"avg_f1_score_weighted\": self.avg_f1_score(average=\"weighted\"),\n            \"kappa_score\": self.kappa_score(),\n        }\n\n\ndef roc_curve(conditions, prediction_scores, pos_label=None, sample_weight=None):\n    return metrics.roc_curve(conditions, prediction_scores, pos_label=pos_label, sample_weight=sample_weight)\n\n\ndef roc_auc_score(conditions, prediction_scores, average=\"micro\", sample_weight=None):\n    try:\n        return metrics.roc_auc_score(conditions, prediction_scores, average=average, sample_weight=sample_weight)\n    except ValueError as ve:\n        logger.info(ve)\n\n\ndef precision_recall_curve(conditions, prediction_scores, pos_label=None, sample_weight=None):\n    return metrics.precision_recall_curve(\n        conditions, prediction_scores, pos_label=pos_label, sample_weight=sample_weight\n    )\n\n\ndef average_precision_score(conditions, prediction_scores, average=\"micro\", sample_weight=None):\n    # average == [micro, macro, sampled, weidhted]\n    return metrics.average_precision_score(conditions, prediction_scores, average=average, sample_weight=sample_weight)\n"
  },
  {
    "path": "ludwig/utils/fs_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2021 Linux Foundation.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport contextlib\nimport errno\nimport functools\nimport logging\nimport os\nimport pathlib\nimport shutil\nimport tempfile\nimport uuid\nfrom urllib.parse import unquote, urlparse\n\nimport certifi\nimport fsspec\nimport h5py\nimport pyarrow.fs\nimport urllib3\nfrom filelock import FileLock\nfrom fsspec.core import split_protocol\n\nfrom ludwig.api_annotations import DeveloperAPI\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\ndef get_default_cache_location() -> str:\n    \"\"\"Returns a path to the default LUDWIG_CACHE location, or $HOME/.ludwig_cache.\"\"\"\n    cache_path = None\n    if \"LUDWIG_CACHE\" in os.environ and os.environ[\"LUDWIG_CACHE\"]:\n        cache_path = os.environ[\"LUDWIG_CACHE\"]\n    else:\n        cache_path = str(pathlib.Path.home().joinpath(\".ludwig_cache\"))\n\n    # Check if the cache path exists, if not create it\n    if not os.path.exists(cache_path):\n        os.makedirs(cache_path)\n    return cache_path\n\n\n@DeveloperAPI\ndef get_fs_and_path(url):\n    protocol, path = split_protocol(url)\n    # Parse the url to get only the escaped url path\n    path = unquote(urlparse(path).path)\n    # Create a windows compatible path from url path\n    path = os.fspath(pathlib.PurePosixPath(path))\n    fs = fsspec.filesystem(protocol)\n    return fs, path\n\n\n@DeveloperAPI\ndef has_remote_protocol(url):\n    protocol, _ = split_protocol(url)\n    return protocol and protocol != \"file\"\n\n\n@DeveloperAPI\ndef is_http(urlpath):\n    protocol, _ = split_protocol(urlpath)\n    return protocol == \"http\" or protocol == \"https\"\n\n\n@DeveloperAPI\ndef upgrade_http(urlpath):\n    protocol, url = split_protocol(urlpath)\n    if protocol == \"http\":\n        return \"https://\" + url\n    return None\n\n\n@DeveloperAPI\n@functools.lru_cache(maxsize=32)\ndef get_bytes_obj_from_path(path: str) -> bytes | None:\n    if is_http(path):\n        try:\n            return get_bytes_obj_from_http_path(path)\n        except Exception as e:\n            logger.warning(e)\n            return None\n    else:\n        try:\n            with open_file(path) as f:\n                return f.read()\n        except OSError as e:\n            logger.warning(e)\n            return None\n\n\n@DeveloperAPI\ndef stream_http_get_request(path: str) -> urllib3.response.HTTPResponse:\n    if upgrade_http(path):\n        http = urllib3.PoolManager()\n    else:\n        http = urllib3.PoolManager(ca_certs=certifi.where())\n    resp = http.request(\"GET\", path, preload_content=False)\n    return resp\n\n\n@DeveloperAPI\n@functools.lru_cache(maxsize=32)\ndef get_bytes_obj_from_http_path(path: str) -> bytes:\n    resp = stream_http_get_request(path)\n    if resp.status == 404:\n        upgraded = upgrade_http(path)\n        if upgraded:\n            logger.info(f\"reading url {path} failed. upgrading to https and retrying\")\n            return get_bytes_obj_from_http_path(upgraded)\n        else:\n            raise urllib3.exceptions.HTTPError(f\"reading url {path} failed and cannot be upgraded to https\")\n\n    # stream data\n    data = b\"\"\n    for chunk in resp.stream(1024):\n        data += chunk\n    return data\n\n\n@DeveloperAPI\ndef find_non_existing_dir_by_adding_suffix(directory_name):\n    fs, _ = get_fs_and_path(directory_name)\n    suffix = 0\n    curr_directory_name = directory_name\n    while fs.exists(curr_directory_name):\n        curr_directory_name = directory_name + \"_\" + str(suffix)\n        suffix += 1\n    return curr_directory_name\n\n\n@DeveloperAPI\ndef abspath(url):\n    protocol, _ = split_protocol(url)\n    if protocol is not None:\n        # we assume any path containing an explicit protovol is fully qualified\n        return url\n    return os.path.abspath(url)\n\n\n@DeveloperAPI\ndef path_exists(url):\n    fs, path = get_fs_and_path(url)\n    return fs.exists(path)\n\n\n@DeveloperAPI\ndef listdir(url):\n    fs, path = get_fs_and_path(url)\n    return fs.listdir(path)\n\n\n@DeveloperAPI\ndef safe_move_file(src, dst):\n    \"\"\"Rename a file from `src` to `dst`. Inspired by: https://alexwlchan.net/2019/03/atomic-cross-filesystem-\n    moves-in-python/\n\n    *   Moves must be atomic.  `shutil.move()` is not atomic.\n\n    *   Moves must work across filesystems.  Sometimes temp directories and the\n        model directories live on different filesystems.  `os.replace()` will\n        throw errors if run across filesystems.\n\n    So we try `os.replace()`, but if we detect a cross-filesystem copy, we\n    switch to `shutil.move()` with some wrappers to make it atomic.\n    \"\"\"\n    try:\n        os.replace(src, dst)\n    except OSError as err:\n        if err.errno == errno.EXDEV:\n            # Generate a unique ID, and copy `<src>` to the target directory with a temporary name `<dst>.<ID>.tmp`.\n            # Because we're copying across a filesystem boundary, this initial copy may not be atomic.  We insert a\n            # random UUID so if different processes are copying into `<dst>`, they don't overlap in their tmp copies.\n            copy_id = uuid.uuid4()\n            tmp_dst = f\"{dst}.{copy_id}.tmp\"\n            shutil.copyfile(src, tmp_dst)\n\n            # Atomic replace file onto the new name, and clean up original source file.\n            os.replace(tmp_dst, dst)\n            os.unlink(src)\n        else:\n            raise\n\n\n@DeveloperAPI\ndef safe_move_directory(src, dst):\n    \"\"\"Recursively moves files from src directory to dst directory and removes src directory.\n\n    If dst directory does not exist, it will be created.\n    \"\"\"\n    try:\n        os.replace(src, dst)\n    except OSError as err:\n        if err.errno == errno.EXDEV:\n            # Generate a unique ID, and copy `<src>` to the target directory with a temporary name `<dst>.<ID>.tmp`.\n            # Because we're copying across a filesystem boundary, this initial copy may not be atomic.  We insert a\n            # random UUID so if different processes are copying into `<dst>`, they don't overlap in their tmp copies.\n            copy_id = uuid.uuid4()\n            tmp_dst = f\"{dst}.{copy_id}.tmp\"\n            shutil.copytree(src, tmp_dst)\n\n            # Atomic replace directory name onto the new name, and clean up original source directory.\n            os.replace(tmp_dst, dst)\n            os.unlink(src)\n        else:\n            raise\n\n\n@DeveloperAPI\ndef rename(src, tgt):\n    protocol, _ = split_protocol(tgt)\n    if protocol is not None:\n        fs = fsspec.filesystem(protocol)\n        fs.mv(src, tgt, recursive=True)\n    else:\n        safe_move_file(src, tgt)\n\n\n@DeveloperAPI\ndef upload_file(src, tgt):\n    protocol, _ = split_protocol(tgt)\n    fs = fsspec.filesystem(protocol)\n    fs.put(src, tgt)\n\n\n@DeveloperAPI\ndef copy(src, tgt, recursive=False):\n    protocol, _ = split_protocol(tgt)\n    fs = fsspec.filesystem(protocol)\n    fs.copy(src, tgt, recursive=recursive)\n\n\n@DeveloperAPI\ndef makedirs(url, exist_ok=False):\n    fs, path = get_fs_and_path(url)\n    fs.makedirs(path, exist_ok=exist_ok)\n\n\n@DeveloperAPI\ndef delete(url, recursive=False):\n    fs, path = get_fs_and_path(url)\n    return fs.delete(path, recursive=recursive)\n\n\n@DeveloperAPI\ndef upload(lpath, rpath):\n    fs, path = get_fs_and_path(rpath)\n    pyarrow.fs.copy_files(lpath, path, destination_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs)))\n\n\n@DeveloperAPI\ndef download(rpath, lpath):\n    fs, path = get_fs_and_path(rpath)\n    pyarrow.fs.copy_files(path, lpath, source_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs)))\n\n\n@DeveloperAPI\ndef checksum(url):\n    fs, path = get_fs_and_path(url)\n    return fs.checksum(path)\n\n\n@DeveloperAPI\ndef to_url(path):\n    protocol, _ = split_protocol(path)\n    if protocol is not None:\n        return path\n    return pathlib.Path(os.path.abspath(path)).as_uri()\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef upload_output_directory(url):\n    if url is None:\n        yield None, None\n        return\n\n    if has_remote_protocol(url):\n        # To avoid extra network load, write all output files locally at runtime,\n        # then upload to the remote fs at the end.\n        with tempfile.TemporaryDirectory() as tmpdir:\n            fs, remote_path = get_fs_and_path(url)\n\n            # In cases where we are resuming from a previous run, we first need to download\n            # the artifacts from the remote filesystem\n            if path_exists(url):\n                fs.get(url, tmpdir + \"/\", recursive=True)\n\n            def put_fn():\n                # Use pyarrow API here as fs.put() is inconsistent in where it uploads the file\n                # See: https://github.com/fsspec/filesystem_spec/issues/1062\n                pyarrow.fs.copy_files(\n                    tmpdir, remote_path, destination_filesystem=pyarrow.fs.PyFileSystem(pyarrow.fs.FSSpecHandler(fs))\n                )\n\n            # Write to temp directory locally\n            yield tmpdir, put_fn\n\n            # Upload to remote when finished\n            put_fn()\n    else:\n        # For local paths (including file:// URIs), use the path directly.\n        _, local_path = get_fs_and_path(url)\n        makedirs(local_path, exist_ok=True)\n        yield local_path, None\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef open_file(url, *args, **kwargs):\n    fs, path = get_fs_and_path(url)\n    with fs.open(path, *args, **kwargs) as f:\n        yield f\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef download_h5(url):\n    with tempfile.TemporaryDirectory() as tmpdir:\n        local_path = os.path.join(tmpdir, os.path.basename(url))\n        fs, path = get_fs_and_path(url)\n        fs.get(path, local_path)\n        with h5py.File(local_path, \"r\") as f:\n            yield f\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef upload_h5(url):\n    with upload_output_file(url) as local_fname:\n        mode = \"w\"\n        if url == local_fname and path_exists(url):\n            mode = \"r+\"\n\n        with h5py.File(local_fname, mode) as f:\n            yield f\n\n\n@DeveloperAPI\n@contextlib.contextmanager\ndef upload_output_file(url):\n    \"\"\"Takes a remote URL as input, returns a temp filename, then uploads it when done.\"\"\"\n    protocol, _ = split_protocol(url)\n    if protocol is not None:\n        fs = fsspec.filesystem(protocol)\n        with tempfile.TemporaryDirectory() as tmpdir:\n            local_fname = os.path.join(tmpdir, \"tmpfile\")\n            yield local_fname\n            fs.put(local_fname, url, recursive=True)\n    else:\n        yield url\n\n\n@DeveloperAPI\nclass file_lock(contextlib.AbstractContextManager):\n    \"\"\"File lock based on filelock package.\"\"\"\n\n    def __init__(self, path: str, ignore_remote_protocol: bool = True, lock_file: str = \".lock\") -> None:\n        if not isinstance(path, (str, os.PathLike, pathlib.Path)):\n            self.lock = None\n        else:\n            path = os.path.join(path, lock_file) if os.path.isdir(path) else f\"{path}./{lock_file}\"\n            if ignore_remote_protocol and has_remote_protocol(path):\n                self.lock = None\n            else:\n                self.lock = FileLock(path, timeout=-1)\n\n    def __enter__(self, *args, **kwargs):\n        if self.lock:\n            return self.lock.__enter__(*args, **kwargs)\n\n    def __exit__(self, *args, **kwargs):\n        if self.lock:\n            return self.lock.__exit__(*args, **kwargs)\n\n\n@DeveloperAPI\ndef list_file_names_in_directory(directory_name: str) -> list[str]:\n    file_path: pathlib.Path  # noqa [F842]  # incorrect flagging of \"local variable is annotated but never used\"\n    file_names: list[str] = [\n        file_path.name for file_path in pathlib.Path(directory_name).iterdir() if file_path.is_file()\n    ]\n    return file_names\n"
  },
  {
    "path": "ludwig/utils/h3_util.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom typing import NamedTuple\n\n\nclass H3Data(NamedTuple):\n    mode: int\n    edge: int\n    resolution: int\n    base_cell: int\n    cells: list[int]\n\n\ndef set_bit(v, index, x):\n    \"\"\"Set the index:th bit of v to 1 if x is truthy, else to 0, and return the new value.\"\"\"\n    mask = 1 << index  # Compute mask, an integer with just bit 'index' set.\n    v &= ~mask  # Clear the bit indicated by the mask (if x is False)\n    if x:\n        v |= mask  # If x was True, set the bit indicated by the mask.\n    return v  # Return the result, we're done.\n\n\ndef set_bits(v, start_bit, slice_length, x):\n    bin_x = bin(x)\n    for i, index in enumerate(range(start_bit, start_bit + slice_length)):\n        val = int(bin_x[-(i + 1)]) if 2 + i < len(bin_x) else 0\n        v = set_bit(v, index, val)\n    return v\n\n\ndef components_to_h3(components):\n    h3 = 18446744073709551615\n    h3 = set_bits(h3, 64 - 5, 4, components[\"mode\"])\n    h3 = set_bits(h3, 64 - 8, 3, components[\"edge\"])\n    h3 = set_bits(h3, 64 - 12, 4, components[\"resolution\"])\n    h3 = set_bits(h3, 64 - 19, 7, components[\"base_cell\"])\n    for i, cell in enumerate(components[\"cells\"]):\n        h3 = set_bits(h3, 64 - 19 - (i + 1) * 3, 3, cell)\n    h3 = set_bits(h3, 64 - 1, 4, 0)\n    return h3\n\n\ndef bitslice(x: int, start_bit: int, slice_length: int) -> int:\n    ones_mask: int = int(2**slice_length - 1)\n    return (x & (ones_mask << start_bit)) >> start_bit\n\n\ndef h3_index_mode(h3_long: int) -> int:\n    return bitslice(h3_long, 64 - 5, 4)\n\n\ndef h3_edge(h3_long: int) -> int:\n    return bitslice(h3_long, 64 - 8, 3)\n\n\ndef h3_resolution(h3_long: int) -> int:\n    return bitslice(h3_long, 64 - 12, 4)\n\n\ndef h3_base_cell(h3_long: int) -> int:\n    return bitslice(h3_long, 64 - 19, 7)\n\n\ndef h3_octal_components(h3_long):\n    res = h3_resolution(h3_long)\n    return \"{0:0{w}o}\".format(bitslice(h3_long + 2**63, 64 - 19 - 3 * res, 3 * res), w=res)\n\n\ndef h3_component(h3_long: int, i: int) -> int:\n    return bitslice(h3_long, 64 - 19 - 3 * i, 3)\n\n\ndef h3_components(h3_long: int) -> list[int]:\n    return [h3_component(h3_long, i) for i in range(1, h3_resolution(h3_long) + 1)]\n\n\ndef h3_to_components(h3_value: int) -> H3Data:\n    \"\"\"Extract the values from an H3 hexadecimal value Refer to this for the bit layout:\n\n    https://uber.github.io/h3/#/documentation/core-library/h3-index-representations\n    \"\"\"\n    # lat_long = (0, 0)  # h3ToGeo(h3_value)\n    return H3Data(\n        mode=h3_index_mode(h3_value),\n        edge=h3_edge(h3_value),\n        resolution=h3_resolution(h3_value),\n        base_cell=h3_base_cell(h3_value),\n        cells=h3_components(h3_value),\n    )\n\n\nif __name__ == \"__main__\":\n    value = 622236723497533439\n    components = h3_to_components(value)\n    h3 = components_to_h3(components)\n    components2 = h3_to_components(h3)\n    print(value)\n    print(components)\n    print(h3)\n    print(components2)\n"
  },
  {
    "path": "ludwig/utils/heuristics.py",
    "content": "from ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.config_utils import has_pretrained_encoder, has_trainable_encoder, has_unstructured_input_feature\n\n\ndef get_auto_learning_rate(config: ModelConfig) -> float:\n    \"\"\"Uses config heuristics to determine an appropriate learning rate.\n\n    The main idea behind the following heuristics is that smaller learning rates are more\n    suitable for features with larger encoders, which are typically used with unstructured features.\n    Note that these are meant to be rough heuristics that are solely based on feature types and the\n    type of the corresponding encoder. More factors could be taken into consideration such as model\n    size, dataset size, batch size, number of features, etc.\n\n    Args:\n        config: Ludwig config used to train the model.\n    \"\"\"\n    if not has_unstructured_input_feature(config):\n        return 0.001\n\n    if not has_pretrained_encoder(config):\n        return 0.0001\n\n    if has_trainable_encoder(config):\n        return 0.00001\n\n    return 0.00002\n"
  },
  {
    "path": "ludwig/utils/hf_utils.py",
    "content": "import logging\nimport os\nimport tempfile\nfrom os import PathLike\n\nfrom transformers import AutoTokenizer, PreTrainedModel\nfrom transformers.tokenization_utils import PreTrainedTokenizer\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.utils.error_handling_utils import default_retry\nfrom ludwig.utils.fs_utils import download, path_exists\nfrom ludwig.utils.upload_utils import hf_hub_login\n\nlogger = logging.getLogger(__name__)\n\n\n@default_retry()\ndef load_pretrained_hf_model_from_hub(\n    model_class: type,\n    pretrained_model_name_or_path: str | PathLike | None,\n    **pretrained_kwargs,\n) -> PreTrainedModel:\n    \"\"\"Download a HuggingFace model.\n\n    Downloads a model from the HuggingFace zoo with retry on failure.\n    Args:\n        model_class: Class of the model to download.\n        pretrained_model_name_or_path: Name of the model to download.\n        pretrained_kwargs: Additional arguments to pass to the model constructor.\n    Returns:\n        The pretrained model object.\n    \"\"\"\n    return model_class.from_pretrained(pretrained_model_name_or_path, **pretrained_kwargs)\n\n\n@default_retry()\ndef load_pretrained_hf_tokenizer(\n    pretrained_model_name_or_path: str | PathLike | None, **pretrained_kwargs\n) -> PreTrainedTokenizer:\n    \"\"\"Download a HuggingFace tokenizer.\n\n    Args:\n        pretrained_model_name_or_path: Name of the tokenizer to download.\n        pretrained_kwargs: Additional arguments to pass to the tokenizer constructor.\n    Returns:\n        The pretrained tokenizer object.\n    \"\"\"\n    return AutoTokenizer.from_pretrained(pretrained_model_name_or_path, **pretrained_kwargs)\n\n\ndef _load_pretrained_hf_model_from_dir(\n    model_class: type,\n    pretrained_model_name_or_path: str | PathLike | None,\n    **pretrained_kwargs,\n) -> PreTrainedModel:\n    \"\"\"Downloads a model to a local temporary directory, and Loads a pretrained HF model from a local directory.\"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        download(pretrained_model_name_or_path, tmpdir)\n        return model_class.from_pretrained(tmpdir, **pretrained_kwargs)\n\n\n@DeveloperAPI\ndef load_pretrained_hf_model_with_hub_fallback(\n    model_class: type,\n    pretrained_model_name_or_path: str | PathLike | None,\n    **pretrained_kwargs,\n) -> tuple[PreTrainedModel, bool]:\n    \"\"\"Returns the model and a boolean indicating whether the model was downloaded from the HuggingFace hub.\n\n    If the `LUDWIG_PRETRAINED_MODELS_DIR` environment variable is set, we attempt to load the HF model from this\n    directory, falling back to downloading from the HF hub if the model is not found, downloading fails, or if model\n    initialization fails.\n\n    `LUDWIG_PRETRAINED_MODELS_DIR` can be an s3 path. Weights are copied to a local temporary directory, and the model\n    is loaded from there.\n\n    The expected structure of the `LUDWIG_PRETRAINED_MODELS_DIR` directory is:\n        {LUDWIG_PRETRAINED_MODELS_DIR}/{pretrained_model_name_or_path}/pytorch_model.bin\n        {LUDWIG_PRETRAINED_MODELS_DIR}/{pretrained_model_name_or_path}/config.json\n\n    For example, if `LUDWIG_PRETRAINED_MODELS_DIR` is set to `s3://my-bucket/pretrained-models`, and\n    `pretrained_model_name_or_path` is set to `bert-base-uncased`, we expect to find the following files:\n        s3://my-bucket/bert-base-uncased/\n            - pytorch_model.bin\n            - config.json\n\n    If the `LUDWIG_PRETRAINED_MODELS_DIR` environment variable is not set, we download the model from the HF hub.\n    \"\"\"\n    pretrained_models_dir = os.environ.get(\"LUDWIG_PRETRAINED_MODELS_DIR\")\n    if pretrained_models_dir:\n        pretrained_model_path = os.path.join(pretrained_models_dir, pretrained_model_name_or_path)\n        if path_exists(pretrained_model_path):\n            try:\n                logger.info(\n                    f\"Found existing pretrained model artifact {pretrained_model_name_or_path} in directory \"\n                    f\"{pretrained_models_dir}. Downloading.\"\n                )\n                return (\n                    _load_pretrained_hf_model_from_dir(model_class, pretrained_model_path, **pretrained_kwargs),\n                    False,\n                )\n            except Exception as e:\n                logger.warning(\n                    f\"Failed to download pretrained model from {pretrained_models_dir} with error {e}. \"\n                    \"Falling back to HuggingFace model hub.\"\n                )\n\n    # Fallback to HF hub.\n    return load_pretrained_hf_model_from_hub(model_class, pretrained_model_name_or_path, **pretrained_kwargs), True\n\n\ndef upload_folder_to_hfhub(\n    repo_id: str,\n    folder_path: str,\n    repo_type: str | None = \"model\",\n    private: bool | None = False,\n    path_in_repo: str | None = None,  # defaults to root of repo\n    commit_message: str | None = None,\n    commit_description: str | None = None,\n) -> None:\n    \"\"\"Uploads a local folder to the Hugging Face Model Hub.\n\n    Args:\n        repo_id (str): The ID of the target repository on the Hugging Face Model Hub.\n        folder_path (str): The local path to the folder to be uploaded.\n        repo_type (str, optional): The type of the repository ('model', 'dataset', or 'space').\n            Defaults to 'model'.\n        private (bool, optional): If True, the repository will be private; otherwise, it will be public.\n            Defaults to False.\n        path_in_repo (str, optional): The relative path within the repository where the folder should be uploaded.\n            Defaults to None, which means the root of the repository.\n        commit_message (str, optional): A message for the commit associated with the upload.\n        commit_description (str, optional): A description for the commit associated with the upload.\n\n    Raises:\n        FileNotFoundError: If the specified folder does not exist.\n        ValueError: If the specified folder is empty, a file, or if an invalid 'repo_type' is provided.\n        ValueError: If the upload process fails for any reason.\n\n    Returns:\n        None\n    \"\"\"\n    # Make sure the folder exists\n    if not os.path.exists(folder_path):\n        raise FileNotFoundError(f\"Folder {folder_path} does not exist.\")\n\n    # Make sure the folder is not a file\n    if os.path.isfile(folder_path):\n        raise ValueError(f\"Folder {folder_path} is a file. Please provide a folder.\")\n\n    # Make sure the folder is not empty\n    if not os.listdir(folder_path):\n        raise ValueError(f\"Folder {folder_path} is empty.\")\n\n    if repo_type not in {\"model\", \"dataset\", \"space\"}:\n        raise ValueError(f\"Invalid repo_type {repo_type}. Valid values are 'model', 'dataset', and 'space'.\")\n\n    # Login to the hub\n    api = hf_hub_login()\n\n    # Create the repo if it doesn't exist. This is a no-op if the repo already exists\n    # This is required because the API doesn't allow uploading to a non-existent repo\n    if not api.repo_exists(repo_id, repo_type=repo_type):\n        logger.info(f\"{repo_id} does not exist. Creating.\")\n        api.create_repo(repo_id, private=private, exist_ok=True, repo_type=repo_type)\n\n    # Upload the folder\n    try:\n        logger.info(f\"Uploading folder {folder_path} to repo {repo_id}.\")\n        api.upload_folder(\n            repo_id=repo_id,\n            folder_path=folder_path,\n            repo_type=repo_type,\n            path_in_repo=path_in_repo,\n            commit_message=commit_message,\n            commit_description=commit_description,\n        )\n    except Exception as e:\n        raise ValueError(f\"Failed to upload folder {folder_path} to repo {repo_id}\") from e\n"
  },
  {
    "path": "ludwig/utils/html_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport re\nfrom html.parser import HTMLParser\n\nfrom ludwig.utils import strings_utils\n\nlogger = logging.getLogger(__name__)\n\n\nclass HTMLStripper(HTMLParser):\n    def __init__(self):\n        super().__init__()\n        self.reset()\n        self.strict = False\n        self.convert_charrefs = True\n        self.fed = []\n\n    def handle_data(self, data):\n        self.fed.append(data)\n\n    def get_data(self):\n        return \"\".join(self.fed)\n\n    def error(self, message):\n        logger.error(message)\n\n\ndef strip_tags(html):\n    stripper = HTMLStripper()\n    stripper.feed(html)\n    return stripper.get_data()\n\n\n# regular expressions for cleaning text\nres_pre = [(re.compile(r\"([^.:;\\?\\!>])(<br/?>)\"), r\"\\1.\\2\"), (re.compile(r\"<br/?>\"), r\" \")]\nres_post = [\n    (re.compile(r\"[ \\t\\0]\"), r\" \"),\n    (re.compile(r\"[–_]\"), r\"-\"),\n    (\n        re.compile(r\"[\\’\\‘]\"),\n        r\"\"\"),\n    (re.compile(r'[”“]]'), r\"\"\",\n    ),\n    (re.compile(r\"℅\"), r\"%\"),\n    (re.compile(r\"([^.>])(<br/?>)\"), r\"\\1.\\2\"),\n    (re.compile(r\"\\\\\\\\[NnRr]\"), r\" \"),\n    (re.compile(r\"\\\\[NnRr]\"), r\" \"),\n    (re.compile(r\"[\\n\\r]\"), r\" \"),\n    (re.compile(r\"\\\\\\\\\"), r\" / \"),\n    (re.compile(r\"<br/?>\"), r\" \"),\n    (re.compile(r\"\\\\\\\\\" \"\"), r\"\\'\"),\n    (re.compile(r\"^\\'([^\\']+)$\"), r\"\\1\"),\n    (re.compile(r\"([\\<\\>\\{\\}\\[\\]\\(\\)\\-\\+\\=:;,\\./\\?\\!\\$%&£#@\\'₹ ])\\1+\"), r\"\\1\"),\n    (\n        re.compile(\n            r\"[^qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\\<\\>\\{\\}\\[\\]\\(\\)\\-\\+\\=:;,\\./\\?\\!\\$%&£#@\\'₹ ]\"  # noqa\n        ),\n        r\" \",\n    ),\n    (re.compile(r\"\\s{2,}\"), r\" \"),\n]\n\n\ndef clean_html(html_text):\n    # print()\n    # print(html_text)\n    html_text, matched = strings_utils.match_replace(html_text, res_pre)\n    # print(html_text)\n    html_text = strip_tags(html_text)\n    # print(html_text)\n    html_text = strings_utils.strip_accents(html_text)\n    # print(html_text)\n    # result = html_text.strip(\n    #     'qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890\\<\\>\\{\\}\\[\\]\\(\\)\\-\\+\\=:;,\\./\\?\\!\\$%&€£#@'₹\\' ')\n    # if result:\n    #     print(result)\n    html_text, matched = strings_utils.match_replace(html_text, res_post)\n    # print(matched)\n    # print(html_text)\n    return html_text\n"
  },
  {
    "path": "ludwig/utils/image_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport warnings\nfrom collections.abc import Callable, Iterable\nfrom dataclasses import dataclass\nfrom io import BytesIO\n\nimport numpy as np\nimport torch\nimport torchvision.transforms.functional as F\nfrom torchvision.io import decode_image, ImageReadMode\nfrom torchvision.models._api import WeightsEnum\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import CROP_OR_PAD, IMAGE_MAX_CLASSES, INTERPOLATE\nfrom ludwig.utils.data_utils import get_abs_path\nfrom ludwig.utils.fs_utils import get_bytes_obj_from_path\nfrom ludwig.utils.registry import Registry\n\n\n@dataclass\nclass TVModelVariant:\n    # Model variant identifier\n    variant_id: str | int\n\n    # TorchVision function to create model class\n    create_model_function: Callable\n\n    # Torchvision class for model weights\n    model_weights: WeightsEnum\n\n\nlogger = logging.getLogger(__name__)\n\nIMAGE_EXTENSIONS = (\".png\", \".jpg\", \".jpeg\", \".tiff\", \".tif\", \".bmp\", \".gif\")\n\n\n@DeveloperAPI\nclass ResizeChannels(torch.nn.Module):\n    def __init__(self, num_channels: int):\n        super().__init__()\n        self.num_channels = num_channels\n\n    def forward(self, imgs: torch.Tensor):\n        original_imgs_shape = imgs.shape\n        if len(original_imgs_shape) == 3:  # if shape is (C, H, W), add batch dimension\n            imgs = imgs.unsqueeze(0)\n\n        channels = imgs.shape[1]\n        if channels > self.num_channels:\n            # take the first `self.num_channels` channels\n            imgs = imgs[:, : self.num_channels, :, :]\n        elif channels < self.num_channels:\n            # repeat and use the first `self.num_channels` channels\n            imgs = imgs.repeat(1, (self.num_channels // channels) + 1, 1, 1)[:, : self.num_channels, :, :]\n\n        if len(original_imgs_shape) == 3:  # if shape was (C, H, W), remove batch dimension\n            return imgs[0]\n        return imgs\n\n\n@DeveloperAPI\ndef get_gray_default_image(num_channels: int, height: int, width: int) -> np.ndarray:\n    return np.full((num_channels, height, width), 128, dtype=np.float32)\n\n\n@DeveloperAPI\ndef get_average_image(image_lst: list[np.ndarray]) -> np.array:\n    return np.mean([x for x in image_lst if x is not None], axis=(0), dtype=np.float32)\n\n\n@DeveloperAPI\ndef is_bytes_image(bytes_obj) -> bool:\n    \"\"\"Check if a bytes object is an image using PIL.\"\"\"\n    try:\n        from io import BytesIO\n\n        from PIL import Image\n\n        if isinstance(bytes_obj, bytes):\n            bytes_obj = BytesIO(bytes_obj)\n        Image.open(bytes_obj).verify()\n        return True\n    except Exception:\n        return False\n\n\ndef is_image(src_path: str, img_entry: bytes | str, column: str) -> bool:\n    if not isinstance(img_entry, str):\n        return False\n    try:\n        from io import BytesIO\n\n        from PIL import Image\n\n        path = get_abs_path(src_path, img_entry)\n        bytes_obj = get_bytes_obj_from_path(path)\n        if isinstance(bytes_obj, bytes):\n            bytes_obj = BytesIO(bytes_obj)\n        Image.open(bytes_obj).verify()\n        return True\n    except Exception as e:\n        logger.warning(f\"While assessing potential image in is_image() for column {column}, encountered exception: {e}\")\n        return False\n\n\n@DeveloperAPI\ndef is_image_score(path):\n    return int(isinstance(path, str) and path.lower().endswith(IMAGE_EXTENSIONS))\n\n\n@DeveloperAPI\ndef get_image_read_mode_from_num_channels(num_channels: int) -> ImageReadMode:\n    \"\"\"Returns the torchvision.io.ImageReadMode corresponding to the number of channels.\n\n    If num_channels is not recognized, returns ImageReadMode.UNCHANGED.\n    \"\"\"\n    mode = ImageReadMode.UNCHANGED\n    if num_channels == 1:\n        mode = ImageReadMode.GRAY\n    elif num_channels == 2:\n        mode = ImageReadMode.GRAY_ALPHA\n    elif num_channels == 3:\n        mode = ImageReadMode.RGB\n    elif num_channels == 4:\n        mode = ImageReadMode.RGB_ALPHA\n    return mode\n\n\n@DeveloperAPI\ndef read_image_from_path(\n    path: str, num_channels: int | None = None, return_num_bytes=False\n) -> torch.Tensor | None | tuple[torch.Tensor | None, int]:\n    \"\"\"Reads image from path.\n\n    Useful for reading from a small number of paths. For more intensive reads, use backend.read_binary_files instead. If\n    `return_num_bytes` is True, returns a tuple of (image, num_bytes).\n    \"\"\"\n    bytes_obj = get_bytes_obj_from_path(path)\n    image = read_image_from_bytes_obj(bytes_obj, num_channels)\n    if return_num_bytes:\n        if bytes_obj is not None:\n            num_bytes = len(bytes_obj)\n        else:\n            num_bytes = None\n        return image, num_bytes\n    else:\n        return image\n\n\n@DeveloperAPI\ndef read_image_from_bytes_obj(bytes_obj: bytes | None = None, num_channels: int | None = None) -> torch.Tensor | None:\n    \"\"\"Tries to read image as a tensor from the path.\n\n    If the path is not decodable as a PNG, attempts to read as a numpy file. If neither of these work, returns None.\n    \"\"\"\n    if bytes_obj is None:\n        return None\n    mode = get_image_read_mode_from_num_channels(num_channels)\n\n    image = read_image_as_png(bytes_obj, mode)\n    if image is None:\n        image = read_image_as_numpy(bytes_obj)\n    if image is None:\n        image = read_image_as_tif(bytes_obj)\n    if image is None:\n        warnings.warn(\"Unable to read image from bytes object.\")\n    return image\n\n\n@DeveloperAPI\ndef read_image_as_png(bytes_obj: bytes, mode: ImageReadMode = ImageReadMode.UNCHANGED) -> torch.Tensor | None:\n    \"\"\"Reads image from bytes object from a PNG file.\"\"\"\n    try:\n        with BytesIO(bytes_obj) as buffer:\n            buffer_view = buffer.getbuffer()\n            if len(buffer_view) == 0:\n                del buffer_view\n                raise Exception(\"Bytes object is empty. This could be due to a failed load from storage.\")\n            image = decode_image(torch.frombuffer(buffer_view, dtype=torch.uint8), mode=mode)\n            del buffer_view\n            return image\n    except Exception as e:\n        warnings.warn(f\"Failed to read image from PNG file. Original exception: {e}\")\n        return None\n\n\n@DeveloperAPI\ndef read_image_as_numpy(bytes_obj: bytes) -> torch.Tensor | None:\n    \"\"\"Reads image from bytes object from a numpy file.\"\"\"\n    try:\n        with BytesIO(bytes_obj) as buffer:\n            image = np.load(buffer)\n            return torch.from_numpy(image)\n    except Exception as e:\n        warnings.warn(f\"Failed to read image from numpy file. Original exception: {e}\")\n        return None\n\n\n@DeveloperAPI\ndef read_image_as_tif(bytes_obj: bytes) -> torch.Tensor | None:\n    \"\"\"Reads image from bytes object from a tif file.\"\"\"\n    try:\n        import tifffile\n\n        with BytesIO(bytes_obj) as buffer:\n            image = tifffile.imread(buffer)\n            if image.dtype == np.uint16:\n                image = image.astype(np.int32)\n            image = torch.from_numpy(image)\n            if len(image.shape) == 2:\n                image = torch.unsqueeze(image, dim=0)\n            return image\n    except Exception as e:\n        warnings.warn(f\"Failed to read image from tif file. Original exception: {e}\")\n        return None\n\n\n@DeveloperAPI\ndef pad(\n    img: torch.Tensor,\n    new_size: int | tuple[int, int],\n) -> torch.Tensor:\n    \"\"\"Torchscript-compatible implementation of pad.\n\n    Args:\n        img (torch.Tensor): image with shape [..., height, width] to pad\n        new_size (Union[int, Tuple[int, int]]): size to pad to. If int, resizes to square image of that size.\n\n    Returns:\n        torch.Tensor: padded image of size [..., size[0], size[1]] or [..., size, size] if size is int.\n    \"\"\"\n    new_size = to_tuple(new_size)\n    old_size = img.shape[-2:]\n    pad_size = (torch.tensor(new_size) - torch.tensor(old_size)) / 2\n    padding = torch.cat((torch.floor(pad_size), torch.ceil(pad_size)))\n    padding[padding < 0] = 0\n    padding = [int(x) for x in padding]\n    return F.pad(img, padding=padding, padding_mode=\"edge\")\n\n\n@DeveloperAPI\ndef crop(\n    img: torch.Tensor,\n    new_size: int | tuple[int, int],\n) -> torch.Tensor:\n    \"\"\"Torchscript-compatible implementation of crop.\n\n    Args:\n        img (torch.Tensor): image with shape [..., height, width] to crop\n        size (Union[int, Tuple[int, int]]): size to crop to. If int, crops to square image of that size.\n\n    Returns:\n        torch.Tensor: cropped image of size [..., size[0], size[1]] or [..., size, size] if size is int.\n    \"\"\"\n    new_size = to_tuple(new_size)\n    return F.center_crop(img, output_size=new_size)\n\n\n@DeveloperAPI\ndef crop_or_pad(img: torch.Tensor, new_size: int | tuple[int, int]):\n    \"\"\"Torchscript-compatible implementation of resize using constants.CROP_OR_PAD.\n\n    Args:\n        img (torch.Tensor): image with shape [..., height, width] to resize\n        new_size (Union[int, Tuple[int, int]]): size to resize to. If int, resizes to square image of that size.\n\n    Returns:\n        torch.Tensor: resized image of size [..., size[0], size[1]] or [..., size, size] if size is int.\n    \"\"\"\n    new_size = to_tuple(new_size)\n    if list(new_size) == list(img.shape[-2:]):\n        return img\n    img = pad(img, new_size)\n    img = crop(img, new_size)\n    return img\n\n\n@DeveloperAPI\ndef resize_image(\n    img: torch.Tensor,\n    new_size: int | tuple[int, int],\n    resize_method: str,\n    crop_or_pad_constant: str = CROP_OR_PAD,\n    interpolate_constant: str = INTERPOLATE,\n) -> torch.Tensor:\n    \"\"\"Torchscript-compatible implementation of resize.\n\n    Args:\n        img (torch.Tensor): image with shape [..., height, width] to resize\n        new_size (Union[int, Tuple[int, int]]): size to resize to. If int, resizes to square image of that size.\n        resize_method (str): method to use for resizing. Either constants.CROP_OR_PAD or constants.INTERPOLATE.\n\n    Returns:\n        torch.Tensor: resized image of size [..., size[0], size[1]] or [..., size, size] if size is int.\n    \"\"\"\n    new_size = to_tuple(new_size)\n    if list(img.shape[-2:]) != list(new_size):\n        if resize_method == crop_or_pad_constant:\n            return crop_or_pad(img, new_size)\n        elif resize_method == interpolate_constant:\n            return F.resize(img, new_size)\n        raise ValueError(f\"Invalid image resize method: {resize_method}\")\n    return img\n\n\n@DeveloperAPI\ndef grayscale(img: torch.Tensor) -> torch.Tensor:\n    \"\"\"Grayscales RGB image.\"\"\"\n    return F.rgb_to_grayscale(img)\n\n\n@DeveloperAPI\ndef num_channels_in_image(img: torch.Tensor):\n    \"\"\"Returns number of channels in image.\"\"\"\n    if img is None or img.ndim < 2:\n        raise ValueError(\"Invalid image data\")\n\n    if img.ndim == 2:\n        return 1\n    else:\n        return img.shape[0]\n\n\n@DeveloperAPI\ndef get_unique_channels(\n    image_sample: list[torch.Tensor],\n    num_channels: int,\n    num_classes: int = None,\n) -> torch.Tensor:\n    \"\"\"Returns a tensor of unique channel values from a list of images.\n    Args:\n        image_sample: A list of images of dimensions [C x H x W] or [H x W], where C is the channel dimension\n        num_channels: The expected number of channels\n        num_classes: The expected number of classes or None\n\n    Return:\n        channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class.\n    \"\"\"\n    n_images = 0\n    no_new_class = 0\n    channel_class_map = None\n    for img in image_sample:\n        if img.ndim < 2:\n            raise ValueError(\"Invalid image dimensions {img.ndim}\")\n        if img.ndim == 2:\n            img = img.unsqueeze(0)\n        if num_channels == 1 and num_channels_in_image(img) != 1:\n            img = grayscale(img)\n        if num_classes == 2 and num_channels_in_image(img) == 1:\n            img = img.type(torch.float32) / 255\n            img = img.round() * 255\n            img = img.type(torch.uint8)\n\n        img = img.flatten(1, 2)\n        img = img.permute(1, 0)\n        uniq_chans = img.unique(dim=0)\n\n        if channel_class_map is None:\n            channel_class_map = uniq_chans\n        else:\n            channel_class_map = torch.concat((channel_class_map, uniq_chans)).unique(dim=0)\n        if channel_class_map.shape[0] > IMAGE_MAX_CLASSES:\n            raise ValueError(\n                f\"Images inferred num classes {channel_class_map.shape[0]} exceeds \" f\"max classes {IMAGE_MAX_CLASSES}.\"\n            )\n\n        n_images += 1\n        if n_images % 25 == 0:\n            logger.info(f\"Processed the first {n_images} images inferring {channel_class_map.shape[0]} classes...\")\n\n        if channel_class_map.shape[0] == uniq_chans.shape[0]:\n            no_new_class += 1\n            if no_new_class >= 4 and channel_class_map.shape[0] == num_classes:\n                break  # early loop exit\n        else:\n            no_new_class = 0\n\n    logger.info(f\"Inferred {channel_class_map.shape[0]} classes from the first {n_images} images.\")\n    return channel_class_map.type(torch.uint8)\n\n\n@DeveloperAPI\ndef get_class_mask_from_image(\n    channel_class_map: torch.Tensor,\n    img: torch.Tensor,\n) -> torch.Tensor:\n    \"\"\"Returns a masked image where each mask value is the channel class of the input.\n    Args:\n        channel_class_map: A tensor mapping channel values to classes, where dim=0 is the class.\n        img: An input image of dimensions [C x H x W] or [H x W], where C is the channel dimension\n\n    Return:\n        [mask] A masked image of dimensions [H x W] where each value is the channel class of the input\n    \"\"\"\n    num_classes = channel_class_map.shape[0]\n    mask = torch.full((img.shape[-2], img.shape[-1]), num_classes, dtype=torch.uint8)\n    if img.ndim == 2:\n        img = img.unsqueeze(0)\n    if num_classes == 2 and num_channels_in_image(img) == 1:\n        img = img.type(torch.float32) / 255\n        img = img.round() * 255\n        img = img.type(torch.uint8)\n    img = img.permute(1, 2, 0)\n    for nclass, value in enumerate(channel_class_map):\n        mask[(img == value).all(-1)] = nclass\n\n    if torch.any(mask.ge(num_classes)):\n        raise ValueError(\n            f\"Image channel could not be mapped to a class because an unknown channel value was detected. \"\n            f\"{num_classes} classes were inferred from the first set of images. This image has a channel \"\n            f\"value that was not previously seen in the first set of images. Check preprocessing parameters \"\n            f\"for image resizing, num channels, num classes and num samples. Image resizing may affect \"\n            f\"channel values. \"\n        )\n\n    return mask\n\n\n@DeveloperAPI\ndef get_image_from_class_mask(\n    channel_class_map: torch.Tensor,\n    mask: np.ndarray,\n) -> np.ndarray:\n    \"\"\"Returns an image with channel values determined from a corresponding mask.\n    Args:\n        channel_class_map: An tensor mapping channel values to classes, where dim=0 is the class.\n        mask: A masked image of dimensions [H x W] where each value is the channel class of the final image\n\n    Return:\n        [img] An image of dimensions [C x H x W], where C is the channel dimension\n    \"\"\"\n    mask = torch.from_numpy(mask)\n    img = torch.zeros(channel_class_map.shape[1], mask.shape[-2], mask.shape[-1], dtype=torch.uint8)\n    img = img.permute(1, 2, 0)\n    mask = mask.unsqueeze(0)\n    mask = mask.permute(1, 2, 0)\n    for nclass, value in enumerate(channel_class_map):\n        img[(mask == nclass).all(-1)] = value\n    img = img.permute(2, 0, 1)\n\n    return img.numpy()\n\n\n@DeveloperAPI\ndef to_tuple(v: int | tuple[int, int]) -> tuple[int, int]:\n    \"\"\"Converts int or tuple to tuple of ints.\"\"\"\n    if torch.jit.isinstance(v, int):\n        return v, v\n    else:\n        return v\n\n\n@DeveloperAPI\ndef to_np_tuple(prop: int | Iterable) -> np.ndarray:\n    \"\"\"Creates a np array of length 2 from a Conv2D property.\n\n    E.g., stride=(2, 3) gets converted into np.array([2, 3]), where the height_stride = 2 and width_stride = 3. stride=2\n    gets converted into np.array([2, 2]).\n    \"\"\"\n    if isinstance(prop, int):\n        return np.ones(2).astype(int) * prop\n    elif isinstance(prop, np.ndarray) and prop.size == 2:\n        return prop.astype(int)\n    elif isinstance(prop, Iterable) and len(prop) == 2:\n        return np.array(list(prop)).astype(int)\n    else:\n        raise TypeError(f\"prop must be int or iterable of length 2, but is {prop}.\")\n\n\n@DeveloperAPI\ndef get_img_output_shape(\n    img_height: int,\n    img_width: int,\n    kernel_size: int | tuple[int],\n    stride: int | tuple[int],\n    padding: int | tuple[int] | str,\n    dilation: int | tuple[int],\n) -> tuple[int]:\n    \"\"\"Returns the height and width of an image after a 2D img op.\n\n    Currently supported for Conv2D, MaxPool2D and AvgPool2d ops.\n    \"\"\"\n    if padding == \"same\":\n        return (img_height, img_width)\n    elif padding == \"valid\":\n        padding = np.zeros(2)\n    else:\n        padding = to_np_tuple(padding)\n\n    kernel_size = to_np_tuple(kernel_size)\n    stride = to_np_tuple(stride)\n    dilation = to_np_tuple(dilation)\n    shape = np.array([img_height, img_width])\n\n    out_shape = np.floor(((shape + 2 * padding - dilation * (kernel_size - 1) - 1) / stride) + 1)\n\n    return tuple(out_shape.astype(int))\n\n\ntorchvision_model_registry = Registry()\n\n\ndef register_torchvision_model_variants(variants: list[TVModelVariant]):\n    def wrap(cls):\n        # prime with empty placeholder\n        torchvision_model_registry[cls.torchvision_model_type] = {}\n\n        # register each variant\n        for variant in variants:\n            torchvision_model_registry[cls.torchvision_model_type][variant.variant_id] = variant\n        return cls\n\n    return wrap\n"
  },
  {
    "path": "ludwig/utils/inference_utils.py",
    "content": "from datetime import datetime\n\nimport pandas as pd\nimport torch\n\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    COLUMN,\n    DATE,\n    IMAGE,\n    NAME,\n    POSTPROCESSOR,\n    PREDICTOR,\n    PREPROCESSOR,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    TYPE,\n    VECTOR,\n)\nfrom ludwig.types import FeatureConfigDict, ModelConfigDict\nfrom ludwig.utils.audio_utils import read_audio_from_path\nfrom ludwig.utils.date_utils import create_vector_from_datetime_obj\nfrom ludwig.utils.image_utils import read_image_from_path\nfrom ludwig.utils.torch_utils import place_on_device\nfrom ludwig.utils.types import TorchDevice, TorchscriptPreprocessingInput\n\nFEATURES_TO_CAST_AS_STRINGS = {BINARY, CATEGORY, BAG, SET, TEXT, SEQUENCE, TIMESERIES, VECTOR}\n\n\ndef get_filename_from_stage(stage: str, device: TorchDevice) -> str:\n    \"\"\"Returns the filename for a stage of inference.\"\"\"\n    if stage not in [PREPROCESSOR, PREDICTOR, POSTPROCESSOR]:\n        raise ValueError(f\"Invalid stage: {stage}.\")\n    # device is only tracked for predictor stage\n    if stage == PREDICTOR:\n        return f\"inference_{stage}-{device}.pt\"\n    else:\n        return f\"inference_{stage}.pt\"\n\n\ndef to_inference_module_input_from_dataframe(\n    dataset: pd.DataFrame, config: ModelConfigDict, load_paths: bool = False, device: torch.device | None = None\n) -> dict[str, TorchscriptPreprocessingInput]:\n    \"\"\"Converts a pandas DataFrame to be compatible with a torchscripted InferenceModule forward pass.\"\"\"\n    inputs = {}\n    for if_config in config[\"input_features\"]:\n        feature_inputs = to_inference_model_input_from_series(\n            dataset[if_config[COLUMN]],\n            if_config[TYPE],\n            load_paths=load_paths,\n            feature_config=if_config,\n        )\n        feature_inputs = place_on_device(feature_inputs, device)\n        inputs[if_config[NAME]] = feature_inputs\n    return inputs\n\n\ndef to_inference_model_input_from_series(\n    s: pd.Series, feature_type: str, load_paths: bool = False, feature_config: FeatureConfigDict | None = None\n) -> TorchscriptPreprocessingInput:\n    \"\"\"Converts a pandas Series to be compatible with a torchscripted InferenceModule forward pass.\"\"\"\n    if feature_type == IMAGE:\n        if load_paths:\n            return [read_image_from_path(v) if isinstance(v, str) else v for v in s]\n    elif feature_type == AUDIO:\n        if load_paths:\n            return [read_audio_from_path(v) if isinstance(v, str) else v for v in s]\n    elif feature_type == DATE:\n        if feature_config is None:\n            raise ValueError('\"date\" feature type requires the associated feature config to be provided.')\n        datetime_format = feature_config[\"preprocessing\"][\"datetime_format\"]\n        return [torch.tensor(create_vector_from_datetime_obj(datetime.strptime(v, datetime_format))) for v in s]\n    elif feature_type in FEATURES_TO_CAST_AS_STRINGS:\n        return s.astype(str).to_list()\n    return torch.from_numpy(s.to_numpy())\n"
  },
  {
    "path": "ludwig/utils/llm_quantization_utils.py",
    "content": "import torch\nfrom torch import nn\n\ntry:\n    from bitsandbytes.functional import dequantize_4bit\n    from bitsandbytes.nn.modules import Linear4bit\nexcept Exception:\n    dequantize_4bit = None\n    Linear4bit = None\n\nfrom ludwig.api_annotations import DeveloperAPI\n\n\n@DeveloperAPI\ndef linear4bit_to_linear(linear4bit_layer):\n    \"\"\"Converts a Linear4Bit layer to a standard Linear layer by dequantizing the weight values and copying the\n    dequantized weights to a new Linear layer.\n\n    Args:\n        linear4bit_layer (Linear4bit): The input Linear4Bit layer.\n\n    Returns:\n        nn.Linear: A new Linear layer with dequantized weights and biases.\n    \"\"\"\n    # Create a new Linear layer with the same shape\n    new_linear_layer = nn.Linear(\n        linear4bit_layer.in_features,\n        linear4bit_layer.out_features,\n        bias=linear4bit_layer.bias is not None,\n        dtype=torch.float16,\n    )\n\n    # Dequantize the weight and bias from the Linear4bit layer and perform an in-place tensor replacement\n    # to update the weights and bias in the new Linear layer. This is done to avoid creating a new tensor\n    # and copying the data, which is slow.\n    new_linear_layer.weight.data.copy_(\n        dequantize_4bit(linear4bit_layer.weight.data, linear4bit_layer.weight.quant_state)\n    )\n    if linear4bit_layer.bias is not None:\n        new_linear_layer.bias.data.copy_(linear4bit_layer.bias.data)\n\n    return new_linear_layer\n\n\n@DeveloperAPI\ndef convert_quantized_linear_to_linear(module):\n    \"\"\"Recursively converts Linear4Bit layers to standard Linear layers in a given module.\n\n    Args:\n        module (nn.Module): The input module containing potentially nested Linear4Bit layers.\n\n    Returns:\n        None\n    \"\"\"\n    for name, child in module.named_children():\n        if isinstance(child, Linear4bit):\n            # Replace Linear4Bit layer with a new Linear layer\n            setattr(module, name, linear4bit_to_linear(child))\n        else:\n            # Recursively apply the conversion for nested modules\n            convert_quantized_linear_to_linear(child)\n"
  },
  {
    "path": "ludwig/utils/llm_utils.py",
    "content": "import copy\nimport logging\nimport tempfile\nfrom typing import TYPE_CHECKING, Union\n\nimport torch\nimport torch.nn.functional as F\nimport transformers\nfrom packaging import version\n\ntry:\n    from bitsandbytes.nn.modules import Embedding as BnbEmbedding\nexcept Exception:\n    BnbEmbedding = None\nfrom transformers import AutoConfig, AutoModelForCausalLM, PreTrainedModel, PreTrainedTokenizer, TextStreamer\n\nfrom ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, PREDICTIONS, PROBABILITIES\nfrom ludwig.schema.trainer import LLMTrainerConfig\nfrom ludwig.utils.error_handling_utils import default_retry\nfrom ludwig.utils.logging_utils import log_once\nfrom ludwig.utils.model_utils import find_embedding_layer_with_path\n\nif TYPE_CHECKING:\n    from ludwig.schema.encoders.text_encoders import LLMEncoderConfig\n    from ludwig.schema.model_types.llm import LLMModelConfig\n\n\nlogger = logging.getLogger(__name__)\n\ntransformers_436 = version.parse(transformers.__version__) >= version.parse(\"4.36.0\")\n\nFALLBACK_CONTEXT_LEN = 2048\n\n_MODELS_WITH_DEVICE_MAP_AUTO_EXCLUSION = set()\n\n\n@default_retry(tries=8)\ndef load_pretrained_from_config(\n    config_obj: Union[\"LLMModelConfig\", \"LLMEncoderConfig\"],\n    model_config: AutoConfig | None = None,\n    weights_save_path: str | None = None,\n) -> PreTrainedModel:\n    load_kwargs = {}\n    if config_obj.quantization:\n        # Apply quantization configuration at model load time\n        load_kwargs[\"dtype\"] = getattr(torch, config_obj.quantization.bnb_4bit_compute_dtype)\n        load_kwargs[\"quantization_config\"] = config_obj.quantization.to_bitsandbytes()\n        load_kwargs[\"device_map\"] = \"auto\"\n\n        if transformers_436:\n            load_kwargs[\"attn_implementation\"] = \"eager\"\n    else:\n        # Load in float32 by default to avoid CUBLAS errors with small hidden sizes\n        # and to ensure numerical stability during training without mixed-precision.\n        load_kwargs[\"dtype\"] = torch.float32\n\n    config_modified = False\n    if config_obj.model_parameters:\n        # Add any model specific parameters to the load kwargs\n        for param_name, param_value in config_obj.model_parameters.to_dict().items():\n            # Not all parameters are supported by all models, so we only add the parameter to the load kwargs\n            # if it is supported by the model.\n            if param_value is None:\n                continue\n\n            if hasattr(model_config, param_name):\n                if isinstance(param_value, dict):\n                    # For nested dict params (e.g. rope_scaling), merge with existing\n                    # config values to preserve defaults like rope_theta.\n                    existing = getattr(model_config, param_name, {}) or {}\n                    existing.update(param_value)\n                    setattr(model_config, param_name, existing)\n                    config_modified = True\n                else:\n                    load_kwargs[param_name] = param_value\n            else:\n                logger.warning(f\"Parameter {param_name} is not supported by {config_obj.base_model}. Skipping.\")\n\n    # Only pass config= when we've directly modified it (e.g. rope_scaling merge).\n    if config_modified:\n        load_kwargs[\"config\"] = model_config\n\n    logger.info(\"Loading large language model...\")\n    pretrained_model_name_or_path = weights_save_path or config_obj.base_model\n    trust_remote_code = getattr(config_obj, \"trust_remote_code\", False)\n    model: PreTrainedModel = AutoModelForCausalLM.from_pretrained(\n        pretrained_model_name_or_path, trust_remote_code=trust_remote_code, **load_kwargs\n    )\n    return model\n\n\ndef to_device(\n    model: PreTrainedModel,\n    device: str | torch.DeviceObjType,\n    config_obj: \"LLMModelConfig\",  # noqa F821\n    curr_device: torch.DeviceObjType,\n) -> tuple[PreTrainedModel, torch.DeviceObjType]:\n    \"\"\"Move an LLM to the requested device, accounting for sharding and adapters.\n\n    Args:\n        model: Pretrained model to put on device\n        config_obj: LLM config\n        curr_device: The current device that the model is on\n\n    Returns:\n        `model` moved to `device`\n    \"\"\"\n    device = torch.device(device)\n\n    if device.type == curr_device.type:\n        log_once(f\"Model already on device'{device}'.\")\n        return model, device\n    else:\n        log_once(f\"Moving LLM from '{curr_device}' to '{device}'.\")\n\n    model_kwargs = {}\n    num_gpus = torch.cuda.device_count()\n    if device == torch.device(\"cuda\") and num_gpus > 1:\n        # TODO: make this configurable in the future. These parameters are from FastChat:\n        # https://github.com/lm-sys/FastChat/blob/0e958b852a14f4bef5f0e9d7a5e7373477329cf2/fastchat/serve/inference.py#L90  # noqa\n        # TODO: Wrap device_map=\"auto\" in a try-except block since it may not be supported for all models (E.g. BertLMHead)  # noqa\n        # We don't add quantization here (float16 or bfloat16) since we may not always want to quantize. We should\n        # make quantization configurable in the future via the trainer config.\n        model_kwargs.update(\n            dict(\n                low_cpu_mem_usage=True,\n                max_memory={i: \"13GiB\" for i in range(num_gpus)},\n            )\n        )\n\n        if config_obj.base_model not in _MODELS_WITH_DEVICE_MAP_AUTO_EXCLUSION:\n            model_kwargs[\"device_map\"] = \"auto\"\n\n        if config_obj.quantization:\n            model_kwargs[\"quantization_config\"] = config_obj.quantization.to_bitsandbytes()\n\n        # we save and reload the weights to ensure that they can be sharded across the GPUs using `from_pretrained`\n        with tempfile.TemporaryDirectory() as tmpdir:\n            model.save_pretrained(tmpdir)\n\n            if config_obj.adapter:\n                model = AutoModelForCausalLM.from_pretrained(\n                    config_obj.base_model,\n                    trust_remote_code=getattr(config_obj, \"trust_remote_code\", False),\n                    **model_kwargs,\n                )\n\n                # Leave this import inline to support a minimal install of Ludwig\n                from peft import PeftModel  # noqa\n\n                model = PeftModel.from_pretrained(\n                    model,\n                    tmpdir,\n                    torch_dtype=torch.float16,\n                )\n            else:\n                model = AutoModelForCausalLM.from_pretrained(\n                    tmpdir,\n                    trust_remote_code=getattr(config_obj, \"trust_remote_code\", False),\n                    **model_kwargs,\n                )\n    else:\n        model = model.to(device)\n\n    return model, device\n\n\ndef _load_peft_config(pretrained_adapter_weights: str):\n    \"\"\"Load a PeftConfig, fixing known compatibility issues with newer PEFT versions.\"\"\"\n    import json\n\n    from huggingface_hub import hf_hub_download\n    from peft import PeftConfig\n\n    config_file = hf_hub_download(pretrained_adapter_weights, \"adapter_config.json\")\n    with open(config_file) as f:\n        config_dict = json.load(f)\n\n    # AdaLoRA requires total_step > 0 in newer PEFT versions, but pretrained\n    # configs may have total_step=None.\n    if config_dict.get(\"peft_type\") == \"ADALORA\" and not config_dict.get(\"total_step\"):\n        config_dict[\"total_step\"] = 10000\n\n    return PeftConfig.from_peft_type(**config_dict)\n\n\ndef initialize_adapter(\n    model: PreTrainedModel, config_obj: \"LLMModelConfig\"  # noqa F821\n) -> Union[\"PeftModel\", PreTrainedModel]:  # noqa F821\n    \"\"\"Wrap a pretrained model with a PEFT model for fine-tuning.\n\n    Args:\n         model: Pretrained model to fine-tune with an adapter.\n         config_obj: LLM config\n\n    Returns:\n        `model` wrapped in a PEFT model if an adapter config was provided, otherwise `model`.\n    \"\"\"\n    # Only load a PEFT model if the config specifies an adapter, otherwise return the model unaltered.\n    if config_obj.adapter:\n        if config_obj.adapter.pretrained_adapter_weights:\n            # Load pretrained adapter weights if specified.\n            logger.info(f\"Using pretrained adapter weights: {config_obj.adapter.pretrained_adapter_weights}\")\n\n            # Leave this import inline to support a minimal install of Ludwig\n            from peft import MODEL_TYPE_TO_PEFT_MODEL_MAPPING, PeftConfig  # noqa\n\n            peft_config = _load_peft_config(config_obj.adapter.pretrained_adapter_weights)\n\n            model = MODEL_TYPE_TO_PEFT_MODEL_MAPPING[peft_config.task_type].from_pretrained(\n                model, config_obj.adapter.pretrained_adapter_weights, config=peft_config\n            )\n        else:\n            # Leave this import inline to support a minimal install of Ludwig\n            from peft import get_peft_model, TaskType  # noqa\n\n            # If no pretrained adapter is provided, we want to load untrained weights into the model\n            peft_config = config_obj.adapter.to_config(\n                task_type=TaskType.CAUSAL_LM, tokenizer_name_or_path=config_obj.base_model\n            )\n\n            model = get_peft_model(model, peft_config)\n\n    return model\n\n\ndef get_context_len(model_config: AutoConfig):\n    \"\"\"Determines the maximum length of the context (input + output tokens) based on the provided model\n    configuration.\n\n    Args:\n        model_config (AutoConfig): The model configuration object containing information about the model's properties.\n\n    Returns:\n        int: The maximum context length, which can be derived from the model configuration. If no relevant attribute\n             is found, the default value of 2048 is returned.\n\n    This function examines the provided model configuration object to identify the attribute that specifies the maximum\n    context length. It checks for attributes in the following order of preference:\n    1. 'max_sequence_length': If this attribute is present in the model configuration, its value is returned.\n    2. 'max_position_embeddings': If 'max_sequence_length' is not found but 'max_position_embeddings' is present, its\n       value is returned.\n    3. 'n_positions': If neither 'max_sequence_length' nor 'max_position_embeddings' are found, and 'n_positions' is\n       present, its value is returned.\n    4. Default: If none of the relevant attributes are present, the function returns a default value of 2048.\n\n    Note:\n    - The maximum context length is important for defining the size of input and output sequences in a model.\n\n    Example Usage:\n    >>> config = AutoConfig.from_pretrained(\"bert-base-uncased\")\n    >>> context_len = get_context_len(config)\n    >>> print(context_len)\n    512\n    \"\"\"\n    if hasattr(model_config, \"max_sequence_length\"):\n        return model_config.max_sequence_length\n    elif hasattr(model_config, \"max_position_embeddings\"):\n        return model_config.max_position_embeddings\n    elif hasattr(model_config, \"n_positions\"):\n        return model_config.n_positions\n    else:\n        return FALLBACK_CONTEXT_LEN\n\n\ndef has_padding_token(input_tensor: torch.Tensor, tokenizer: PreTrainedTokenizer):\n    \"\"\"Checks if the input tensor contains any padding tokens.\n\n    Args:\n        input_tensor (torch.Tensor): The input tensor.\n        tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input.\n\n    Returns:\n        bool: True if the input tensor contains any padding tokens, False otherwise.\n\n    Example:\n        >>> import torch\n        >>> from transformers import PreTrainedTokenizer\n        >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased')\n        >>> input_sentence = \"This is an example sentence.\"\n        >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True)\n        >>> padded_input_ids = torch.nn.functional.pad(input_ids, (0, 10 - len(input_ids)))\n        >>> has_padding = has_padding_token(padded_input_ids, tokenizer)\n        >>> has_padding\n        True\n    \"\"\"\n    if input_tensor.dim() == 1:\n        return torch.any(input_tensor == tokenizer.pad_token_id).item()\n    elif input_tensor.dim() == 2:\n        return torch.any(input_tensor == tokenizer.pad_token_id, dim=-1).item()\n    else:\n        raise ValueError(\"Input tensor must be 1D or 2D\")\n\n\ndef remove_left_padding(input_ids_sample: torch.Tensor, tokenizer: PreTrainedTokenizer):\n    \"\"\"Removes left padding and other tokens until the first BOS token from the input_ids tensor.\n\n    Args:\n        input_ids_sample (torch.Tensor): The input tensor with padding and other tokens.\n        tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input.\n\n    Returns:\n        torch.Tensor: The input tensor without left padding and other tokens until the first BOS token.\n\n    Example:\n        >>> import torch\n        >>> from transformers import PreTrainedTokenizer\n        >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased')\n        >>> input_sentence = \"This is an example sentence.\"\n        >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True)\n        >>> padded_input_ids = torch.nn.functional.pad(input_ids, (10 - len(input_ids), 0))\n        >>> input_ids_no_padding = remove_left_padding(padded_input_ids, tokenizer)\n        >>> input_ids_no_padding\n        tensor([[1, 2, 3]])\n    \"\"\"\n    # Remove all PAD tokens\n    pad_idxs = torch.where(input_ids_sample == tokenizer.pad_token_id)[0]  # all PAD token locations\n    input_ids_no_padding = input_ids_sample\n    if len(pad_idxs) != 0:\n        pad_idx = pad_idxs[-1]  # get last PAD token location\n        input_ids_no_padding = input_ids_sample[pad_idx + 1 :]\n\n    # Start from the first BOS token\n    bos_idxs = torch.where(input_ids_no_padding == tokenizer.bos_token_id)[0]  # all BOS token locations\n    if len(bos_idxs) != 0:\n        bos_idx = bos_idxs[0]  # get first BOS token location\n    else:\n        bos_idx = 0\n\n    input_ids_no_bos = input_ids_no_padding[bos_idx:].unsqueeze(0)\n    return input_ids_no_bos\n\n\ndef add_left_padding(input_ids, max_length, pad_value=0):\n    \"\"\"Adds left padding to the input_ids tensor.\n\n    Args:\n        input_ids (torch.Tensor): The input tensor.\n        max_length (int): The maximum length of the tensor after padding.\n        pad_value (int, optional): The value used for padding. Defaults to 0.\n\n    Returns:\n        torch.Tensor: The input_ids tensor with left padding.\n\n    Example:\n        >>> input_ids = torch.tensor([1, 2, 3])\n        >>> max_length = 5\n        >>> padded_tensor = add_left_padding(input_ids, max_length)\n        >>> padded_tensor\n        tensor([0, 0, 1, 2, 3])\n    \"\"\"\n    padding = torch.tensor([pad_value] * (max_length - input_ids.shape[0]), dtype=torch.int64, device=input_ids.device)\n    return torch.cat((padding, input_ids), dim=-1)\n\n\ndef create_attention_mask(input_ids: torch.Tensor, tokenizer: PreTrainedTokenizer):\n    \"\"\"Creates an attention mask for the input_ids tensor. This also sets the last padding token ID to 1 if it\n    exists.\n\n    Args:\n        input_ids (torch.Tensor): The input tensor.\n        tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input.\n\n    Returns:\n        torch.Tensor: The attention mask tensor.\n\n    Example:\n        >>> import torch # noqa\n        >>> from transformers import PreTrainedTokenizer\n        >>> tokenizer = PreTrainedTokenizer.from_pretrained('bert-base-uncased')\n        >>> input_sentence = \"This is an example sentence.\"\n        >>> input_ids = tokenizer.encode(input_sentence, add_special_tokens=True)\n        >>> attention_mask = create_attention_mask(input_ids, tokenizer)\n        >>> attention_mask\n        tensor([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])\n    \"\"\"\n    attention_mask = input_ids != tokenizer.pad_token_id\n    # Last token may not be padding if we've already hit the max sequence length\n    if not attention_mask[-1]:\n        # last token is padding, always attended to even if it is padding\n        attention_mask[-1] = 1\n    attention_mask = attention_mask.to(torch.int64)\n    return attention_mask\n\n\ndef find_last_matching_index(tensor_a: torch.Tensor, tensor_b: torch.Tensor):\n    \"\"\"Returns the last index of `tensor_a` that matches `tensor_b`. Specifically, this checks whether the tensor_b\n    is in the last tensor_b.shape[0] elements of tensor_a.\n\n    Args:\n        tensor_a (torch.Tensor): The first tensor.\n        tensor_b (torch.Tensor): The second tensor.\n\n    Returns:\n        int: The last index of `tensor_a` that matches `tensor_b`. Returns -1 if there is no matching index.\n\n    Example:\n        >>> import torch\n        >>> tensor_a = torch.tensor([1, 2, 3, 4, 5, 6, 7, 8])\n        >>> tensor_b = torch.tensor([6, 7, 8])\n        >>> last_matching_index = find_last_matching_index(tensor_a, tensor_b)\n        >>> last_matching_index\n        5\n    \"\"\"\n    last_index = -1\n\n    tensor_a_length = tensor_a.shape[0]\n    tensor_b_length = tensor_b.shape[0]\n\n    # Get the last tensor_b_length elements of tensor_a.\n    tensor_a_truncated = tensor_a[-tensor_b_length:]\n\n    # Find the last matching index.\n    for i in range(tensor_b_length):\n        if torch.equal(tensor_a_truncated[i:], tensor_b[: tensor_b_length - i]):\n            last_index = tensor_a_length - tensor_b_length + i\n            break\n\n    return last_index\n\n\ndef pad_target_tensor_for_fine_tuning(\n    targets: dict[str, torch.Tensor],\n    predictions: dict[str, torch.Tensor],\n    model_inputs: torch.Tensor,\n    of_name: str,\n) -> dict[str, torch.Tensor]:\n    \"\"\"Pad and adjust target tensors for fine-tuning LLMS models.\n\n    This function is used to pad and adjust the target tensors with IGNORE_INDEX_TOKEN_ID based on the model inputs and\n    predictions during the fine-tuning process of Language Models. Here's what this function does:\n        1. If none of the tokens from the target were in the model inputs, we create a tensor of the length of model\n            inputs with value IGNORE_INDEX_TOKEN_IDs. This ignores this row from affecting loss.\n        2. If the target tokens were entirely inside the model inputs, we want to pad all the tokens in model_inputs\n            coming from the input with IGNORE_INDEX_TOKEN_IDs and leave the target tokens as is. This ensures that all\n            of the target tokens are used during loss computation.\n        3. In the scenario that only some part of the target tokens were in the model inputs, we want to pad the model\n            inputs until that point and only leave the partial tokens of the target as is. This ensures that we will\n            only compute loss on the target tokens that were in the model inputs.\n\n    Args:\n        targets (Dict[str, torch.Tensor]): A dictionary containing the target tensors.\n        predictions (Dict[str, torch.Tensor]): A dictionary containing the predicted tensors.\n        model_inputs (torch.Tensor): The input tensor passed into the model's forward pass.\n        of_name (str): The name of the target tensor to be padded and adjusted.\n\n    Returns:\n        Dict[str, torch.Tensor]: A dictionary containing the updated target\n        dictionaries.\n    \"\"\"\n    target_length = targets.get(of_name).size()[1]\n    prediction_length = predictions[of_name].get(PREDICTIONS).size()[1]\n\n    if target_length == prediction_length:\n        return targets\n\n    updated_targets = []\n    for idx, target in enumerate(targets[of_name]):\n        # Remove any leading IGNORE_INDEX_TOKEN_IDs in the target that were temporarily added for alignment\n        end_index = (target != IGNORE_INDEX_TOKEN_ID).nonzero()[0]\n        target = target[end_index:]\n        target_device = target.device\n\n        # See if any part of the target was in the tensor passed into the model's forward pass\n        last_matching_index = find_last_matching_index(model_inputs[idx], target)\n\n        # If the last matching index is -1, it means that the input tensor passed into the model was truncated\n        # and did not contain the target tensor. In this case, we need to truncate the target tensors as well\n        # and just set it to a tensor of IGNORE_INDEX_TOKEN_ID so that we don't compute loss on this target tensor.\n        if last_matching_index == -1:\n            updated_targets.append(torch.full((prediction_length,), IGNORE_INDEX_TOKEN_ID).to(device=target_device))\n\n        # If the last matching index is not -1, it means that the input tensor passed into the model was not\n        # truncated and contained either a part of the target tensor or the entire target tensor. In this case,\n        # we need to set the target tensor to the part of the target tensor that was passed into the model while\n        # also padding it to the correct length with IGNORE_INDEX_TOKEN_ID.\n        else:\n            padding = torch.full((last_matching_index,), IGNORE_INDEX_TOKEN_ID).to(device=target_device)\n            updated_targets.append(torch.cat((padding, target), dim=-1)[:prediction_length])\n\n    targets[of_name] = torch.stack(updated_targets).to(device=targets.get(of_name).device, dtype=torch.int64)\n\n    return targets\n\n\ndef generate_merged_ids(\n    input_ids: torch.tensor, target_ids: torch.tensor, tokenizer: PreTrainedTokenizer, max_sequence_length: int = None\n):\n    \"\"\"Generate merged input and target IDs tensor.\n\n    This function merges the input_ids and target_ids together to create a unified tensor\n    to pass into the model. It also returns attention masks for the merged tensors.\n\n    Args:\n        input_ids (torch.Tensor): The input IDs tensor.\n        target_ids (torch.Tensor or None): The target IDs tensor or None.\n        max_sequence_length (int or None): The maximum sequence length to pad or truncate to.\n        tokenizer (PreTrainedTokenizer): The tokenizer used to encode the input_ids and target_ids.\n\n    Returns:\n        torch.Tensor: The merged input and target IDs tensor.\n        torch.Tensor: The attention masks for the merged tensor.\n    \"\"\"\n    merged_input_and_targets = []\n    lengths = []\n\n    eos_tensor = torch.tensor([tokenizer.eos_token_id]).to(target_ids[0].device)\n\n    # Merge input_ids and target_ids by concatenating them together.\n    # We remove the left padding from both input_ids and target_ids before concatenating them.\n    for input_id_sample, target_id_sample in zip(input_ids, target_ids):\n        input_id_sample_no_padding = remove_left_padding(input_id_sample, tokenizer)[0]\n        target_id_sample_no_padding = remove_left_padding(target_id_sample, tokenizer)[0]\n        target_id_sample_no_padding = torch.cat((target_id_sample_no_padding, eos_tensor), dim=-1)\n\n        merged_sample_ids = torch.cat((input_id_sample_no_padding, target_id_sample_no_padding), dim=-1)\n        # If the merged tensor is longer than the maximum sequence length, we truncate it.\n        if max_sequence_length and merged_sample_ids.shape[0] > max_sequence_length:\n            merged_sample_ids = merged_sample_ids[:max_sequence_length]\n\n        merged_input_and_targets.append(merged_sample_ids)\n        lengths.append(merged_sample_ids.shape[0])\n\n    # Since we remove the left padding from the target_ids, the merged input_ids and target_ids\n    # may not have the same lengths. We need to align them to the same length by adding left padding\n    # and generate an attention mask for just the part of the input that is not padding.\n    max_length = max(lengths)\n    attention_masks = []\n    for i, merged_sample_ids in enumerate(merged_input_and_targets):\n        merged_input_and_targets[i] = add_left_padding(merged_sample_ids, max_length)\n        attention_masks.append(create_attention_mask(merged_input_and_targets[i], tokenizer))\n\n    return torch.stack(merged_input_and_targets), torch.stack(attention_masks)\n\n\ndef _get_decoded_targets_and_predictions(\n    targets: dict[str, torch.Tensor],\n    predictions: dict[str, dict[str, torch.Tensor]],\n    tokenizer: PreTrainedTokenizer,\n    of_name: str,\n):\n    \"\"\"Returns the decoded targets and predictions, accounting for IGNORE_INDEX_TOKEN_ID.\"\"\"\n    target_tensor = targets[of_name]\n    pred_tensor = predictions[of_name][PREDICTIONS]\n    # Ensure targets and predictions are on the same device\n    if target_tensor.device != pred_tensor.device:\n        target_tensor = target_tensor.to(pred_tensor.device)\n    sanitized_targets = torch.where(target_tensor != IGNORE_INDEX_TOKEN_ID, target_tensor, tokenizer.pad_token_id)\n    sanitized_predictions = torch.where(\n        pred_tensor != IGNORE_INDEX_TOKEN_ID,\n        pred_tensor,\n        tokenizer.pad_token_id,\n    )\n    decoded_targets = tokenizer.batch_decode(sanitized_targets, skip_special_tokens=True)\n    decoded_predictions = tokenizer.batch_decode(sanitized_predictions, skip_special_tokens=True)\n    return decoded_targets, decoded_predictions\n\n\ndef get_realigned_target_and_prediction_tensors_for_inference(\n    targets: dict[str, torch.Tensor],\n    predictions: dict[str, dict[str, torch.Tensor]],\n    of_name: str,\n    tokenizer: PreTrainedTokenizer,\n    pad_value: int = None,\n) -> tuple[dict[str, torch.Tensor], dict[str, torch.Tensor]]:\n    \"\"\"Realigns the target tensor with the predictions.\n\n    This is necessary for text metrics that require the target and prediction to be of the same length.\n\n    Args:\n        targets: The target tensor.\n        predictions: The prediction tensor.\n        of_name: The output feature's name.\n        tokenizer: The HF tokenizer.\n        pad_direction: The direction to pad the tensors. Can be 'left' or 'right'.\n            Defaults to 'right'.\n\n    Returns:\n        Tuple of realigned (targets, decoded_targets, predictions, decoded_predictions).\n        - targets is a map of feature name -> tensor of token ids.\n        - predictions is a map from output feature name -> map of tensors with the following items:\n            - \"predictions\": tensor of token ids.\n            - \"probabilities\": tensor of probabilities.\n            - \"logits\": tensor of logits.\n    \"\"\"\n    target_length = targets.get(of_name).size()[1]\n    prediction_length = predictions[of_name].get(PREDICTIONS).size()[1]\n\n    if target_length == prediction_length:\n        return targets, predictions\n\n    if not pad_value:\n        pad_value = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id\n\n    zeros_to_add = (\n        target_length - prediction_length if target_length > prediction_length else prediction_length - target_length\n    )\n\n    # We don't want to modify the original targets and predictions tensors, so we create a copy of them.\n    _targets = copy.deepcopy(targets)\n    _predictions = copy.deepcopy(predictions)\n\n    # Align target and prediction tensors for text to text metric computation\n    if target_length > prediction_length:\n        # Pad the predictions.\n        _predictions[of_name][PREDICTIONS] = F.pad(\n            _predictions[of_name][PREDICTIONS], (0, zeros_to_add), value=pad_value\n        ).to(torch.int64)\n\n        _predictions[of_name][PROBABILITIES] = F.pad(_predictions[of_name][PROBABILITIES], (0, 0, 0, zeros_to_add)).to(\n            torch.float32\n        )\n\n        _predictions[of_name][LOGITS] = F.pad(_predictions[of_name][LOGITS], (0, 0, 0, zeros_to_add)).to(torch.float32)\n    else:\n        _targets[of_name] = F.pad(_targets[of_name], (0, zeros_to_add), value=pad_value).to(torch.int64)\n\n    return _targets, _predictions\n\n\ndef update_embedding_layer(model: AutoModelForCausalLM, config_obj: LLMTrainerConfig) -> AutoModelForCausalLM:\n    \"\"\"Updates the embedding layer of the model to use the 8-bit embedding layer from bitsandbytes.nn.modules.\n\n    This is necessary when using 8-bit optimizers from bitsandbytes.\n    See: https://github.com/TimDettmers/bitsandbytes#tldr\n    \"\"\"\n    # If we're using an 8-bit optimizer, we need to replace the embedding layer with a custom embedding layer from\n    # bnb.nn.modules.Embedding.\n    if hasattr(config_obj, \"optimizer\") and config_obj.optimizer.is_8bit:\n        embedding_layer, module_path = find_embedding_layer_with_path(model)\n        if embedding_layer is None:\n            raise ValueError(\n                \"Could not find an embedding layer in the model. This is required when using 8-bit optimizers\"\n                \"  since a custom 8-bit embedding layer is used in place of the original embedding layer.\"\n            )\n\n        # Initialize the BNB embedding layer with the same parameters and weights as the original embedding layer.\n        bnb_embedding = BnbEmbedding(\n            num_embeddings=embedding_layer.num_embeddings,\n            embedding_dim=embedding_layer.embedding_dim,\n            padding_idx=embedding_layer.padding_idx,\n            max_norm=embedding_layer.max_norm,\n            norm_type=embedding_layer.norm_type,\n            scale_grad_by_freq=embedding_layer.scale_grad_by_freq,\n            sparse=embedding_layer.sparse,\n            _weight=embedding_layer.weight,\n            device=model.device,\n        )\n\n        # Update the model's original embedding layer to use the BNB embedding layer using the module_path\n        # returned by find_embedding_layer_with_path.\n        module_path = module_path.split(\".\")\n        module = model\n        for module_name in module_path[:-1]:\n            module = getattr(module, module_name)\n        setattr(module, module_path[-1], bnb_embedding)\n\n        # Set the get input embeddings lambda function to return the BNB embedding layer\n        model.get_input_embeddings = lambda: bnb_embedding\n\n        logger.info(\"Updated the pretrained embedding layer to use the embedding layer from bitsandbytes.\")\n\n    return model\n\n\ndef create_text_streamer(tokenizer: PreTrainedTokenizer) -> TextStreamer:\n    \"\"\"Creates a TextStreamer object for streaming text to stdout during generation.\"\"\"\n    return TextStreamer(tokenizer=tokenizer, skip_prompt=True)\n"
  },
  {
    "path": "ludwig/utils/logging_utils.py",
    "content": "_logged = set()\n\n\ndef log_once(key: str) -> bool:\n    \"\"\"Returns True if this is the \"first\" call for a given key.\n\n    Example:\n        if log_once(\"some_key\"):\n            logger.info(\"Some verbose logging statement\") # noqa\n    \"\"\"\n\n    if key not in _logged:\n        _logged.add(key)\n        return True\n    return False\n"
  },
  {
    "path": "ludwig/utils/loss_utils.py",
    "content": "import torch\n\n\ndef rmspe_loss(targets: torch.Tensor, predictions: torch.Tensor) -> torch.Tensor:\n    \"\"\"Root mean square percentage error.\n\n    Bad predictions can lead to arbitrarily large RMSPE values, especially if some values of targets are very close to\n    zero. We return a large value instead of inf when (some) targets are zero.\n    \"\"\"\n    epsilon = 1e-4\n    # add epsilon if targets are zero to avoid division by zero\n    denominator = targets + epsilon * (targets == 0).float()\n    loss = torch.sqrt(torch.mean(((targets - predictions).float() / denominator) ** 2))\n    return loss\n\n\ndef mean_confidence_penalty(probabilities: torch.Tensor, num_classes: int) -> torch.Tensor:\n    max_entropy = torch.log(torch.tensor(num_classes))\n    # clipping needed for avoiding log(0) = -inf\n    entropy_per_class, _ = torch.max(-probabilities * torch.log(torch.clamp(probabilities, 1e-10, 1)), dim=0)\n    entropy = torch.sum(entropy_per_class, -1)\n    penalty = (max_entropy - entropy) / max_entropy\n    return torch.mean(penalty)\n"
  },
  {
    "path": "ludwig/utils/math_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport math\n\nimport numpy as np\n\n\ndef softmax(x, temperature=1.0):\n    e_x = np.exp((x - np.max(x)) / temperature)\n    return e_x / e_x.sum()\n\n\ndef int_type(number):\n    if number <= np.iinfo(np.int8).max:\n        return np.int8\n    elif number <= np.iinfo(np.int16).max:\n        return np.int16\n    elif number <= np.iinfo(np.int32).max:\n        return np.int32\n    else:  # if number <= np.iinfo(np.int64).max:\n        return np.int64\n\n\ndef convert_size(size_bytes):\n    if size_bytes == 0:\n        return \"0B\"\n    size_name = (\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\")\n    i = int(math.floor(math.log(size_bytes, 1024)))\n    p = math.pow(1024, i)\n    s = round(size_bytes / p, 2)\n    return f\"{s} {size_name[i]}\"\n\n\ndef round2precision(val, precision: int = 0, which: str = \"\"):\n    assert precision >= 0\n    val *= 10**precision\n    round_callback = round\n    if which.lower() == \"up\":\n        round_callback = math.ceil\n    if which.lower() == \"down\":\n        round_callback = math.floor\n    return \"{1:.{0}f}\".format(precision, round_callback(val) / 10**precision)\n\n\ndef cumsum(x: list[int]) -> list[int]:\n    results = []\n    j = 0\n    for i in range(0, len(x)):\n        j += x[i]\n        results.append(j)\n    return results\n"
  },
  {
    "path": "ludwig/utils/metric_utils.py",
    "content": "from collections import defaultdict, namedtuple\n\nimport torch\nfrom torch import Tensor\nfrom torchmetrics.metric import Metric\n\nfrom ludwig.constants import COMBINED, LOSS, NAME, TYPE\nfrom ludwig.modules.metric_registry import get_metric_names_for_type\nfrom ludwig.types import FeatureConfigDict\n\n\ndef sequence_mask(lengths: Tensor, maxlen: int | None = None, dtype=torch.bool) -> Tensor:\n    \"\"\"Implements tf.sequence_mask in torch.\n\n    From https://discuss.pytorch.org/t/pytorch-equivalent-for-tf-sequence-mask/39036/2.\n    \"\"\"\n    if maxlen is None:\n        maxlen = lengths.max()\n    row_vector = torch.arange(0, maxlen, 1).to(lengths.device)\n    matrix = torch.unsqueeze(lengths, dim=-1)\n    mask = row_vector < matrix\n\n    return mask.type(dtype)\n\n\ndef dynamic_partition(data: Tensor, partitions: Tensor, num_partitions: int) -> list[Tensor]:\n    \"\"\"Implements tf.dynamic_partition in torch.\n\n    From https://discuss.pytorch.org/t/equivalent-of-tf-dynamic-partition/53735.\n    \"\"\"\n    assert data.size() == partitions.size()\n\n    # Flatten data into 1D vectors to do partitioning correctly.\n    data = data.view(-1)\n    partitions = partitions.view(-1)\n    result = []\n    for i in range(num_partitions):\n        result += [data[(partitions == i).nonzero().squeeze(1)]]\n    return result\n\n\ndef masked_correct_predictions(targets: Tensor, preds: Tensor, targets_sequence_lengths: Tensor) -> Tensor:\n    \"\"\"Masks out special symbols, and returns tensor of correct predictions.\n\n    Args:\n        targets: 2D tensor [batch_size, sequence_length]\n        preds: 2D tensor [batch_size, sequence_length]\n\n    Returns:\n        1D tensor of all correct predictions.\n    \"\"\"\n    correct_preds = preds == targets\n\n    mask = sequence_mask(lengths=targets_sequence_lengths, maxlen=correct_preds.shape[1], dtype=torch.int32)\n    _, masked_correct_preds = dynamic_partition(data=correct_preds, partitions=mask, num_partitions=2)\n\n    return masked_correct_preds.type(torch.float32)\n\n\ndef get_scalar_from_ludwig_metric(metric: Metric) -> float:\n    \"\"\"Returns the scalar value of a Ludwig metric.\n\n    Params:\n        metric: Metric object\n\n    Returns:\n        float: scalar value of the metric\n    \"\"\"\n    return metric.compute().detach().cpu().numpy().item()\n\n\n# Data for training and evaluation metrics.\nTrainerMetric = namedtuple(\"TrainerMetric\", (\"epoch\", \"step\", \"value\"))\n\n\ndef reduce_trainer_metrics_dict(\n    dict_dict_trainer_metrics: dict[str, dict[str, list[TrainerMetric]]],\n) -> dict[str, dict[str, list[float]]]:\n    \"\"\"Reduces Dict[feature_name, Dict[metric_name, List[TrainerMetric]]] to Dict[feature_name, Dict[metric_name,\n    List[float]]].\n\n    Used for flattening the results returned by trainer.py::train(), which come from ProgressTracker.\n    \"\"\"\n    flattened_dict = defaultdict(lambda: defaultdict(list))\n    for feature_name, trainer_metric_dict in dict_dict_trainer_metrics.items():\n        for metric_name, trainer_metrics in trainer_metric_dict.items():\n            for trainer_metric in trainer_metrics:\n                flattened_dict[feature_name][metric_name].append(trainer_metric[-1])\n    # Convert defaultdict to dict so JSON serialization works with dataclasses.asdict().\n    return {k: dict(v) for k, v in flattened_dict.items()}\n\n\ndef get_metric_names(output_features: dict[str, \"OutputFeature\"]) -> dict[str, list[str]]:  # noqa\n    \"\"\"Returns a dict of output_feature_name -> list of metric names.\"\"\"\n    metrics_names = {}\n    for output_feature_name, output_feature in output_features.items():\n        metrics_names[output_feature_name] = sorted(list(get_metric_names_for_type(output_feature.type())))\n    # Add combined loss.\n    metrics_names[COMBINED] = [LOSS]\n    return metrics_names\n\n\ndef get_feature_to_metric_names_map(output_features: list[FeatureConfigDict]) -> dict[str, list[str]]:\n    \"\"\"Returns a dict of output_feature_name -> list of metric names.\"\"\"\n    metrics_names = {}\n    for output_feature in output_features:\n        output_feature_name = output_feature[NAME]\n        output_feature_type = output_feature[TYPE]\n        metrics_names[output_feature_name] = get_metric_names_for_type(output_feature_type)\n    metrics_names[COMBINED] = [LOSS]\n    return metrics_names\n\n\ndef get_feature_to_metric_names_map_from_feature_collection(\n    output_features: \"FeatureCollection\",  # noqa\n) -> dict[str, list[str]]:\n    \"\"\"Returns a dict of output_feature_name -> list of metric names.\"\"\"\n    metrics_names = {\n        output_feature.name: get_metric_names_for_type(output_feature.type) for output_feature in output_features\n    }\n    metrics_names[COMBINED] = [LOSS]\n    return metrics_names\n"
  },
  {
    "path": "ludwig/utils/metrics_printed_table.py",
    "content": "import logging\n\nfrom tabulate import tabulate\n\nfrom ludwig.constants import COMBINED, LOSS\nfrom ludwig.utils.metric_utils import TrainerMetric\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_metric_value_or_empty(metrics_log: dict[str, list[TrainerMetric]], metric_name: str):\n    \"\"\"Returns the metric value if it exists or empty.\"\"\"\n    if metric_name not in metrics_log:\n        return \"\"\n    return metrics_log[metric_name][-1][-1]\n\n\ndef print_table_for_single_output_feature(\n    train_metrics_log: dict[str, list[TrainerMetric]],\n    validation_metrics_log: dict[str, list[TrainerMetric]],\n    test_metrics_log: dict[str, list[TrainerMetric]],\n    combined_loss_for_each_split: list[float],\n) -> None:\n    \"\"\"Prints the metrics table for a single output feature.\n\n    Args:\n        train_metrics_log: Dict from metric name to list of TrainerMetric.\n        validation_metrics_log: Dict from metric name to list of TrainerMetric.\n        test_metrics_log: Dict from metric name to list of TrainerMetric.\n    \"\"\"\n    # Get the superset of metric names across all splits.\n    all_metric_names = set()\n    all_metric_names.update(train_metrics_log.keys())\n    all_metric_names.update(validation_metrics_log.keys())\n    all_metric_names.update(test_metrics_log.keys())\n    all_metric_names = sorted(list(all_metric_names))\n\n    # Assemble the printed table.\n    # Each item in the printed_table corresponds to a row in the printed table.\n    printed_table = [[\"train\", \"validation\", \"test\"]]\n    for metric_name in all_metric_names:\n        metrics_for_each_split = [\n            get_metric_value_or_empty(train_metrics_log, metric_name),\n            get_metric_value_or_empty(validation_metrics_log, metric_name),\n            get_metric_value_or_empty(test_metrics_log, metric_name),\n        ]\n        printed_table.append([metric_name] + metrics_for_each_split)\n\n    # Add combined loss.\n    printed_table.append([\"combined_loss\"] + combined_loss_for_each_split)\n\n    logger.info(tabulate(printed_table, headers=\"firstrow\", tablefmt=\"fancy_grid\", floatfmt=\".4f\"))\n\n\ndef print_metrics_table(\n    output_features: dict[str, \"OutputFeature\"],  # noqa\n    train_metrics_log: dict[str, dict[str, list[TrainerMetric]]],\n    validation_metrics_log: dict[str, dict[str, list[TrainerMetric]]],\n    test_metrics_log: dict[str, dict[str, list[TrainerMetric]]],\n):\n    \"\"\"Prints a table of metrics table for each output feature, for each split.\n\n    Example:\n    ╒═══════════════╤═════════╤══════════════╤════════╕\n    │               │   train │   validation │   test │\n    ╞═══════════════╪═════════╪══════════════╪════════╡\n    │ accuracy      │  0.8157 │       0.6966 │ 0.8090 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ loss          │  0.4619 │       0.5039 │ 0.4488 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ precision     │  0.8274 │       0.6250 │ 0.7818 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ recall        │  0.6680 │       0.4545 │ 0.6615 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ roc_auc       │  0.8471 │       0.7706 │ 0.8592 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ specificity   │  0.9105 │       0.8393 │ 0.8938 │\n    ├───────────────┼─────────┼──────────────┼────────┤\n    │ combined_loss │  0.4619 │       0.5039 │ 0.4488 │\n    ╘═══════════════╧═════════╧══════════════╧════════╛\n    \"\"\"\n    # Obtain the combined loss, which is the same across all output features.\n    combined_loss_for_each_split = [\n        get_metric_value_or_empty(train_metrics_log[COMBINED], LOSS),\n        get_metric_value_or_empty(validation_metrics_log[COMBINED], LOSS),\n        get_metric_value_or_empty(test_metrics_log[COMBINED], LOSS),\n    ]\n\n    for output_feature_name in sorted(output_features.keys()):\n        if output_feature_name == COMBINED:\n            # Skip the combined output feature. The combined loss will be added to each output feature's table.\n            continue\n        print_table_for_single_output_feature(\n            train_metrics_log[output_feature_name],\n            validation_metrics_log[output_feature_name],\n            test_metrics_log[output_feature_name],\n            combined_loss_for_each_split,\n        )\n"
  },
  {
    "path": "ludwig/utils/misc_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport functools\nimport os\nimport random\nimport subprocess\nimport weakref\nfrom collections import OrderedDict\nfrom collections.abc import Mapping\nfrom typing import Any, TYPE_CHECKING\n\nimport numpy\nimport torch\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import PROC_COLUMN\nfrom ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME\nfrom ludwig.utils import fs_utils\nfrom ludwig.utils.fs_utils import find_non_existing_dir_by_adding_suffix\n\nif TYPE_CHECKING:\n    from ludwig.schema.model_types.base import ModelConfig\n\n\n@DeveloperAPI\ndef set_random_seed(random_seed):\n    os.environ[\"PYTHONHASHSEED\"] = str(random_seed)\n    random.seed(random_seed)\n    numpy.random.seed(random_seed)\n    torch.manual_seed(random_seed)\n    if torch.cuda.is_available() and torch.cuda.device_count() > 0:\n        torch.cuda.manual_seed(random_seed)\n\n\n@DeveloperAPI\ndef merge_dict(dct, merge_dct):\n    \"\"\"Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys,\n    dict_merge recurses down into dicts nested to an arbitrary depth, updating keys. The ``merge_dct`` is merged\n    into ``dct``.\n\n    :param dct: dict onto which the merge is executed\n    :param merge_dct: dct merged into dct\n    :return: None\n    \"\"\"\n    dct = copy.deepcopy(dct)\n    for k, v in merge_dct.items():\n        if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], Mapping):\n            dct[k] = merge_dict(dct[k], merge_dct[k])\n        else:\n            dct[k] = merge_dct[k]\n    return dct\n\n\n@DeveloperAPI\ndef sum_dicts(dicts, dict_type=dict):\n    summed_dict = dict_type()\n    for d in dicts:\n        for key, value in d.items():\n            if key in summed_dict:\n                prev_value = summed_dict[key]\n                if isinstance(value, (dict, OrderedDict)):\n                    summed_dict[key] = sum_dicts([prev_value, value], dict_type=type(value))\n                elif isinstance(value, numpy.ndarray):\n                    summed_dict[key] = numpy.concatenate((prev_value, value))\n                else:\n                    summed_dict[key] = prev_value + value\n            else:\n                summed_dict[key] = value\n    return summed_dict\n\n\n@DeveloperAPI\ndef get_from_registry(key, registry):\n    if hasattr(key, \"lower\"):\n        key = key.lower()\n    if key in registry:\n        return registry[key]\n    else:\n        raise ValueError(f\"Key '{key}' not in registry, available options: {registry.keys()}\")\n\n\n@DeveloperAPI\ndef set_default_value(dictionary, key, value):\n    if key not in dictionary:\n        dictionary[key] = value\n\n\n@DeveloperAPI\ndef set_default_values(dictionary: dict, default_value_dictionary: dict):\n    \"\"\"This function sets multiple default values recursively for various areas of the config. By using the helper\n    function set_default_value, It parses input values that contain nested dictionaries, only setting values for\n    parameters that have not already been defined by the user.\n\n    Args:\n        dictionary (dict): The dictionary to set default values for, generally a section of the config.\n        default_value_dictionary (dict): The dictionary containing the default values for the config.\n    \"\"\"\n    for key, value in default_value_dictionary.items():\n        if key not in dictionary:  # Event where the key is not in the dictionary yet\n            dictionary[key] = value\n        elif value == {}:  # Event where dict is empty\n            set_default_value(dictionary, key, value)\n        elif isinstance(value, dict) and value:  # Event where dictionary is nested - recursive call\n            set_default_values(dictionary[key], value)\n        else:\n            set_default_value(dictionary, key, value)\n\n\n@DeveloperAPI\ndef get_class_attributes(c):\n    return {i for i in dir(c) if not callable(getattr(c, i)) and not i.startswith(\"_\")}\n\n\n@DeveloperAPI\ndef get_output_directory(output_directory, experiment_name, model_name=\"run\"):\n    base_dir_name = os.path.join(output_directory, experiment_name + (\"_\" if model_name else \"\") + (model_name or \"\"))\n    return fs_utils.abspath(find_non_existing_dir_by_adding_suffix(base_dir_name))\n\n\n@DeveloperAPI\ndef get_file_names(output_directory):\n    description_fn = os.path.join(output_directory, DESCRIPTION_FILE_NAME)\n    training_stats_fn = os.path.join(output_directory, \"training_statistics.json\")\n\n    model_dir = os.path.join(output_directory, MODEL_FILE_NAME)\n\n    return description_fn, training_stats_fn, model_dir\n\n\n@DeveloperAPI\ndef get_combined_features(config):\n    return config[\"input_features\"] + config[\"output_features\"]\n\n\n@DeveloperAPI\ndef get_proc_features(config):\n    return get_proc_features_from_lists(config[\"input_features\"], config[\"output_features\"])\n\n\n@DeveloperAPI\ndef get_proc_features_from_lists(*args):\n    return {feature[PROC_COLUMN]: feature for features in args for feature in features}\n\n\n@DeveloperAPI\ndef set_saved_weights_in_checkpoint_flag(config_obj: \"ModelConfig\"):\n    \"\"\"Adds a flag to all input feature encoder configs indicating that the weights are saved in the checkpoint.\n\n    Next time the model is loaded we will restore pre-trained encoder weights from ludwig model (and not load from cache\n    or model hub).\n    \"\"\"\n    for input_feature in config_obj.input_features:\n        encoder_obj = input_feature.encoder\n        encoder_obj.saved_weights_in_checkpoint = True\n\n\n@DeveloperAPI\ndef remove_empty_lines(str):\n    return \"\\n\".join([line.rstrip() for line in str.split(\"\\n\") if line.rstrip()])\n\n\n@DeveloperAPI\ndef memoized_method(*lru_args, **lru_kwargs):\n    def decorator(func):\n        @functools.wraps(func)\n        def wrapped_func(self, *args, **kwargs):\n            # We're storing the wrapped method inside the instance. If we had\n            # a strong reference to self the instance would never die.\n            self_weak = weakref.ref(self)\n\n            @functools.wraps(func)\n            @functools.lru_cache(*lru_args, **lru_kwargs)\n            def cached_method(*args, **kwargs):\n                return func(self_weak(), *args, **kwargs)\n\n            setattr(self, func.__name__, cached_method)\n            return cached_method(*args, **kwargs)\n\n        return wrapped_func\n\n    return decorator\n\n\n@DeveloperAPI\ndef get_commit_hash():\n    \"\"\"If Ludwig is run from a git repository, get the commit hash of the current HEAD.\n\n    Returns None if git is not executable in the current environment or Ludwig is not run in a git repo.\n    \"\"\"\n    try:\n        with open(os.devnull, \"w\") as devnull:\n            is_a_git_repo = subprocess.call([\"git\", \"branch\"], stderr=subprocess.STDOUT, stdout=devnull) == 0\n        if is_a_git_repo:\n            commit_hash = subprocess.check_output([\"git\", \"rev-parse\", \"HEAD\"]).decode(\"utf-8\")\n            return commit_hash\n    except:  # noqa: E722\n        pass\n    return None\n\n\n@DeveloperAPI\ndef scrub_creds(config_dict: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Returns a copy of a config dict with all sensitive fields scrubbed.\"\"\"\n    if config_dict.get(\"backend\", {}) and \"credentials\" in config_dict.get(\"backend\", {}):\n        config_dict[\"backend\"][\"credentials\"] = {}\n    return config_dict\n"
  },
  {
    "path": "ludwig/utils/model_utils.py",
    "content": "import logging\nfrom collections import OrderedDict\n\nimport numpy as np\nimport torch\n\nlogger = logging.getLogger(__name__)\n\nNUMPY_TO_TORCH_DTYPE = {\n    bool: torch.bool,\n    np.bool_: torch.bool,\n    np.uint8: torch.uint8,\n    np.int8: torch.int8,\n    np.int16: torch.int16,\n    np.int32: torch.int32,\n    np.int64: torch.int64,\n    np.float16: torch.float16,\n    np.float32: torch.float32,\n    np.float64: torch.float64,\n    np.complex64: torch.complex64,\n    np.complex128: torch.complex128,\n}\n\n\ndef extract_tensors(model: torch.nn.Module) -> tuple[torch.nn.Module, list[dict]]:\n    \"\"\"Remove the tensors from a PyTorch model, convert them to NumPy arrays, and return the stripped model and\n    tensors.\n\n    Reference implementation: https://medium.com/ibm-data-ai/how-to-load-pytorch-models-340-times-faster-with-\n    ray-8be751a6944c  # noqa\n    \"\"\"\n\n    tensors = []\n    for _, module in model.named_modules():\n        # Store the tensors as numpy arrays in Python dictionaries\n        # Delete the same tensors since we no longer need them and we want to reduce memory pressure.\n        # This ensures that throughout this process, we keep memory nearly linear w.r.t model parameters.\n        params = OrderedDict()\n        buffers = OrderedDict()\n        for name, param in module.named_parameters(recurse=False):\n            params[name] = torch.clone(param).detach().numpy()\n            del param\n        for name, buf in module.named_buffers(recurse=False):\n            buffers[name] = torch.clone(buf).detach().numpy()\n            del buf\n        tensors.append({\"params\": params, \"buffers\": buffers})\n\n    # Strip all tensors and buffers out of the original model.\n    for _, module in model.named_modules():\n        for name in [name for name, _ in module.named_parameters(recurse=False)] + [\n            name for name, _ in module.named_buffers(recurse=False)\n        ]:\n            setattr(module, name, None)\n\n    return model, tensors\n\n\ndef replace_tensors(m: torch.nn.Module, tensors: list[dict], device: torch.device):\n    \"\"\"Restore the tensors that extract_tensors() stripped out of a PyTorch model. This operation is performed in\n    place.\n\n    Reference implementation: https://medium.com/ibm-data-ai/how-to-load-pytorch-models-340-times-faster-with-\n    ray-8be751a6944c  # noqa\n    \"\"\"\n    modules = [module for _, module in m.named_modules()]\n    for module, tensor_dict in zip(modules, tensors):\n        # There are separate APIs to set parameters and buffers.\n        for name, array in tensor_dict[\"params\"].items():\n            module.register_parameter(\n                name,\n                torch.nn.Parameter(torch.as_tensor(array, device=device, dtype=NUMPY_TO_TORCH_DTYPE.get(array.dtype))),\n            )\n\n        for name, array in tensor_dict[\"buffers\"].items():\n            module.register_buffer(\n                name,\n                torch.as_tensor(array, device=device, dtype=NUMPY_TO_TORCH_DTYPE.get(array.dtype)),\n            )\n\n\ndef find_embedding_layer_with_path(module, module_names=[]):\n    \"\"\"Recursively search through a module to find an embedding layer and its module path.\n\n    Returns a tuple containing the embedding layer and its module path.\n    \"\"\"\n    for name, child_module in module.named_children():\n        if isinstance(child_module, torch.nn.Embedding):\n            # If an embedding layer is found, return it along with the module path\n            return child_module, \".\".join(module_names + [name])\n        else:\n            # Recursively search in the child module and update the module_names list\n            found, path = find_embedding_layer_with_path(child_module, module_names + [name])\n            if found is not None:\n                return found, path\n    return None, None\n\n\ndef contains_nan_or_inf_tensors(module: torch.nn.Module) -> bool:\n    \"\"\"Check for NaN or infinity (inf) values in the tensors (parameters and buffers) of a PyTorch module. This\n    function recursively inspects the module's parameters and buffers to identify NaN or inf values. It is designed\n    to ensure the numerical stability of the model by detecting any irregularities in the tensor values.\n\n    Parameters:\n        module (torch.nn.Module): The PyTorch module to check for NaN or inf values.\n\n    Returns:\n        bool: Returns True if any NaN or inf values are found in the module's tensors. Otherwise, returns False.\n    \"\"\"\n    for name, param in module.named_parameters():\n        if param.requires_grad and (torch.isnan(param).any() or torch.isinf(param).any()):\n            logger.info(f\"Found NaN or inf values in parameter '{name}' of module '{module.__class__.__name__}'\")\n            return True\n\n    for name, buffer in module.named_buffers():\n        if torch.isnan(buffer).any() or torch.isinf(buffer).any():\n            logger.info(f\"Found NaN or inf values in buffer '{name}' of module '{module.__class__.__name__}'\")\n            return True\n\n    for name, submodule in module.named_children():\n        if contains_nan_or_inf_tensors(submodule):\n            logger.info(f\"Found NaN or inf values in submodule '{name}' of module '{module.__class__.__name__}'\")\n            return True\n\n    return False\n"
  },
  {
    "path": "ludwig/utils/nlp_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport sys\n\nlogger = logging.getLogger(__name__)\n\nnlp_pipelines = {\n    \"en\": None,\n    \"it\": None,\n    \"es\": None,\n    \"de\": None,\n    \"fr\": None,\n    \"pt\": None,\n    \"nl\": None,\n    \"el\": None,\n    \"nb\": None,\n    \"lt\": None,\n    \"da\": None,\n    \"pl\": None,\n    \"ro\": None,\n    \"ja\": None,\n    \"zh\": None,\n    \"xx\": None,\n}\nlanguage_module_registry = {\n    \"en\": \"en_core_web_sm\",\n    \"it\": \"it_core_news_sm\",\n    \"es\": \"es_core_news_sm\",\n    \"de\": \"de_core_news_sm\",\n    \"fr\": \"fr_core_news_sm\",\n    \"pt\": \"pt_core_news_sm\",\n    \"nl\": \"nl_core_news_sm\",\n    \"el\": \"el_core_news_sm\",\n    \"nb\": \"nb_core_news_sm\",\n    \"lt\": \"lt_core_news_sm\",\n    \"da\": \"da_core_news_sm\",\n    \"pl\": \"pl_core_news_sm\",\n    \"ro\": \"ro_core_news_sm\",\n    \"ja\": \"ja_core_news_sm\",\n    \"zh\": \"zh_core_web_sm\",\n    \"xx\": \"xx_ent_wiki_sm\",\n}\ndefault_characters = [\n    \" \",\n    \"a\",\n    \"b\",\n    \"c\",\n    \"d\",\n    \"e\",\n    \"f\",\n    \"g\",\n    \"h\",\n    \"i\",\n    \"j\",\n    \"k\",\n    \"l\",\n    \"m\",\n    \"n\",\n    \"o\",\n    \"p\",\n    \"q\",\n    \"r\",\n    \"s\",\n    \"t\",\n    \"u\",\n    \"v\",\n    \"w\",\n    \"x\",\n    \"y\",\n    \"z\",\n    \"0\",\n    \"1\",\n    \"2\",\n    \"3\",\n    \"4\",\n    \"5\",\n    \"6\",\n    \"8\",\n    \"9\",\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]\npunctuation = {\".\", \",\", \"@\", \"$\", \"%\", \"/\", \":\", \";\", \"+\", \"=\"}\n\n\ndef load_nlp_pipeline(language=\"xx\"):\n    if language not in language_module_registry:\n        logger.error(\n            \"Language {} is not supported.\"\n            \"Suported languages are: {}\".format(language, language_module_registry.keys())\n        )\n        raise ValueError\n    else:\n        spacy_module_name = language_module_registry[language]\n    if nlp_pipelines[language] is None:\n        logger.info(\"Loading NLP pipeline\")\n        try:\n            import spacy\n        except ImportError:\n            logger.error(\n                \" spacy is not installed. \"\n                \"In order to install all text feature dependencies run \"\n                \"pip install ludwig[text]\"\n            )\n            sys.exit(-1)\n\n        try:\n            nlp_pipelines[language] = spacy.load(spacy_module_name, disable=[\"parser\", \"tagger\", \"ner\"])\n        except OSError:\n            logger.info(\" spaCy {} model is missing, downloading it \" \"(this will only happen once)\")\n            from spacy.cli import download\n\n            download(spacy_module_name)\n            nlp_pipelines[language] = spacy.load(spacy_module_name, disable=[\"parser\", \"tagger\", \"ner\"])\n\n    return nlp_pipelines[language]\n\n\ndef pass_filters(\n    token, filter_numbers=False, filter_punctuation=False, filter_short_tokens=False, filter_stopwords=False\n):\n    passes_filters = True\n    if filter_numbers:\n        passes_filters = not token.like_num\n    if passes_filters and filter_punctuation:\n        passes_filters = not bool(set(token.orth_) & punctuation)\n    if passes_filters and filter_short_tokens:\n        passes_filters = len(token) > 2\n    if passes_filters and filter_stopwords:\n        passes_filters = not token.is_stop\n    return passes_filters\n\n\ndef process_text(\n    text,\n    nlp_pipeline,\n    return_lemma=False,\n    filter_numbers=False,\n    filter_punctuation=False,\n    filter_short_tokens=False,\n    filter_stopwords=False,\n):\n    doc = nlp_pipeline(text)\n    return [\n        token.lemma_ if return_lemma else token.text\n        for token in doc\n        if pass_filters(token, filter_numbers, filter_punctuation, filter_short_tokens, filter_stopwords)\n    ]\n\n\nif __name__ == \"__main__\":\n    text = (\n        \"Hello John, how are you doing my good old friend? Are you still number 732 in the list? Did you pay $32.43 or \"\n        \"54.21 for the book?\"\n    )\n    print(process_text(text, load_nlp_pipeline()))\n    print(\n        process_text(text, load_nlp_pipeline(), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True)\n    )\n    print(process_text(text, load_nlp_pipeline(), filter_stopwords=True))\n    print(process_text(text, load_nlp_pipeline(), return_lemma=True))\n    print(\n        process_text(\n            text,\n            load_nlp_pipeline(),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n    )\n    print(process_text(text, load_nlp_pipeline(), return_lemma=True, filter_stopwords=True))\n"
  },
  {
    "path": "ludwig/utils/numerical_test_utils.py",
    "content": "# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom typing import Any\n\nimport numpy as np\n\n\ndef _dict_like(x):\n    \"\"\"Returns true if an object is a dict or convertible to one, false if not.\"\"\"\n    try:\n        _ = dict(x)\n    except (TypeError, ValueError):\n        return False\n    return True\n\n\ndef _enumerable(x):\n    \"\"\"Returns true if an object is enumerable, false if not.\"\"\"\n    try:\n        _ = enumerate(x)\n    except (TypeError, ValueError):\n        return False\n    return True\n\n\ndef assert_all_finite(x: Any, keypath=\"\"):\n    \"\"\"Ensures that all scalars at all levels of the dictionary, list, array, or scalar are finite.\n\n    keypath is only used for logging error messages, to indicate where the non-finite value was detected.\n    \"\"\"\n    path_description = f\" at {keypath} \" if keypath else \" \"\n    if np.isscalar(x):\n        assert np.isfinite(x), f\"Value{path_description}should be finite, but is {str(x)}.\"\n    elif isinstance(x, np.ndarray):\n        non_finite_indices = np.nonzero(~np.isfinite(x))\n        non_finite_values = x[non_finite_indices]\n        assert np.all(np.isfinite(x)), (\n            f\"All values{path_description}should be finite, but found {str(non_finite_values)} \"\n            \"at positions {str(np.array(non_finite_indices).flatten())}.\"\n        )\n    elif _dict_like(x):\n        # x is either a dict or convertible to one\n        for k, v in dict(x).items():\n            assert_all_finite(v, keypath=keypath + \".\" + str(k) if keypath else str(k))\n    elif _enumerable(x):\n        # x is a list, set or other enumerable type, but not a string, dict, or numpy array.\n        for i, v in enumerate(x):\n            assert_all_finite(v, keypath=keypath + f\"[{i}]\")\n    else:\n        assert False, f\"Unhandled type {str(type(x))} for value{path_description}\"\n"
  },
  {
    "path": "ludwig/utils/output_feature_utils.py",
    "content": "\"\"\"Utilities used for managing output feature dicts.\"\"\"\n\nimport numpy as np\nimport torch\n\nfrom ludwig.utils.torch_utils import sequence_length_3D, sequence_mask\n\n\ndef get_feature_concat_name(feature_name: str, tensor_name: str) -> str:\n    return feature_name + \"::\" + tensor_name\n\n\ndef get_tensor_name_from_concat_name(concat_name: str) -> str:\n    return concat_name.split(\"::\")[-1]\n\n\ndef get_feature_name_from_concat_name(concat_name: str) -> str:\n    return \"::\".join(concat_name.split(\"::\")[:-1])\n\n\ndef get_single_output_feature_tensors(\n    output_feature_dict: dict[str, torch.Tensor], feature_name: str\n) -> dict[str, torch.Tensor]:\n    \"\"\"Returns a map of tensors related to the given feature_name.\"\"\"\n    single_output_feature_tensors = {}\n    for concat_name, tensor in output_feature_dict.items():\n        if get_feature_name_from_concat_name(concat_name) == feature_name:\n            single_output_feature_tensors[get_tensor_name_from_concat_name(concat_name)] = tensor\n    return single_output_feature_tensors\n\n\ndef get_output_feature_tensor(\n    output_dict: dict[str, torch.Tensor], feature_name: str, tensor_name: str\n) -> torch.Tensor:\n    \"\"\"Returns a tensor related for the given feature_name and tensor_name.\"\"\"\n    concat_name = get_feature_concat_name(feature_name, tensor_name)\n    if concat_name not in output_dict:\n        raise ValueError(\n            f\"Could not find {tensor_name} for {feature_name} in the output_dict with keys: {output_dict.keys()}\"\n        )\n    return output_dict[get_feature_concat_name(feature_name, tensor_name)]\n\n\ndef set_output_feature_tensor(\n    output_dict: dict[str, torch.Tensor], feature_name: str, tensor_name: str, tensor: torch.Tensor\n):\n    \"\"\"Adds tensor for the given feature_name and tensor_name to the tensor dict.\"\"\"\n    output_dict[get_feature_concat_name(feature_name, tensor_name)] = tensor\n\n\ndef concat_dependencies(\n    feature_name: str,\n    dependencies: list[str],\n    dependency_reducers: torch.ModuleDict,\n    combiner_hidden_state: torch.Tensor,\n    other_output_feature_states: dict[str, torch.Tensor],\n) -> torch.Tensor:\n    \"\"\"Concatenates combiner_hidden_state with other output feature hidden states based on listed dependencies.\"\"\"\n    # No dependencies.\n    if not dependencies:\n        return combiner_hidden_state\n\n    dependency_hidden_states = []\n    for feature_name in dependencies:\n        # The dependent feature should be present since ECD does a topological sort over output features.\n        feature_hidden_state = other_output_feature_states[feature_name]\n\n        # This feature is sequential.\n        if len(combiner_hidden_state.shape) > 2:\n            if len(feature_hidden_state.shape) > 2:\n                # The dependent feature is also sequential.\n                # matrix matrix -> concat\n                assert combiner_hidden_state.shape[1] == feature_hidden_state.shape[1]\n                dependency_hidden_states.append(feature_hidden_state)\n            else:\n                # The dependent feature is not sequential.\n                # matrix vector -> tile concat\n                sequence_max_length = combiner_hidden_state.shape[1]\n                multipliers = (1, sequence_max_length, 1)\n                tiled_representation = torch.tile(torch.unsqueeze(feature_hidden_state, 1), multipliers)\n\n                sequence_length = sequence_length_3D(combiner_hidden_state)\n                mask = sequence_mask(sequence_length, sequence_max_length)\n                tiled_representation = torch.mul(\n                    tiled_representation,\n                    mask[:, :, np.newaxis].type(torch.float32),\n                )\n\n                dependency_hidden_states.append(tiled_representation)\n\n        else:\n            # This feature is not sequential.\n            if len(feature_hidden_state.shape) > 2:\n                # The dependent feature is sequential.\n                # vector matrix -> reduce concat\n                reducer = dependency_reducers[feature_name]\n                dependency_hidden_states.append(reducer(feature_hidden_state))\n            else:\n                # The dependent feature is not sequential.\n                # vector vector -> concat\n                dependency_hidden_states.append(feature_hidden_state)\n\n    try:\n        hidden = torch.cat([combiner_hidden_state] + dependency_hidden_states, dim=-1)\n    except Exception as e:\n        raise ValueError(\n            f\"Shape mismatch {e} while concatenating dependent features of {feature_name}: \"\n            f\"{dependencies}. Concatenating the feature activations tensor {combiner_hidden_state} \"\n            f\"with activation tensors of dependencies: {dependency_hidden_states}. The error is \"\n            \"likely due to a mismatch of the second dimension (sequence length) or a \"\n            \"difference in ranks. Likely solutions are setting the maximum_sequence_length \"\n            \"of all sequential features to be the same,  or reduce the output of some \"\n            \"features, or disabling the bucketing setting bucketing_field to None / null, \"\n            \"as activating it will reduce the length of the field the bucketing is \"\n            \"performed on.\"\n        )\n    return hidden\n"
  },
  {
    "path": "ludwig/utils/package_utils.py",
    "content": "import importlib\nimport types\n\n\nclass LazyLoader(types.ModuleType):\n    \"\"\"Lazily import a module, mainly to avoid pulling in large dependencies.\n\n    `contrib`, and `ffmpeg` are examples of modules that are large and not always\n    needed, and this allows them to only be loaded when they are used.\n\n    Copied from: https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/util/lazy_loader.py\n    \"\"\"\n\n    # The lint error here is incorrect.\n    def __init__(self, local_name, parent_module_globals, name):  # pylint: disable=super-on-old-class\n        self._local_name = local_name\n        self._parent_module_globals = parent_module_globals\n\n        super().__init__(name)\n\n    def _load(self):\n        # Import the target module and insert it into the parent's namespace\n        module = importlib.import_module(self.__name__)\n        self._parent_module_globals[self._local_name] = module\n\n        # Update this object's dict so that if someone keeps a reference to the\n        #   LazyLoader, lookups are efficient (__getattr__ is only called on lookups\n        #   that fail).\n        self.__dict__.update(module.__dict__)\n\n        return module\n\n    def __getattr__(self, item):\n        module = self._load()\n        return getattr(module, item)\n\n    def __dir__(self):\n        module = self._load()\n        return dir(module)\n"
  },
  {
    "path": "ludwig/utils/print_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nfrom collections import OrderedDict\nfrom pprint import pformat\n\nfrom ludwig.api_annotations import DeveloperAPI\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\ndef get_logging_level_registry() -> dict[str, int]:\n    return {\n        \"critical\": logging.CRITICAL,\n        \"error\": logging.ERROR,\n        \"warning\": logging.WARNING,\n        \"info\": logging.INFO,\n        \"debug\": logging.DEBUG,\n        \"notset\": logging.NOTSET,\n    }\n\n\n@DeveloperAPI\ndef get_logo(message, ludwig_version):\n    return \"\\n\".join(\n        [\n            \"███████████████████████\",\n            \"█ █ █ █  ▜█ █ █ █ █   █\",\n            \"█ █ █ █ █ █ █ █ █ █ ███\",\n            \"█ █   █ █ █ █ █ █ █ ▌ █\",\n            \"█ █████ █ █ █ █ █ █ █ █\",\n            \"█     █  ▟█     █ █   █\",\n            \"███████████████████████\",\n            f\"ludwig v{ludwig_version} - {message}\",\n            \"\",\n        ]\n    )\n\n\n@DeveloperAPI\ndef print_ludwig(message, ludwig_version):\n    logger.info(get_logo(message, ludwig_version))\n\n\n@DeveloperAPI\ndef print_boxed(text, print_fun=logger.info):\n    box_width = len(text) + 2\n    print_fun(\"\")\n    print_fun(\"╒{}╕\".format(\"═\" * box_width))\n    print_fun(f\"│ {text.upper()} │\")\n    print_fun(\"╘{}╛\".format(\"═\" * box_width))\n    print_fun(\"\")\n\n\n@DeveloperAPI\ndef repr_ordered_dict(d: OrderedDict):\n    return \"{\" + \",\\n  \".join(f\"{x}: {pformat(y, indent=4)}\" for x, y in d.items()) + \"}\"\n\n\n@DeveloperAPI\ndef query_yes_no(question: str, default: str | None = \"yes\"):\n    \"\"\"Ask a yes/no question via raw_input() and return their answer.\n\n    Args:\n        question: String presented to the user\n        default: The presumed answer from the user. Must be \"yes\", \"no\", or None (Answer is required)\n\n    Returns: Boolean based on prompt response\n    \"\"\"\n    valid = {\"yes\": True, \"y\": True, \"ye\": True, \"no\": False, \"n\": False}\n    if default is None:\n        prompt = \" [y/n] \"\n    elif default == \"yes\":\n        prompt = \" [Y/n] \"\n    elif default == \"no\":\n        prompt = \" [y/N] \"\n    else:\n        raise ValueError(\"invalid default answer: '%s'\" % default)\n\n    while True:\n        logger.info(question + prompt)\n        choice = input().lower()\n        if default is not None and choice == \"\":\n            return valid[default]\n        elif choice in valid:\n            return valid[choice]\n        else:\n            logger.info(\"Please respond with 'yes' or 'no' \" \"(or 'y' or 'n').\\n\")\n"
  },
  {
    "path": "ludwig/utils/registry.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom collections import UserDict\nfrom typing import Generic, TypeVar\n\nDEFAULT_KEYS = [\"None\", \"none\", \"null\", None]\n\n\nT = TypeVar(\"T\")\n\n\nclass Registry(UserDict, Generic[T]):\n    \"\"\"Registry is like a normal dict, but with an optional parent dict.\n\n    Items are considered to exist in the registry if they are added to either the registry itself, or its parent.\n    \"\"\"\n\n    def __init__(self, source=None):\n        init_data = None\n        parent = {}\n        if isinstance(source, Registry):\n            parent = source\n        else:\n            init_data = source\n\n        self.parent = parent\n        super().__init__(init_data)\n\n    def __getitem__(self, key: str) -> T:\n        if self.parent and key not in self.data:\n            return self.parent.__getitem__(key)\n        return self.data.__getitem__(key)\n\n    def __contains__(self, key: str):\n        return key in self.data or key in self.parent\n\n    def __len__(self) -> int:\n        return len(self.data) + len(self.parent)\n\n    def __iter__(self):\n        return self._merged().__iter__()\n\n    def keys(self):\n        return self._merged().keys()\n\n    def values(self):\n        return self._merged().values()\n\n    def items(self):\n        return self._merged().items()\n\n    def _merged(self):\n        return {**self.parent, **self.data}\n\n    def register(self, name: str, default: bool = False):\n        def wrap(cls):\n            self[name] = cls\n            if default:\n                for key in DEFAULT_KEYS:\n                    self[key] = cls\n            return cls\n\n        return wrap\n"
  },
  {
    "path": "ludwig/utils/server_utils.py",
    "content": "import json\nimport os\nimport tempfile\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nfrom starlette.datastructures import UploadFile\nfrom starlette.responses import JSONResponse\n\nfrom ludwig.utils.data_utils import NumpyEncoder\n\n\ndef serialize_payload(data_source: pd.DataFrame | pd.Series) -> tuple:\n    \"\"\"\n    Generates two dictionaries to be sent via REST API for Ludwig prediction\n    service.\n    First dictionary created is payload_dict. Keys found in payload_dict:\n    raw_data: this is json string created by pandas to_json() method\n    source_type: indicates if the data_source is either a pandas dataframe or\n        pandas series.  This is needed to know how to rebuild the structure.\n    ndarray_dtype:  this is a dictionary where each entry is for any ndarray\n        data found in the data_source.  This could be an empty dictioinary if no\n        ndarray objects are present in data_source. Key for this dictionary is\n        column name if data_source is dataframe or index name if data_source is\n        series.  The value portion of the dictionary is the dtype of the\n        ndarray.  This value is used to set the correct dtype when rebuilding\n        the entry.\n\n    Second dictionary created is called payload_files, this contains information\n    and content for files to be sent to the server.  NOTE: if no files are to be\n    sent, this will be an empty dictionary.\n    Entries in this dictionary:\n    Key: file path string for file to be sent to server\n    Value: tuple(file path string, byte encoded file content,\n                 'application/octet-stream')\n\n    Args:\n        data_source: input features to be sent to Ludwig server\n\n    Returns: tuple(payload_dict, payload_files)\n\n    \"\"\"\n    payload_dict = {}\n    payload_dict[\"ndarray_dtype\"] = {}\n    payload_files = {}\n    if isinstance(data_source, pd.DataFrame):\n        payload_dict[\"raw_data\"] = data_source.to_json(orient=\"columns\")\n        payload_dict[\"source_type\"] = \"dataframe\"\n        for col in data_source.columns:\n            if isinstance(data_source[col].iloc[0], np.ndarray):\n                # if we have any ndarray columns, record dtype\n                payload_dict[\"ndarray_dtype\"][col] = str(data_source[col].iloc[0].dtype)\n            elif isinstance(data_source[col].iloc[0], str) and os.path.exists(data_source[col].iloc[0]):\n                # if we have file path feature, prepare file for transport\n                for v in data_source[col]:\n                    payload_files[v] = (v, open(v, \"rb\"), \"application/octet-stream\")\n    elif isinstance(data_source, pd.Series):\n        payload_dict[\"raw_data\"] = data_source.to_json(orient=\"index\")\n        payload_dict[\"source_type\"] = \"series\"\n        for col in data_source.index:\n            if isinstance(data_source[col], np.ndarray):\n                # for ndarrays record dtype for reconstruction\n                payload_dict[\"ndarray_dtype\"][col] = str(data_source[col].dtype)\n            elif isinstance(data_source[col], str) and os.path.exists(data_source[col]):\n                # if we have file path feature, prepare file for transport\n                v = data_source[col]\n                payload_files[v] = (v, open(v, \"rb\"), \"application/octet-stream\")\n    else:\n        ValueError(\n            '\"data_source\" must be either a pandas DataFrame or Series, '\n            \"format found to be {}\".format(type(data_source))\n        )\n\n    return payload_dict, payload_files\n\n\ndef _write_file(v, files):\n    # Convert UploadFile to a NamedTemporaryFile to ensure it's on the disk\n    suffix = os.path.splitext(v.filename)[1]\n    named_file = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)\n    files.append(named_file)\n    named_file.write(v.file.read())\n    named_file.close()\n    return named_file.name\n\n\ndef deserialize_payload(json_string: str) -> pd.DataFrame:\n    \"\"\"This function performs the inverse of the serialize_payload function and rebuilds the object represented in\n    json_string to a pandas DataFrame.\n\n    Args:\n        json_string: representing object to be rebuilt.\n\n    Returns: pandas.DataFrame\n    \"\"\"\n    payload_dict = json.loads(json_string)\n\n    # extract raw data from json string\n    raw_data_dict = json.loads(payload_dict[\"raw_data\"])\n    # rebuild based on original data source\n    if payload_dict[\"source_type\"] == \"dataframe\":\n        # reconstitute the pandas dataframe\n        df = pd.DataFrame.from_dict(raw_data_dict, orient=\"columns\")\n    elif payload_dict[\"source_type\"] == \"series\":\n        # reconstitute series into single row dataframe\n        df = pd.DataFrame(pd.Series(raw_data_dict)).T\n    else:\n        ValueError(\n            'Unknown \"source_type\" found.  Valid values are \"dataframe\" or '\n            '\"series\".  Instead found {}'.format(payload_dict[\"source_type\"])\n        )\n\n    # if source has ndarrays, rebuild those from list and set\n    # original dtype.\n    if payload_dict[\"ndarray_dtype\"]:\n        # yes, now covert list representation to ndarray representation\n        for col in payload_dict[\"ndarray_dtype\"]:\n            dtype = payload_dict[\"ndarray_dtype\"][col]\n            df[col] = df[col].apply(lambda x: np.array(x).astype(dtype))\n\n    return df\n\n\ndef deserialize_request(form) -> tuple:\n    \"\"\"This function will deserialize the REST API request packet to create a pandas dataframe that is input to the\n    Ludwig predict method and a list of files that will be cleaned up at the end of processing.\n\n    Args:\n        form: REST API provide form data\n\n    Returns: tuple(pandas.DataFrame, list of temporary files to clean up)\n    \"\"\"\n    files = []\n    file_index = {}\n    for k, v in form.multi_items():\n        if type(v) is UploadFile:\n            file_index[v.filename] = _write_file(v, files)\n\n    # reconstruct the dataframe\n    df = deserialize_payload(form[\"payload\"])\n\n    # insert files paths of the temporary files in place of the original\n    # file paths specified by the user.\n    # pd.DataFrame.replace() method is used to replace file path string\n    # specified by the user context with the file path string where a\n    # temporary file containing the same content.\n    # parameters for replace() method:\n    #   to_replace: list of file path strings that the user provided\n    #   value: list of temporary files created for each input file\n    #\n    # IMPORTANT: There is a one-to-one correspondence of the to_replace list\n    # and the value list. Each list must be the same size.\n    df.replace(to_replace=list(file_index.keys()), value=list(file_index.values()), inplace=True)\n\n    return df, files\n\n\nclass NumpyJSONResponse(JSONResponse):\n    def render(self, content: dict[str, Any]) -> str:\n        \"\"\"Override the default JSONResponse behavior to encode numpy arrays.\n\n        Args:\n            content: JSON object to be serialized.\n\n        Returns: str\n        \"\"\"\n        return json.dumps(\n            content, ensure_ascii=False, allow_nan=False, indent=None, separators=(\",\", \":\"), cls=NumpyEncoder\n        ).encode(\"utf-8\")\n"
  },
  {
    "path": "ludwig/utils/state_dict_backward_compatibility.py",
    "content": "# Copyright (c) 2023 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\n\ndef _update_transformers_to_freeze_module(state_dict):\n    \"\"\"Updates pre-trained encoders which were saved prior to the addition of FreezeModule.\"\"\"\n    return {\n        (\n            k.replace(\"encoder_obj.transformer.\", \"encoder_obj.transformer.module.\")\n            if \"encoder_obj.transformer.module\" not in k\n            else k\n        ): v\n        for k, v in state_dict.items()\n    }\n\n\ndef _update_combiner_no_input_features(state_dict):\n    \"\"\"Removed combiner.input_features from state_dict following DeepSpeed integration.\"\"\"\n    return {k: v for k, v in state_dict.items() if not k.startswith(\"combiner.input_features.\")}\n\n\ndef _update_combiner_no_device_tensor(state_dict):\n    \"\"\"Removed device_tensor from state_dict following DeepSpeed integration.\"\"\"\n    return {k: v for k, v in state_dict.items() if not k.endswith(\"device_tensor\")}\n\n\ndef update_state_dict(state_dict):\n    \"\"\"Checks state_dict on load, updates state dict if needed.\"\"\"\n    state_dict = _update_transformers_to_freeze_module(state_dict)\n    state_dict = _update_combiner_no_input_features(state_dict)\n    state_dict = _update_combiner_no_device_tensor(state_dict)\n    return state_dict\n"
  },
  {
    "path": "ludwig/utils/strings_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport re\nimport unicodedata\nfrom collections import Counter\nfrom dataclasses import dataclass\nfrom enum import Enum\n\nimport numpy as np\nfrom dateutil.parser import parse as parse_datetime\n\nfrom ludwig.constants import PADDING_SYMBOL, START_SYMBOL, STOP_SYMBOL, UNKNOWN_SYMBOL\nfrom ludwig.data.dataframe.base import DataFrameEngine\nfrom ludwig.data.dataframe.pandas import PANDAS\nfrom ludwig.utils.fs_utils import open_file\nfrom ludwig.utils.math_utils import int_type\nfrom ludwig.utils.tokenizers import get_tokenizer_from_registry\nfrom ludwig.utils.types import Series\n\nPANDAS_TRUE_STRS = {\"true\"}\nPANDAS_FALSE_STRS = {\"false\"}\n\nBOOL_TRUE_STRS = {\"yes\", \"y\", \"true\", \"t\", \"1\", \"1.0\"}\nBOOL_FALSE_STRS = {\"no\", \"n\", \"false\", \"f\", \"0\", \"0.0\", \"-1\", \"-1.0\"}\n\nlogger = logging.getLogger(__name__)\n\n\nclass SpecialSymbol(Enum):\n    \"\"\"Special symbols used for text features.\"\"\"\n\n    STOP = 0\n    START = 1\n    PADDING = 2\n    UNKNOWN = 3\n\n\ndef all_bool_strs():\n    \"\"\"Returns all valid boolean strings, with varied capitalization.\"\"\"\n    fns = [lambda x: x, lambda x: x.upper(), lambda x: x.capitalize()]\n    return sorted({fn(x) for fn in fns for x in BOOL_TRUE_STRS | BOOL_FALSE_STRS})\n\n\ndef make_safe_filename(s):\n    def safe_char(c):\n        if c.isalnum():\n            return c\n        else:\n            return \"_\"\n\n    return \"\".join(safe_char(c) for c in s).rstrip(\"_\")\n\n\ndef strip_accents(s):\n    return \"\".join(c for c in unicodedata.normalize(\"NFD\", s) if unicodedata.category(c) != \"Mn\")\n\n\ndef str2bool(v: str, fallback_true_label=None) -> bool:\n    \"\"\"Returns bool representation of the given value v.\n\n    Check the value against global bool string lists.\n    Fallback to using fallback_true_label as True if the value isn't in the global bool lists.\n\n    args:\n        v: Value to get the bool representation for.\n        fallback_true_label: (str) label to use as 'True'.\n    \"\"\"\n    v_str = str(v).lower()\n    if v_str in BOOL_TRUE_STRS:\n        return True\n    if v_str in BOOL_FALSE_STRS:\n        return False\n    if fallback_true_label is None:\n        raise ValueError(\n            f\"Cannot automatically map value '{v}' to a boolean and no `preprocessing.fallback_true_label` specified\"\n        )\n    return v == fallback_true_label\n\n\ndef values_are_pandas_numbers(values: list[str]):\n    \"\"\"Returns True if values would be read by pandas as dtype float or int.\"\"\"\n    for v in values:\n        try:\n            float(v)\n        except ValueError:\n            return False\n    return True\n\n\ndef values_are_pandas_bools(values: list[str]):\n    \"\"\"Returns True if values would be read by pandas as dtype bool.\"\"\"\n    lowercase_values_set = {str(v).lower() for v in values}\n    return lowercase_values_set.issubset(PANDAS_FALSE_STRS | PANDAS_TRUE_STRS)\n\n\ndef are_conventional_bools(values: list[str | bool]) -> bool:\n    \"\"\"Returns whether all values are conventional booleans.\"\"\"\n    for value in values:\n        lower_value = str(value).lower()\n        if lower_value not in BOOL_TRUE_STRS and lower_value not in BOOL_FALSE_STRS:\n            return False\n    return True\n\n\ndef is_number(s: str | int | float):\n    \"\"\"Returns whether specified value is number.\"\"\"\n    if isinstance(s, str) and s.lower() == \"nan\":\n        return True\n    try:\n        float(s)\n        return True\n    except ValueError:\n        return False\n\n\ndef is_datetime(s: str | int | float):\n    \"\"\"Returns whether specified value is datetime.\"\"\"\n    if is_number(s):\n        return False\n\n    try:\n        parse_datetime(s)\n        return True\n    except Exception:\n        return False\n\n\ndef are_all_datetimes(values: list[str | int | float]):\n    \"\"\"Returns whether all values are datetimes.\"\"\"\n    for value in values:\n        if not is_datetime(value):\n            return False\n    return True\n\n\ndef are_all_numbers(values: list[str | int | float]):\n    \"\"\"Returns whether all values are numbers.\"\"\"\n    for value in values:\n        if not is_number(value):\n            return False\n    return True\n\n\ndef is_integer(s: str | int | float):\n    \"\"\"Returns whether specified value is an integer.\"\"\"\n    try:\n        float(s)\n    except ValueError:\n        return False\n    else:\n        return float(s).is_integer() and not np.isnan(float(s))\n\n\ndef are_sequential_integers(values: list[str | int | float]):\n    \"\"\"Returns whether distinct values form sequential integer list.\"\"\"\n    int_list = []\n    for value in values:\n        if not is_integer(value):\n            return False\n        int_list.append(int(float(value)))\n    return (max(int_list) - min(int_list) + 1) == len(int_list)\n\n\ndef match_replace(string_to_match, list_regex):\n    \"\"\"Matches strings against regular expressions.\n\n    arguments:\n    string_to_match -- the string to match\n\n    returns:\n    string_to_match -- the cleaned string\n    matched -- the list of regular expressions that matched\n    \"\"\"\n    matched = []\n    for regex in list_regex:\n        match = re.search(regex[0], string_to_match)\n        if match:\n            string_to_match = re.sub(regex[0], regex[1], string_to_match)\n            matched.append(regex[0].pattern)\n    return string_to_match, matched\n\n\ndef load_vocabulary(vocab_file):\n    with open_file(vocab_file, \"r\", encoding=\"utf-8\") as f:\n        vocabulary = []\n        for line in f:\n            line = line.strip()\n            if \" \" in line:\n                line = line.split(\" \")[0]\n            vocabulary.append(line)\n        return vocabulary\n\n\ndef add_or_move_symbol(vocab_list: list[str], vocab_set: set[str], symbol: str, index: int):\n    \"\"\"Inserts or moves the symbol to the specified index.\"\"\"\n    if symbol in vocab_set:\n        vocab_list.remove(symbol)\n    vocab_list.insert(index, symbol)\n\n\n@dataclass\nclass Vocabulary:\n    vocab: list[str]\n    \"\"\"List of strings representing the computed vocabulary.\"\"\"\n\n    str2idx: dict[str, int]\n    \"\"\"Map of symbol to index.\"\"\"\n\n    str2freq: dict[str, int]\n    \"\"\"Map of symbol to frequency.\"\"\"\n\n    str2idf: dict[str, int] | None\n    \"\"\"Map of symbol to inverse document frequency.\"\"\"\n\n    max_sequence_length: int\n    \"\"\"Maximum sequence length.\"\"\"\n\n    sequence_length_99ptile: int\n    \"\"\"99th percentile of maximum sequence length.\"\"\"\n\n    pad_idx: int\n    \"\"\"Index to padding symbol.\"\"\"\n\n    padding_symbol: str\n    \"\"\"Actual padding symbol.\"\"\"\n\n    unknown_symbol: str\n    \"\"\"Actual unknown symbol.\"\"\"\n\n    prompt_template_num_tokens: int = 0\n    \"\"\"The number of tokens in the prompt template.\n\n    If -1, then there is no prompt template.\n    \"\"\"\n\n\ndef _get_vocab_from_dict(vocab: dict[str, int]) -> list[str]:\n    \"\"\"Returns a vocab in list format from a vocab token=>idx dictionary.\"\"\"\n    vocab_values = list(vocab.values())\n    if len(set(vocab_values)) != len(vocab_values):\n        raise ValueError(\"Vocabulary has duplicate mappings in its vocabulary. This should never happen.\")\n\n    # construct a vocab that is a list that reflects the token=>index mapping in HF's vocab\n    # pre-allocate a list to make sure each index is inited to prevent OBO errors caused by missing indices\n    max_idx = max(vocab_values)\n    vocab_list = [None for _ in range(max_idx + 1)]\n    for token, idx in vocab.items():\n        vocab_list[idx] = token\n    return vocab_list\n\n\ndef _get_vocabulary(\n    tokenizer_type: str,\n    tokenizer,\n    vocab_file: str,\n    unknown_symbol: str,\n    add_special_symbols: bool,\n    padding_symbol: str,\n    unit_counts: Counter,\n    num_most_frequent: int,\n) -> list[str] | None:\n    \"\"\"Returns the vocabulary from the tokenizer_type, tokenizer, or vocab_file.\n\n    If the `tokenizer_type` is 'hf_tokenizer', then the set vocabulary from the tokenizer is used.\n\n    If there's no vocab_file or if the tokenizer has no set vocabulary (e.g. space_punct), then the vocabulary is\n    determined from the tokenized data (unit_counts).\n\n    The UNKNOWN special symbol is always included in the final vocabulary. Additional special symbols (PADDING, START,\n    STOP) are added if add_special_symbols=True. If the tokenizer is a pre-trained huggingface tokenizer, then the\n    special symbols are taken from the tokenizer's vocabulary.\n    \"\"\"\n    # Pre-trained huggingface tokenizer. Use the pre-existing vocabulary and special symbols.\n    if tokenizer_type == \"hf_tokenizer\":\n        try:\n            return _get_vocab_from_dict(tokenizer.get_vocab())\n        except NotImplementedError:\n            logger.warning(\n                \"HuggingFace tokenizer does not have a get_vocab() method. \"\n                + \"Using tokenizer.tokenizer.vocab_size and tokenizer.tokenizer._convert_id_to_token \"\n                + \"to build the vocabulary.\"\n            )\n            vocab = []\n            for idx in range(tokenizer.tokenizer.vocab_size):\n                vocab.append(tokenizer.tokenizer._convert_id_to_token(idx))\n            vocab += tokenizer.tokenizer.added_tokens_encoder.keys()\n            return vocab\n\n    # The tokenizer has a preset vocabulary.\n    if hasattr(tokenizer, \"get_vocab\"):\n        return _get_vocab_from_dict(tokenizer.get_vocab())\n\n    # Load the vocabulary from the vocab file.\n    if vocab_file is not None:\n        return load_vocabulary(vocab_file)\n\n    # The tokenizer had no preset vocabulary, for example space_punct.\n    # Compute the vocabulary from tokenized data.\n    return [unit for unit, _ in unit_counts.most_common(num_most_frequent)]\n\n\ndef remove_bracketed_elements(prompt_template: str) -> str:\n    \"\"\"Example: <The {pronoun} sits on the {object}> -> <The  sits on the >.\"\"\"\n    pattern = r\"\\{.*?\\}\"\n    return re.sub(pattern, \"\", prompt_template)\n\n\ndef create_vocabulary(\n    data: Series,\n    tokenizer_type: str = \"space\",\n    lowercase: bool = True,\n    num_most_frequent: int = None,\n    vocab_file: str = None,\n    add_special_symbols: bool = True,\n    unknown_symbol: str = UNKNOWN_SYMBOL,\n    padding_symbol: str = PADDING_SYMBOL,\n    start_symbol: str = START_SYMBOL,\n    stop_symbol: str = STOP_SYMBOL,\n    pretrained_model_name_or_path: str = None,\n    ngram_size: int | None = None,\n    compute_idf: bool = False,\n    processor: DataFrameEngine = PANDAS,\n    prompt_template: str = \"\",\n) -> Vocabulary:\n    \"\"\"Computes a vocabulary over the provided data frame.\n\n    This function is used when the data consists of multiple tokens within one example. E.g., words in a text feature,\n    items in a set feature, etc. If the feature only contains a single token like for category features,\n    `create_vocabulary_single_token` should be used instead, as it is more efficient.\n\n    A tokenizer is specified using the `tokenizer_type`. The tokenizer will be used to process all of the data\n    provided, producing an indexed vocabulary with frequency counts. If the `tokenizer_type` is 'hf_tokenizer',\n    then a pre-trained huggingface tokenizer is loaded from `pretrained_model_name_or_path` and that vocabulary is\n    used directly.\n\n    The UNKNOWN special symbol is always included in the final vocabulary. Additional special symbols (PADDING, START,\n    STOP) are added if add_special_symbols=True. If the tokenizer is a pre-trained huggingface tokenizer, then the\n    special symbols are taken from the tokenizer's vocabulary.\n\n    Args:\n        prompt_template: The prompt template for the model. Applicable only to LLMs.\n        data: Series of string data.\n        tokenizer_type: Tokenizer type. Can be a tokenizer registry value or 'hf_tokenizer' for huggingface.\n        lowercase: Whether to lowercase all strings.\n        num_most_frequent: Upper limit on vocabulary size.,\n        add_special_symbols: If True, START, STOP, PADDING special symbols are added to the vocabulary. UNKNOWN is\n            always added.\n        unknown_symbol: String representation for the UNKNOWN symbol.\n        padding_symbol: String representation for the PADDING symbol.\n        start_symbol: String representation for the START symbol.\n        stop_symbol: String representation for the STOP symbol.\n        pretrained_model_name_or_path: Name/path to huggingface model.\n        ngram_size: Size of the n-gram when using `ngram` tokenizer.\n        compute_idf: If True, computes the inverse document frequency for each token.\n        processor: Which processor to use to process data.\n\n    Returns:\n        Vocabulary object containing metadata about the vocab.\n\n    TODO(Justin): Clean up pad_idx, padding_symbol, unknown_symbol return, as no one seems to be using it.\n    \"\"\"\n    tokenizer = get_tokenizer_from_registry(tokenizer_type)(\n        vocab_file=vocab_file,\n        pretrained_model_name_or_path=pretrained_model_name_or_path,\n        ngram_size=ngram_size,\n    )\n\n    # Number of tokens in template.\n    prompt_template_num_tokens = -1\n    if prompt_template:\n        prompt_without_bracketed_elements = remove_bracketed_elements(prompt_template)\n        prompt_template_num_tokens = len(tokenizer(prompt_without_bracketed_elements))\n\n    # Tokenize the data.\n    def process_line(line):\n        return tokenizer(line.lower() if lowercase else line)\n\n    processed_lines = processor.map_objects(data, process_line)\n    processed_counts = processed_lines.explode().value_counts(sort=False)\n    processed_counts = processor.compute(processed_counts)\n    unit_counts = Counter(dict(processed_counts))\n    max_sequence_length = processor.compute(processed_lines.map(len).max())\n    sequence_length_99ptile = processor.compute(processed_lines.map(len).quantile(0.99))\n\n    if tokenizer_type != \"hf_tokenizer\":\n        # For non-HF tokenizers, add 2 for start and stop symbols.\n        max_sequence_length += 2\n        sequence_length_99ptile += 2\n\n    pad_idx = None\n    if tokenizer_type == \"hf_tokenizer\":\n        # Replace the special symbols with the ones from the tokenizer.\n        unknown_symbol = tokenizer.get_unk_token()\n        padding_symbol = tokenizer.get_pad_token()\n        pad_idx = tokenizer.convert_token_to_id(padding_symbol)\n\n    vocab: list[str] = _get_vocabulary(\n        tokenizer_type,\n        tokenizer,\n        vocab_file,\n        unknown_symbol,\n        add_special_symbols,\n        padding_symbol,\n        unit_counts,\n        num_most_frequent,\n    )\n    vocab_set = set(vocab)\n\n    doc_unit_counts = None\n    if compute_idf:\n        # The document frequency used for TF-IDF. Similar to unit_counts, but de-duped by document.\n        document_counts = processed_lines.map(lambda x: set(x)).explode().value_counts(sort=False)\n        document_counts = processor.compute(document_counts)\n        doc_unit_counts = Counter(dict(document_counts))\n\n    if tokenizer_type != \"hf_tokenizer\":\n        if add_special_symbols:\n            add_or_move_symbol(vocab, vocab_set, stop_symbol, SpecialSymbol.STOP.value)\n            add_or_move_symbol(vocab, vocab_set, start_symbol, SpecialSymbol.START.value)\n            add_or_move_symbol(vocab, vocab_set, padding_symbol, SpecialSymbol.PADDING.value)\n        # Always add the UNKNOWN symbol if we're using our own tokenizer.\n        add_or_move_symbol(vocab, vocab_set, unknown_symbol, SpecialSymbol.UNKNOWN.value)\n\n    str2idx = {unit: i for i, unit in enumerate(vocab)}\n    str2freq = {unit: unit_counts.get(unit) if unit in unit_counts else 0 for unit in vocab}\n    str2idf = (\n        {unit: np.log(len(vocab) / (1 + doc_unit_counts.get(unit))) if unit in doc_unit_counts else 0 for unit in vocab}\n        if compute_idf\n        else None\n    )\n\n    if pad_idx is None and padding_symbol in str2idx.keys():\n        pad_idx = str2idx[padding_symbol]\n\n    return Vocabulary(\n        vocab=vocab,\n        str2idx=str2idx,\n        str2freq=str2freq,\n        str2idf=str2idf,\n        max_sequence_length=max_sequence_length,\n        sequence_length_99ptile=sequence_length_99ptile,\n        pad_idx=pad_idx,\n        padding_symbol=padding_symbol,\n        unknown_symbol=unknown_symbol,\n        prompt_template_num_tokens=prompt_template_num_tokens,\n    )\n\n\ndef create_vocabulary_single_token(\n    data: Series,\n    num_most_frequent: int | None = None,\n    processor: DataFrameEngine = PANDAS,\n    unknown_symbol: str = UNKNOWN_SYMBOL,\n):\n    \"\"\"Computes a vocabulary over the provided data frame.\n\n    This function should be used iff the values in each row of data should be considered as a single token, e.g.,\n    category features (\"interested\", \"not interested\", \"somewhat interested\").\n\n    This assumption allows us to be more efficient than `create_vocabulary()` as we can skip tokenization and\n    computing the maximum sequence length, which are unnecessary for category features.\n\n    Args:\n        data: Series of string data.\n        num_most_frequent: Upper limit on vocabulary size.\n        unknown_symbol: String representation for the UNKNOWN symbol.\n        processor: Which processor to use to process data.\n\n    Returns:\n        Tuple of:\n            vocab: List of strings representing the computed vocabulary.\n            str2idx: Map of symbol to index.\n            str2freq: Map of symbol to frequency.\n    \"\"\"\n    processed_counts = data.str.strip().value_counts(sort=True)\n    processed_counts = processor.compute(processed_counts)\n    full_vocab = processed_counts.index.tolist()\n    # Only add unknown symbol if num most frequent tokens is less than total number of unique tokens\n    if num_most_frequent < len(full_vocab):\n        vocab = [unknown_symbol] + full_vocab[:num_most_frequent]\n    else:\n        vocab = full_vocab\n    str2idx = {unit: i for i, unit in enumerate(vocab)}\n    str2freq = processed_counts.to_dict()\n    str2freq = {k: str2freq.get(k, 0) for k in vocab}\n    return vocab, str2idx, str2freq\n\n\ndef _get_sequence_vector(\n    sequence, tokenizer, tokenizer_type, format_dtype, unit_to_id, lowercase=True, unknown_symbol=UNKNOWN_SYMBOL\n) -> np.ndarray:\n    unit_sequence = tokenizer(sequence.lower() if lowercase else sequence)\n\n    unit_indices_vector = np.empty(len(unit_sequence), dtype=format_dtype)\n    for i in range(len(unit_sequence)):\n        curr_unit = unit_sequence[i]\n        if tokenizer_type == \"hf_tokenizer\":\n            unit_indices_vector[i] = curr_unit\n        else:\n            if curr_unit in unit_to_id:\n                unit_indices_vector[i] = unit_to_id[curr_unit]\n            else:\n                unit_indices_vector[i] = unit_to_id[unknown_symbol]\n\n    # Add start and stop symbols.\n    # Huggingface's pretrained tokenizers take care of this implicitly:\n    # https://huggingface.co/docs/transformers/preprocessing\n    if tokenizer_type != \"hf_tokenizer\":\n        unit_indices_vector = np.append(unit_indices_vector, unit_to_id[STOP_SYMBOL])\n        unit_indices_vector = np.insert(unit_indices_vector, 0, unit_to_id[START_SYMBOL])\n    return unit_indices_vector\n\n\ndef build_sequence_matrix(\n    sequences,  # pd.core.series.Series\n    inverse_vocabulary,\n    tokenizer_type,\n    length_limit,\n    padding_symbol=PADDING_SYMBOL,\n    padding=\"right\",\n    unknown_symbol=UNKNOWN_SYMBOL,\n    lowercase=True,\n    tokenizer_vocab_file=None,\n    pretrained_model_name_or_path=None,\n    processor=PANDAS,\n) -> np.ndarray:\n    tokenizer = get_tokenizer_from_registry(tokenizer_type)(\n        vocab_file=tokenizer_vocab_file,\n        pretrained_model_name_or_path=pretrained_model_name_or_path,\n    )\n\n    format_dtype = int_type(len(inverse_vocabulary) - 1)\n\n    unit_vectors = sequences.map(\n        lambda sequence: _get_sequence_vector(\n            sequence,\n            tokenizer,\n            tokenizer_type,\n            format_dtype,\n            inverse_vocabulary,\n            lowercase=lowercase,\n            unknown_symbol=unknown_symbol,\n        )\n    )\n\n    max_length = processor.compute(unit_vectors.map(len).max())\n    if max_length < length_limit:\n        logger.debug(f\"max length of {format}: {max_length} < limit: {length_limit}\")\n    max_length = length_limit\n\n    if tokenizer_type == \"hf_tokenizer\":\n        padding_symbol = tokenizer.get_pad_token()\n        pad_token_id = tokenizer.convert_token_to_id(padding_symbol)\n    else:\n        pad_token_id = inverse_vocabulary[padding_symbol]\n\n    def pad(vector):\n        sequence = np.full((int(max_length),), pad_token_id, dtype=format_dtype)\n        limit = min(vector.shape[0], max_length)\n        if padding == \"right\":\n            sequence[:limit] = vector[:limit]\n        else:  # if padding == 'left\n            sequence[max_length - limit :] = vector[:limit]\n        return sequence\n\n    padded = processor.map_objects(unit_vectors, pad)\n    return padded\n\n\ndef get_tokenizer(tokenizer_type: str, tokenizer_vocab_file: str, pretrained_model_name_or_path: str):\n    \"\"\"Returns a tokenizer object based on the tokenizer type.\"\"\"\n    return get_tokenizer_from_registry(tokenizer_type)(\n        vocab_file=tokenizer_vocab_file,\n        pretrained_model_name_or_path=pretrained_model_name_or_path,\n    )\n"
  },
  {
    "path": "ludwig/utils/structural_warning.py",
    "content": "import warnings\n\nfrom ludwig.utils.logging_utils import log_once\n\n\ndef warn_structure_refactor(old_module: str, new_module: str, direct: bool = True) -> None:\n    \"\"\"Create structure refactor warning to indicate modules new location post.\n\n    Only creates a warning once per module.\n    \"\"\"\n    old_module = old_module.replace(\".py\", \"\")\n    if log_once(old_module):\n        warning = (\n            f\"The module `{old_module}` has been moved to `{new_module}` and the old \"\n            f\"location will be deprecated soon. Please adjust your imports to point \"\n            f\"to the new location.\"\n        )\n\n        if direct:\n            warning += f\" Example: Do a global search and \" f\"replace `{old_module}` with `{new_module}`.\"\n        else:\n            warning += (\n                f\"\\nATTENTION: This module may have been split or refactored. Please \"\n                f\"check the contents of `{new_module}` before making changes.\"\n            )\n\n        with warnings.catch_warnings():\n            warnings.simplefilter(\"always\")\n            warnings.warn(warning, DeprecationWarning, stacklevel=3)\n"
  },
  {
    "path": "ludwig/utils/system_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nfrom dataclasses import dataclass\n\nfrom ludwig.api_annotations import DeveloperAPI\n\n\n@DeveloperAPI\n@dataclass\nclass Resources:\n    cpus: int\n    gpus: int\n"
  },
  {
    "path": "ludwig/utils/time_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport time\nfrom datetime import datetime, timedelta\n\nlogger = logging.getLogger(__name__)\n\n\nclass WithTimer:\n    def __init__(self, title=\"\", quiet=False):\n        self.title = title\n        self.quiet = quiet\n\n    def elapsed(self):\n        return time.time() - self.wall, time.process_time() - self.proc\n\n    def enter(self):\n        \"\"\"Manually trigger enter.\"\"\"\n        self.__enter__()\n\n    def __enter__(self):\n        self.proc = time.process_time()\n        self.wall = time.time()\n        return self\n\n    def __exit__(self, *args):\n        if not self.quiet:\n            elapsed_wp = self.elapsed()\n            logger.info(f\"Elapsed {self.title}: wall {elapsed_wp[0]:.06f}, sys {elapsed_wp[1]:.06f}\")\n\n\nclass Timer:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self._proc = time.process_time()\n        self._wall = time.time()\n\n    def elapsed(self):\n        return self.wall(), self.proc()\n\n    def elapsed_str(self):\n        return strdelta(self.wall() * 1000.0), strdelta(self.proc() * 1000.0)\n\n    def wall(self):\n        return time.time() - self._wall\n\n    def proc(self):\n        return time.process_time() - self._proc\n\n    def tic(self):\n        \"\"\"Like Matlab tic/toc for wall time and processor time.\"\"\"\n        self.reset()\n\n    def toc(self):\n        \"\"\"Like Matlab tic/toc for wall time.\"\"\"\n        return self.wall()\n\n    def tocproc(self):\n        \"\"\"Like Matlab tic/toc, but for processor time.\"\"\"\n        return self.proc()\n\n\ndef timestamp():\n    return f\"{datetime.now():%Y_%m_%d_%H_%M_%S}\"\n\n\ndef strdelta(tdelta):\n    if isinstance(tdelta, (int, float)):\n        tdelta = timedelta(milliseconds=tdelta)\n    d = {\"D\": tdelta.days}\n    d[\"H\"], rem = divmod(tdelta.seconds, 3600)\n    d[\"M\"], d[\"S\"] = divmod(rem, 60)\n    d[\"f\"] = str(tdelta.microseconds)[0:4]\n    if d[\"D\"] > 0:\n        t = \"{D}d {H}h {M}m {S}.{f}s\"\n    elif d[\"H\"] > 0:\n        t = \"{H}h {M}m {S}.{f}s\"\n    elif d[\"M\"] > 0:\n        t = \"{M}m {S}.{f}s\"\n    else:\n        t = \"{S}.{f}s\"\n    return t.format(**d)\n"
  },
  {
    "path": "ludwig/utils/tokenizers.py",
    "content": "\"\"\"Ludwig string tokenizers including string-based, spacy-based, and huggingface-based implementations.\n\nTo add a new tokenizer, 1) implement a subclass of BaseTokenizer and 2) add it to the tokenizer_registry.\n\nOnce it's in the registry, tokenizers can be used in a ludwig config, e.g..\n\n```\ninput_features:\n    -   name: title\n        type: text\n        preprocessing:\n            tokenizer: <NEW_TOKENIZER>\n```\n\"\"\"\n\nimport logging\nimport re\nfrom abc import abstractmethod\nfrom typing import Any\n\nimport torch\n\nfrom ludwig.utils.nlp_utils import load_nlp_pipeline, process_text\n\nlogger = logging.getLogger(__name__)\n\n\nSPACE_PUNCTUATION_REGEX = re.compile(r\"\\w+|[^\\w\\s]\")\nCOMMA_REGEX = re.compile(r\"\\s*,\\s*\")\nUNDERSCORE_REGEX = re.compile(r\"\\s*_\\s*\")\n\nTORCHSCRIPT_COMPATIBLE_TOKENIZERS = {\"space\", \"space_punct\"}\n\n\nclass BaseTokenizer:\n    @abstractmethod\n    def __init__(self, **kwargs):\n        pass\n\n    @abstractmethod\n    def __call__(self, text: str):\n        pass\n\n\nclass CharactersToListTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return [char for char in text]\n\n\nclass SpaceStringToListTokenizer(torch.nn.Module):\n    \"\"\"Implements torchscript-compatible whitespace tokenization.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def forward(self, v: str | list[str] | torch.Tensor) -> Any:\n        if isinstance(v, torch.Tensor):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        inputs: list[str] = []\n        # Ludwig calls map on List[str] objects, so we need to handle individual strings as well.\n        if isinstance(v, str):\n            inputs.append(v)\n        else:\n            inputs.extend(v)\n\n        tokens: list[list[str]] = []\n        for sequence in inputs:\n            split_sequence = sequence.strip().split(\" \")\n            token_sequence: list[str] = []\n            for token in split_sequence:\n                if len(token) > 0:\n                    token_sequence.append(token)\n            tokens.append(token_sequence)\n\n        return tokens[0] if isinstance(v, str) else tokens\n\n\nclass SpacePunctuationStringToListTokenizer(torch.nn.Module):\n    \"\"\"Implements torchscript-compatible space_punct tokenization.\"\"\"\n\n    def __init__(self, **kwargs):\n        super().__init__()\n\n    def is_regex_w(self, c: str) -> bool:\n        return c.isalnum() or c == \"_\"\n\n    def forward(self, v: str | list[str] | torch.Tensor) -> Any:\n        if isinstance(v, torch.Tensor):\n            raise ValueError(f\"Unsupported input: {v}\")\n\n        inputs: list[str] = []\n        # Ludwig calls map on List[str] objects, so we need to handle individual strings as well.\n        if isinstance(v, str):\n            inputs.append(v)\n        else:\n            inputs.extend(v)\n\n        tokens: list[list[str]] = []\n        for sequence in inputs:\n            token_sequence: list[str] = []\n            word: list[str] = []\n            for c in sequence:\n                if self.is_regex_w(c):\n                    word.append(c)\n                elif len(word) > 0:  # if non-empty word and non-alphanumeric char, append word to token sequence\n                    token_sequence.append(\"\".join(word))\n                    word.clear()\n\n                if not self.is_regex_w(c) and not c.isspace():  # non-alphanumeric, non-space char is punctuation\n                    token_sequence.append(c)\n\n            if len(word) > 0:  # add last word\n                token_sequence.append(\"\".join(word))\n\n            tokens.append(token_sequence)\n\n        return tokens[0] if isinstance(v, str) else tokens\n\n\nclass StringSplitTokenizer(BaseTokenizer):\n    \"\"\"Splits a string by a given separator.\"\"\"\n\n    def __init__(self, separator: str = \" \", **kwargs):\n        self.separator = separator\n\n    def __call__(self, text):\n        return text.split(self.separator)\n\n\nclass NgramTokenizer(BaseTokenizer):\n    \"\"\"Tokenizes text into unigrams + ngrams up to n.\"\"\"\n\n    def __init__(self, n: int = 2, **kwargs):\n        self.n = n\n\n    def __call__(self, text):\n        tokens = text.strip().split()\n        result = list(tokens)\n        for i in range(2, self.n + 1):\n            for j in range(len(tokens) - i + 1):\n                result.append(\" \".join(tokens[j : j + i]))\n        return result\n\n\nclass UnderscoreStringToListTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return UNDERSCORE_REGEX.split(text.strip())\n\n\nclass CommaStringToListTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return COMMA_REGEX.split(text.strip())\n\n\nclass UntokenizedStringToListTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return [text]\n\n\nclass StrippedStringToListTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return [text.strip()]\n\n\nclass EnglishTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"en\"))\n\n\nclass EnglishFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"en\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass EnglishRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"en\"), filter_stopwords=True)\n\n\nclass EnglishLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        process_text(text, load_nlp_pipeline(\"en\"), return_lemma=True)\n\n\nclass EnglishLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"en\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass EnglishLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"en\"), return_lemma=True, filter_stopwords=True)\n\n\nclass ItalianTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"it\"))\n\n\nclass ItalianFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"it\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass ItalianRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"it\"), filter_stopwords=True)\n\n\nclass ItalianLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"it\"), return_lemma=True)\n\n\nclass ItalianLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"it\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass ItalianLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"it\"), return_lemma=True, filter_stopwords=True)\n\n\nclass SpanishTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"es\"))\n\n\nclass SpanishFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"es\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass SpanishRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"es\"), filter_stopwords=True)\n\n\nclass SpanishLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"es\"), return_lemma=True)\n\n\nclass SpanishLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"es\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass SpanishLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"es\"), return_lemma=True, filter_stopwords=True)\n\n\nclass GermanTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"de\"))\n\n\nclass GermanFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"de\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass GermanRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"de\"), filter_stopwords=True)\n\n\nclass GermanLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"de\"), return_lemma=True)\n\n\nclass GermanLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"de\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass GermanLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"de\"), return_lemma=True, filter_stopwords=True)\n\n\nclass FrenchTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"fr\"))\n\n\nclass FrenchFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"fr\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass FrenchRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"fr\"), filter_stopwords=True)\n\n\nclass FrenchLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"fr\"), return_lemma=True)\n\n\nclass FrenchLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"fr\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass FrenchLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"fr\"), return_lemma=True, filter_stopwords=True)\n\n\nclass PortugueseTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pt\"))\n\n\nclass PortugueseFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"pt\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass PortugueseRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pt\"), filter_stopwords=True)\n\n\nclass PortugueseLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pt\"), return_lemma=True)\n\n\nclass PortugueseLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"pt\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass PortugueseLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pt\"), return_lemma=True, filter_stopwords=True)\n\n\nclass DutchTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nl\"))\n\n\nclass DutchFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"nl\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass DutchRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nl\"), filter_stopwords=True)\n\n\nclass DutchLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nl\"), return_lemma=True)\n\n\nclass DutchLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"nl\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass DutchLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nl\"), return_lemma=True, filter_stopwords=True)\n\n\nclass GreekTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"el\"))\n\n\nclass GreekFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"el\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass GreekRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"el\"), filter_stopwords=True)\n\n\nclass GreekLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"el\"), return_lemma=True)\n\n\nclass GreekLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"el\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass GreekLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"el\"), return_lemma=True, filter_stopwords=True)\n\n\nclass NorwegianTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nb\"))\n\n\nclass NorwegianFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"nb\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass NorwegianRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nb\"), filter_stopwords=True)\n\n\nclass NorwegianLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nb\"), return_lemma=True)\n\n\nclass NorwegianLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"nb\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass NorwegianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"nb\"), return_lemma=True, filter_stopwords=True)\n\n\nclass LithuanianTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"lt\"))\n\n\nclass LithuanianFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"lt\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass LithuanianRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"lt\"), filter_stopwords=True)\n\n\nclass LithuanianLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"lt\"), return_lemma=True)\n\n\nclass LithuanianLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"lt\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass LithuanianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"lt\"), return_lemma=True, filter_stopwords=True)\n\n\nclass DanishTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"da\"))\n\n\nclass DanishFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"da\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass DanishRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"da\"), filter_stopwords=True)\n\n\nclass DanishLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"da\"), return_lemma=True)\n\n\nclass DanishLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"da\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass DanishLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"da\"), return_lemma=True, filter_stopwords=True)\n\n\nclass PolishTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pl\"))\n\n\nclass PolishFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"pl\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass PolishRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pl\"), filter_stopwords=True)\n\n\nclass PolishLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pl\"), return_lemma=True)\n\n\nclass PolishLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"pl\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass PolishLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"pl\"), return_lemma=True, filter_stopwords=True)\n\n\nclass RomanianTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"ro\"))\n\n\nclass RomanianFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"ro\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass RomanianRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"ro\"), filter_stopwords=True)\n\n\nclass RomanianLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"ro\"), return_lemma=True)\n\n\nclass RomanianLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"ro\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass RomanianLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"ro\"), return_lemma=True, filter_stopwords=True)\n\n\nclass JapaneseTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"jp\"))\n\n\nclass JapaneseFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"jp\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass JapaneseRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"jp\"), filter_stopwords=True)\n\n\nclass JapaneseLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"jp\"), return_lemma=True)\n\n\nclass JapaneseLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"jp\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass JapaneseLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"jp\"), return_lemma=True, filter_stopwords=True)\n\n\nclass ChineseTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"zh\"))\n\n\nclass ChineseFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"zh\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass ChineseRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"zh\"), filter_stopwords=True)\n\n\nclass ChineseLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"zh\"), return_lemma=True)\n\n\nclass ChineseLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"zh\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass ChineseLemmatizeRemoveStopwordsFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"zh\"), return_lemma=True, filter_stopwords=True)\n\n\nclass MultiTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"xx\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass MultiFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text, load_nlp_pipeline(\"xx\"), filter_numbers=True, filter_punctuation=True, filter_short_tokens=True\n        )\n\n\nclass MultiRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"xx\"), filter_stopwords=True)\n\n\nclass MultiLemmatizeTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"xx\"), return_lemma=True)\n\n\nclass MultiLemmatizeFilterTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(\n            text,\n            load_nlp_pipeline(\"xx\"),\n            return_lemma=True,\n            filter_numbers=True,\n            filter_punctuation=True,\n            filter_short_tokens=True,\n        )\n\n\nclass MultiLemmatizeRemoveStopwordsTokenizer(BaseTokenizer):\n    def __call__(self, text):\n        return process_text(text, load_nlp_pipeline(\"xx\"), return_lemma=True, filter_stopwords=True)\n\n\nclass HFTokenizer(BaseTokenizer):\n    def __init__(self, pretrained_model_name_or_path, **kwargs):\n        super().__init__()\n        from transformers import AutoTokenizer\n\n        self.tokenizer = AutoTokenizer.from_pretrained(\n            pretrained_model_name_or_path,\n            trust_remote_code=kwargs.get(\"trust_remote_code\", False),\n        )\n        # Some models (e.g. LLaMA) don't have a pad_token by default.\n        # Set it to eos_token to avoid NoneType errors in preprocessing.\n        if self.tokenizer.pad_token is None and self.tokenizer.eos_token is not None:\n            self.tokenizer.pad_token = self.tokenizer.eos_token\n\n    def __call__(self, text):\n        return self.tokenizer.encode(text, truncation=True)\n\n    def get_vocab(self):\n        return self.tokenizer.get_vocab()\n\n    def get_pad_token(self) -> str:\n        return self.tokenizer.pad_token\n\n    def get_unk_token(self) -> str:\n        return self.tokenizer.unk_token\n\n    def convert_token_to_id(self, token: str) -> int:\n        if token is None:\n            return 0\n        return self.tokenizer.convert_tokens_to_ids(token)\n\n\ntokenizer_registry = {\n    # Torchscript-compatible tokenizers.\n    \"space\": SpaceStringToListTokenizer,\n    \"space_punct\": SpacePunctuationStringToListTokenizer,\n    # Tokenizers not compatible with torchscript\n    \"characters\": CharactersToListTokenizer,\n    \"underscore\": UnderscoreStringToListTokenizer,\n    \"comma\": CommaStringToListTokenizer,\n    \"untokenized\": UntokenizedStringToListTokenizer,\n    \"stripped\": StrippedStringToListTokenizer,\n    \"english_tokenize\": EnglishTokenizer,\n    \"english_tokenize_filter\": EnglishFilterTokenizer,\n    \"english_tokenize_remove_stopwords\": EnglishRemoveStopwordsTokenizer,\n    \"english_lemmatize\": EnglishLemmatizeTokenizer,\n    \"english_lemmatize_filter\": EnglishLemmatizeFilterTokenizer,\n    \"english_lemmatize_remove_stopwords\": EnglishLemmatizeRemoveStopwordsTokenizer,\n    \"italian_tokenize\": ItalianTokenizer,\n    \"italian_tokenize_filter\": ItalianFilterTokenizer,\n    \"italian_tokenize_remove_stopwords\": ItalianRemoveStopwordsTokenizer,\n    \"italian_lemmatize\": ItalianLemmatizeTokenizer,\n    \"italian_lemmatize_filter\": ItalianLemmatizeFilterTokenizer,\n    \"italian_lemmatize_remove_stopwords\": ItalianLemmatizeRemoveStopwordsTokenizer,\n    \"spanish_tokenize\": SpanishTokenizer,\n    \"spanish_tokenize_filter\": SpanishFilterTokenizer,\n    \"spanish_tokenize_remove_stopwords\": SpanishRemoveStopwordsTokenizer,\n    \"spanish_lemmatize\": SpanishLemmatizeTokenizer,\n    \"spanish_lemmatize_filter\": SpanishLemmatizeFilterTokenizer,\n    \"spanish_lemmatize_remove_stopwords\": SpanishLemmatizeRemoveStopwordsTokenizer,\n    \"german_tokenize\": GermanTokenizer,\n    \"german_tokenize_filter\": GermanFilterTokenizer,\n    \"german_tokenize_remove_stopwords\": GermanRemoveStopwordsTokenizer,\n    \"german_lemmatize\": GermanLemmatizeTokenizer,\n    \"german_lemmatize_filter\": GermanLemmatizeFilterTokenizer,\n    \"german_lemmatize_remove_stopwords\": GermanLemmatizeRemoveStopwordsTokenizer,\n    \"french_tokenize\": FrenchTokenizer,\n    \"french_tokenize_filter\": FrenchFilterTokenizer,\n    \"french_tokenize_remove_stopwords\": FrenchRemoveStopwordsTokenizer,\n    \"french_lemmatize\": FrenchLemmatizeTokenizer,\n    \"french_lemmatize_filter\": FrenchLemmatizeFilterTokenizer,\n    \"french_lemmatize_remove_stopwords\": FrenchLemmatizeRemoveStopwordsTokenizer,\n    \"portuguese_tokenize\": PortugueseTokenizer,\n    \"portuguese_tokenize_filter\": PortugueseFilterTokenizer,\n    \"portuguese_tokenize_remove_stopwords\": PortugueseRemoveStopwordsTokenizer,\n    \"portuguese_lemmatize\": PortugueseLemmatizeTokenizer,\n    \"portuguese_lemmatize_filter\": PortugueseLemmatizeFilterTokenizer,\n    \"portuguese_lemmatize_remove_stopwords\": PortugueseLemmatizeRemoveStopwordsTokenizer,\n    \"dutch_tokenize\": DutchTokenizer,\n    \"dutch_tokenize_filter\": DutchFilterTokenizer,\n    \"dutch_tokenize_remove_stopwords\": DutchRemoveStopwordsTokenizer,\n    \"dutch_lemmatize\": DutchLemmatizeTokenizer,\n    \"dutch_lemmatize_filter\": DutchLemmatizeFilterTokenizer,\n    \"dutch_lemmatize_remove_stopwords\": DutchLemmatizeRemoveStopwordsTokenizer,\n    \"greek_tokenize\": GreekTokenizer,\n    \"greek_tokenize_filter\": GreekFilterTokenizer,\n    \"greek_tokenize_remove_stopwords\": GreekRemoveStopwordsTokenizer,\n    \"greek_lemmatize\": GreekLemmatizeTokenizer,\n    \"greek_lemmatize_filter\": GreekLemmatizeFilterTokenizer,\n    \"greek_lemmatize_remove_stopwords\": GreekLemmatizeRemoveStopwordsFilterTokenizer,\n    \"norwegian_tokenize\": NorwegianTokenizer,\n    \"norwegian_tokenize_filter\": NorwegianFilterTokenizer,\n    \"norwegian_tokenize_remove_stopwords\": NorwegianRemoveStopwordsTokenizer,\n    \"norwegian_lemmatize\": NorwegianLemmatizeTokenizer,\n    \"norwegian_lemmatize_filter\": NorwegianLemmatizeFilterTokenizer,\n    \"norwegian_lemmatize_remove_stopwords\": NorwegianLemmatizeRemoveStopwordsFilterTokenizer,\n    \"lithuanian_tokenize\": LithuanianTokenizer,\n    \"lithuanian_tokenize_filter\": LithuanianFilterTokenizer,\n    \"lithuanian_tokenize_remove_stopwords\": LithuanianRemoveStopwordsTokenizer,\n    \"lithuanian_lemmatize\": LithuanianLemmatizeTokenizer,\n    \"lithuanian_lemmatize_filter\": LithuanianLemmatizeFilterTokenizer,\n    \"lithuanian_lemmatize_remove_stopwords\": LithuanianLemmatizeRemoveStopwordsFilterTokenizer,\n    \"danish_tokenize\": DanishTokenizer,\n    \"danish_tokenize_filter\": DanishFilterTokenizer,\n    \"danish_tokenize_remove_stopwords\": DanishRemoveStopwordsTokenizer,\n    \"danish_lemmatize\": DanishLemmatizeTokenizer,\n    \"danish_lemmatize_filter\": DanishLemmatizeFilterTokenizer,\n    \"danish_lemmatize_remove_stopwords\": DanishLemmatizeRemoveStopwordsFilterTokenizer,\n    \"polish_tokenize\": PolishTokenizer,\n    \"polish_tokenize_filter\": PolishFilterTokenizer,\n    \"polish_tokenize_remove_stopwords\": PolishRemoveStopwordsTokenizer,\n    \"polish_lemmatize\": PolishLemmatizeTokenizer,\n    \"polish_lemmatize_filter\": PolishLemmatizeFilterTokenizer,\n    \"polish_lemmatize_remove_stopwords\": PolishLemmatizeRemoveStopwordsFilterTokenizer,\n    \"romanian_tokenize\": RomanianTokenizer,\n    \"romanian_tokenize_filter\": RomanianFilterTokenizer,\n    \"romanian_tokenize_remove_stopwords\": RomanianRemoveStopwordsTokenizer,\n    \"romanian_lemmatize\": RomanianLemmatizeTokenizer,\n    \"romanian_lemmatize_filter\": RomanianLemmatizeFilterTokenizer,\n    \"romanian_lemmatize_remove_stopwords\": RomanianLemmatizeRemoveStopwordsFilterTokenizer,\n    \"japanese_tokenize\": JapaneseTokenizer,\n    \"japanese_tokenize_filter\": JapaneseFilterTokenizer,\n    \"japanese_tokenize_remove_stopwords\": JapaneseRemoveStopwordsTokenizer,\n    \"japanese_lemmatize\": JapaneseLemmatizeTokenizer,\n    \"japanese_lemmatize_filter\": JapaneseLemmatizeFilterTokenizer,\n    \"japanese_lemmatize_remove_stopwords\": JapaneseLemmatizeRemoveStopwordsFilterTokenizer,\n    \"chinese_tokenize\": ChineseTokenizer,\n    \"chinese_tokenize_filter\": ChineseFilterTokenizer,\n    \"chinese_tokenize_remove_stopwords\": ChineseRemoveStopwordsTokenizer,\n    \"chinese_lemmatize\": ChineseLemmatizeTokenizer,\n    \"chinese_lemmatize_filter\": ChineseLemmatizeFilterTokenizer,\n    \"chinese_lemmatize_remove_stopwords\": ChineseLemmatizeRemoveStopwordsFilterTokenizer,\n    \"multi_tokenize\": MultiTokenizer,\n    \"multi_tokenize_filter\": MultiFilterTokenizer,\n    \"multi_tokenize_remove_stopwords\": MultiRemoveStopwordsTokenizer,\n    \"multi_lemmatize\": MultiLemmatizeTokenizer,\n    \"multi_lemmatize_filter\": MultiLemmatizeFilterTokenizer,\n    \"multi_lemmatize_remove_stopwords\": MultiLemmatizeRemoveStopwordsTokenizer,\n}\n\n\nclass SentencePieceTokenizer(torch.nn.Module):\n    \"\"\"SentencePiece tokenizer using HuggingFace transformers (XLMR-based).\"\"\"\n\n    def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs):\n        super().__init__()\n        from transformers import AutoTokenizer\n\n        if pretrained_model_name_or_path is None:\n            pretrained_model_name_or_path = \"xlm-roberta-base\"\n        self.tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path)\n\n    def forward(self, v: str | list[str] | torch.Tensor):\n        if isinstance(v, torch.Tensor):\n            raise ValueError(f\"Unsupported input: {v}\")\n        if isinstance(v, str):\n            return self.tokenizer.tokenize(v)\n        return [self.tokenizer.tokenize(s) for s in v]\n\n\nclass CLIPTokenizer(torch.nn.Module):\n    \"\"\"CLIP tokenizer using HuggingFace transformers.\"\"\"\n\n    def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs):\n        super().__init__()\n        from transformers import CLIPTokenizer as HFCLIPTokenizer\n\n        if pretrained_model_name_or_path is None:\n            pretrained_model_name_or_path = \"openai/clip-vit-base-patch32\"\n        self.tokenizer = HFCLIPTokenizer.from_pretrained(pretrained_model_name_or_path)\n\n    def __call__(self, text):\n        if isinstance(text, str):\n            return self.tokenizer.tokenize(text)\n        return [self.tokenizer.tokenize(t) for t in text]\n\n    def get_vocab(self):\n        return self.tokenizer.get_vocab()\n\n\nclass GPT2BPETokenizer(torch.nn.Module):\n    \"\"\"GPT-2 BPE tokenizer using HuggingFace transformers.\"\"\"\n\n    def __init__(self, pretrained_model_name_or_path: str | None = None, **kwargs):\n        super().__init__()\n        from transformers import GPT2Tokenizer\n\n        if pretrained_model_name_or_path is None:\n            pretrained_model_name_or_path = \"gpt2\"\n        self.tokenizer = GPT2Tokenizer.from_pretrained(pretrained_model_name_or_path)\n\n    def __call__(self, text):\n        if isinstance(text, str):\n            return self.tokenizer.tokenize(text)\n        return [self.tokenizer.tokenize(t) for t in text]\n\n    def get_vocab(self):\n        return self.tokenizer.get_vocab()\n\n\nclass BERTTokenizer(torch.nn.Module):\n    \"\"\"BERT tokenizer using HuggingFace transformers.\"\"\"\n\n    def __init__(\n        self,\n        vocab_file: str | None = None,\n        pretrained_model_name_or_path: str | None = None,\n        is_hf_tokenizer: bool | None = False,\n        do_lower_case: bool | None = None,\n        **kwargs,\n    ):\n        super().__init__()\n        from transformers import BertTokenizer\n\n        if pretrained_model_name_or_path is None:\n            pretrained_model_name_or_path = \"bert-base-uncased\"\n        tokenizer_kwargs = {}\n        if do_lower_case is not None:\n            tokenizer_kwargs[\"do_lower_case\"] = do_lower_case\n        self.tokenizer = BertTokenizer.from_pretrained(pretrained_model_name_or_path, **tokenizer_kwargs)\n        self.is_hf_tokenizer = is_hf_tokenizer\n        self.pad_token = self.tokenizer.pad_token\n        self.unk_token = self.tokenizer.unk_token\n        self.cls_token_id = self.tokenizer.cls_token_id\n        self.sep_token_id = self.tokenizer.sep_token_id\n\n    def __call__(self, text):\n        if isinstance(text, str):\n            texts = [text]\n        else:\n            texts = text\n\n        if self.is_hf_tokenizer:\n            results = [self.tokenizer.encode(t) for t in texts]\n        else:\n            results = [self.tokenizer.tokenize(t) for t in texts]\n\n        return results[0] if isinstance(text, str) else results\n\n    def get_vocab(self):\n        return self.tokenizer.get_vocab()\n\n    def get_pad_token(self) -> str:\n        return self.pad_token\n\n    def get_unk_token(self) -> str:\n        return self.unk_token\n\n    def convert_token_to_id(self, token: str) -> int:\n        if token is None:\n            return 0\n        return self.tokenizer.convert_tokens_to_ids(token)\n\n\ntokenizer_registry.update(\n    {\n        \"sentencepiece\": SentencePieceTokenizer,\n        \"clip\": CLIPTokenizer,\n        \"gpt2bpe\": GPT2BPETokenizer,\n        \"bert\": BERTTokenizer,\n    }\n)\n\n\ndef get_hf_tokenizer(pretrained_model_name_or_path, **kwargs):\n    \"\"\"Gets a HuggingFace-based tokenizer that follows HF convention.\n\n    Args:\n        pretrained_model_name_or_path: Name of the model in the HF repo. Example: \"bert-base-uncased\".\n    Returns:\n        A HF tokenizer.\n    \"\"\"\n    model_name_lower = pretrained_model_name_or_path.lower()\n    # Use BERTTokenizer only for actual BERT models, not for models like albert/roberta\n    # that have \"bert\" in their name but use different tokenization (SentencePiece, BPE, etc.)\n    if \"bert\" in model_name_lower and not any(x in model_name_lower for x in (\"albert\", \"roberta\", \"distilbert\")):\n        logger.info(f\"Loading BERT tokenizer for {pretrained_model_name_or_path}\")\n        return BERTTokenizer(pretrained_model_name_or_path=pretrained_model_name_or_path, is_hf_tokenizer=True)\n\n    logger.info(f\"Loading HuggingFace tokenizer for {pretrained_model_name_or_path}\")\n    return HFTokenizer(pretrained_model_name_or_path)\n\n\ntokenizer_registry.update(\n    {\n        \"hf_tokenizer\": get_hf_tokenizer,\n    }\n)\n\n\ndef get_tokenizer_from_registry(tokenizer_name: str) -> torch.nn.Module:\n    \"\"\"Returns the appropriate tokenizer from the tokenizer registry.\"\"\"\n    if tokenizer_name in tokenizer_registry:\n        return tokenizer_registry[tokenizer_name]\n    raise KeyError(f\"Invalid tokenizer name: '{tokenizer_name}'. Available tokenizers: {tokenizer_registry.keys()}\")\n"
  },
  {
    "path": "ludwig/utils/torch_utils.py",
    "content": "import math\nimport os\nimport warnings\nfrom abc import abstractmethod\nfrom functools import lru_cache\n\nimport torch\nfrom torch import nn\nfrom torch.nn import Module, ModuleDict\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.utils.strings_utils import SpecialSymbol\n\n_TORCH_INIT_PARAMS: tuple | None = None\n\n\n@DeveloperAPI\ndef get_torch_device():\n    if torch.cuda.is_available() and torch.cuda.device_count() > 0:\n        # Use cublasLt for batched GEMM operations. The default cublas library has known\n        # bugs with cublasSgemmStridedBatched on certain GPU/driver combinations.\n        torch.backends.cuda.preferred_blas_library(\"cublaslt\")\n        return \"cuda\"\n\n    if bool(os.environ.get(\"LUDWIG_ENABLE_MPS\")):\n        if torch.backends.mps.is_available() and torch.backends.mps.is_built():\n            if not bool(os.environ.get(\"PYTORCH_ENABLE_MPS_FALLBACK\")):\n                warnings.warn(\n                    \"LUDWIG_ENABLE_MPS is set and MPS is available, but PYTORCH_ENABLE_MPS_FALLBACK has not been set. \"\n                    \"Depending on your model config, some operations may not be compatible. If errors occur, try \"\n                    \"setting `PYTORCH_ENABLE_MPS_FALLBACK=1` and resubmitting.\"\n                )\n            return \"mps\"\n        else:\n            warnings.warn(\"LUDWIG_ENABLE_MPS is set but MPS is not available, falling back to CPU.\")\n\n    return \"cpu\"\n\n\nDEVICE = get_torch_device()\n\n\n@DeveloperAPI\ndef place_on_device(x, device):\n    \"\"\"Recursively places the input on the specified device.\"\"\"\n    if isinstance(x, list):\n        return [place_on_device(xi, device) for xi in x]\n    elif isinstance(x, dict):\n        return {k: place_on_device(v, device) for k, v in x.items()}\n    elif isinstance(x, set):\n        return {place_on_device(xi, device) for xi in x}\n    elif isinstance(x, tuple):\n        return tuple(place_on_device(xi, device) for xi in x)\n    elif isinstance(x, torch.Tensor):\n        return x.to(device)\n    else:\n        return x\n\n\n@DeveloperAPI\ndef sequence_length_2D(sequence: torch.Tensor) -> torch.Tensor:\n    \"\"\"Returns the number of non-padding elements per sequence in batch.\n\n    :param sequence: (torch.Tensor) A 2D tensor of shape [batch size x max sequence length].  # Return\n    :returns: (torch.Tensor) The count on non-zero elements per sequence.\n    \"\"\"\n    used = (sequence != SpecialSymbol.PADDING.value).type(torch.int32)\n    length = torch.sum(used, 1)\n    return length\n\n\n@DeveloperAPI\ndef sequence_length_3D(sequence: torch.Tensor) -> torch.Tensor:\n    \"\"\"Returns the number of non-zero elements per sequence in batch.\n\n    :param sequence: (torch.Tensor) A 3D tensor of shape [batch size x max sequence length x hidden size].  # Return\n    :returns: (torch.Tensor) The count on non-zero elements per sequence.\n    \"\"\"\n    used = torch.sign(torch.amax(torch.abs(sequence), dim=2))\n    length = torch.sum(used, 1)\n    length = length.int()\n    return length\n\n\n@DeveloperAPI\ndef sequence_mask(lengths: torch.Tensor, maxlen: int | None = None, dtype: torch.dtype = torch.bool):\n    \"\"\"Returns a mask of shape (batch_size x maxlen), where mask[i] is True for each element up to lengths[i],\n    otherwise False i.e. if maxlen=5 and lengths[i] = 3, mask[i] = [True, True True, False False].\n\n    :param lengths: (torch.Tensor) A 1d integer tensor of shape [batch size].\n    :param maxlen: (Optional[int]) The maximum sequence length.  If not specified, the max(lengths) is used.\n    :param dtype: (type) The type to output.  # Return\n    :returns: (torch.Tensor) A sequence mask tensor of shape (batch_size x maxlen).\n    \"\"\"\n    if maxlen is None:\n        maxlen = lengths.max()\n    matrix = torch.unsqueeze(lengths, dim=-1)\n    row_vector = torch.arange(0, maxlen, 1, device=lengths.device)\n    mask = row_vector < matrix\n    mask = mask.type(dtype)\n    return mask\n\n\n@DeveloperAPI\ndef periodic(inputs: torch.Tensor, period: int) -> torch.Tensor:\n    \"\"\"Returns periodic representation assuming 0 is start of period.\"\"\"\n    return torch.cos(inputs * 2 * math.pi / period)\n\n\ninitializer_registry = {\n    \"uniform\": nn.init.uniform_,\n    \"normal\": nn.init.normal_,\n    \"constant\": nn.init.constant_,\n    \"ones\": nn.init.ones_,\n    \"zeros\": nn.init.zeros_,\n    \"eye\": nn.init.eye_,\n    \"dirac\": nn.init.dirac_,\n    \"xavier_uniform\": nn.init.xavier_uniform_,\n    \"xavier_normal\": nn.init.xavier_normal_,\n    \"kaiming_uniform\": nn.init.kaiming_uniform_,\n    \"kaiming_normal\": nn.init.kaiming_normal_,\n    \"orthogonal\": nn.init.orthogonal_,\n    \"sparse\": nn.init.sparse_,\n    \"identity\": nn.init.eye_,\n}\n\nactivations = {\n    \"elu\": nn.ELU,\n    \"leakyRelu\": nn.LeakyReLU,\n    \"logSigmoid\": nn.LogSigmoid,\n    \"relu\": nn.ReLU,\n    \"sigmoid\": nn.Sigmoid,\n    \"tanh\": nn.Tanh,\n    \"softmax\": nn.Softmax,\n    None: nn.Identity,\n}\n\n\n@DeveloperAPI\ndef get_activation(activation):\n    return activations[activation]()\n\n\n@DeveloperAPI\ndef reg_loss(model: nn.Module, regularizer: str, l1: float = 0.01, l2: float = 0.01):\n    \"\"\"Computes the regularization loss for a given model.\n\n    Parameters:\n        model: torch.nn.Module object to compute regularization loss for.\n        regularizer: regularizer to use (currently l1, l2 and l1_l2 supported).\n        l1: L1 regularization coefficient.\n        l2: L2 regularization coefficient.\n\n    Returns:\n        Regularization loss for the model (float).\n    \"\"\"\n\n    if regularizer == \"l1\":\n        l1_reg = l1 * sum(torch.abs(p).sum() for p in model.parameters())\n        return l1_reg\n    if regularizer == \"l2\":\n        l2_reg = l2 * sum(torch.square(p).sum() for p in model.parameters())\n        return l2_reg\n    if regularizer == \"l1_l2\":\n        l1_reg = l1 * sum(torch.abs(p).sum() for p in model.parameters())\n        l2_reg = l2 * sum(torch.square(p).sum() for p in model.parameters())\n        return l1_reg + l2_reg\n\n\n@DeveloperAPI\nclass LudwigModule(Module):\n    def __init__(self):\n        super().__init__()\n        self._losses = {}\n        self.register_buffer(\"device_tensor\", torch.zeros(0), persistent=False)\n\n    @property\n    def device(self):\n        return self.device_tensor.device\n\n    def prepare_for_training(self):\n        \"\"\"This is called from within the Trainer object to do any final instantiation before model training.\"\"\"\n\n    def losses(self):\n        collected_losses = []\n        for loss in self._losses.values():\n            collected_losses.append(loss)\n\n        for child in self.children():\n            if isinstance(child, LudwigModule):\n                collected_losses.extend(child.losses())\n            elif isinstance(child, ModuleDict):\n                for c in child.values():\n                    if hasattr(c, \"losses\"):  # Some modules, i.e. SequenceReducers, don't have losses.\n                        collected_losses.extend(c.losses())\n            elif isinstance(child, Module):\n                pass\n            else:\n                raise ValueError\n\n        return collected_losses\n\n    def update_loss(self, key: str, loss: torch.Tensor):\n        \"\"\"This should be called in the forward pass to add a custom loss term to the combined loss.\"\"\"\n        self._losses[key] = loss\n\n    @property\n    def input_dtype(self):\n        return torch.float32\n\n    @property\n    @abstractmethod\n    def input_shape(self) -> torch.Size:\n        \"\"\"Returns size of the input tensor without the batch dimension.\"\"\"\n        # raise NotImplementedError(\"Abstract class.\")\n\n    @property\n    def output_shape(self) -> torch.Size:\n        \"\"\"Returns size of the output tensor without the batch dimension.\"\"\"\n        return self._computed_output_shape()\n\n    @lru_cache(maxsize=1)\n    def _computed_output_shape(self) -> torch.Size:\n        dummy_input = torch.rand(2, *self.input_shape, device=self.device)\n        output_tensor = self.forward(dummy_input.type(self.input_dtype))\n\n        if isinstance(output_tensor, torch.Tensor):\n            return output_tensor.size()[1:]\n        elif isinstance(output_tensor, dict) and ENCODER_OUTPUT in output_tensor:\n            return output_tensor[ENCODER_OUTPUT].size()[1:]\n        else:\n            raise ValueError(\"Unknown output tensor type.\")\n\n\ndef freeze_parameters(module: nn.Module):\n    \"\"\"Freezes the parameters of a torch module.\"\"\"\n    for p in module.parameters():\n        p.requires_grad = False\n\n\n@DeveloperAPI\nclass FreezeModule(nn.Module):\n    def __init__(self, module: nn.Module, frozen: bool):\n        super().__init__()\n        if frozen:\n            freeze_parameters(module)\n            module.eval()\n        else:\n            module.train()\n        self.module = module\n        self.frozen = frozen\n\n    def train(self, mode: bool = True):\n        if self.frozen:\n            # Ignores any attempt to set params trainable\n            return self\n\n        return super().train(mode)\n\n\n@DeveloperAPI\nclass Dense(LudwigModule):\n    def __init__(\n        self,\n        input_size,\n        output_size,\n        use_bias=True,\n        weights_initializer=\"xavier_uniform\",\n        bias_initializer=\"zeros\",\n    ):\n        super().__init__()\n        self.dense = nn.Linear(in_features=input_size, out_features=output_size, bias=use_bias)\n        weights_initializer = initializer_registry[weights_initializer]\n        weights_initializer(self.dense.weight)\n\n        if use_bias:\n            bias_initializer = initializer_registry[bias_initializer]\n            bias_initializer(self.dense.bias)\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return self.dense.input_shape\n\n    def forward(self, input: torch.Tensor) -> torch.Tensor:\n        output = torch.squeeze(self.dense(input), dim=-1)\n        return output\n\n\n@DeveloperAPI\ndef initialize_pytorch(\n    gpus: int | str | list[int] | None = None,\n    gpu_memory_limit: float | None = None,\n    allow_parallel_threads: bool = True,\n):\n    param_tuple = (gpus, gpu_memory_limit, allow_parallel_threads)\n    if _TORCH_INIT_PARAMS is not None:\n        if _TORCH_INIT_PARAMS != param_tuple:\n            warnings.warn(\n                \"PyTorch has already been initialized. Changes to `gpus`, \"\n                \"`gpu_memory_limit`, and `allow_parallel_threads` will be ignored. \"\n                \"Start a new Python process to modify these values.\"\n            )\n        return\n\n    # For reproducivility / determinism, set parallel threads to 1.\n    # For performance, leave unset to allow PyTorch to select the best value automatically.\n    if not allow_parallel_threads:\n        torch.set_num_threads(1)\n        torch.set_num_interop_threads(1)\n        if torch.cuda.is_available() and torch.cuda.device_count() > 0:\n            torch.backends.cudnn.deterministic = True\n            torch.backends.cudnn.benchmark = False\n\n    if isinstance(gpus, int):\n        gpus = [gpus]\n    elif isinstance(gpus, str):\n        gpus = gpus.strip()\n        gpus = [int(g) for g in gpus.split(\",\")]\n\n    if gpus and len(gpus) == 1 and gpus[0] == -1:\n        # CUDA_VISIBLE_DEVICES syntax for disabling all GPUs\n        os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"\"\n    elif torch.cuda.is_available() and torch.cuda.device_count() > 0:\n        # Set visible devices so GPU utilization is isolated\n        # (no GPU contention between workers).\n        if gpus is not None:\n            if len(gpus) == 1:\n                torch.cuda.set_device(gpus[0])\n            elif len(gpus) > 1:\n                os.environ[\"CUDA_VISIBLE_DEVICES\"] = \",\".join(str(i) for i in gpus)\n\n        # Limit the amount of memory that can be consumed per GPU\n        if gpu_memory_limit is not None:\n            for gpu in gpus or range(torch.cuda.device_count()):\n                torch.cuda.memory.set_per_process_memory_fraction(gpu_memory_limit, gpu)\n\n    _set_torch_init_params(param_tuple)\n\n\ndef _set_torch_init_params(params: tuple | None):\n    global _TORCH_INIT_PARAMS\n    _TORCH_INIT_PARAMS = params\n\n\ndef _get_torch_init_params() -> tuple | None:\n    return _TORCH_INIT_PARAMS\n\n\n@DeveloperAPI\ndef model_size(model: nn.Module):\n    \"\"\"Computes PyTorch model size in bytes.\"\"\"\n    size = 0\n    size += sum(param.nelement() * param.element_size() for param in model.parameters())\n    size += sum(buffer.nelement() * buffer.element_size() for buffer in model.buffers())\n    return size\n"
  },
  {
    "path": "ludwig/utils/trainer_utils.py",
    "content": "import logging\nimport re\nfrom collections import defaultdict\nfrom typing import TYPE_CHECKING\n\ntry:\n    from typing import Literal\nexcept ImportError:\n    from typing import Literal\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import AUTO, COMBINED, LOSS\nfrom ludwig.models.base import BaseModel\nfrom ludwig.models.ecd import ECD\nfrom ludwig.models.llm import LLM\nfrom ludwig.modules.metric_modules import get_best_function\nfrom ludwig.schema.trainer import ECDTrainerConfig, FineTuneTrainerConfig\nfrom ludwig.utils.data_utils import save_json\nfrom ludwig.utils.metric_utils import TrainerMetric\n\nif TYPE_CHECKING:\n    from ludwig.features.base_feature import OutputFeature\n    from ludwig.schema.trainer import BaseTrainerConfig\n\n\nlogger = logging.getLogger(__name__)\n\n\n@DeveloperAPI\ndef initialize_trainer_metric_dict(output_features) -> dict[str, dict[str, list[TrainerMetric]]]:\n    \"\"\"Returns a dict of dict of metrics, output_feature_name -> metric_name -> List[TrainerMetric].\"\"\"\n    metrics = defaultdict(lambda: defaultdict(list))\n    return metrics\n\n\ndef get_latest_metrics_dict(\n    progress_tracker_metrics: dict[str, dict[str, list[TrainerMetric]]],\n) -> dict[str, dict[str, float]]:\n    \"\"\"Returns a dict of field name -> metric name -> latest metric value.\"\"\"\n    latest_metrics_dict = defaultdict(dict)\n    for feature_name, metrics_dict in progress_tracker_metrics.items():\n        for metric_name, metrics in metrics_dict.items():\n            if metrics:\n                # Metrics may be missing if computing metrics was excepted, if the metrics are entirely empty\n                # due to a missing subset, or if evaluate_training_set is False.\n                latest_metrics_dict[feature_name][metric_name] = metrics[-1][-1]\n    return latest_metrics_dict\n\n\n@DeveloperAPI\ndef get_new_progress_tracker(\n    batch_size: int,\n    best_eval_metric_value: float,\n    best_increase_batch_size_eval_metric: float,\n    learning_rate: float,\n    output_features: dict[str, \"OutputFeature\"],\n):\n    \"\"\"Returns a new instance of a ProgressTracker with empty metrics.\"\"\"\n    return ProgressTracker(\n        epoch=0,\n        batch_size=batch_size,\n        steps=0,\n        tune_checkpoint_num=0,\n        checkpoint_number=0,\n        best_eval_metric_steps=0,\n        best_eval_metric_epoch=0,\n        best_eval_metric_checkpoint_number=0,\n        last_learning_rate_reduction_steps=0,\n        last_increase_batch_size_steps=0,\n        last_improvement_steps=0,\n        best_eval_metric_value=best_eval_metric_value,\n        best_increase_batch_size_eval_metric=best_increase_batch_size_eval_metric,\n        last_increase_batch_size_eval_metric_improvement=0,\n        learning_rate=learning_rate,\n        num_reductions_learning_rate=0,\n        num_increases_batch_size=0,\n        train_metrics=initialize_trainer_metric_dict(output_features),\n        validation_metrics=initialize_trainer_metric_dict(output_features),\n        test_metrics=initialize_trainer_metric_dict(output_features),\n        last_learning_rate_reduction=0,\n        last_increase_batch_size=0,\n        best_eval_train_metrics={},\n        best_eval_validation_metrics={},\n        best_eval_test_metrics={},\n        llm_eval_examples={},\n        checkpoint_to_step={},\n        checkpoint_to_epoch={},\n        incremental_step_token_usage={},\n        cumulative_step_token_usage={},\n        incremental_checkpoint_token_usage={},\n        cumulative_checkpoint_token_usage={},\n        total_tokens_used=0,\n    )\n\n\n@DeveloperAPI\nclass ProgressTracker:\n    def __init__(\n        self,\n        epoch: int,\n        batch_size: int,\n        steps: int,\n        tune_checkpoint_num: int,\n        checkpoint_number: int,\n        best_eval_metric_steps: int,\n        best_eval_metric_epoch: int,\n        best_eval_metric_checkpoint_number: int,\n        last_improvement_steps: int,\n        last_learning_rate_reduction_steps: int,\n        last_increase_batch_size_steps: int,\n        best_eval_metric_value: float,\n        best_increase_batch_size_eval_metric: float,\n        last_increase_batch_size_eval_metric_improvement: int,\n        learning_rate: float,\n        num_reductions_learning_rate: int,\n        num_increases_batch_size: int,\n        train_metrics: dict[str, dict[str, list[TrainerMetric]]],\n        validation_metrics: dict[str, dict[str, list[TrainerMetric]]],\n        test_metrics: dict[str, dict[str, list[TrainerMetric]]],\n        last_learning_rate_reduction: int,\n        last_increase_batch_size: int,\n        best_eval_train_metrics: dict[str, dict[str, float]],\n        best_eval_validation_metrics: dict[str, dict[str, float]],\n        best_eval_test_metrics: dict[str, dict[str, float]],\n        llm_eval_examples: dict[str, list[str]] = None,\n        checkpoint_to_step: dict[str, int] = None,\n        checkpoint_to_epoch: dict[str, int] = None,\n        incremental_step_token_usage: dict[str, int] = None,\n        cumulative_step_token_usage: dict[str, int] = None,\n        incremental_checkpoint_token_usage: dict[str, int] = None,\n        cumulative_checkpoint_token_usage: dict[str, int] = None,\n        total_tokens_used: int = 0,\n    ):\n        \"\"\"JSON-serializable holder object that stores information related to training progress.\n\n        [train/vali/test]_metrics is a nested dictionary of TrainerMetrics: feature_name -> metric_name ->\n        List[TrainerMetrics], with one entry per training checkpoint.\n\n        When the model is saved, all of the progress tracker's attributes are serialized to JSON as\n        `training_progress.json` under the model output directory.\n\n        JSON serialization automatically converts all dictionary top-level keys to strings, and the string typing\n        is preserved when the progress tracker is deserialized from JSON when model resumes training from a checkpoint.\n\n        For this reason, all of the dictionary attributes of the progress tracker are keyed by strings to ensure a\n        consistent interface before or after deserialization. For example, the `tokens` dictionaries are keyed by steps,\n        as strings.\n\n        When the progress tracker is deserialized from JSON like when a model resumes training from a checkpoint, the\n        TrainerMetrics namedtuples are automatically converted into regular (epoch, steps, value) tuples, which is why\n        in trainer.py, we often use `[-1]` to index into the last element of the TrainerMetric namedtuple to get the\n        actual metric value instead of the named field.\n\n        Args:\n            epoch: The current epoch number.\n            steps: The current step of training.\n            batch_size: The current batch size.\n            tune_checkpoint_num: The hyperopt checkpoint number (Ray Tune).\n            checkpoint_number: The current checkpoint number.\n\n            best_eval_metric_steps: The step of training that has the best evaluation so far.\n            best_eval_metric_epoch: The epoch of training that has the best evaluation so far.\n            best_eval_metric_checkpoint_number: The checkpoint number that has the best evaluation so far.\n\n            last_improvement_steps: The number of steps since the last improvement.\n            last_learning_rate_reduction_steps: The training step of the last learning rate reduction.\n            last_increase_batch_size_steps: The training_step of the the last batch size increase.\n\n            best_eval_metric_value: The metric value of the best evaluation so far.\n            best_increase_batch_size_eval_metric:\n                The metric value of the best evaluation so far, for increasing the batch size.\n\n            last_learning_rate_reduction: The number of steps since the last learning rate reduction.\n            last_increase_batch_size: The number of steps since the last batch size increase.\n\n            last_increase_batch_size_eval_metric_improvement:\n                The number of checkpoints since the last batch size increase.\n\n            num_reductions_learning_rate: The number of total reductions in learning rate.\n            num_increases_batch_size: The number of total increases in batch size.\n\n            train_metrics: Training metrics. <output feature name> -> <metric name> -> History of metrics.\n            validation_metrics: Validation metrics. <output feature name> -> <metric name> -> History of metrics.\n            test_metrics: Test metrics. <output feature name> -> <metric name> -> History of metrics.\n\n            best_eval_train_metrics:\n                Best eval train metrics: <output feature name> -> <metric name> -> <metric value>.\n            best_eval_validation_metrics:\n                Best eval validation metrics: <output feature name> -> <metric name> -> <metric value>.\n            best_eval_test_metrics:\n                Best eval test metrics: <output feature name> -> <metric name> -> <metric value>.\n\n            llm_eval_examples:\n                Dictionary whose keys are \"inputs\", \"targets\", and \"outputs\" and whose values are dicts.\n                The keys of each subdict are the names of the input/target/output features and the values are lists of\n                example tensors. This is only set for LLM fine-tuning.\n\n            checkpoint_to_step: Map of checkpoint number to step number.\n            checkpoint_to_epoch: Map of checkpoint number to epoch number.\n\n            incremental_step_token_usage: Map of step number to number of tokens used in that step.\n            cumulative_step_token_usage: Map of step number to cumulative number of tokens used up to that step.\n            incremental_checkpoint_token_usage: Map of checkpoint number to number of tokens used up to that checkpoint\n                since the last checkpoint.\n            cumulative_checkpoint_token_usage: Map of checkpoint number to cumulative number of tokens used up to that\n                checkpoint.\n            total_tokens_used: Total number of tokens used.\n        \"\"\"\n        self.batch_size = batch_size\n        self.epoch = epoch\n        self.steps = steps\n        self.tune_checkpoint_num = tune_checkpoint_num\n        self.checkpoint_number = checkpoint_number\n        self.best_eval_metric_steps = best_eval_metric_steps\n        self.best_eval_metric_epoch = best_eval_metric_epoch\n        self.best_eval_metric_checkpoint_number = best_eval_metric_checkpoint_number\n        self.last_improvement_steps = last_improvement_steps\n        self.last_learning_rate_reduction_steps = last_learning_rate_reduction_steps\n        self.last_learning_rate_reduction = last_learning_rate_reduction\n        self.last_increase_batch_size_steps = last_increase_batch_size_steps\n        self.last_increase_batch_size = last_increase_batch_size\n        self.learning_rate = learning_rate\n        self.best_eval_metric_value = best_eval_metric_value\n        self.best_increase_batch_size_eval_metric = best_increase_batch_size_eval_metric\n        self.last_increase_batch_size_eval_metric_improvement = last_increase_batch_size_eval_metric_improvement\n        self.num_reductions_learning_rate = num_reductions_learning_rate\n        self.num_increases_batch_size = num_increases_batch_size\n        self.train_metrics = train_metrics\n        self.validation_metrics = validation_metrics\n        self.test_metrics = test_metrics\n\n        # This should be an dictionary whose keys are \"inputs\", \"targets\", and \"outputs\" and whose values are dicts.\n        # The keys of each subdict are the names of the input/target/output features and the values are lists of\n        # example tensors. This is only set for LLM fine-tuning.\n        self.llm_eval_examples = llm_eval_examples\n\n        # Best metrics.\n        self.best_eval_train_metrics = best_eval_train_metrics\n        self.best_eval_validation_metrics = best_eval_validation_metrics\n        self.best_eval_test_metrics = best_eval_test_metrics\n\n        # Checkpoint tracking.\n        self.checkpoint_to_step = checkpoint_to_step\n        self.checkpoint_to_epoch = checkpoint_to_epoch\n\n        # Token usage.\n        self.incremental_step_token_usage = incremental_step_token_usage\n        self.cumulative_step_token_usage = cumulative_step_token_usage\n        self.incremental_checkpoint_token_usage = incremental_checkpoint_token_usage\n        self.cumulative_checkpoint_token_usage = cumulative_checkpoint_token_usage\n        self.total_tokens_used = total_tokens_used\n\n    def save(self, filepath):\n        # sort_keys=False to ensure that token usage dictionaries (keyed by integers) are encodable.\n        # save_json(filepath, self.__dict__, sort_keys=False)\n        save_json(filepath, self.__dict__)\n\n    @staticmethod\n    def load(progress_tracking_dict: dict):\n        from ludwig.utils.backward_compatibility import upgrade_model_progress\n\n        loaded = upgrade_model_progress(progress_tracking_dict)\n        return ProgressTracker(**loaded)\n\n    def log_metrics(self):\n        log_metrics = {\n            \"batch_size\": self.batch_size,\n            \"epoch\": self.epoch,\n            \"steps\": self.steps,\n            \"tune_checkpoint_num\": self.tune_checkpoint_num,\n            \"checkpoint_number\": self.checkpoint_number,\n            \"last_improvement_steps\": self.last_improvement_steps,\n            \"best_eval_metric_steps\": self.best_eval_metric_steps,\n            \"best_eval_metric_epoch\": self.best_eval_metric_epoch,\n            \"best_eval_metric_checkpoint_number\": self.best_eval_metric_checkpoint_number,\n            \"learning_rate\": self.learning_rate,\n            \"best_valid_metric\": self.best_eval_metric_value,\n            \"num_reductions_lr\": self.num_reductions_learning_rate,\n            \"num_increases_bs\": self.num_increases_batch_size,\n            \"total_tokens_used\": self.total_tokens_used,\n        }\n\n        # This is a non-numerical metric that is only for LLM fine-tuning\n        # This should be an dictionary whose keys are \"inputs\", \"targets\", and \"outputs\" and whose values are dicts.\n        # The keys of each subdict are the names of the input/target/output features and the values are lists of\n        # example tensors.\n        if self.llm_eval_examples:\n            log_metrics[\"llm_eval_examples\"] = self.llm_eval_examples\n\n        for metrics_dict_name in [\n            \"train_metrics\",\n            \"validation_metrics\",\n            \"test_metrics\",\n        ]:\n            metrics_dict = getattr(self, metrics_dict_name)\n            for feature_name in metrics_dict:\n                for metric_name, metrics_tuples in metrics_dict[feature_name].items():\n                    if metrics_tuples:\n                        # For logging, get the latest metrics. The second \"-1\" indexes into the TrainerMetric\n                        # namedtuple. The last element of the TrainerMetric namedtuple is the actual metric value.\n                        #\n                        # TODO: when loading an existing model, this loses metric values for all but the last epoch.\n                        log_metrics[f\"{metrics_dict_name}.{feature_name}.{metric_name}\"] = metrics_tuples[-1][-1]\n\n        # Add best metrics.\n        for feature_name, metrics in self.best_eval_train_metrics.items():\n            for metric_name, metric_value in metrics.items():\n                log_metrics[f\"best.train_metrics.{feature_name}.{metric_name}\"] = metric_value\n        for feature_name, metrics in self.best_eval_validation_metrics.items():\n            for metric_name, metric_value in metrics.items():\n                log_metrics[f\"best.validation_metrics.{feature_name}.{metric_name}\"] = metric_value\n        for feature_name, metrics in self.best_eval_test_metrics.items():\n            for metric_name, metric_value in metrics.items():\n                log_metrics[f\"best.test_metrics.{feature_name}.{metric_name}\"] = metric_value\n\n        return log_metrics\n\n    def _add_checkpoint_entry_for_used_tokens(self, checkpoint_number: int):\n        \"\"\"Adds an entry to the token usage dictionaries for the given checkpoint number.\n\n        Assumes that the token usage dictionaries for steps are filled.\n        \"\"\"\n        self.cumulative_checkpoint_token_usage[str(checkpoint_number)] = self.total_tokens_used\n\n        if checkpoint_number <= 0:\n            raise ValueError(\"Checkpoint number should be greater than 0.\")\n\n        if checkpoint_number == 1:\n            # The incremental token usage for checkpoint 0 is the same as the total tokens used so far.\n            self.incremental_checkpoint_token_usage[str(checkpoint_number)] = self.total_tokens_used\n        else:\n            # The incremental token usage for this checkpoint is the total tokens used minus the cumulative tokens used\n            # up to the previous checkpoint.\n            previous_checkpoint_number = checkpoint_number - 1\n\n            tokens_used_since_previous_checkpoint = (\n                self.total_tokens_used - self.cumulative_checkpoint_token_usage[str(previous_checkpoint_number)]\n            )\n            self.incremental_checkpoint_token_usage[str(checkpoint_number)] = tokens_used_since_previous_checkpoint\n\n    def increment_checkpoint(self):\n        \"\"\"Update the progress tracker for a new checkpoint.\"\"\"\n        self.checkpoint_number += 1\n\n        # Set checkpoint -> step/epoch lookup maps.\n        self.checkpoint_to_step[str(self.checkpoint_number)] = self.steps\n        self.checkpoint_to_epoch[str(self.checkpoint_number)] = self.epoch\n\n        # Set checkpoint -> used tokens lookup maps.\n        self._add_checkpoint_entry_for_used_tokens(self.checkpoint_number)\n\n    def set_token_usage_for_this_step(self, used_tokens: int):\n        \"\"\"Update the token usage for the current step.\"\"\"\n        steps_str = str(self.steps)\n        self.incremental_step_token_usage[steps_str] = used_tokens\n        self.total_tokens_used += used_tokens\n        self.cumulative_step_token_usage[steps_str] = self.total_tokens_used\n\n\n@DeveloperAPI\ndef append_metrics(\n    model: BaseModel,\n    dataset_name: Literal[\"train\", \"validation\", \"test\"],\n    results: dict[str, dict[str, float]],\n    metrics_log: dict[str, dict[str, list[TrainerMetric]]],\n    progress_tracker: ProgressTracker,\n) -> dict[str, dict[str, list[TrainerMetric]]]:\n    epoch = progress_tracker.epoch\n    steps = progress_tracker.steps\n    for output_feature in model.output_features:\n        scores = [dataset_name]\n\n        # collect metric names based on output features metrics to\n        # ensure consistent order of reporting metrics\n        metric_names = sorted(results[output_feature].keys())\n\n        for metric in metric_names:\n            if metric in results[output_feature]:\n                # Some metrics may have been excepted and excluded from results.\n                score = results[output_feature][metric]\n                metrics_log[output_feature][metric].append(TrainerMetric(epoch=epoch, step=steps, value=score))\n                scores.append(score)\n\n    metrics_log[COMBINED][LOSS].append(TrainerMetric(epoch=epoch, step=steps, value=results[COMBINED][LOSS]))\n    return metrics_log\n\n\n@DeveloperAPI\ndef get_total_steps(epochs: int, steps_per_epoch: int, train_steps: int):\n    \"\"\"Returns train_steps if provided, otherwise epochs * steps_per_epoch.\"\"\"\n    if train_steps:\n        return train_steps\n    return epochs * steps_per_epoch\n\n\n@DeveloperAPI\ndef get_final_steps_per_checkpoint(\n    steps_per_epoch: int, steps_per_checkpoint: int = 0, checkpoints_per_epoch: float = 0, should_log: bool = False\n):\n    \"\"\"Returns the steps per checkpoint to use for the training loop, given user+default inputs.\"\"\"\n    if steps_per_checkpoint != 0 and checkpoints_per_epoch != 0:\n        raise ValueError(\n            \"It is invalid to specify both checkpoints_per_epoch AND steps_per_checkpoint. Please specify one or the \"\n            \"other, or specify neither to checkpoint/eval the model every epoch.\"\n        )\n\n    # Set steps_per_checkpoint based on the checkpoints_per_epoch, if checkpoints_per_epoch was specified.\n    if checkpoints_per_epoch != 0:\n        steps_per_checkpoint = int(steps_per_epoch / checkpoints_per_epoch)\n\n    # Cap steps_per_checkpoint at steps_per_epoch.\n    if steps_per_checkpoint > steps_per_epoch:\n        if should_log:\n            logger.info(\n                f\"Note: steps_per_checkpoint (was {steps_per_checkpoint}) is now set to the number of \"\n                f\"steps per epoch: {steps_per_epoch}.\\n\"\n            )\n        return steps_per_epoch\n\n    # steps_per_checkpoint wasn't specified. Use steps_per_epoch.\n    if steps_per_checkpoint == 0:\n        return steps_per_epoch\n\n    return steps_per_checkpoint\n\n\ndef get_total_expected_checkpoints(total_steps: int, final_steps_per_checkpoint: int, epochs: int) -> int:\n    return total_steps // final_steps_per_checkpoint + epochs\n\n\n@DeveloperAPI\ndef get_training_report(\n    validation_field: str,\n    validation_metric: str,\n    include_test_set: bool,\n    train_valiset_stats: dict[str, dict[str, list[float]]],\n    train_testset_stats: dict[str, dict[str, list[float]]],\n) -> list[tuple[str, str]]:\n    \"\"\"Returns a training report in the form of a list [(report item, value)].\"\"\"\n    validation_field_result = train_valiset_stats[validation_field]\n    best_function = get_best_function(validation_metric)\n\n    training_report = []\n    best_vali_index, (\n        epoch_best_validation_metric,\n        step_best_validation_metric,\n        best_validation_metric,\n    ) = best_function(\n        enumerate(validation_field_result[validation_metric]),\n        # -1 for the last element of the TrainerMetric namedtuple.\n        key=lambda index_epoch_step_value: index_epoch_step_value[1][-1],\n    )\n    training_report.append([\"Validation feature\", validation_field])\n    training_report.append([\"Validation metric\", validation_metric])\n    training_report.append([\"Best model step\", step_best_validation_metric])\n    training_report.append([\"Best model epoch\", epoch_best_validation_metric + 1])\n    training_report.append(\n        [\n            f\"Best model's validation {validation_metric}\",\n            best_validation_metric,\n        ]\n    )\n    if include_test_set:\n        validation_selected_test_metric_score = train_testset_stats[validation_field][validation_metric][\n            best_vali_index\n        ][\n            -1\n        ]  # -1 for the last element of the TrainerMetric namedtuple.\n\n        training_report.append(\n            [\n                f\"Best model's test {validation_metric}\",\n                validation_selected_test_metric_score,\n            ]\n        )\n    return training_report\n\n\ndef get_rendered_batch_size_grad_accum(config: \"BaseTrainerConfig\", num_workers: int) -> tuple[int, int]:\n    \"\"\"Returns the batch size and gradient accumulation steps to use for training.\n\n    For batch_size==AUTO:\n    1. effective_batch_size is not AUTO and gradient_accumulation_steps is not AUTO:\n        batch size is set to the effective batch size divided by the gradient accumulation steps, divided by the\n        number of workers.\n    2. effective_batch_size is AUTO or gradient_accumulation_steps is AUTO:\n        batch size remains AUTO.\n\n    For gradient_accumulation_steps==AUTO:\n    1. batch size is AUTO:\n        gradient accumulation steps remains AUTO.\n    2. batch_size is not AUTO and effective batch size is not AUTO:\n        gradient accumulation steps is set to the effective batch size divided by the batch size, divided by the number\n        of workers.\n    3. batch size is not AUTO and effective batch size is AUTO:\n        gradient accumulation steps is set to 1.\n    \"\"\"\n    effective_batch_size = config.effective_batch_size\n    batch_size = config.batch_size\n    gradient_accumulation_steps = config.gradient_accumulation_steps\n\n    if config.batch_size == AUTO:\n        if config.effective_batch_size != AUTO and config.gradient_accumulation_steps != AUTO:\n            batch_size = max(int(effective_batch_size / gradient_accumulation_steps / num_workers), 1)\n\n    if config.gradient_accumulation_steps == AUTO:\n        if config.batch_size != AUTO:\n            if config.effective_batch_size != AUTO:\n                gradient_accumulation_steps = max(int(effective_batch_size / batch_size / num_workers), 1)\n            else:\n                gradient_accumulation_steps = 1\n\n    return batch_size, gradient_accumulation_steps\n\n\ndef freeze_layers_regex(config: ECDTrainerConfig | FineTuneTrainerConfig, model: ECD | LLM) -> None:\n    \"\"\"Freezes layers in a model whose names match a specified regular expression pattern.\n\n    This function iterates over all parameters of the model, checking each parameter's name against\n    the regular expression defined in the configuration object.\n    If a match is found, the parameter's `requires_grad` attribute is set to False,\n    effectively freezing the layer for training purposes.\n    If no matches are found, an error is logged indicating the issue with the regex or the model's layer names.\n\n    Parameters:\n    - config (Union[ECDTrainerConfig, FineTuneTrainerConfig]):\n    - model (Union[ECD, LLM]): The model object containing layers and parameters. This could be an instance of either\n    ECD or LLM classes, which should have a method `named_parameters()` that yields the name and parameter\n    object of each layer.\n\n    Raises:\n    - re.error: If the regular expression pattern in `config.layers_to_freeze_regex` is invalid, an error is logged\n    and the function exits.\n\n    Returns:\n    - None: This function does not return any value but modifies the model in-place by freezing certain layers.\n    \"\"\"\n    pattern = re.compile(config.layers_to_freeze_regex)\n    matched_layers = set()\n\n    for name, p in model.named_parameters():\n        if re.search(pattern, str(name)):\n            p.requires_grad = False\n            matched_layers.add(name)\n    if matched_layers:\n        logger.info(f\"Layers where requires_grad was set to False: {matched_layers}\")\n    else:\n        logger.error(f\"No regex match for {config.layers_to_freeze_regex}! Check layer names and regex syntax.\")\n\n    count_parameters(model)\n\n\ndef count_parameters(model) -> None:\n    \"\"\"Counts number of trainable parameters post freezing.\n\n    Returns:\n    - None: This function does not return any value.\n    \"\"\"\n    total_params = 0\n    for _, parameter in model.named_parameters():\n        if not parameter.requires_grad:\n            continue\n        params = parameter.numel()\n\n        total_params += params\n\n    logger.info(f\"Total Trainable Parameters after freezing: {total_params}\")\n"
  },
  {
    "path": "ludwig/utils/triton_utils.py",
    "content": "import importlib.util\nimport os\nimport re\nimport shutil\nimport tempfile\nfrom dataclasses import dataclass\n\nimport pandas as pd\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BINARY,\n    CATEGORY,\n    DATE,\n    IMAGE,\n    INPUT_FEATURES,\n    POSTPROCESSOR,\n    PREDICTOR,\n    PREPROCESSOR,\n    SEQUENCE,\n    SET,\n    TEXT,\n    TIMESERIES,\n    TYPE,\n    VECTOR,\n)\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset\nfrom ludwig.models.inference import (\n    _InferencePostprocessor,\n    _InferencePredictor,\n    _InferencePreprocessor,\n    InferenceModule,\n)\nfrom ludwig.types import ModelConfigDict\nfrom ludwig.utils.inference_utils import to_inference_module_input_from_dataframe\nfrom ludwig.utils.misc_utils import remove_empty_lines\nfrom ludwig.utils.torch_utils import model_size, place_on_device\nfrom ludwig.utils.types import TorchAudioTuple, TorchscriptPreprocessingInput\n\nFEATURES_TO_CAST_AS_STRINGS = {BINARY, CATEGORY, BAG, SET, TEXT, SEQUENCE, TIMESERIES, VECTOR}\n\nINFERENCE_STAGES = [PREPROCESSOR, PREDICTOR, POSTPROCESSOR]\nINPUT = \"INPUT\"\nOUTPUT = \"OUTPUT\"\nENSEMBLE = \"ensemble\"\n\nINFERENCE_MODULE_TEMPLATE = \"\"\"\nfrom typing import Any, Dict, List, Union\nimport torch\nfrom ludwig.utils.types import TorchscriptPreprocessingInput\n\nclass GeneratedInferenceModule(torch.nn.Module):\n    def __init__(self, inference_module):\n        super().__init__()\n        self.inference_module = inference_module\n\n    def forward(self, {input_signature}):\n        with torch.no_grad():\n            inputs: Dict[str, {input_type}] = {input_dict}\n            results = self.inference_module(inputs)\n            return {output_tuple}\n\"\"\"\n\nFEATURE_RESHAPE_SPEC = \"\"\"reshape: {{ shape: [ {reshape_dims} ] }}\n\"\"\"\n\nTRITON_SPEC = \"\"\"\n    {{\n        name: \"{key}\"\n        data_type: {data_type}\n        dims: [ {data_dims} ]\n        {reshape_spec}\n    }}\"\"\"\n\nINSTANCE_SPEC = \"\"\"\n    {{\n        count: {count}\n        kind: {kind}\n    }}\"\"\"\n\nDYNAMIC_BATCHING_TEMPLATE = \"\"\"dynamic_batching {{\n    max_queue_delay_microseconds: {delay}\n}}\"\"\"\n\nTRITON_CONFIG_TEMPLATE = \"\"\"name: \"{model_name}\"\nplatform: \"pytorch_libtorch\"\nmax_batch_size: {max_batch_size}\n{dynamic_batching_spec}\ninput [{input_spec}\n]\noutput [{output_spec}\n]\ninstance_group [{instance_spec}\n]\n\"\"\"\n\nENSEMBLE_SCHEDULING_INPUT_MAP = \"\"\"\n      input_map {{\n        key: \"{key}\"\n        value: \"{value}\"\n      }}\"\"\"\nENSEMBLE_SCHEDULING_OUTPUT_MAP = \"\"\"\n      output_map {{\n        key: \"{key}\"\n        value: \"{value}\"\n      }}\"\"\"\n\nENSEMBLE_SCHEDULING_STEP = \"\"\"\n    {{\n      model_name: \"{ensemble_model_name}\"\n      model_version: -1\n      {input_maps}\n      {output_maps}\n    }}\"\"\"\n\nTRITON_ENSEMBLE_CONFIG_TEMPLATE = \"\"\"name: \"{model_name}\"\nplatform: \"ensemble\"\nmax_batch_size: 0\ninput [{input_spec}\n]\noutput [{output_spec}\n]\nensemble_scheduling {{\n  step [{ensemble_scheduling_steps}\n  ]\n}}\n\"\"\"\n\n\ndef _get_type_map(dtype: str) -> str:\n    \"\"\"Return the Triton API type mapped to numpy type.\"\"\"\n    # see: https://github.com/triton-inference-server/server/blob/main/docs/model_configuration.md\n    return {\n        \"bool\": \"TYPE_BOOL\",\n        \"uint8\": \"TYPE_UINT8\",\n        \"uint16\": \"TYPE_UINT16\",\n        \"uint32\": \"TYPE_UINT32\",\n        \"uint64\": \"TYPE_UINT64\",\n        \"int8\": \"TYPE_INT8\",\n        \"int16\": \"TYPE_INT16\",\n        \"int32\": \"TYPE_INT32\",\n        \"int64\": \"TYPE_INT64\",\n        \"float16\": \"TYPE_FP16\",\n        \"float32\": \"TYPE_FP32\",\n        \"float64\": \"TYPE_FP64\",\n        \"string\": \"TYPE_STRING\",\n        \"torch.float32\": \"TYPE_FP32\",\n        \"torch.float\": \"TYPE_FP32\",\n        \"torch.float64\": \"TYPE_FP64\",\n        \"torch.double\": \"TYPE_FP64\",\n        \"torch.float16\": \"TYPE_FP16\",\n        \"torch.half\": \"TYPE_FP16\",\n        \"torch.uint8\": \"TYPE_UINT8\",\n        \"torch.int8\": \"TYPE_INT8\",\n        \"torch.int16\": \"TYPE_INT16\",\n        \"torch.short\": \"TYPE_INT16\",\n        \"torch.int32\": \"TYPE_INT32\",\n        \"torch.int\": \"TYPE_INT32\",\n        \"torch.int64\": \"TYPE_INT64\",\n        \"torch.long\": \"TYPE_INT64\",\n        \"torch.bool\": \"TYPE_BOOL\",\n    }[dtype]\n\n\ndef to_triton_dimension(content: list[str] | list[torch.Tensor] | list[TorchAudioTuple] | torch.Tensor):\n    # todo (Wael): tests for all types.\n    if isinstance(content, list) and content:\n        if isinstance(content[0], str):\n            return [len(content)]\n    elif isinstance(content, torch.Tensor):\n        return list(content.size())\n    return [-1]\n\n\ndef to_triton_type(content: list[str] | list[torch.Tensor] | list[TorchAudioTuple] | torch.Tensor):\n    # todo (Wael): tests for all types.\n    if isinstance(content, list) and content:\n        if isinstance(content[0], str):\n            return _get_type_map(\"string\")\n    elif isinstance(content, torch.Tensor):\n        return _get_type_map(str(content.dtype))\n\n\n@DeveloperAPI\n@dataclass\nclass TritonArtifact:\n    \"\"\"Dataclass for exported Triton artifacts.\"\"\"\n\n    # Name of the model.\n    model_name: str\n\n    # Model version.\n    model_version: int | str\n\n    # Triton backend (e.g. \"pytorch_libtorch\").\n    platform: str\n\n    # Model path.\n    path: str\n\n    # Type of artifact (application/octet-stream, plain text, etc.)\n    content_type: str\n\n    # Size of the artifact in bytes.\n    content_length: int\n\n\n@DeveloperAPI\n@dataclass\nclass TritonConfigFeature:\n    \"\"\"Represents an input/output feature in a Triton config.\"\"\"\n\n    # Name of the feature.\n    name: str\n\n    # Ludwig type of the feature, or \"tensor\"\n    ludwig_type: str\n\n    # The data contents of the feature.\n    content: TorchscriptPreprocessingInput | torch.Tensor\n\n    # One of PREPROCESSOR, PREDICTOR, POSTPROCESSOR.\n    inference_stage: str\n\n    # One of INPUT, OUTPUT.\n    kind: str\n\n    # Index of the feature in the Triton Config.\n    index: int\n\n    def __post_init__(self):\n        # removing non-alphanumeric characters as this will go in the wrapper function header.\n        self.wrapper_signature_name = re.sub(r\"[\\W]+\", \"_\", self.name)\n        # get Triton type\n        self.type = to_triton_type(self.content)\n        # get dimension\n        self.dimension = to_triton_dimension(self.content)\n        # get ensemble_scheduling output_map key (same as \"name\" in input/output)\n        self.key = f\"{self.kind}__{self.index}\"\n        self.value = self._get_feature_ensemble_value()\n\n    def _get_feature_ensemble_value(self) -> str:\n        # get ensemble_scheduling output_map value.\n        if self.inference_stage == PREPROCESSOR and self.kind == INPUT:\n            return self.name\n        if self.inference_stage == PREDICTOR and self.kind == INPUT:\n            # PREPROCESSOR outputs and PREDICTOR inputs must have the same \"value\" attribute.\n            return f\"{PREPROCESSOR}_{OUTPUT}_{self.index}\"\n        elif self.inference_stage == POSTPROCESSOR and self.kind == INPUT:\n            # PREDICTOR outputs and POSTPROCESSOR inputs must have the same \"value\" attribute.\n            return f\"{PREDICTOR}_{OUTPUT}_{self.index}\"\n        elif self.inference_stage == POSTPROCESSOR and self.kind == OUTPUT:\n            return self.name\n        else:\n            return f\"{self.inference_stage}_{self.kind}_{self.index}\"\n\n    def _get_wrapper_signature_type(self) -> str:\n        if self.ludwig_type in FEATURES_TO_CAST_AS_STRINGS:\n            return \"List[str]\"\n        elif self.ludwig_type in [IMAGE, AUDIO, DATE, \"tensor\"]:\n            return {\n                IMAGE: \"List[torch.Tensor]\",\n                AUDIO: \"TorchAudioTuple\",\n                DATE: \"List[torch.Tensor]\",\n                \"tensor\": \"torch.Tensor\",\n            }[self.ludwig_type]\n        return \"torch.Tensor\"\n\n\n@DeveloperAPI\n@dataclass\nclass TritonMaster:\n    \"\"\"Provides access to the Triton Config and the scripted module.\"\"\"\n\n    # The inference module.\n    module: _InferencePreprocessor | _InferencePredictor | _InferencePostprocessor\n\n    # An input for the module that will help determine the input and output dimensions.\n    input_data_example: dict[str, TorchscriptPreprocessingInput | torch.Tensor]\n\n    # One of PREPROCESSOR, PREDICTOR, POSTPROCESSOR.\n    inference_stage: str\n\n    # Max batch size of the model. (Triton config param).\n    max_batch_size: int\n\n    # Max time a request to the Triton model spends in the queue. (Triton config param).\n    max_queue_delay_microseconds: int\n\n    # Name of the model as specified by the caller of the triton_export function.\n    model_name: str\n\n    # Base output directory. Corresponds to the Triton model registry.\n    output_path: str\n\n    # Triton model version.\n    model_version: int | str\n\n    # Ludwig config.\n    ludwig_config: ModelConfigDict\n\n    # One of \"cpu\", \"cuda\".\n    device: str\n\n    # Number of model instances on device.\n    model_instance_count: int\n\n    def __post_init__(self):\n        \"\"\"Extract input and output features and necessary information for a Triton config.\"\"\"\n        if self.inference_stage not in INFERENCE_STAGES:\n            raise ValueError(f\"Invalid inference stage. Choose one of {INFERENCE_STAGES}\")\n\n        self.full_model_name = self.model_name + \"_\" + self.inference_stage\n        self.base_path = os.path.join(self.output_path, self.full_model_name)\n        os.makedirs(self.base_path, exist_ok=True)\n\n        self.output_data_example: dict[str, TorchscriptPreprocessingInput | torch.Tensor] = self.module(\n            self.input_data_example\n        )\n\n        # generate input and output features.\n        self.input_features: list[TritonConfigFeature] = []\n        for i, (feature_name, content) in enumerate(self.input_data_example.items()):\n            ludwig_type = \"tensor\"\n            if self.inference_stage == PREPROCESSOR:\n                ludwig_type = self.ludwig_config[INPUT_FEATURES][i][TYPE]\n            self.input_features.append(\n                TritonConfigFeature(feature_name, ludwig_type, content, self.inference_stage, INPUT, i)\n            )\n\n        self.output_features: list[TritonConfigFeature] = []\n        for i, (feature_name, content) in enumerate(self.output_data_example.items()):\n            ludwig_type = \"tensor\"\n            self.output_features.append(\n                TritonConfigFeature(feature_name, ludwig_type, content, self.inference_stage, OUTPUT, i)\n            )\n\n    def save_model(self) -> TritonArtifact:\n        \"\"\"Script the model and saves it.\n\n        Return the appropriate artifact.\n        \"\"\"\n        if isinstance(self.model_version, str) and self.model_version.isdigit():\n            self.model_version = int(self.model_version)\n        if not isinstance(self.model_version, int) or self.model_version < 1:\n            raise ValueError(\"Model version has to be a non-zero positive integer\")\n\n        # wrapper.py is optional and is just for visualizing the inputs/outputs to the model exported to Triton.\n        wrapper_definition = TritonModel(\n            self.module, self.input_features, self.output_features, self.inference_stage\n        ).generate_inference_module_wrapper()\n        with open(os.path.join(self.base_path, \"wrapper.py\"), \"w\") as f:\n            f.write(wrapper_definition)\n\n        os.makedirs(os.path.join(self.base_path, str(self.model_version)), exist_ok=True)\n        model_path = os.path.join(self.base_path, str(self.model_version), \"model.pt\")\n\n        self.model_ts = TritonModel(\n            self.module, self.input_features, self.output_features, self.inference_stage\n        ).generate_scripted_module()\n        self.model_ts.save(model_path)\n\n        model_artifact = TritonArtifact(\n            model_name=self.full_model_name,\n            model_version=self.model_version,\n            platform=\"pytorch_libtorch\",\n            path=model_path,\n            content_type=\"application/octet-stream\",\n            content_length=model_size(self.model_ts),\n        )\n\n        return model_artifact\n\n    def save_config(self) -> TritonArtifact:\n        \"\"\"Save the Triton config.\n\n        Return the appropriate artifact.\n        \"\"\"\n        device = self.device\n        if self.inference_stage != PREDICTOR:\n            device = \"cpu\"\n        self.config = TritonConfig(\n            self.full_model_name,\n            self.input_features,\n            self.output_features,\n            self.max_batch_size,\n            self.max_queue_delay_microseconds,\n            device,\n            self.model_instance_count,\n            self.inference_stage,\n        )\n\n        config_path = os.path.join(self.base_path, \"config.pbtxt\")\n        with open(config_path, \"w\") as f:\n            formatted_config = remove_empty_lines(self.config.get_model_config())\n            f.write(formatted_config)\n\n        config_artifact = TritonArtifact(\n            model_name=self.full_model_name,\n            model_version=self.model_version,\n            platform=\"pytorch_libtorch\",\n            path=config_path,\n            content_type=\"text/x-protobuf\",\n            content_length=os.path.getsize(config_path),\n        )\n\n        return config_artifact\n\n\n@DeveloperAPI\n@dataclass\nclass TritonEnsembleConfig:\n    \"\"\"Dataclass for creating and saving the Triton ensemble config.\"\"\"\n\n    # TritonMaster object for the preprocessor.\n    triton_master_preprocessor: TritonMaster\n\n    # TritonMaster object for the predictor.\n    triton_master_predictor: TritonMaster\n\n    # TritonMaster object for the postprocessor.\n    triton_master_postprocessor: TritonMaster\n\n    # Name of the model as specified by the caller of the triton_export function.\n    model_name: str\n\n    # Base output directory. Corresponds to the Triton model registry.\n    output_path: str\n\n    # Triton model version.\n    model_version: int | str\n\n    def __post_init__(self):\n        self.ensemble_model_name = self.model_name\n        self.base_path = os.path.join(self.output_path, self.ensemble_model_name)\n        os.makedirs(self.base_path, exist_ok=True)\n\n    def _get_ensemble_scheduling_input_maps(self, triton_features: list[TritonConfigFeature]) -> str:\n        return \"\".join(\n            ENSEMBLE_SCHEDULING_INPUT_MAP.format(key=feature.key, value=feature.value) for feature in triton_features\n        )\n\n    def _get_ensemble_scheduling_output_maps(self, triton_features: list[TritonConfigFeature]) -> str:\n        return \"\".join(\n            ENSEMBLE_SCHEDULING_OUTPUT_MAP.format(key=feature.key, value=feature.value) for feature in triton_features\n        )\n\n    def _get_ensemble_scheduling_step(self, triton_master: TritonMaster) -> str:\n        return ENSEMBLE_SCHEDULING_STEP.format(\n            ensemble_model_name=triton_master.config.full_model_name,\n            input_maps=self._get_ensemble_scheduling_input_maps(triton_master.input_features),\n            output_maps=self._get_ensemble_scheduling_output_maps(triton_master.output_features),\n        )\n\n    def _get_ensemble_spec(self, triton_features: list[TritonConfigFeature]) -> str:\n        spec = []\n        for feature in triton_features:\n            spec.append(\n                TRITON_SPEC.format(\n                    key=feature.value,\n                    data_type=feature.type,\n                    data_dims=\", \".join(str(dim) for dim in feature.dimension),  # check correctness\n                    reshape_spec=\"\",\n                )\n            )\n        return \",\".join(spec)\n\n    def get_config(self) -> str:\n        triton_masters = [\n            self.triton_master_preprocessor,\n            self.triton_master_predictor,\n            self.triton_master_postprocessor,\n        ]\n        ensemble_scheduling_steps = \",\".join(\n            [self._get_ensemble_scheduling_step(triton_master) for triton_master in triton_masters]\n        )\n        return TRITON_ENSEMBLE_CONFIG_TEMPLATE.format(\n            model_name=self.ensemble_model_name,\n            input_spec=self._get_ensemble_spec(self.triton_master_preprocessor.input_features),\n            output_spec=self._get_ensemble_spec(self.triton_master_postprocessor.output_features),\n            ensemble_scheduling_steps=ensemble_scheduling_steps,\n        )\n\n    def save_ensemble_config(self) -> TritonArtifact:\n        config_path = os.path.join(self.base_path, \"config.pbtxt\")\n        with open(config_path, \"w\") as f:\n            formatted_config = remove_empty_lines(self.get_config())\n            f.write(formatted_config)\n\n        config_artifact = TritonArtifact(\n            model_name=self.ensemble_model_name,\n            model_version=self.model_version,\n            platform=\"ensemble\",\n            path=config_path,\n            content_type=\"text/x-protobuf\",\n            content_length=os.path.getsize(config_path),\n        )\n\n        return config_artifact\n\n    def save_ensemble_dummy_model(self) -> TritonArtifact:\n        \"\"\"Scripts the model and saves it.\"\"\"\n        if isinstance(self.model_version, str) and self.model_version.isdigit():\n            self.model_version = int(self.model_version)\n        if not isinstance(self.model_version, int) or self.model_version < 1:\n            raise ValueError(\"Model version has to be a non-zero positive integer\")\n\n        os.makedirs(os.path.join(self.base_path, str(self.model_version)), exist_ok=True)\n        model_path = os.path.join(self.base_path, str(self.model_version), \"model.txt\")\n        with open(model_path, \"w\") as f:\n            f.write(\"no model for the ensemble\")\n\n        model_artifact = TritonArtifact(\n            model_name=self.ensemble_model_name,\n            model_version=self.model_version,\n            platform=\"ensemble\",\n            path=model_path,\n            content_type=\"text/plain\",\n            content_length=os.path.getsize(model_path),\n        )\n\n        return model_artifact\n\n\n@DeveloperAPI\n@dataclass\nclass TritonConfig:\n    \"\"\"Enables the creation and export of a Triton config.\"\"\"\n\n    # Name of the model. Must be the same as the directory where the config is saved.\n    full_model_name: str\n\n    # Input features of the model.\n    input_features: list[TritonConfigFeature]\n\n    # Output features of the model.\n    output_features: list[TritonConfigFeature]\n\n    # Max batch size of the model. (Triton config param).\n    max_batch_size: int\n\n    # Max time a request to the Triton model spends in the queue. (Triton config param).\n    max_queue_delay_microseconds: int\n\n    # One of \"cpu\", \"cuda\".\n    device: str\n\n    # Number of model instances on device.\n    model_instance_count: int\n\n    # One of PREPROCESSOR, PREDICTOR, POSTPROCESSOR.\n    inference_stage: str\n\n    def _get_triton_spec(self, triton_features: list[TritonConfigFeature]) -> str:\n        spec = []\n        for feature in triton_features:\n            spec.append(\n                TRITON_SPEC.format(\n                    key=feature.key,\n                    data_type=feature.type,\n                    data_dims=\", \".join(str(dim) for dim in feature.dimension),  # check correctness\n                    reshape_spec=self._get_reshape_spec(feature),\n                )\n            )\n        return \",\".join(spec)\n\n    def _get_reshape_spec(self, feature) -> str:\n        if feature.kind == INPUT and self.inference_stage == PREDICTOR:\n            return FEATURE_RESHAPE_SPEC.format(reshape_dims=\", \".join(str(dim) for dim in feature.dimension[1:]))\n        return \"\"\n\n    def _get_instance_spec(self) -> str:\n        if self.device == \"cpu\":\n            kind = \"KIND_CPU\"\n        else:\n            kind = \"KIND_GPU\"\n        spec = INSTANCE_SPEC.format(count=self.model_instance_count, kind=kind)\n        return spec\n\n    def _get_dynamic_batching_spec(self) -> str:\n        if self.inference_stage == PREDICTOR:\n            return DYNAMIC_BATCHING_TEMPLATE.format(delay=self.max_queue_delay_microseconds)\n        return \"\"\n\n    def get_model_config(self) -> str:\n        \"\"\"Generate a Triton config for a model from the input and output features.\"\"\"\n        max_batch_size = self.max_batch_size\n        if self.inference_stage != PREDICTOR:\n            max_batch_size = 0\n\n        config = TRITON_CONFIG_TEMPLATE.format(\n            model_name=self.full_model_name,\n            max_batch_size=max_batch_size,\n            dynamic_batching_spec=self._get_dynamic_batching_spec(),\n            input_spec=self._get_triton_spec(self.input_features),\n            output_spec=self._get_triton_spec(self.output_features),\n            instance_spec=self._get_instance_spec(),\n        )\n        return config\n\n\n@DeveloperAPI\n@dataclass\nclass TritonModel:\n    \"\"\"Enables the scripting and export of a model.\"\"\"\n\n    # The inference module.\n    module: _InferencePreprocessor | _InferencePredictor | _InferencePostprocessor\n\n    # Input features of the model.\n    input_features: list[TritonConfigFeature]\n\n    # Output features of the model.\n    output_features: list[TritonConfigFeature]\n\n    # One of PREPROCESSOR, PREDICTOR, POSTPROCESSOR.\n    inference_stage: str\n\n    def _get_dict_type_hint(self) -> str:\n        return {\n            PREPROCESSOR: \"TorchscriptPreprocessingInput\",\n            PREDICTOR: \"torch.Tensor\",\n            POSTPROCESSOR: \"torch.Tensor\",\n        }[self.inference_stage]\n\n    def _get_input_signature(self, triton_features: list[TritonConfigFeature]) -> str:\n        elems = [\n            f\"{feature.wrapper_signature_name}: {feature._get_wrapper_signature_type()}\" for feature in triton_features\n        ]\n        return \", \".join(elems)\n\n    def _get_input_dict(self, triton_features: list[TritonConfigFeature]) -> str:\n        elems = [f'\"{feature.name}\": {feature.wrapper_signature_name}' for feature in triton_features]\n        return \"{\" + \", \".join(elems) + \"}\"\n\n    def _get_output_tuple(self, triton_features: list[TritonConfigFeature]) -> str:\n        elems = [f'results[\"{feature.name}\"]' for feature in triton_features]\n        return \"(\" + \", \".join(elems) + \",)\"\n\n    def generate_inference_module_wrapper(self) -> str:\n        \"\"\"Generate the class wrapper around an inference module.\"\"\"\n        return INFERENCE_MODULE_TEMPLATE.format(\n            input_signature=self._get_input_signature(self.input_features),\n            input_type=self._get_dict_type_hint(),\n            input_dict=self._get_input_dict(self.input_features),\n            output_tuple=self._get_output_tuple(self.output_features),\n        )\n\n    def generate_scripted_module(self):\n        \"\"\"Generate the scripted module from the wrapper class.\"\"\"\n        wrapper_definition = self.generate_inference_module_wrapper()\n        with tempfile.TemporaryDirectory() as tmpdir:\n            ts_path = os.path.join(tmpdir, \"generated.py\")\n            with open(ts_path, \"w\") as f:\n                f.write(wrapper_definition)\n\n            spec = importlib.util.spec_from_file_location(\"generated.ts\", ts_path)\n            gen_ts = importlib.util.module_from_spec(spec)\n            spec.loader.exec_module(gen_ts)\n\n            gen_module = gen_ts.GeneratedInferenceModule(self.module)\n            scripted_module = torch.jit.script(gen_module)\n        return scripted_module\n\n\ndef get_device_types_and_counts(\n    preprocessor_num_instances: int,\n    predictor_device_type: str,\n    predictor_num_instances: int,\n    postprocessor_num_instances: int,\n) -> tuple[list[str], list[int]]:\n    \"\"\"Retrun device types and instance counts for each of the three inference modules.\"\"\"\n    if predictor_device_type not in [\"cuda\", \"cpu\"]:\n        raise ValueError('Invalid predictor device type. Choose one of [\"cpu\", \"cuda\"].')\n    elif predictor_device_type == \"cuda\" and not torch.cuda.is_available():\n        raise ValueError(\"Specified num_gpus > 0, but CUDA isn't available.\")\n\n    preprocessor_device_type = \"cpu\"\n    postprocessor_device_type = \"cpu\"\n    device_types = [preprocessor_device_type, predictor_device_type, postprocessor_device_type]\n    device_counts = [preprocessor_num_instances, predictor_num_instances, postprocessor_num_instances]\n    return device_types, device_counts\n\n\ndef get_inference_modules(model: LudwigModel, predictor_device_type: str) -> list[torch.jit.ScriptModule]:\n    \"\"\"Return the three inference modules.\"\"\"\n    inference_module = InferenceModule.from_ludwig_model(\n        model.model, model.config, model.training_set_metadata, device=predictor_device_type\n    )\n    return [inference_module.preprocessor, inference_module.predictor, inference_module.postprocessor]\n\n\ndef get_example_input(\n    model: LudwigModel, device_types: list[str], data_example: None | pd.DataFrame\n) -> dict[str, TorchscriptPreprocessingInput]:\n    \"\"\"Return an inference module-compatible input example.\n\n    Generates a synthetic example if one is not provided.\n    \"\"\"\n    config = model.config\n    if data_example is None:\n        features = config[\"input_features\"] + config[\"output_features\"]\n        df = build_synthetic_dataset(dataset_size=1, features=features)\n        data = [row for row in df]\n        data_example = pd.DataFrame(data[1:], columns=data[0])\n    return to_inference_module_input_from_dataframe(\n        data_example.head(1), config, load_paths=True, device=device_types[0]\n    )\n\n\ndef clean_up_synthetic_data():\n    \"\"\"Clean up synthetic example generated data for audio and image features.\"\"\"\n    shutil.rmtree(\"audio_files\", ignore_errors=True)\n    shutil.rmtree(\"image_files\", ignore_errors=True)\n\n\n@DeveloperAPI\ndef export_triton(\n    model: LudwigModel,\n    data_example: pd.DataFrame | None = None,\n    output_path: str | None = \"model_repository\",\n    model_name: str | None = \"ludwig_model\",\n    model_version: int | str | None = 1,\n    preprocessor_num_instances: int | None = 1,\n    predictor_device_type: str | None = \"cpu\",\n    predictor_num_instances: int | None = 1,\n    postprocessor_num_instances: int | None = 1,\n    predictor_max_batch_size: int | None = 64,\n    max_queue_delay_microseconds: int | None = 100,\n) -> list[TritonArtifact]:\n    \"\"\"Exports a torchscript model to a output path that serves as a repository for Triton Inference Server.\n\n    # Inputs\n    :param model: (LudwigModel) A ludwig model.\n    :param data_example: (pd.DataFrame) an example from the dataset. Used to get dimensions throughout the pipeline.\n    :param output_path: (str) The output path for the model repository.\n    :param model_name: (str) The optional model name.\n    :param model_version: (Union[int,str]) The optional model verison.\n    :param preprocessor_num_instances: (int) number of instances for the preprocessor (on CPU).\n    :param predictor_device_type: (str) device type for the predictor to be deployed on. One of \"cpu\" or \"cuda\"\n    :param predictor_num_instances: (int) number of instances for the predictor.\n    :param postprocessor_num_instances: (int) number of instances for the postprocessor (on CPU).\n    :param predictor_max_batch_size: (int) max_batch_size parameter for the predictor Triton config.\n    :param max_queue_delay_microseconds: (int) max_queue_delay_microseconds for all Triton configs.  # Return\n    :return: (List[TritonArtifact]) list of TritonArtifacts that contains information about exported artifacts.\n    \"\"\"\n\n    device_types, instance_counts = get_device_types_and_counts(\n        preprocessor_num_instances, predictor_device_type, predictor_num_instances, postprocessor_num_instances\n    )\n    split_modules = get_inference_modules(model, device_types[1])\n    example_input = get_example_input(model, device_types, data_example)\n\n    triton_masters = []\n    triton_artifacts = []\n    for i, module in enumerate(split_modules):\n        example_input = place_on_device(example_input, device_types[i])\n        triton_master = TritonMaster(\n            module=module,\n            input_data_example=example_input,\n            inference_stage=INFERENCE_STAGES[i],\n            max_batch_size=predictor_max_batch_size,\n            max_queue_delay_microseconds=max_queue_delay_microseconds,\n            model_name=model_name,\n            output_path=output_path,\n            model_version=model_version,\n            ludwig_config=model.config,\n            device=device_types[i],\n            model_instance_count=instance_counts[i],\n        )\n        example_input = triton_master.output_data_example\n        config_artifact = triton_master.save_config()\n        model_artifact = triton_master.save_model()\n        triton_masters.append(triton_master)\n        triton_artifacts.extend([config_artifact, model_artifact])\n\n    # saving ensemble config\n    triton_master_preprocessor, triton_master_predictor, triton_master_postprocessor = triton_masters\n    ensemble_config = TritonEnsembleConfig(\n        triton_master_preprocessor,\n        triton_master_predictor,\n        triton_master_postprocessor,\n        model_name,\n        output_path,\n        model_version,\n    )\n    config_artifact = ensemble_config.save_ensemble_config()\n    model_artifact = ensemble_config.save_ensemble_dummy_model()\n    triton_artifacts.extend([config_artifact, model_artifact])\n    clean_up_synthetic_data()\n\n    return triton_artifacts\n"
  },
  {
    "path": "ludwig/utils/types.py",
    "content": "from typing import Union\n\nimport pandas as pd\nimport torch\n\ntry:\n    import dask.dataframe as dd\n\n    DataFrame = Union[pd.DataFrame, dd.DataFrame]\n    Series = Union[pd.Series, dd.Series]\nexcept ImportError:\n    DataFrame = pd.DataFrame\n    Series = pd.Series\n\n# torchaudio.load returns the audio tensor and the sampling rate as a tuple.\nTorchAudioTuple = tuple[torch.Tensor, int]\nTorchscriptPreprocessingInput = Union[list[str], list[torch.Tensor], list[TorchAudioTuple], torch.Tensor]\nTorchDevice = Union[str, torch.device]\n"
  },
  {
    "path": "ludwig/utils/upload_utils.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport os\nfrom abc import ABC, abstractmethod\n\nfrom huggingface_hub import HfApi, login\nfrom huggingface_hub.hf_api import CommitInfo\n\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\n\nlogger = logging.getLogger(__name__)\n\n\nclass BaseModelUpload(ABC):\n    \"\"\"Abstract base class for uploading trained model artifacts to different repositories.\n\n    This class defines the interface for uploading trained model artifacts to various repositories such as Huggingface\n    Hub, without specifying the concrete implementation for each repository. Subclasses of this base class must\n    implement the 'login' and 'upload' methods.\n    \"\"\"\n\n    @abstractmethod\n    def login(self):\n        \"\"\"Abstract method to handle authentication with the target repository.\n\n        Subclasses must implement this method to provide the necessary authentication\n        mechanisms required by the repository where the model artifacts will be uploaded.\n\n        Raises:\n            NotImplementedError: If this method is not implemented in the subclass.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def upload(\n        self,\n        repo_id: str,\n        model_path: str,\n        repo_type: str | None = None,\n        private: bool | None = False,\n        commit_message: str | None = None,\n        commit_description: str | None = None,\n        dataset_file: str | None = None,\n        dataset_name: str | None = None,\n    ) -> bool:\n        \"\"\"Abstract method to upload trained model artifacts to the target repository.\n\n        Subclasses must implement this method to define the process of pushing model\n        artifacts to the respective repository. This may include creating a new model version,\n        uploading model files, and any other specific steps required by the model repository\n        service.\n\n        Returns:\n            bool: True if the model artifacts were successfully uploaded, False otherwise.\n\n        Raises:\n            NotImplementedError: If this method is not implemented in the subclass.\n        \"\"\"\n        raise NotImplementedError()\n\n    @staticmethod\n    def _validate_upload_parameters(\n        repo_id: str,\n        model_path: str,\n        repo_type: str | None = None,\n        private: bool | None = False,\n        commit_message: str | None = None,\n        commit_description: str | None = None,\n    ):\n        \"\"\"Validate parameters before uploading trained model artifacts.\n\n        This method checks if the input parameters meet the necessary requirements before uploading\n        trained model artifacts to the target repository.\n\n        Args:\n            repo_id (str): The ID of the target repository. Each provider will verify their specific rules.\n            model_path (str): The path to the directory containing the trained model artifacts.\n                This is the parent-folder of the folder where the 'model_weights' folder and the\n                'model_hyperparameters.json' file are stored.\n            repo_type (str, optional): The type of the repository. Not used in the base class, but subclasses\n                may use it for specific repository implementations. Defaults to None.\n            private (bool, optional): Whether the repository should be private or not. Not used in the base class,\n                but subclasses may use it for specific repository implementations. Defaults to False.\n            commit_message (str, optional): A message to attach to the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n            commit_description (str, optional): A description of the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n\n        Raises:\n            FileNotFoundError: If the model_path does not exist.\n            Exception: If the trained model artifacts are not found at the expected location within model_path, or\n                if the artifacts are not in the required format (i.e., 'pytorch_model.bin'; or 'adapter_model.bin' or\n                'adapter_model.safetensors').\n        \"\"\"\n        # Make sure the model's save path is actually a valid path\n        if not os.path.exists(model_path):\n            raise FileNotFoundError(f\"The path '{model_path}' does not exist.\")\n\n        # Make sure the model is actually trained\n        trained_model_artifacts_path = os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)\n        if not os.path.exists(trained_model_artifacts_path):\n            raise Exception(\n                f\"Model artifacts not found at {trained_model_artifacts_path}. \"\n                f\"It is possible that model at '{model_path}' hasn't been trained yet, or something went\"\n                \"wrong during training where the model's weights were not saved.\"\n            )\n\n\ndef hf_hub_login():\n    \"\"\"Login to huggingface hub using the token stored in ~/.cache/huggingface/token and returns a HfApi client\n    object that can be used to interact with HF Hub.\"\"\"\n    cached_token_path = os.path.join(os.path.expanduser(\"~\"), \".cache\", \"huggingface\", \"token\")\n\n    if not os.path.exists(cached_token_path):\n        login(add_to_git_credential=True)\n\n    with open(cached_token_path) as f:\n        hf_token = f.read()\n\n    hf_api = HfApi(token=hf_token)\n    assert hf_api.token == hf_token\n\n    return hf_api\n\n\nclass HuggingFaceHub(BaseModelUpload):\n    def __init__(self):\n        self.api = None\n        self.login()\n\n    def login(self):\n        \"\"\"Login to huggingface hub using the token stored in ~/.cache/huggingface/token and return a HfApi client\n        object that can be used to interact with HF Hub.\"\"\"\n        self.api = hf_hub_login()\n\n    @staticmethod\n    def _validate_upload_parameters(\n        repo_id: str,\n        model_path: str,\n        repo_type: str | None = None,\n        private: bool | None = False,\n        commit_message: str | None = None,\n        commit_description: str | None = None,\n    ):\n        \"\"\"Validate parameters before uploading trained model artifacts.\n\n        This method checks if the input parameters meet the necessary requirements before uploading\n        trained model artifacts to the target repository.\n\n        Args:\n            repo_id (str): The ID of the target repository. It must be a namespace (user or an organization)\n                and a repository name separated by a '/'. For example, if your HF username is 'johndoe' and you\n                want to create a repository called 'test', the repo_id should be 'johndoe/test'.\n            model_path (str): The path to the directory containing the trained model artifacts.\n                This is the parent-folder of the folder where the 'model_weights' folder and the\n                'model_hyperparameters.json' file are stored.\n            repo_type (str, optional): The type of the repository. Not used in the base class, but subclasses\n                may use it for specific repository implementations. Defaults to None.\n            private (bool, optional): Whether the repository should be private or not. Not used in the base class,\n                but subclasses may use it for specific repository implementations. Defaults to False.\n            commit_message (str, optional): A message to attach to the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n            commit_description (str, optional): A description of the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n\n        Raises:\n            ValueError: If the repo_id does not have both a namespace and a repo name separated by a '/'.\n        \"\"\"\n        # Validate repo_id has both a namespace and a repo name\n        if \"/\" not in repo_id:\n            raise ValueError(\n                \"`repo_id` must be a namespace (user or an organization) and a repo name separated by a `/`.\"\n                \" For example, if your HF username is `johndoe` and you want to create a repository called `test`, the\"\n                \" repo_id should be johndoe/test\"\n            )\n        BaseModelUpload._validate_upload_parameters(\n            repo_id,\n            model_path,\n            repo_type,\n            private,\n            commit_message,\n            commit_description,\n        )\n\n        trained_model_artifacts_path = os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)\n        \"\"\"Make sure the model's saved artifacts either contain:\n\n        1. pytorch_model.bin -> regular model training, such as ECD or for LLMs\n        2. adapter_model.bin or adapter_model.safetensors -> LLM fine-tuning using PEFT\n           <Alex(12/10/2023): TODO>\n                As of PEFT version \"0.7.0\", \"adapter_model\" storage format was changed from \".bin\" to \".safetensors\".\n                For backward compatibility, both formats will be supported, until depracating \".bin\" format formally.\n           </Alex(12/10/2023): TODO>\n        \"\"\"\n        files = set(os.listdir(trained_model_artifacts_path))\n        acceptable_model_artifact_file_names: set[str] = {\n            \"pytorch_model.bin\",\n            \"adapter_model.bin\",  # Delete per formal deprecation policy TBD (per above comment).\n            \"adapter_model.safetensors\",  # New format as of PEFT version \"0.7.0\" (per above comment).\n        }\n        if not (files & acceptable_model_artifact_file_names):\n            raise ValueError(\n                f\"Can't find model weights at {trained_model_artifacts_path}. Trained model weights should \"\n                \"either be saved as `pytorch_model.bin` for regular model training, or have `adapter_model.bin`\"\n                \"or `adapter_model.safetensors` if using parameter efficient fine-tuning methods like LoRA.\"\n            )\n        model_hyperparameters_path: str = os.path.join(model_path, MODEL_FILE_NAME)\n        if MODEL_HYPERPARAMETERS_FILE_NAME not in os.listdir(model_hyperparameters_path):\n            raise ValueError(f\"Can't find '{MODEL_HYPERPARAMETERS_FILE_NAME}' at {model_hyperparameters_path}.\")\n\n    def upload(\n        self,\n        repo_id: str,\n        model_path: str,\n        repo_type: str | None = None,\n        private: bool | None = False,\n        commit_message: str | None = None,\n        commit_description: str | None = None,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Create an empty repo on the HuggingFace Hub and upload trained model artifacts to that repo.\n\n        Args:\n            repo_id (`str`):\n                A namespace (user or an organization) and a repo name separated\n                by a `/`.\n            model_path (`str`):\n                The path of the saved model. This is the parent-folder of the folder\n                where the 'model_weights' folder and the 'model_hyperparameters.json' file\n                are stored.\n            repo_type (`str`, *optional*):\n                Set to `\"dataset\"` or `\"space\"` if uploading to a dataset or\n                space, `None` or `\"model\"` if uploading to a model. Default is\n                `None`.\n            private (`bool`, *optional*, defaults to `False`):\n                Whether the model repo should be private.\n            commit_message (`str`, *optional*):\n                The summary / title / first line of the generated commit. Defaults to:\n                `f\"Upload {path_in_repo} with huggingface_hub\"`\n            commit_description (`str` *optional*):\n                The description of the generated commit\n        \"\"\"\n        # Validate upload parameters are in the right format\n        HuggingFaceHub._validate_upload_parameters(\n            repo_id,\n            model_path,\n            repo_type,\n            private,\n            commit_message,\n            commit_description,\n        )\n\n        # Create empty model repo using repo_id, but it is okay if it already exists.\n        self.api.create_repo(\n            repo_id=repo_id,\n            private=private,\n            repo_type=repo_type,\n            exist_ok=True,\n        )\n\n        # Upload all artifacts in model weights folder\n        commit_message_weights: str | None = f\"{commit_message} (weights)\" if commit_message else commit_message\n        commit_description_weights: str | None = (\n            f\"{commit_description} (weights)\" if commit_description else commit_description\n        )\n        folder_path = os.path.join(model_path, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME)\n        upload_path_weights: CommitInfo = self.api.upload_folder(\n            folder_path=folder_path,\n            repo_id=repo_id,\n            repo_type=repo_type,\n            commit_message=commit_message_weights,\n            commit_description=commit_description_weights,\n        )\n\n        if upload_path_weights:\n            logger.info(f\"Model weights uploaded to `{upload_path_weights}` with repository name `{repo_id}`\")\n            # Upload the ludwig configuration file\n            commit_message_config: str | None = f\"{commit_message} (config)\" if commit_message else commit_message\n            commit_description_config: str | None = (\n                f\"{commit_description} (config)\" if commit_description else commit_description\n            )\n            path_or_fileobj = os.path.join(model_path, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)\n            upload_path_config: CommitInfo = self.api.upload_file(\n                path_or_fileobj=path_or_fileobj,\n                path_in_repo=\"ludwig_config.json\",\n                repo_id=repo_id,\n                repo_type=repo_type,\n                commit_message=commit_message_config,\n                commit_description=commit_description_config,\n            )\n\n            if upload_path_config:\n                logger.info(f\"Model config uploaded to `{upload_path_config}` with repository name `{repo_id}`\")\n                return True\n\n        return False\n\n\nclass Predibase(BaseModelUpload):\n    def __init__(self):\n        self.pc = None\n        self.login()\n\n    def login(self):\n        \"\"\"Login to Predibase using the token stored in the PREDIBASE_API_TOKEN environment variable and return a\n        PredibaseClient object that can be used to interact with Predibase.\"\"\"\n        from predibase import PredibaseClient\n\n        token = os.environ.get(\"PREDIBASE_API_TOKEN\")\n        if token is None:\n            raise ValueError(\n                \"Unable to find PREDIBASE_API_TOKEN environment variable. Please log into Predibase, \"\n                \"generate a token and use `export PREDIBASE_API_TOKEN=` to use Predibase\"\n            )\n\n        try:\n            pc = PredibaseClient()\n\n            # TODO: Check if subscription has expired\n\n            self.pc = pc\n        except Exception as e:\n            raise Exception(f\"Failed to login to Predibase: {e}\")\n            return False\n\n        return True\n\n    @staticmethod\n    def _validate_upload_parameters(\n        repo_id: str,\n        model_path: str,\n        repo_type: str | None = None,\n        private: bool | None = False,\n        commit_message: str | None = None,\n        commit_description: str | None = None,\n    ):\n        \"\"\"Validate parameters before uploading trained model artifacts.\n\n        This method checks if the input parameters meet the necessary requirements before uploading\n        trained model artifacts to the target repository.\n\n        Args:\n            repo_id (str): The ID of the target repository. It must be a less than 256 characters.\n            model_path (str): The path to the directory containing the trained model artifacts. It should contain\n                the model's weights, usually saved under 'model/model_weights'.\n            repo_type (str, optional): The type of the repository. Not used in the base class, but subclasses\n                may use it for specific repository implementations. Defaults to None.\n            private (bool, optional): Whether the repository should be private or not. Not used in the base class,\n                but subclasses may use it for specific repository implementations. Defaults to False.\n            commit_message (str, optional): A message to attach to the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n            commit_description (str, optional): A description of the commit when uploading to version control\n                systems. Not used in the base class, but subclasses may use it for specific repository\n                implementations. Defaults to None.\n\n        Raises:\n            ValueError: If the repo_id is too long.\n        \"\"\"\n        if len(repo_id) > 255:\n            raise ValueError(\"`repo_id` must be 255 characters or less.\")\n\n        BaseModelUpload._validate_upload_parameters(\n            repo_id,\n            model_path,\n            repo_type,\n            private,\n            commit_message,\n            commit_description,\n        )\n\n    def upload(\n        self,\n        repo_id: str,\n        model_path: str,\n        commit_description: str | None = None,\n        dataset_file: str | None = None,\n        dataset_name: str | None = None,\n        **kwargs,\n    ) -> bool:\n        \"\"\"Create an empty repo in Predibase and upload trained model artifacts to that repo.\n\n        Args:\n            model_path (`str`):\n                The path of the saved model. This is the top level directory where\n                the models weights as well as other associated training artifacts\n                are saved.\n            repo_name (`str`):\n                A repo name.\n            repo_description (`str` *optional*):\n                The description of the repo.\n            dataset_file (`str` *optional*):\n                The path to the dataset file. Required if `service` is set to\n                `\"predibase\"` for new model repos.\n            dataset_name (`str` *optional*):\n                The name of the dataset. Used by the `service`\n                `\"predibase\"`. Falls back to the filename.\n        \"\"\"\n        # Validate upload parameters are in the right format\n        Predibase._validate_upload_parameters(\n            repo_id,\n            model_path,\n            None,\n            False,\n            \"\",\n            commit_description,\n        )\n\n        # Upload the dataset to Predibase\n        try:\n            dataset = self.pc.upload_dataset(file_path=dataset_file, name=dataset_name)\n        except Exception as e:\n            raise RuntimeError(\"Failed to upload dataset to Predibase\") from e\n\n        # Create empty model repo using repo_name, but it is okay if it already exists.\n        try:\n            repo = self.pc.create_model_repo(\n                name=repo_id,\n                description=commit_description,\n                exists_ok=True,\n            )\n        except Exception as e:\n            raise RuntimeError(\"Failed to create repo in Predibase\") from e\n\n        # Upload the zip file to Predibase\n        try:\n            self.pc.upload_model(\n                repo=repo,\n                model_path=model_path,\n                dataset=dataset,\n            )\n        except Exception as e:\n            raise RuntimeError(\"Failed to upload model to Predibase\") from e\n\n        logger.info(f\"Model uploaded to Predibase with repository name `{repo_id}`\")\n        return True\n"
  },
  {
    "path": "ludwig/utils/version_transformation.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2022 Predibase, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport logging\nfrom collections import defaultdict\nfrom collections.abc import Callable\nfrom functools import total_ordering\n\nfrom packaging import version as pkg_version\n\nlogger = logging.getLogger(__name__)\n\n\n@total_ordering\nclass VersionTransformation:\n    \"\"\"Wrapper class for transformations to config dicts.\"\"\"\n\n    def __init__(self, transform: Callable[[dict], dict], version: str, prefixes: list[str] = None):\n        \"\"\"Constructor.\n\n        Args:\n            transform: A function or other callable from Dict -> Dict which returns a modified version of the config.\n                       The callable may update the config in-place and return it, or return a new dict.\n            version: The Ludwig version, should be the first version which requires this transform.\n            prefixes: A list of config prefixes this transform should apply to, i.e. [\"hyperopt\"].  If not specified,\n                      transform will be called with the entire config dictionary.\n        \"\"\"\n        self.transform = transform\n        self.version = version\n        self.pkg_version = pkg_version.parse(version)\n        self.prefixes = prefixes if prefixes else []\n\n    def transform_config(self, config: dict):\n        \"\"\"Transforms the sepcified config, returns the transformed config.\"\"\"\n        prefixes = self.prefixes if self.prefixes else [\"\"]\n        for prefix in prefixes:\n            if prefix and (prefix not in config or not config[prefix]):\n                # If the prefix is non-empty (transformation applies to a specific section), but the section is either\n                # absent or empty, then skip.\n                continue\n            config = self.transform_config_with_prefix(config, prefix)\n        return config\n\n    def transform_config_with_prefix(self, config: dict, prefix: str | None = None) -> dict:\n        \"\"\"Applied this version transformation to a specified prefix of the config, returns the updated config. If\n        prefix names a list, i.e. \"input_features\", applies the transformation to each list element (input\n        feature).\n\n        Args:\n            config: A config dictionary.\n            prefix: An optional keypath prefix i.e. \"input_features\". If no prefix specified, transformation is applied\n                    to config itself.\n\n        Returns The updated config.\n        \"\"\"\n        if prefix:\n            components = prefix.split(\".\", 1)\n            key = components[0]\n            rest_of_prefix = components[1] if len(components) > 1 else \"\"\n            if key in config:\n                subsection = config[key]\n                if isinstance(subsection, list):\n                    config[key] = [\n                        self.transform_config_with_prefix(v, prefix=rest_of_prefix) if isinstance(v, dict) else v\n                        for v in subsection\n                    ]\n                elif isinstance(subsection, dict):\n                    config[key] = self.transform_config_with_prefix(subsection, prefix=rest_of_prefix)\n            return config\n        else:\n            # Base case: no prefix specified, pass entire dictionary to transform function.\n            transformed_config = self.transform(config)\n            if transformed_config is None:\n                logger.error(\"Error: version transformation returned None. Check for missing return statement.\")\n            return transformed_config\n\n    @property\n    def max_prefix_length(self):\n        \"\"\"Returns the length of the longest prefix.\"\"\"\n        return max(len(prefix.split(\".\")) for prefix in self.prefixes) if self.prefixes else 0\n\n    @property\n    def longest_prefix(self):\n        \"\"\"Returns the longest prefix, or empty string if no prefixes specified.\"\"\"\n        prefixes = self.prefixes\n        if not prefixes:\n            return \"\"\n        max_index = max(range(len(prefixes)), key=lambda i: prefixes[i])\n        return prefixes[max_index]\n\n    def __lt__(self, other):\n        \"\"\"Defines sort order of version transformations. Sorted by:\n\n        - version (ascending)\n        - max_prefix_length (ascending) Process outer config transformations before inner.\n        - longest_prefix (ascending) Order alphabetically by prefix if max_prefix_length equal.\n        \"\"\"\n        return (self.pkg_version, self.max_prefix_length, self.longest_prefix) < (\n            other.pkg_version,\n            other.max_prefix_length,\n            other.longest_prefix,\n        )\n\n    def __repr__(self):\n        return f'VersionTransformation(<function>, version=\"{self.version}\", prefixes={repr(self.prefixes)})'\n\n\nclass VersionTransformationRegistry:\n    \"\"\"A registry of transformations which update versioned config files.\"\"\"\n\n    def __init__(self):\n        self._registry = defaultdict(list)  # Maps version number to list of transformations.\n\n    def register(self, transformation: VersionTransformation):\n        \"\"\"Registers a version transformation.\"\"\"\n        self._registry[transformation.version].append(transformation)\n\n    def get_transformations(self, from_version: str, to_version: str) -> list[VersionTransformation]:\n        \"\"\"Filters transformations to create an ordered list of the config transformations from one version to\n        another. All transformations returned have version st. from_version < version <= to_version.\n\n        Args:\n            from_version: The ludwig version of the input config.\n            to_version: The version to update the config to (usually the current LUDWIG_VERSION).\n\n        Returns an ordered list of transformations to apply to the config to update it.\n        \"\"\"\n        from_version = pkg_version.parse(from_version)\n\n        # Ignore pre-release, development versions. Otherwise transformations for upcoming releases will not be applied.\n        to_version = pkg_version.parse(to_version)\n        to_version = pkg_version.parse(f\"{to_version.major}.{to_version.minor}\")\n\n        def in_range(v, to_version, from_version):\n            v = pkg_version.parse(v)\n            return from_version <= v <= to_version\n\n        versions = [v for v in self._registry.keys() if in_range(v, to_version, from_version)]\n\n        transforms = sorted(t for v in versions for t in self._registry[v])\n        return transforms\n\n    def update_config(self, config: dict, from_version: str, to_version: str) -> dict:\n        \"\"\"Applies the transformations from an older version to a newer version.\n\n        Args:\n            config: The config, created by ludwig at from_version.\n            from_version: The version of ludwig which wrote the older config.\n            to_version: The version of ludwig to update to (usually the current LUDWIG_VERSION).\n\n        Returns The updated config after applying update transformations and updating the \"ludwig_version\" key.\n        \"\"\"\n        transformations = self.get_transformations(from_version, to_version)\n        updated_config = copy.deepcopy(config)\n        for t in transformations:\n            updated_config = t.transform_config(updated_config)\n        updated_config[\"ludwig_version\"] = to_version\n        return updated_config\n"
  },
  {
    "path": "ludwig/utils/visualization_utils.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport logging\nfrom collections import Counter\nfrom sys import platform\n\nimport numpy as np\nimport pandas as pd\nimport ptitprince as pt\n\nfrom ludwig.constants import SPACE, TRAINING, VALIDATION\n\nlogger = logging.getLogger(__name__)\n\ntry:\n    import matplotlib as mpl\n\n    if platform == \"darwin\":  # OS X\n        try:\n            mpl.use(\"TkAgg\")\n        except ModuleNotFoundError:\n            logger.warning(\"Unable to set TkAgg backend for matplotlib. Your Python may not be configured for Tk\")\n    import matplotlib.patches as patches\n    import matplotlib.path as path\n    import matplotlib.patheffects as PathEffects\n    import matplotlib.pyplot as plt\n    import seaborn as sns\n    from matplotlib import ticker\n    from matplotlib.lines import Line2D\n    from mpl_toolkits.mplot3d import Axes3D\nexcept ImportError as e:\n    raise RuntimeError(\n        \"matplotlib or seaborn are not installed. \"\n        \"In order to install all visualization dependencies run \"\n        \"pip install ludwig[viz]\"\n    ) from e\n\nINT_QUANTILES = 10\nFLOAT_QUANTILES = 10\n\n# mapping from RayTune search space to Ludwig types (float, int, category) for hyperopt visualizations\nRAY_TUNE_FLOAT_SPACES = {\"uniform\", \"quniform\", \"loguniform\", \"qloguniform\", \"randn\", \"qrandn\"}\nRAY_TUNE_INT_SPACES = {\"randint\", \"qrandint\", \"lograndint\", \"qlograndint\"}\nRAY_TUNE_CATEGORY_SPACES = {\"choice\", \"grid_search\"}\n\n\ndef visualize_callbacks(callbacks, fig):\n    if callbacks is None:\n        return\n    for callback in callbacks:\n        callback.on_visualize_figure(fig)\n\n\ndef learning_curves_plot(\n    train_values,\n    vali_values,\n    metric,\n    x_label=\"epoch\",\n    x_step=1,\n    algorithm_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    num_algorithms = len(train_values)\n    max_len = max(len(tv) for tv in train_values)\n\n    fig, ax = plt.subplots()\n\n    sns.set_style(\"whitegrid\")\n\n    if title is not None:\n        ax.set_title(title)\n\n    if num_algorithms == 1:\n        colors = plt.get_cmap(\"tab10\").colors\n    else:  # num_algorithms > 1\n        colors = plt.get_cmap(\"tab20\").colors\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n    ax.set_xlabel(x_label)\n    ax.set_ylabel(metric.replace(\"_\", \" \"))\n\n    xs = np.arange(1, (max_len * x_step) + 1, x_step)\n\n    for i in range(num_algorithms):\n        name_prefix = algorithm_names[i] + \" \" if algorithm_names is not None and i < len(algorithm_names) else \"\"\n        ax.plot(\n            xs[: len(train_values[i])], train_values[i], label=name_prefix + TRAINING, color=colors[i * 2], linewidth=3\n        )\n        if i < len(vali_values) and vali_values[i] is not None and len(vali_values[i]) > 0:\n            ax.plot(\n                xs[: len(vali_values[i])],\n                vali_values[i],\n                label=name_prefix + VALIDATION,\n                color=colors[i * 2 + 1],\n                linewidth=3,\n            )\n\n    ax.legend()\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef compare_classifiers_plot(\n    scores,\n    metrics,\n    algoritm_names=None,\n    adaptive=False,\n    decimals=4,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(scores) == len(metrics)\n    assert len(scores) > 0\n\n    num_metrics = len(metrics)\n\n    sns.set_style(\"whitegrid\")\n\n    fig, ax = plt.subplots()\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n    ax.set_xticklabels([], minor=True)\n\n    if title is not None:\n        ax.set_title(title)\n\n    width = 0.8 / num_metrics if num_metrics > 1 else 0.4\n    ticks = np.arange(len(scores[0]))\n\n    if num_metrics <= 10:\n        colors = plt.get_cmap(\"tab10\").colors\n    else:\n        colors = plt.get_cmap(\"tab20\").colors\n    if adaptive:\n        maximum = max(max(score) for score in scores)\n    else:\n        ax.set_xlim([0, 1])\n        ax.set_xticks(np.linspace(0.0, 1.0, num=21), minor=True)\n        ax.set_xticks(np.linspace(0.0, 1.0, num=11))\n        maximum = 1\n\n    half_total_width = 0.4 if num_metrics > 1 else 0.2\n    ax.set_yticks(ticks + half_total_width - width / 2)\n    ax.set_yticklabels(algoritm_names if algoritm_names is not None else \"\")\n    ax.invert_yaxis()  # labels read top-to-bottom\n\n    for i, metric in enumerate(metrics):\n        ax.barh(ticks + (i * width), scores[i], width, label=metric, color=colors[i])\n\n        for j, v in enumerate(scores[i]):\n            if v < maximum * (0.025 * decimals + 0.1):\n                x = v + maximum * 0.01\n                horizontal_alignment = \"left\"\n            else:\n                x = v - maximum * 0.01\n                horizontal_alignment = \"right\"\n            txt = ax.text(\n                x,\n                ticks[j] + (i * width),\n                (\"{:.\" + str(decimals) + \"f}\").format(v),\n                color=\"white\",\n                fontweight=\"bold\",\n                verticalalignment=\"center\",\n                horizontalalignment=horizontal_alignment,\n            )\n            txt.set_path_effects([PathEffects.withStroke(linewidth=3, foreground=\"black\")])\n\n    plt.setp(ax.get_xminorticklabels(), visible=False)\n\n    ax.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5))\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef compare_classifiers_line_plot(\n    xs,\n    scores,\n    metric,\n    algorithm_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(scores) > 0\n\n    sns.set_style(\"whitegrid\")\n\n    if len(scores) <= 10:\n        colors = plt.get_cmap(\"tab10\").colors\n    else:\n        colors = plt.get_cmap(\"tab20\").colors\n\n    fig, ax = plt.subplots()\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.set_xticks(xs)\n    ax.set_xticklabels(xs)\n    ax.set_xlabel(\"k\")\n    ax.set_ylabel(metric)\n\n    for i, score in enumerate(scores):\n        ax.plot(\n            xs,\n            score,\n            label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f\"Algorithm {i}\",\n            color=colors[i],\n            linewidth=3,\n            marker=\"o\",\n        )\n\n    ax.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5))\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef compare_classifiers_multiclass_multimetric_plot(\n    scores,\n    metrics,\n    labels=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(scores) > 0\n\n    sns.set_style(\"whitegrid\")\n\n    fig, ax = plt.subplots()\n\n    if title is not None:\n        ax.set_title(title)\n\n    width = 0.9 / len(scores)\n    ticks = np.arange(len(scores[0]))\n\n    if len(scores) <= 10:\n        colors = plt.get_cmap(\"tab10\").colors\n    else:\n        colors = plt.get_cmap(\"tab20\").colors\n    ax.set_xlabel(\"class\")\n    ax.set_xticks(ticks + width)\n    if labels is not None:\n        ax.set_xticklabels(labels, rotation=90)\n    else:\n        ax.set_xticklabels(ticks, rotation=90)\n\n    for i, score in enumerate(scores):\n        ax.bar(ticks + i * width, score, width, label=metrics[i], color=colors[i])\n\n    ax.legend(loc=\"center left\", bbox_to_anchor=(1, 0.5))\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef radar_chart(\n    ground_truth,\n    predictions,\n    algorithms=None,\n    log_scale=False,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    if title is not None:\n        plt.title(title)\n\n    ground_truth = ground_truth[0:10]\n    predictions = [pred[0:10] for pred in predictions]\n\n    gt_argsort = np.argsort(-ground_truth)  # sort deacreasing\n    logger.info(gt_argsort)\n    ground_truth = ground_truth[gt_argsort]\n    predictions = [pred[gt_argsort] for pred in predictions]\n\n    maximum = max(max(ground_truth), max(max(p) for p in predictions))\n\n    ax = plt.subplot(111, polar=True)\n    ax.set_theta_zero_location(\"N\")\n    ax.set_theta_direction(-1)\n    ax.set_rmax(maximum)\n    ax.set_rlabel_position(305)\n    ax.set_ylabel(\"Probability\")\n    # ax.set_rscale('log')\n    ax.grid(True)\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    num_classes = len(ground_truth)\n\n    # Set ticks to the number of properties (in radians)\n    t = np.arange(0, 2 * np.pi, 2 * np.pi / num_classes)\n    ax.set_xticks(t)\n    ax.set_xticklabels(np.arange(0, num_classes))\n\n    # Set yticks from 0 to 10\n    # ax.set_yticks(np.linspace(0, 10, 11))\n    # Set axes limits\n    # ax.set_rlim(0, 1)\n    # ax.set_rscale('log')\n\n    def draw_polygon(values, label, color=\"grey\"):\n        points = [(x, y) for x, y in zip(t, values)]\n        points.append(points[0])\n        points = np.array(points)\n\n        codes = [path.Path.MOVETO] + [path.Path.LINETO] * (len(values) - 1) + [path.Path.CLOSEPOLY]\n        _path = path.Path(points, codes)\n        _patch = patches.PathPatch(_path, fill=True, color=color, linewidth=0, alpha=0.2)\n        ax.add_patch(_patch)\n        _patch = patches.PathPatch(_path, fill=False, color=color, linewidth=3)\n        ax.add_patch(_patch)\n\n        # Draw circles at value points\n        # line = ax.scatter(points[:, 0], points[:, 1], linewidth=3,\n        #            s=50, color='white', edgecolor=color, zorder=10)\n        ax.plot(\n            points[:, 0],\n            points[:, 1],\n            linewidth=3,\n            marker=\"o\",\n            fillstyle=\"full\",\n            markerfacecolor=\"white\",\n            markeredgecolor=color,\n            markeredgewidth=2,\n            color=color,\n            zorder=10,\n            label=label,\n        )\n\n    draw_polygon(ground_truth, \"Ground Truth\")\n\n    # Draw polygon representing values\n    for i, alg_predictions in enumerate(predictions):\n        draw_polygon(alg_predictions, algorithms[i], colors[i])\n\n    ax.legend(frameon=True, loc=\"upper left\")\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef pie(ax, values, **kwargs):\n    total = sum(values)\n\n    def formatter(pct):\n        if pct > 0:\n            return f\"{pct * total / 100:0.0f}\\n({pct:0.1f}%)\"\n        else:\n            return \"\"\n\n    wedges, _, labels = ax.pie(values, autopct=formatter, **kwargs)\n    return wedges\n\n\ndef donut(\n    inside_values,\n    inside_labels,\n    outside_values,\n    outside_labels,\n    outside_groups,\n    title=None,\n    tight_layout=None,\n    filename=None,\n    callbacks=None,\n):\n    fig, ax = plt.subplots(figsize=(7, 5))\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.axis(\"equal\")\n\n    width = 0.35\n    colors_tab20c = list(plt.get_cmap(\"tab20c\").colors)\n    colors_set2 = list(plt.get_cmap(\"Set2\").colors)\n    colors_set3 = list(plt.get_cmap(\"Set3\").colors)\n    colors_pastel1 = list(plt.get_cmap(\"Pastel1\").colors)\n\n    # swap green and red\n    # for i in range(4):\n    #    tmp = colors[4 + i]\n    #    colors[4 + i] = colors[8 + i]\n    #    colors[8 + i] = tmp\n\n    colors = []\n    colors.extend(colors_tab20c[8:12])\n    colors.append(colors_set2[5])\n    colors.append(colors_set3[11])\n    colors.append(colors_set3[1])\n    colors.append(colors_pastel1[5])\n    colors.extend(colors_tab20c[4:8])\n\n    inside_colors = [colors[x * 4] for x in range(len(inside_values))]\n\n    group_count = Counter(outside_groups)\n    outside_colors = [colors[(i * 4) + ((j % 3) + 1)] for i in list(set(outside_groups)) for j in range(group_count[i])]\n\n    outside = pie(\n        ax,\n        outside_values,\n        radius=1,\n        pctdistance=1 - width / 2,\n        colors=outside_colors,\n        startangle=90,\n        counterclock=False,\n        textprops={\n            \"color\": \"w\",\n            \"weight\": \"bold\",\n            \"path_effects\": [PathEffects.withStroke(linewidth=3, foreground=\"black\")],\n        },\n    )\n    inside = pie(\n        ax,\n        inside_values,\n        radius=1 - width,\n        pctdistance=1 - (width / 2) / (1 - width),\n        colors=inside_colors,\n        startangle=90,\n        counterclock=False,\n        textprops={\n            \"color\": \"w\",\n            \"weight\": \"bold\",\n            \"path_effects\": [PathEffects.withStroke(linewidth=3, foreground=\"black\")],\n        },\n    )\n    plt.setp(inside + outside, width=width, edgecolor=\"white\")\n\n    wedges = []\n    labels = []\n    so_far = 0\n    for i in list(set(outside_groups)):\n        wedges.append(inside[i])\n        labels.append(inside_labels[i])\n        for j in range(group_count[i]):\n            wedges.append(outside[so_far])\n            labels.append(outside_labels[so_far])\n            so_far += 1\n\n    if tight_layout:\n        ax.legend(wedges, labels, frameon=True, loc=1, bbox_to_anchor=(1.30, 1.00))\n    else:\n        ax.legend(wedges, labels, frameon=True, loc=1, bbox_to_anchor=(1.50, 1.00))\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename, bbox_inches=\"tight\")\n    else:\n        plt.show()\n\n\ndef confidence_filtering_plot(\n    thresholds,\n    accuracies,\n    dataset_kepts,\n    algorithm_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(accuracies) == len(dataset_kepts)\n    num_algorithms = len(accuracies)\n\n    sns.set_style(\"whitegrid\")\n\n    if num_algorithms == 1:\n        colors = plt.get_cmap(\"tab10\").colors\n    else:  # num_algorithms > 1\n        colors = plt.get_cmap(\"tab20\").colors\n\n    y_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    y_ticks_major = np.linspace(0.0, 1.0, num=11)\n    y_ticks_major_labels = [f\"{y * 100:3.0f}%\" for y in y_ticks_major]\n\n    fig, ax1 = plt.subplots()\n\n    if title is not None:\n        ax1.set_title(title)\n\n    ax1.grid(which=\"both\")\n    ax1.grid(which=\"minor\", alpha=0.5)\n    ax1.grid(which=\"major\", alpha=0.75)\n    ax1.set_xticks([x for idx, x in enumerate(thresholds) if idx % 2 == 0])\n    ax1.set_xticks(thresholds, minor=True)\n\n    ax1.set_xlim(-0.05, 1.05)\n    ax1.set_xlabel(\"confidence threshold\")\n\n    ax1.set_ylim(0, 1.05)\n    ax1.set_yticks(y_ticks_major)\n    ax1.set_yticklabels(y_ticks_major_labels)\n    ax1.set_yticks(y_ticks_minor, minor=True)\n\n    ax2 = ax1.twinx()\n\n    ax2.set_ylim(0, 1.05)\n    ax2.set_yticks(y_ticks_major)\n    ax2.set_yticklabels(y_ticks_major_labels)\n    ax2.set_yticks(y_ticks_minor, minor=True)\n\n    for i in range(len(accuracies)):\n        algorithm_name = algorithm_names[i] + \" \" if algorithm_names is not None and i < len(algorithm_names) else \"\"\n        ax1.plot(thresholds, accuracies[i], label=f\"{algorithm_name} accuracy\", color=colors[i * 2], linewidth=3)\n        ax1.plot(\n            thresholds, dataset_kepts[i], label=f\"{algorithm_name} data coverage\", color=colors[i * 2 + 1], linewidth=3\n        )\n\n    ax1.legend(frameon=True, loc=3)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef confidence_filtering_data_vs_acc_plot(\n    accuracies,\n    dataset_kepts,\n    model_names=None,\n    dotted=False,\n    decimal_digits=0,\n    y_label=\"accuracy\",\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(accuracies) == len(dataset_kepts)\n\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    max_dataset_kept = max(max(dataset_kept) for dataset_kept in dataset_kepts)\n\n    x_ticks_minor = np.linspace(0.0, max_dataset_kept, num=21)\n    x_ticks_major = np.linspace(0.0, max_dataset_kept, num=11)\n    x_ticks_major_labels = [\n        \"{value:3.{decimal_digits}f}%\".format(decimal_digits=decimal_digits, value=x * 100) for x in x_ticks_major\n    ]\n    y_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    y_ticks_major = np.linspace(0.0, 1.0, num=11)\n\n    fig, ax = plt.subplots()\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n    ax.set_xticks(x_ticks_major)\n    ax.set_xticks(x_ticks_minor, minor=True)\n    ax.set_xticklabels(x_ticks_major_labels)\n    ax.set_xlim(0, max_dataset_kept)\n    ax.set_xlabel(\"data coverage\")\n\n    ax.set_ylim(0, 1)\n    ax.set_yticks(y_ticks_major)\n    ax.set_yticks(y_ticks_minor, minor=True)\n    ax.set_ylabel(y_label)\n\n    for i in range(len(accuracies)):\n        curr_dotted = dotted[i] if isinstance(dotted, (list, tuple)) and i < len(dotted) else dotted\n        algorithm_name = model_names[i] + \" \" if model_names is not None and i < len(model_names) else \"\"\n        ax.plot(\n            dataset_kepts[i],\n            accuracies[i],\n            label=algorithm_name,\n            color=colors[i],\n            linewidth=3,\n            linestyle=\":\" if curr_dotted else \"-\",\n        )\n\n    ax.legend(frameon=True, loc=3)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef confidence_filtering_data_vs_acc_multiline_plot(\n    accuracies,\n    dataset_kepts,\n    models_names,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(accuracies) == len(dataset_kepts)\n\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab20\").colors\n\n    max_dataset_kept = max(max(dataset_kept) for dataset_kept in dataset_kepts)\n\n    x_ticks_minor = np.linspace(0.0, max_dataset_kept, num=21)\n    x_ticks_major = np.linspace(0.0, max_dataset_kept, num=11)\n    x_ticks_major_labels = [f\"{x * 100:3.0f}%\" for x in x_ticks_major]\n    y_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    y_ticks_major = np.linspace(0.0, 1.0, num=11)\n\n    fig, ax = plt.subplots()\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n    ax.set_xticks(x_ticks_major)\n    ax.set_xticks(x_ticks_minor, minor=True)\n    ax.set_xticklabels(x_ticks_major_labels)\n    ax.set_xlim(0, max_dataset_kept)\n    ax.set_xlabel(\"data coverage\")\n\n    ax.set_ylim(0, 1)\n    ax.set_yticks(y_ticks_major)\n    ax.set_yticks(y_ticks_minor, minor=True)\n    ax.set_ylabel(\"accuracy\")\n\n    for i in range(len(accuracies)):\n        ax.plot(dataset_kepts[i], accuracies[i], color=colors[0], linewidth=1.0, alpha=0.35)\n\n    legend_elements = [Line2D([0], [0], linewidth=1.0, color=colors[0])]\n    ax.legend(legend_elements, models_names)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef confidence_filtering_3d_plot(\n    thresholds_1,\n    thresholds_2,\n    accuracies,\n    dataset_kepts,\n    threshold_output_feature_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(accuracies) == len(dataset_kepts)\n    assert len(thresholds_1) == len(thresholds_2)\n\n    thresholds_1, thresholds_2 = np.meshgrid(thresholds_1, thresholds_2)\n\n    colors = plt.get_cmap(\"tab10\").colors\n    sns.set_style(\"white\")\n\n    z_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    z_ticks_major = np.linspace(0.0, 1.0, num=11)\n    z_ticks_major_labels = [f\"{z * 100:3.0f}%\" for z in z_ticks_major]\n\n    fig = plt.figure()\n    ax = Axes3D\n    ax = fig.add_subplot(111, projection=\"3d\")\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n\n    ax.set_xlabel(f\"{threshold_output_feature_names[0]} probability\")\n    ax.set_ylabel(f\"{threshold_output_feature_names[1]} probability\")\n\n    ax.set_xlim(np.min(thresholds_1), np.max(thresholds_1))\n    ax.set_ylim(np.min(thresholds_2), np.max(thresholds_2))\n    ax.set_zlim(0, 1)\n    ax.set_zticks(z_ticks_major)\n    ax.set_zticklabels(z_ticks_major_labels)\n    ax.set_zticks(z_ticks_minor, minor=True)\n\n    # HACK: Remove padding from 3D axes by adjusting coordinate info\n    from mpl_toolkits.mplot3d.axis3d import Axis\n\n    if not hasattr(Axis, \"_get_coord_info_old\"):\n\n        def _get_coord_info_new(self):\n            result = self._get_coord_info_old()\n            mins, maxs = result[0], result[1]\n            deltas = maxs - mins\n            mins += deltas / 4\n            maxs -= deltas / 4\n            return (mins, maxs) + result[2:]\n\n        Axis._get_coord_info_old = Axis._get_coord_info\n        Axis._get_coord_info = _get_coord_info_new\n\n    surf_1 = ax.plot_surface(\n        thresholds_1,\n        thresholds_2,\n        accuracies,\n        alpha=0.5,\n        label=\"accuracy\",\n        cmap=plt.get_cmap(\"winter\"),\n        edgecolor=\"none\",\n    )\n    surf_2 = ax.plot_surface(\n        thresholds_1,\n        thresholds_2,\n        dataset_kepts,\n        alpha=0.5,\n        label=\"data coverage\",\n        cmap=plt.get_cmap(\"autumn\"),\n        edgecolor=\"none\",\n    )\n\n    handle_1 = copy.copy(surf_1)\n    handle_2 = copy.copy(surf_2)\n\n    handle_1.set_color(colors[0])\n    handle_2.set_color(colors[1])\n\n    # ## the next block is needed because matplotlib 3.3.3 renamed\n    # _edgecolors3d -> _edgecolor3d\n    # _facecolors3d -> _facecolor3d\n    # but we want to try to keep compatibility with older versions\n    # #### BEGIN COMPATIBILITY BLOCK #####\n    if hasattr(handle_1, \"_edgecolors3d\"):\n        edgecolor3d = handle_1._edgecolors3d\n    else:\n        edgecolor3d = handle_1._edgecolor3d\n    handle_1._edgecolors2d = edgecolor3d\n    handle_1._edgecolor2d = edgecolor3d\n\n    if hasattr(handle_2, \"_edgecolors3d\"):\n        edgecolor3d = handle_2._edgecolors3d\n    else:\n        edgecolor3d = handle_2._edgecolor3d\n    handle_2._edgecolors2d = edgecolor3d\n    handle_2._edgecolor2d = edgecolor3d\n\n    if hasattr(handle_1, \"_facecolors3d\"):\n        facecolor3d = handle_1._facecolors3d\n    else:\n        facecolor3d = handle_1._facecolor3d\n    handle_1._facecolors2d = facecolor3d\n    handle_1._facecolor2d = facecolor3d\n\n    if hasattr(handle_2, \"_facecolors3d\"):\n        facecolor3d = handle_2._facecolors3d\n    else:\n        facecolor3d = handle_2._facecolor3d\n    handle_2._facecolors2d = facecolor3d\n    handle_2._facecolor2d = facecolor3d\n    # #### END COMPATIBILITY BLOCK #####\n\n    ax.legend(frameon=True, loc=3, handles=[handle_1, handle_2])\n\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef threshold_vs_metric_plot(\n    thresholds,\n    scores,\n    algorithm_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    # y_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    # y_ticks_major = np.linspace(0.0, 1.0, num=11)\n    # y_ticks_major_labels = ['{:3.0f}%'.format(y * 100) for y in y_ticks_major]\n\n    fig, ax1 = plt.subplots()\n\n    if title is not None:\n        ax1.set_title(title)\n\n    ax1.grid(which=\"both\")\n    ax1.grid(which=\"minor\", alpha=0.5)\n    ax1.grid(which=\"major\", alpha=0.75)\n    ax1.set_xticks([x for idx, x in enumerate(thresholds) if idx % 2 == 0])\n    ax1.set_xticks(thresholds, minor=True)\n\n    # ax1.set_xlim(0, 1)\n    ax1.set_xlabel(\"confidence threshold\")\n\n    # ax1.set_ylim(0, 1)\n    # ax1.set_yticks(y_ticks_major)\n    # ax1.set_yticklabels(y_ticks_major_labels)\n    # ax1.set_yticks(y_ticks_minor, minor=True)\n\n    for i in range(len(scores)):\n        algorithm_name = algorithm_names[i] + \" \" if algorithm_names is not None and i < len(algorithm_names) else \"\"\n        ax1.plot(thresholds, scores[i], label=algorithm_name, color=colors[i], linewidth=3, marker=\"o\")\n\n    ax1.legend(frameon=True)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef roc_curves(\n    fpr_tprs,\n    algorithm_names=None,\n    title=None,\n    graded_color=False,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n    colormap = plt.get_cmap(\"RdYlGn\")\n\n    y_ticks_minor = np.linspace(0.0, 1.0, num=21)\n    y_ticks_major = np.linspace(0.0, 1.0, num=11)\n\n    fig, ax = plt.subplots()\n\n    if title is not None:\n        ax.set_title(title)\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n\n    ax.set_xlim(0, 1)\n    ax.set_xlabel(\"False positive rate\")\n\n    ax.set_ylim(0, 1)\n    ax.set_yticks(y_ticks_major)\n    ax.set_yticks(y_ticks_minor, minor=True)\n    ax.set_ylabel(\"True positive rate\")\n\n    plt.plot([0, 1], [0, 1], color=\"black\", linewidth=3, linestyle=\"--\")\n\n    for i in range(len(fpr_tprs)):\n        algorithm_name = algorithm_names[i] + \" \" if algorithm_names is not None and i < len(algorithm_names) else \"\"\n        color = colormap(i / len(fpr_tprs)) if graded_color else colors[i]\n        ax.plot(fpr_tprs[i][0], fpr_tprs[i][1], label=algorithm_name, color=color, linewidth=3)\n\n    ax.legend(frameon=True)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef precision_recall_curves_plot(\n    precision_recalls: dict[str, list[float]],\n    model_names: list[str],\n    title: str = None,\n    filename: str = None,\n    callbacks=None,\n):\n    \"\"\"Generates a precision recall curve for each model in the model_names list.\n\n    Args:\n        precision_recalls: A list of dictionaries representing the precision and recall values for each model\n            in model_names. Each dictionary has two keys: \"precisions\" and \"recalls\".\n    \"\"\"\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    _, ax = plt.subplots()\n\n    ax.set_xlim(0, 1)\n    # Create ticks for every 0.1 increment\n    ax.set_xticks(np.linspace(0, 1, 11))\n    ax.set_xlabel(\"Recall\")\n\n    ax.set_ylim(0, 1)\n    # Create ticks for every 0.1 increment\n    ax.set_yticks(np.linspace(0, 1, 11))\n    ax.set_ylabel(\"Precision\")\n\n    if title is not None:\n        ax.set_title(title)\n\n    for i in range(len(precision_recalls)):\n        model_name = model_names[i] if model_names is not None and i < len(model_names) else \"\"\n        ax.plot(\n            precision_recalls[i][\"recalls\"],\n            precision_recalls[i][\"precisions\"],\n            label=model_name,\n            color=colors[i],\n            linewidth=3,\n        )\n\n    ax.legend(frameon=True)\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef calibration_plot(\n    fraction_positives,\n    mean_predicted_values,\n    algorithm_names=None,\n    class_name=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(fraction_positives) == len(mean_predicted_values)\n\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    num_algorithms = len(fraction_positives)\n\n    plt.figure(figsize=(9, 9))\n    plt.grid(which=\"both\")\n    plt.grid(which=\"minor\", alpha=0.5)\n    plt.grid(which=\"major\", alpha=0.75)\n\n    plt.plot([0, 1], [0, 1], \"k:\", label=\"Perfectly calibrated\")\n\n    for i in range(num_algorithms):\n        # ax1.plot(mean_predicted_values[i], fraction_positives[i],\n        #         label=algorithms[i] if algorithm_names is not None and i < len(algorithms) else '')\n\n        # sns.tsplot(mean_predicted_values[i], fraction_positives[i], ax=ax1, color=colors[i])\n\n        assert len(mean_predicted_values[i]) == len(fraction_positives[i])\n        order = min(3, len(mean_predicted_values[i]) - 1)\n\n        sns.regplot(\n            x=mean_predicted_values[i],\n            y=fraction_positives[i],\n            order=order,\n            x_estimator=np.mean,\n            color=colors[i],\n            marker=\"o\",\n            scatter_kws={\"s\": 40},\n            label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f\"Model {i}\",\n        )\n\n    ticks = np.linspace(0.0, 1.0, num=11)\n    plt.xlim([-0.05, 1.05])\n    plt.xticks(ticks)\n    plt.xlabel(\"Predicted probability\")\n    plt.ylabel(\"Observed probability\")\n    plt.ylim([-0.05, 1.05])\n    plt.yticks(ticks)\n    plt.legend(loc=\"lower right\")\n    if class_name is not None:\n        plt.title(f\"{class_name}: Calibration (reliability curve)\")\n    else:\n        plt.title(\"Calibration (reliability curve)\")\n\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef brier_plot(\n    brier_scores,\n    algorithm_names=None,\n    class_names=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    # Dynamically set the size of the plot based on the number of labels\n    # Use minimum size to prevent plot from being too small\n    default_width, default_height = plt.rcParams.get(\"figure.figsize\")\n    width = max(default_width, len(class_names) / 2)\n    height = max(default_height, len(class_names) / 2)\n    fig, ax = plt.subplots(figsize=(width, height))\n\n    if title is not None:\n        plt.title(title)\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    n_algorithms = brier_scores.shape[1]\n    n_classes = brier_scores.shape[0]\n    x = np.arange(n_classes)\n\n    max_width = 0.35\n    bar_width = min(0.5 / n_algorithms, max_width)\n    bar_left = -bar_width * (n_algorithms // 2) + ((bar_width / 2) if (n_algorithms % 2) == 0 else 0)\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n    ax.set_xlabel(\"class\")\n    ax.set_ylabel(\"brier score\")\n    if class_names is not None:\n        ax.set_xticks(\n            x,\n            class_names,\n            rotation=45,\n            ha=\"center\",\n        )\n    else:\n        ax.set_xticks(\n            x,\n            [str(i) for i in range(n_classes)],\n            rotation=45,\n            ha=\"center\",\n        )\n\n    for i in range(n_algorithms):\n        # Plot bar for each class\n        label = algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else f\"Model {i}\"\n        ax.bar(x + bar_left + (bar_width * i), brier_scores[:, i], bar_width, color=colors[i], label=label)\n\n    ax.legend()\n    fig.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef predictions_distribution_plot(\n    probabilities,\n    algorithm_names=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    num_algorithms = len(probabilities)\n\n    plt.figure(figsize=(9, 9))\n    plt.grid(which=\"both\")\n    plt.grid(which=\"minor\", alpha=0.5)\n    plt.grid(which=\"major\", alpha=0.75)\n\n    for i in range(num_algorithms):\n        plt.hist(\n            probabilities[i],\n            range=(0, 1),\n            bins=41,\n            color=colors[i],\n            label=algorithm_names[i] if algorithm_names is not None and i < len(algorithm_names) else \"\",\n            histtype=\"stepfilled\",\n            alpha=0.5,\n            lw=2,\n        )\n\n    plt.xlabel(\"Mean predicted value\")\n    plt.xlim([0, 1])\n    plt.xticks(np.linspace(0.0, 1.0, num=21))\n    plt.ylabel(\"Count\")\n    plt.legend(loc=\"upper center\", ncol=2)\n\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef confusion_matrix_plot(\n    confusion_matrix,\n    labels=None,\n    output_feature_name=None,\n    filename=None,\n    callbacks=None,\n):\n    mpl.rcParams.update({\"figure.autolayout\": True})\n\n    # Dynamically set the size of the plot based on the number of labels\n    # Use minimum size to prevent plot from being too small\n    default_width, default_height = plt.rcParams.get(\"figure.figsize\")\n    width = max(default_width, len(labels))\n    height = max(default_height, len(labels))\n    fig, ax = plt.subplots(figsize=(width, height))\n\n    ax.invert_yaxis()\n    ax.xaxis.tick_top()\n    ax.xaxis.set_label_position(\"top\")\n\n    # Set alpha value to prevent blue hues from being too dark\n    cax = ax.matshow(confusion_matrix, cmap=\"Blues\", alpha=0.6)\n    # Annotate confusion matrix plot\n    for (i, j), z in np.ndenumerate(confusion_matrix):\n        # Format differently based on whether the value is normalized or not\n        if z.is_integer():\n            z_format = f\"{z:.0f}\"\n        else:\n            z_format = f\"{z:.3f}\"\n        ax.text(\n            j,\n            i,\n            z_format,\n            ha=\"center\",\n            va=\"center\",\n            color=\"black\",\n            fontweight=\"medium\",\n            wrap=True,\n        )\n\n    ax.xaxis.set_major_locator(ticker.MultipleLocator(1))\n    ax.yaxis.set_major_locator(ticker.MultipleLocator(1))\n    ax.set_xticklabels([\"\"] + labels, rotation=45, ha=\"left\")\n    ax.set_yticklabels([\"\"] + labels, rotation=45, ha=\"right\")\n    ax.grid(False)\n    ax.tick_params(axis=\"both\", which=\"both\", length=0)\n    # https://stackoverflow.com/a/26720422/10102370 works nicely for square plots\n    fig.colorbar(cax, ax=ax, extend=\"max\", fraction=0.046, pad=0.04)\n    ax.set_xlabel(f\"Predicted {output_feature_name}\")\n    ax.set_ylabel(f\"Actual {output_feature_name}\")\n\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename, bbox_inches=\"tight\")\n    else:\n        plt.show()\n\n\ndef double_axis_line_plot(\n    y1_sorted,\n    y2,\n    y1_name,\n    y2_name,\n    labels=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    # Dynamically adjust figure size based on number of labels\n    default_width, default_height = plt.rcParams.get(\"figure.figsize\")\n    width = max(default_width, len(labels) / 3)\n    height = max(default_height, len(labels) / 3)\n    fig, ax1 = plt.subplots(layout=\"constrained\", figsize=(width, height))\n\n    if title is not None:\n        ax1.set_title(title)\n\n    ax1.set_xlabel(f\"class (sorted by {y1_name})\")\n    ax1.set_xlim(0, len(y1_sorted) - 1)\n    if labels is not None:\n        ax1.set_xticklabels(labels, rotation=45, ha=\"right\")\n        ax1.set_xticks(np.arange(len(labels)))\n\n    ax1.set_ylabel(y1_name, color=colors[1])\n    ax1.tick_params(\"y\", colors=colors[1])\n    ax1.set_ylim(min(y1_sorted), max(y1_sorted))\n\n    ax2 = ax1.twinx()\n    ax2.set_ylabel(y2_name, color=colors[0])\n    ax2.tick_params(\"y\", colors=colors[0])\n    ax2.set_ylim(min(y2), max(y2))\n\n    ax1.plot(y1_sorted, label=y1_name, color=colors[1], linewidth=4)\n    ax2.plot(y2, label=y2_name, color=colors[0], linewidth=3)\n\n    fig.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef plot_matrix(\n    matrix,\n    cmap=\"hot\",\n    filename=None,\n    callbacks=None,\n):\n    plt.figure()\n    plt.matshow(matrix, cmap=cmap)\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef plot_distributions(\n    distributions,\n    labels=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    fig, ax1 = plt.subplots()\n\n    if title is not None:\n        ax1.set_title(title)\n\n    ax1.grid(which=\"both\")\n    ax1.grid(which=\"minor\", alpha=0.5)\n    ax1.grid(which=\"major\", alpha=0.75)\n\n    ax1.set_xlabel(\"class\")\n\n    ax1.set_ylabel(\"p\")\n    ax1.tick_params(\"y\")\n\n    for i, distribution in enumerate(distributions):\n        ax1.plot(\n            distribution,\n            color=colors[i],\n            alpha=0.6,\n            label=labels[i] if labels is not None and i < len(labels) else f\"Distribution {i}\",\n        )\n\n    ax1.legend(frameon=True)\n    fig.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef plot_distributions_difference(\n    distribution,\n    labels=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    sns.set_style(\"whitegrid\")\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    fig, ax1 = plt.subplots()\n\n    if title is not None:\n        ax1.set_title(title)\n\n    ax1.grid(which=\"both\")\n    ax1.grid(which=\"minor\", alpha=0.5)\n    ax1.grid(which=\"major\", alpha=0.75)\n\n    ax1.set_xlabel(\"class\")\n\n    ax1.set_ylabel(\"p\")\n    ax1.tick_params(\"y\")\n\n    ax1.plot(distribution, color=colors[0])\n\n    fig.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef bar_plot(\n    xs,\n    ys,\n    decimals=4,\n    labels=None,\n    title=None,\n    filename=None,\n    callbacks=None,\n):\n    assert len(xs) == len(ys)\n    assert len(xs) > 0\n\n    sns.set_style(\"whitegrid\")\n\n    # Dynamically set the size of the plot based on the number of labels\n    # Use minimum size to prevent plot from being too small\n    default_width, default_height = plt.rcParams.get(\"figure.figsize\")\n    width = max(default_width, len(labels) / 2)\n    _, ax = plt.subplots(figsize=(width, default_height))\n\n    ax.grid(which=\"both\")\n    ax.grid(which=\"minor\", alpha=0.5)\n    ax.grid(which=\"major\", alpha=0.75)\n\n    if title is not None:\n        ax.set_title(title)\n\n    colors = plt.get_cmap(\"tab10\").colors\n\n    ax.invert_yaxis()  # labels read top-to-bottom\n\n    maximum = ys.max()\n    ticks = np.arange(len(xs))\n    ax.set_yticks(ticks)\n    if labels is None:\n        ax.set_yticklabels(xs)\n    else:\n        ax.set_yticklabels(labels)\n\n    ax.barh(ticks, ys, color=colors[0], align=\"center\")\n\n    for i, v in enumerate(ys):\n        if v < maximum * (0.025 * decimals + 0.1):\n            x = v + maximum * 0.01\n            horizontal_alignment = \"left\"\n        else:\n            x = v - maximum * 0.01\n            horizontal_alignment = \"right\"\n        txt = ax.text(\n            x,\n            ticks[i],\n            (\"{:.\" + str(decimals) + \"f}\").format(v),\n            color=\"white\",\n            fontweight=\"bold\",\n            verticalalignment=\"center\",\n            horizontalalignment=horizontal_alignment,\n        )\n        txt.set_path_effects([PathEffects.withStroke(linewidth=3, foreground=\"black\")])\n\n    plt.tight_layout()\n    visualize_callbacks(callbacks, plt.gcf())\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef hyperopt_report(hyperparameters, hyperopt_results_df, metric, filename_template, float_precision=3):\n    title = \"Hyperopt Report: {}\"\n    for hp_name, hp_params in hyperparameters.items():\n        if hp_params[SPACE] in RAY_TUNE_INT_SPACES:\n            hyperopt_int_plot(\n                hyperopt_results_df,\n                hp_name,\n                metric,\n                title.format(hp_name),\n                filename_template.format(hp_name) if filename_template else None,\n            )\n        elif hp_params[SPACE] in RAY_TUNE_FLOAT_SPACES:\n            hyperopt_float_plot(\n                hyperopt_results_df,\n                hp_name,\n                metric,\n                title.format(hp_name),\n                filename_template.format(hp_name) if filename_template else None,\n                log_scale_x=hp_params[\"scale\"] == \"log\" if \"scale\" in hp_params else False,\n            )\n        elif hp_params[SPACE] in RAY_TUNE_CATEGORY_SPACES:\n            hyperopt_category_plot(\n                hyperopt_results_df,\n                hp_name,\n                metric,\n                title.format(hp_name),\n                filename_template.format(hp_name) if filename_template else None,\n            )\n        else:\n            # TODO: more research needed on how to handle RayTune \"sample_from\" search space\n            raise ValueError(\n                f\"{hp_params[SPACE]} search space not supported in Ludwig.  \"  # noqa: E713\n                f\"Supported values are {RAY_TUNE_FLOAT_SPACES | RAY_TUNE_INT_SPACES | RAY_TUNE_CATEGORY_SPACES}.\"\n            )\n\n    # quantize float and int columns\n    for hp_name, hp_params in hyperparameters.items():\n        if hp_params[SPACE] in RAY_TUNE_INT_SPACES:\n            num_distinct_values = len(hyperopt_results_df[hp_name].unique())\n            if num_distinct_values > INT_QUANTILES:\n                hyperopt_results_df[hp_name] = pd.qcut(hyperopt_results_df[hp_name], q=INT_QUANTILES, precision=0)\n        elif hp_params[SPACE] in RAY_TUNE_FLOAT_SPACES:\n            hyperopt_results_df[hp_name] = pd.qcut(\n                hyperopt_results_df[hp_name],\n                q=FLOAT_QUANTILES,\n                precision=float_precision,\n                duplicates=\"drop\",\n            )\n\n    hyperopt_pair_plot(\n        hyperopt_results_df,\n        metric,\n        title.format(\"pair plot\"),\n        filename_template.format(\"pair_plot\") if filename_template else None,\n    )\n\n\ndef hyperopt_int_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale_x=False, log_scale_y=True):\n    sns.set_style(\"whitegrid\")\n    plt.figure()\n    seaborn_figure = sns.scatterplot(x=hp_name, y=metric, data=hyperopt_results_df)\n    seaborn_figure.set_title(title)\n    if log_scale_x:\n        seaborn_figure.set(xscale=\"log\")\n    if log_scale_y:\n        seaborn_figure.set(yscale=\"log\")\n    seaborn_figure.xaxis.set_major_locator(ticker.MaxNLocator(integer=True))\n    seaborn_figure.xaxis.set_major_formatter(ticker.ScalarFormatter())\n    seaborn_figure.xaxis.set_minor_formatter(ticker.NullFormatter())\n    seaborn_figure.figure.tight_layout()\n    if filename:\n        seaborn_figure.figure.savefig(filename)\n    else:\n        seaborn_figure.figure.show()\n\n\ndef hyperopt_float_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale_x=False, log_scale_y=True):\n    sns.set_style(\"whitegrid\")\n    plt.figure()\n    seaborn_figure = sns.scatterplot(x=hp_name, y=metric, data=hyperopt_results_df)\n    seaborn_figure.set_title(title)\n    seaborn_figure.set(ylabel=metric)\n    if log_scale_x:\n        seaborn_figure.set(xscale=\"log\")\n    if log_scale_y:\n        seaborn_figure.set(yscale=\"log\")\n    seaborn_figure.figure.tight_layout()\n    if filename:\n        seaborn_figure.figure.savefig(filename)\n    else:\n        seaborn_figure.figure.show()\n\n\ndef hyperopt_category_plot(hyperopt_results_df, hp_name, metric, title, filename, log_scale=True):\n    sns.set_style(\"whitegrid\")\n    plt.figure()\n\n    # Ensure that all parameter values have at least 2 trials, otherwise the Raincloud Plot will create awkward\n    # looking \"flat clouds\" in the cloud part of the plot (the \"rain\" part is ok with 1 trial). In this case,\n    # just use stripplots since they are categorical scatter plots.\n    parameter_to_trial_count = hyperopt_results_df[hp_name].value_counts()\n    parameter_to_trial_count = parameter_to_trial_count[parameter_to_trial_count < 2]\n\n    if len(parameter_to_trial_count) != 0:\n        seaborn_figure = sns.stripplot(x=hp_name, y=metric, data=hyperopt_results_df, size=5)\n    else:\n        seaborn_figure = pt.RainCloud(\n            x=hp_name,\n            y=metric,\n            data=hyperopt_results_df,\n            palette=\"Set2\",\n            bw=0.2,\n            width_viol=0.7,\n            point_size=6,\n            cut=1,\n        )\n\n    seaborn_figure.set_title(title)\n    seaborn_figure.set(ylabel=metric)\n    sns.despine()\n    if log_scale:\n        seaborn_figure.set(yscale=\"log\")\n    plt.tight_layout()\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef hyperopt_pair_plot(hyperopt_results_df, metric, title, filename):\n    params = sorted(list(hyperopt_results_df.keys()))\n    params.remove(metric)\n    num_param = len(params)\n\n    # Pair plot is empty if there's only 1 parameter, so skip creating a pair plot\n    if num_param == 1:\n        return\n\n    sns.set_style(\"white\")\n    fig = plt.figure(figsize=(20, 20))\n    fig.suptitle(title)\n    gs = fig.add_gridspec(num_param, num_param)\n\n    for i, param1 in enumerate(params):\n        for j, param2 in enumerate(params):\n            if i != j:\n                ax = fig.add_subplot(gs[i, j])\n                heatmap = hyperopt_results_df.pivot_table(index=param1, columns=param2, values=metric, aggfunc=\"mean\")\n                sns.heatmap(\n                    heatmap,\n                    linewidths=1,\n                    cmap=\"viridis\",\n                    cbar_kws={\"label\": metric},\n                    ax=ax,\n                )\n\n    plt.tight_layout(pad=5)\n    if filename:\n        plt.savefig(filename)\n    else:\n        plt.show()\n\n\ndef hyperopt_hiplot(\n    hyperopt_df,\n    filename,\n):\n    import hiplot as hip\n\n    experiment = hip.Experiment.from_dataframe(hyperopt_df)\n    experiment.to_html(filename)\n"
  },
  {
    "path": "ludwig/vector_index/__init__.py",
    "content": "import logging\n\nfrom ludwig.api_annotations import DeveloperAPI\nfrom ludwig.vector_index.base import VectorIndex\n\nlogger = logging.getLogger(__name__)\n\n\nFAISS = \"faiss\"\n\nALL_INDICES = [FAISS]\n\n\ndef get_faiss_index_cls() -> type[VectorIndex]:\n    from ludwig.vector_index.faiss import FaissIndex\n\n    return FaissIndex\n\n\n# TODO(travis): add other indexing structures\nvector_index_registry = {\n    FAISS: get_faiss_index_cls,\n}\n\n\n@DeveloperAPI\ndef get_vector_index_cls(type: str) -> type[VectorIndex]:\n    return vector_index_registry[type]()\n"
  },
  {
    "path": "ludwig/vector_index/base.py",
    "content": "from abc import ABC, abstractmethod\n\nimport numpy as np\n\n\nclass VectorIndex(ABC):\n    @abstractmethod\n    def search(self, query: np.ndarray, k: int) -> np.ndarray:\n        pass\n\n    @abstractmethod\n    def save(self, path: str):\n        pass\n\n    @classmethod\n    @abstractmethod\n    def from_path(cls, path: str) -> \"VectorIndex\":\n        pass\n\n    @classmethod\n    @abstractmethod\n    def from_embeddings(cls, embeddings: np.ndarray) -> \"VectorIndex\":\n        pass\n"
  },
  {
    "path": "ludwig/vector_index/faiss.py",
    "content": "import faiss\nimport numpy as np\n\nfrom ludwig.vector_index.base import VectorIndex\n\n\nclass FaissIndex(VectorIndex):\n    def __init__(self, index: faiss.Index):\n        self.index = index\n\n    def search(self, query: np.ndarray, k: int) -> np.ndarray:\n        top_k = self.index.search(query.reshape(1, -1), k)\n        return top_k[1].tolist()[0]\n\n    def save(self, path: str):\n        faiss.write_index(self.index, path)\n\n    @classmethod\n    def from_path(cls, path: str) -> \"VectorIndex\":\n        index = faiss.read_index(path)\n        return cls(index)\n\n    @classmethod\n    def from_embeddings(cls, embeddings: np.ndarray) -> \"VectorIndex\":\n        index = faiss.IndexFlatL2(embeddings.shape[1])\n        index.add(embeddings)\n        return cls(index)\n"
  },
  {
    "path": "ludwig/visualize.py",
    "content": "#! /usr/bin/env python\n# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport argparse\nimport itertools\nimport logging\nimport os\nimport sys\nfrom collections.abc import Callable\nfrom functools import partial\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport sklearn\nfrom scipy.stats import entropy\nfrom sklearn.calibration import calibration_curve\nfrom sklearn.metrics import brier_score_loss\nfrom yaml import warnings\n\nfrom ludwig.api import EvaluationFrequency, TrainingStats\nfrom ludwig.api_annotations import DeveloperAPI, PublicAPI\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import ACCURACY, EDIT_DISTANCE, HITS_AT_K, LOSS, PREDICTIONS, SPACE, SPLIT\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.utils import visualization_utils\nfrom ludwig.utils.data_utils import (\n    CACHEABLE_FORMATS,\n    data_reader_registry,\n    figure_data_format_dataset,\n    load_array,\n    load_from_file,\n    load_json,\n    replace_file_extension,\n)\nfrom ludwig.utils.dataframe_utils import to_numpy_dataset, unflatten_df\nfrom ludwig.utils.fs_utils import path_exists\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.print_utils import get_logging_level_registry\nfrom ludwig.utils.types import DataFrame\n\nlogger = logging.getLogger(__name__)\n\n_PREDICTIONS_SUFFIX = \"_predictions\"\n_PROBABILITIES_SUFFIX = \"_probabilities\"\n_CSV_SUFFIX = \"csv\"\n_PARQUET_SUFFIX = \"parquet\"\n\n\ndef _convert_ground_truth(ground_truth, feature_metadata, ground_truth_apply_idx, positive_label):\n    \"\"\"Converts non-np.array representation to be np.array.\"\"\"\n    if \"str2idx\" in feature_metadata:\n        # categorical output feature as binary\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n        # convert category index to binary representation\n        ground_truth = ground_truth == positive_label\n    else:\n        # binary output feature\n        if \"str2bool\" in feature_metadata:\n            # non-standard boolean representation\n            ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2bool\"], ground_truth_apply_idx)\n        else:\n            # standard boolean representation\n            ground_truth = ground_truth.values\n\n        # ensure positive_label is 1 for binary feature\n        positive_label = 1\n\n    # convert to 0/1 representation and return\n    return ground_truth.astype(int), positive_label\n\n\ndef _vectorize_ground_truth(\n    ground_truth: pd.Series, str2idx: np.array, ground_truth_apply_idx: bool = True\n) -> np.array:\n    # raw hdf5 files generated during preprocessing don't need to be converted with str2idx\n    if not ground_truth_apply_idx:\n        return np.vectorize(lambda x, y: x)(ground_truth, str2idx)\n\n    try:\n        return np.vectorize(_encode_categorical_feature)(ground_truth, str2idx)\n    except KeyError as e:\n        logger.info(f\"Unable to vectorize using str2idx with exception {e}. Falling back to ignoring str2idx\")\n        return np.vectorize(lambda x, y: x)(ground_truth, str2idx)\n\n\n@DeveloperAPI\ndef validate_conf_thresholds_and_probabilities_2d_3d(probabilities, threshold_output_feature_names):\n    \"\"\"Ensure probabilities and threshold output_feature_names arrays have two members each.\n\n    :param probabilities: List of probabilities per model\n    :param threshhold_output_feature_names: List of threshhold output_feature_names per model\n    :raise: RuntimeError\n    \"\"\"\n    validation_mapping = {\n        \"probabilities\": probabilities,\n        \"threshold_output_feature_names\": threshold_output_feature_names,\n    }\n    for item, value in validation_mapping.items():\n        item_len = len(value)\n        if item_len != 2:\n            exception_message = \"Two {} should be provided - \" \"{} was given.\".format(item, item_len)\n            logger.error(exception_message)\n            raise RuntimeError(exception_message)\n\n\n@DeveloperAPI\ndef load_data_for_viz(load_type, model_file_statistics, dtype=int, ground_truth_split=2) -> dict[str, Any]:\n    \"\"\"Load JSON files (training stats, evaluation stats...) for a list of models.\n\n    :param load_type: type of the data loader to be used.\n    :param model_file_statistics: JSON file or list of json files containing any model experiment stats. :return List of\n        training statistics loaded as json objects.\n    \"\"\"\n    supported_load_types = dict(\n        load_json=load_json,\n        load_from_file=partial(load_from_file, dtype=dtype, ground_truth_split=ground_truth_split),\n    )\n    loader = supported_load_types[load_type]\n    # Loads training stats from JSON file(s).\n    try:\n        stats_per_model = [loader(stats_f) for stats_f in model_file_statistics]\n    except (TypeError, AttributeError):\n        logger.exception(f\"Unable to open model statistics file {model_file_statistics}!\")\n        raise\n    return stats_per_model\n\n\ndef _load_training_stats(data: dict) -> TrainingStats:\n    \"\"\"Construct a TrainingStats from a dict loaded from JSON.\"\"\"\n    eval_freq = data.get(\"evaluation_frequency\")\n    if isinstance(eval_freq, dict):\n        eval_freq = EvaluationFrequency(**eval_freq)\n    elif eval_freq is None:\n        eval_freq = EvaluationFrequency()\n    return TrainingStats(\n        training=data.get(\"training\", {}),\n        validation=data.get(\"validation\", {}),\n        test=data.get(\"test\", {}),\n        evaluation_frequency=eval_freq,\n    )\n\n\n@DeveloperAPI\ndef load_training_stats_for_viz(load_type, model_file_statistics, dtype=int, ground_truth_split=2) -> TrainingStats:\n    \"\"\"Load model file data (specifically training stats) for a list of models.\n\n    :param load_type: type of the data loader to be used.\n    :param model_file_statistics: JSON file or list of json files containing any model experiment stats. :return List of\n        model statistics loaded as TrainingStats objects.\n    \"\"\"\n    stats_per_model = load_data_for_viz(\n        load_type, model_file_statistics, dtype=dtype, ground_truth_split=ground_truth_split\n    )\n    try:\n        stats_per_model = [_load_training_stats(j) for j in stats_per_model]\n    except Exception:\n        logger.exception(f\"Failed to load model statistics {model_file_statistics}!\")\n        raise\n    return stats_per_model\n\n\n@DeveloperAPI\ndef convert_to_list(item):\n    \"\"\"If item is not list class instance or None put inside a list.\n\n    :param item: object to be checked and converted\n    :return: original item if it is a list instance or list containing the item.\n    \"\"\"\n    return item if item is None or isinstance(item, list) else [item]\n\n\ndef _validate_output_feature_name_from_train_stats(output_feature_name, train_stats_per_model):\n    \"\"\"Validate prediction output_feature_name from model train stats and return it as list.\n\n    :param output_feature_name: output_feature_name containing ground truth\n    :param train_stats_per_model: list of per model train stats\n    :return output_feature_names: list of output_feature_name(s) containing ground truth\n    \"\"\"\n    output_feature_names_set = set()\n    for train_stats in train_stats_per_model:\n        for key in itertools.chain(train_stats.training.keys(), train_stats.validation.keys(), train_stats.test.keys()):\n            output_feature_names_set.add(key)\n    try:\n        if output_feature_name in output_feature_names_set:\n            return [output_feature_name]\n        else:\n            return output_feature_names_set\n    # raised if output_feature_name is empty iterable (e.g. [] in set())\n    except TypeError:\n        return output_feature_names_set\n\n\ndef _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model):\n    \"\"\"Validate prediction output_feature_name from model test stats and return it as list.\n\n    :param output_feature_name: output_feature_name containing ground truth\n    :param test_stats_per_model: list of per model test stats\n    :return output_feature_names: list of output_feature_name(s) containing ground truth\n    \"\"\"\n    output_feature_names_set = set()\n    for ls in test_stats_per_model:\n        for key in ls:\n            output_feature_names_set.add(key)\n    try:\n        if output_feature_name in output_feature_names_set:\n            return [output_feature_name]\n        else:\n            return output_feature_names_set\n    # raised if output_feature_name is empty iterable (e.g. [] in set())\n    except TypeError:\n        return output_feature_names_set\n\n\ndef _encode_categorical_feature(raw: np.array, str2idx: dict) -> np.array:\n    \"\"\"Encodes raw categorical string value to encoded numeric value.\n\n    Args:\n    :param raw: (np.array) string categorical representation\n    :param str2idx: (dict) dictionary that maps string representation to\n        encoded value.\n\n    Returns:\n        np.array\n    \"\"\"\n    return str2idx[raw]\n\n\ndef _get_ground_truth_df(ground_truth: str) -> DataFrame:\n    # determine ground truth data format and get appropriate reader\n    data_format = figure_data_format_dataset(ground_truth)\n    if data_format not in CACHEABLE_FORMATS:\n        raise ValueError(\n            \"{} is not supported for ground truth file, \" \"valid types are {}\".format(data_format, CACHEABLE_FORMATS)\n        )\n    reader = get_from_registry(data_format, data_reader_registry)\n\n    # retrieve ground truth from source data set\n    if data_format in {\"csv\", \"tsv\"}:\n        return reader(ground_truth, dtype=None, df_lib=pd)  # allow type inference\n    return reader(ground_truth, df_lib=pd)\n\n\ndef _extract_ground_truth_values(\n    ground_truth: str | DataFrame,\n    output_feature_name: str,\n    ground_truth_split: int,\n    split_file: str | None = None,\n) -> pd.Series:\n    \"\"\"Helper function to extract ground truth values.\n\n    Args:\n    :param ground_truth: (str, DataFrame) path to source data containing ground truth or ground truth dataframe\n    :param output_feature_name: (str) output feature name for ground\n        truth values.\n    :param ground_truth_split: (int) dataset split to use for ground truth,\n        defaults to 2.\n    :param split_file: (Union[str, None]) optional file path to split values.\n\n    # Return\n\n    :return pd.Series: ground truth values from source data set\n    \"\"\"\n    ground_truth_df = _get_ground_truth_df(ground_truth) if isinstance(ground_truth, str) else ground_truth\n\n    # extract ground truth for visualization\n    if SPLIT in ground_truth_df:\n        # get split value from source data set\n        split = ground_truth_df[SPLIT]\n        gt = ground_truth_df[output_feature_name][split == ground_truth_split]\n    elif split_file is not None:\n        # retrieve from split file\n        if split_file.endswith(\".csv\"):\n            # Legacy code path for previous split file format\n            warnings.warn(\n                \"Using a CSV split file is deprecated and will be removed in a future version. \"\n                \"Please retrain or convert to Parquet\",\n                DeprecationWarning,\n            )\n            split = load_array(split_file)\n            mask = split == ground_truth_split\n        else:\n            split = pd.read_parquet(split_file)\n\n            # Realign index from the split file with the ground truth to account for\n            # dropped rows during preprocessing.\n            # https://stackoverflow.com/a/65731168\n            mask = split.iloc[:, 0] == ground_truth_split\n            mask = mask.reindex(ground_truth_df.index, fill_value=False)\n\n        gt = ground_truth_df[output_feature_name][mask]\n    else:\n        # use all the data in ground_truth\n        gt = ground_truth_df[output_feature_name]\n\n    return gt\n\n\ndef _get_cols_from_predictions(predictions_paths, cols, metadata):\n    results_per_model = []\n    for predictions_path in predictions_paths:\n        pred_df = pd.read_parquet(predictions_path)\n\n        shapes_fname = replace_file_extension(predictions_path, \"shapes.json\")\n        if path_exists(shapes_fname):\n            column_shapes = load_json(shapes_fname)\n            pred_df = unflatten_df(pred_df, column_shapes, LOCAL_BACKEND.df_engine)\n\n        for col in cols:\n            # Convert categorical features back to indices\n            if col.endswith(_PREDICTIONS_SUFFIX):\n                feature_name = col[: -len(_PREDICTIONS_SUFFIX)]\n                feature_metadata = metadata[feature_name]\n                if \"str2idx\" in feature_metadata:\n                    pred_df[col] = pred_df[col].map(lambda x: feature_metadata[\"str2idx\"][x])\n\n        pred_df = to_numpy_dataset(pred_df, LOCAL_BACKEND)\n        results_per_model += [pred_df[col] for col in cols]\n\n    return results_per_model\n\n\n@DeveloperAPI\ndef generate_filename_template_path(output_dir, filename_template):\n    \"\"\"Ensure path to template file can be constructed given an output dir.\n\n    Create output directory if yet does exist.\n    :param output_dir: Directory that will contain the filename_template file\n    :param filename_template: name of the file template to be appended to the filename template path\n    :return: path to filename template inside the output dir or None if the output dir is None\n    \"\"\"\n    if output_dir:\n        os.makedirs(output_dir, exist_ok=True)\n        return os.path.join(output_dir, filename_template)\n    return None\n\n\n@DeveloperAPI\ndef compare_performance_cli(test_statistics: str | list[str], **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by compare_performance.\n\n    # Inputs\n\n    :param test_statistics: (Union[str, List[str]]) path to experiment test statistics file.\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    compare_performance(test_stats_per_model, **kwargs)\n\n\n@DeveloperAPI\ndef learning_curves_cli(training_statistics: str | list[str], **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by learning_curves.\n\n    # Inputs\n\n    :param training_statistics: (Union[str, List[str]]) path to experiment training statistics file\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    train_stats_per_model = load_training_stats_for_viz(\"load_json\", training_statistics)\n    learning_curves(train_stats_per_model, **kwargs)\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_from_prob_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_from_prob.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n        results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n    :return None:\n    \"\"\"\n\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # translate string to encoded numeric value\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(\n        ground_truth, output_feature_name, ground_truth_split, split_file=split_file\n    )\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n\n    compare_classifiers_performance_from_prob(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_from_pred_cli(\n    predictions: list[str],\n    ground_truth: str,\n    ground_truth_metadata: str,\n    ground_truth_split: int,\n    split_file: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_from_pred.\n\n    # Inputs\n\n    :param predictions: (List[str]) list of prediction results file names\n        to extract predictions from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_metadata: (str) path to ground truth metadata file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n        results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PREDICTIONS_SUFFIX}\"\n    predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata)\n\n    compare_classifiers_performance_from_pred(\n        predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_subset_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_subset.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n\n    compare_classifiers_performance_subset(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_changing_k_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_changing_k.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    compare_classifiers_performance_changing_k(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_multiclass_multimetric_cli(\n    test_statistics: str | list[str], ground_truth_metadata: str, **kwargs: dict\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_multiclass.\n\n    # Inputs\n\n    :param test_statistics: (Union[str, List[str]]) path to experiment test statistics file.\n    :param ground_truth_metadata: (str) path to ground truth metadata file.\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    metadata = load_json(ground_truth_metadata)\n    compare_classifiers_multiclass_multimetric(test_stats_per_model, metadata=metadata, **kwargs)\n\n\n@DeveloperAPI\ndef compare_classifiers_predictions_cli(\n    predictions: list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_predictions.\n\n    # Inputs\n\n    :param predictions: (List[str]) list of prediction results file names\n        to extract predictions from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PREDICTIONS_SUFFIX}\"\n    predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata)\n\n    compare_classifiers_predictions(\n        predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_predictions_distribution_cli(\n    predictions: list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_predictions_distribution.\n\n    # Inputs\n\n    :param predictions: (List[str]) list of prediction results file names\n        to extract predictions from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PREDICTIONS_SUFFIX}\"\n    predictions_per_model = _get_cols_from_predictions(predictions, [col], metadata)\n    compare_classifiers_predictions_distribution(\n        predictions_per_model, ground_truth, metadata, output_feature_name, output_directory=output_directory, **kwargs\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by confidence_thresholding.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    confidence_thresholding(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by confidence_thresholding_data_vs_acc_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    confidence_thresholding_data_vs_acc(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc_subset_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by confidence_thresholding_data_vs_acc_subset.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    confidence_thresholding_data_vs_acc_subset(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc_subset_per_class_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_metadata: str,\n    ground_truth_split: int,\n    split_file: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by compare_classifiers_multiclass.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_metadata: (str) path to ground truth metadata file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    confidence_thresholding_data_vs_acc_subset_per_class(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_2thresholds_2d_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    threshold_output_feature_names: list[str],\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by confidence_thresholding_2thresholds_2d_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param threshold_output_feature_names: (List[str]) name of the output\n        feature to visualizes.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth0 = _extract_ground_truth_values(\n        ground_truth, threshold_output_feature_names[0], ground_truth_split, split_file\n    )\n\n    ground_truth1 = _extract_ground_truth_values(\n        ground_truth, threshold_output_feature_names[1], ground_truth_split, split_file\n    )\n\n    cols = [f\"{feature_name}{_PROBABILITIES_SUFFIX}\" for feature_name in threshold_output_feature_names]\n    probabilities_per_model = _get_cols_from_predictions(probabilities, cols, metadata)\n\n    confidence_thresholding_2thresholds_2d(\n        probabilities_per_model,\n        [ground_truth0, ground_truth1],\n        metadata,\n        threshold_output_feature_names,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_2thresholds_3d_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    threshold_output_feature_names: list[str],\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by confidence_thresholding_2thresholds_3d_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param threshold_output_feature_names: (List[str]) name of the output\n        feature to visualizes.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth0 = _extract_ground_truth_values(\n        ground_truth, threshold_output_feature_names[0], ground_truth_split, split_file\n    )\n\n    ground_truth1 = _extract_ground_truth_values(\n        ground_truth, threshold_output_feature_names[1], ground_truth_split, split_file\n    )\n\n    cols = [f\"{feature_name}{_PROBABILITIES_SUFFIX}\" for feature_name in threshold_output_feature_names]\n    probabilities_per_model = _get_cols_from_predictions(probabilities, cols, metadata)\n    confidence_thresholding_2thresholds_3d(\n        probabilities_per_model,\n        [ground_truth0, ground_truth1],\n        metadata,\n        threshold_output_feature_names,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef binary_threshold_vs_metric_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by binary_threshold_vs_metric_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    binary_threshold_vs_metric(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef precision_recall_curves_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by precision_recall_curves_cli.\n\n    Args\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    Return\n\n    :return None:\n    \"\"\"\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    precision_recall_curves(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef roc_curves_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by roc_curves_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file.\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    roc_curves(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef roc_curves_from_test_statistics_cli(test_statistics: str | list[str], **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by roc_curves_from_test_statistics_cli.\n\n    # Inputs\n    :param test_statistics: (Union[str, List[str]]) path to experiment test statistics file.\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    roc_curves_from_test_statistics(test_stats_per_model, **kwargs)\n\n\n@DeveloperAPI\ndef precision_recall_curves_from_test_statistics_cli(test_statistics: str | list[str], **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by precision_recall_curves_from_test_statistics_cli.\n\n    Args:\n\n    :param test_statistics: (Union[str, List[str]]) path to experiment test\n        statistics file.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    Return:\n\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    precision_recall_curves_from_test_statistics(test_stats_per_model, **kwargs)\n\n\n@DeveloperAPI\ndef calibration_1_vs_all_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    output_feature_proc_name: str | None = None,\n    ground_truth_apply_idx: bool = True,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by calibration_1_vs_all_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param output_feature_proc_name: (str) name of the output feature column in ground_truth. If ground_truth is a\n        preprocessed parquet or hdf5 file, the column name will be <output_feature>_<hash>\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(\n        ground_truth, output_feature_proc_name or output_feature_name, ground_truth_split, split_file\n    )\n    feature_metadata = metadata[output_feature_name]\n    ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    calibration_1_vs_all(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef calibration_multiclass_cli(\n    probabilities: str | list[str],\n    ground_truth: str,\n    ground_truth_split: int,\n    split_file: str,\n    ground_truth_metadata: str,\n    output_feature_name: str,\n    output_directory: str,\n    **kwargs: dict,\n) -> None:\n    \"\"\"Load model data from files to be shown by calibration_multiclass_cli.\n\n    # Inputs\n\n    :param probabilities: (Union[str, List[str]]) list of prediction results file names\n        to extract probabilities from.\n    :param ground_truth: (str) path to ground truth file\n    :param ground_truth_split: (str) type of ground truth split -\n        `0` for training split, `1` for validation split or\n        2 for `'test'` split.\n    :param split_file: (str, None) file path to csv file containing split values\n    :param ground_truth_metadata: (str) file path to feature metadata json file\n        created during training.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param output_directory: (str) name of output directory containing training\n         results.\n    :param kwargs: (dict) parameters for the requested visualizations.\n\n    # Return\n\n    :return None:\n    \"\"\"\n\n    # retrieve feature metadata to convert raw predictions to encoded value\n    metadata = load_json(ground_truth_metadata)\n\n    # retrieve ground truth from source data set\n    ground_truth = _extract_ground_truth_values(ground_truth, output_feature_name, ground_truth_split, split_file)\n\n    col = f\"{output_feature_name}{_PROBABILITIES_SUFFIX}\"\n    probabilities_per_model = _get_cols_from_predictions(probabilities, [col], metadata)\n    calibration_multiclass(\n        probabilities_per_model,\n        ground_truth,\n        metadata,\n        output_feature_name,\n        output_directory=output_directory,\n        **kwargs,\n    )\n\n\n@DeveloperAPI\ndef confusion_matrix_cli(test_statistics: str | list[str], ground_truth_metadata: str, **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by confusion_matrix.\n\n    # Inputs\n\n    :param test_statistics: (Union[str, List[str]]) path to experiment test statistics file.\n    :param ground_truth_metadata: (str) path to ground truth metadata file.\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    metadata = load_json(ground_truth_metadata)\n    confusion_matrix(test_stats_per_model, metadata, **kwargs)\n\n\n@DeveloperAPI\ndef frequency_vs_f1_cli(test_statistics: str | list[str], ground_truth_metadata: str, **kwargs: dict) -> None:\n    \"\"\"Load model data from files to be shown by frequency_vs_f1.\n\n    # Inputs\n\n    :param test_statistics: (Union[str, List[str]]) path to experiment test statistics file.\n    :param ground_truth_metadata: (str) path to ground truth metadata file.\n    :param kwargs: (dict) parameters for the requested visualizations.  # Return\n    :return None:\n    \"\"\"\n    test_stats_per_model = load_data_for_viz(\"load_json\", test_statistics)\n    metadata = load_json(ground_truth_metadata)\n    frequency_vs_f1(test_stats_per_model, metadata, **kwargs)\n\n\n@DeveloperAPI\ndef learning_curves(\n    train_stats_per_model: list[dict],\n    output_feature_name: str | None = None,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    callbacks: list[Callback] = None,\n    **kwargs,\n) -> None:\n    \"\"\"Show how model metrics change over training and validation data epochs.\n\n    For each model and for each output feature and metric of the model,\n    it produces a line plot showing how that metric changed over the course\n    of the epochs of training on the training and validation sets.\n\n    # Inputs\n\n    :param train_stats_per_model: (List[dict]) list containing dictionary of\n        training statistics per model.\n    :param output_feature_name: (Union[str, `None`], default: `None`) name of the output feature\n        to use for the visualization.  If `None`, use all output features.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param callbacks: (list, default: `None`) a list of\n        `ludwig.callbacks.Callback` objects that provide hooks into the\n        Ludwig pipeline.\n\n    # Return\n    :return: (None)\n    \"\"\"\n    filename_template = \"learning_curves_{}_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    train_stats_per_model_list = convert_to_list(train_stats_per_model)\n    model_names_list = convert_to_list(model_names)\n    output_feature_names = _validate_output_feature_name_from_train_stats(\n        output_feature_name, train_stats_per_model_list\n    )\n\n    metrics = [LOSS, ACCURACY, HITS_AT_K, EDIT_DISTANCE]\n    for output_feature_name in output_feature_names:\n        for metric in metrics:\n            if metric in train_stats_per_model_list[0].training[output_feature_name]:\n                filename = None\n                if filename_template_path:\n                    filename = filename_template_path.format(output_feature_name, metric)\n\n                training_stats = [\n                    learning_stats.training[output_feature_name][metric]\n                    for learning_stats in train_stats_per_model_list\n                ]\n\n                validation_stats = []\n                for learning_stats in train_stats_per_model_list:\n                    if learning_stats.validation and output_feature_name in learning_stats.validation:\n                        validation_stats.append(learning_stats.validation[output_feature_name][metric])\n                    else:\n                        validation_stats.append(None)\n\n                evaluation_frequency = train_stats_per_model_list[0].evaluation_frequency\n\n                visualization_utils.learning_curves_plot(\n                    training_stats,\n                    validation_stats,\n                    metric,\n                    x_label=evaluation_frequency.period,\n                    x_step=evaluation_frequency.frequency,\n                    algorithm_names=model_names_list,\n                    title=f\"Learning Curves {output_feature_name}\",\n                    filename=filename,\n                    callbacks=callbacks,\n                )\n\n\n@DeveloperAPI\ndef compare_performance(\n    test_stats_per_model: list[dict],\n    output_feature_name: str | None = None,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Produces model comparison barplot visualization for each overall metric.\n\n    For each model (in the aligned lists of test_statistics and model_names)\n    it produces bars in a bar plot, one for each overall metric available\n    in the test_statistics file for the specified output_feature_name.\n\n    # Inputs\n\n    :param test_stats_per_model: (List[dict]) dictionary containing evaluation\n        performance statistics.\n    :param output_feature_name: (Union[str, `None`], default: `None`) name of the output feature\n        to use for the visualization.  If `None`, use all output features.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n\n    # Example usage:\n\n    ```python\n    model_a = LudwigModel(config)\n    model_a.train(dataset)\n    a_evaluation_stats, _, _ = model_a.evaluate(eval_set)\n    model_b = LudwigModel.load(\"path/to/model/\")\n    b_evaluation_stats, _, _ = model_b.evaluate(eval_set)\n    compare_performance([a_evaluation_stats, b_evaluation_stats], model_names=[\"A\", \"B\"])\n    ```\n    \"\"\"\n    ignore_names = {\n        \"overall_stats\",\n        \"confusion_matrix\",\n        \"per_class_stats\",\n        \"predictions\",\n        \"probabilities\",\n        \"roc_curve\",\n        \"precision_recall_curve\",\n        LOSS,\n    }\n\n    filename_template = \"compare_performance_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n\n    test_stats_per_model_list = convert_to_list(test_stats_per_model)\n    model_names_list = convert_to_list(model_names)\n    output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list)\n\n    for output_feature_name in output_feature_names:\n        metric_names_sets = list(set(tspr[output_feature_name].keys()) for tspr in test_stats_per_model_list)\n        metric_names = metric_names_sets[0]\n        for metric_names_set in metric_names_sets:\n            metric_names = metric_names.intersection(metric_names_set)\n        metric_names = metric_names - ignore_names\n        metrics_dict = {name: [] for name in metric_names}\n\n        for test_stats_per_model in test_stats_per_model_list:\n            for metric_name in metric_names:\n                metrics_dict[metric_name].append(test_stats_per_model[output_feature_name][metric_name])\n\n        # are there any metrics to compare?\n        if metrics_dict:\n            metrics = []\n            metrics_names = []\n            min_val = float(\"inf\")\n            max_val = float(\"-inf\")\n            for metric_name, metric_vals in metrics_dict.items():\n                if len(metric_vals) > 0:\n                    metrics.append(metric_vals)\n                    metrics_names.append(metric_name)\n                    curr_min = min(metric_vals)\n                    if curr_min < min_val:\n                        min_val = curr_min\n                    curr_max = max(metric_vals)\n                    if curr_max > max_val:\n                        max_val = curr_max\n\n            filename = None\n\n            if filename_template_path:\n                filename = filename_template_path.format(output_feature_name)\n                os.makedirs(output_directory, exist_ok=True)\n\n            visualization_utils.compare_classifiers_plot(\n                metrics,\n                metrics_names,\n                model_names_list,\n                adaptive=min_val < 0 or max_val > 1,\n                title=f\"Performance comparison on {output_feature_name}\",\n                filename=filename,\n            )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_from_prob(\n    probabilities_per_model: list[np.ndarray],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int = 0,\n    top_n_classes: list[int] | int = 3,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Produces model comparison barplot visualization from probabilities.\n\n    For each model it produces bars in a bar plot, one for each overall metric\n    computed on the fly from the probabilities of predictions for the specified\n    `model_names`.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[np.ndarray]) path to experiment\n        probabilities file\n    :param ground_truth: (pd.Series) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param top_n_classes: (List[int]) list containing the number of classes\n        to plot.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    top_n_classes_list = convert_to_list(top_n_classes)\n    k = top_n_classes_list[0]\n    model_names_list = convert_to_list(model_names)\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n\n    probs = probabilities_per_model\n    accuracies = []\n    hits_at_ks = []\n    mrrs = []\n\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        prob = np.argsort(prob, axis=1)\n        top1 = prob[:, -1]\n        topk = prob[:, -k:]\n\n        accuracies.append((ground_truth == top1).sum() / len(ground_truth))\n\n        hits_at_k = 0\n        for j in range(len(ground_truth)):\n            hits_at_k += np.in1d(ground_truth[j], topk[j])\n        hits_at_ks.append(hits_at_k.item() / len(ground_truth))\n\n        mrr = 0\n        for j in range(len(ground_truth)):\n            ground_truth_pos_in_probs = prob[j] == ground_truth[j]\n            if np.any(ground_truth_pos_in_probs):\n                mrr += 1 / -(np.argwhere(ground_truth_pos_in_probs).item() - prob.shape[1])\n        mrrs.append(mrr / len(ground_truth))\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"compare_classifiers_performance_from_prob.\" + file_format)\n\n    visualization_utils.compare_classifiers_plot(\n        [accuracies, hits_at_ks, mrrs], [ACCURACY, HITS_AT_K, \"mrr\"], model_names_list, filename=filename\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_from_pred(\n    predictions_per_model: list[np.ndarray],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Produces model comparison barplot visualization from predictions.\n\n    For each model it produces bars in a bar plot, one for each overall metric\n    computed on the fly from the predictions for the specified\n    `model_names`.\n\n    # Inputs\n\n    :param predictions_per_model: (List[str]) path to experiment predictions file.\n    :param ground_truth: (pd.Series) ground truth values\n    :param metadata: (dict) feature metadata dictionary.\n    :param output_feature_name: (str) name of the output feature to visualize.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    predictions_per_model = [np.ndarray.flatten(np.array(pred)) for pred in predictions_per_model]\n\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n\n    preds = predictions_per_model\n    model_names_list = convert_to_list(model_names)\n    mapped_preds = []\n    try:\n        for pred in preds:\n            mapped_preds.append([metadata[output_feature_name][\"str2idx\"][val] for val in pred])\n        preds = mapped_preds\n    # If predictions are coming from npy file there is no need to convert to\n    # numeric labels using metadata\n    except (TypeError, KeyError):\n        pass\n    accuracies = []\n    precisions = []\n    recalls = []\n    f1s = []\n\n    for i, pred in enumerate(preds):\n        accuracies.append(sklearn.metrics.accuracy_score(ground_truth, pred))\n        precisions.append(sklearn.metrics.precision_score(ground_truth, pred, average=\"macro\"))\n        recalls.append(sklearn.metrics.recall_score(ground_truth, pred, average=\"macro\"))\n        f1s.append(sklearn.metrics.f1_score(ground_truth, pred, average=\"macro\"))\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"compare_classifiers_performance_from_pred.\" + file_format)\n\n    visualization_utils.compare_classifiers_plot(\n        [accuracies, precisions, recalls, f1s],\n        [ACCURACY, \"precision\", \"recall\", \"f1\"],\n        model_names_list,\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_subset(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    top_n_classes: list[int],\n    labels_limit: int,\n    subset: str,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Produces model comparison barplot visualization from train subset.\n\n    For each model  it produces bars in a bar plot, one for each overall metric\n     computed on the fly from the probabilities predictions for the\n     specified `model_names`, considering only a subset of the full training set.\n     The way the subset is obtained is using the `top_n_classes` and\n     `subset` parameters.\n\n     # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param top_n_classes: (List[int]) list containing the number of classes\n        to plot.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param subset: (str) string specifying type of subset filtering.  Valid\n        values are `ground_truth` or `predictions`.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    top_n_classes_list = convert_to_list(top_n_classes)\n    k = top_n_classes_list[0]\n    model_names_list = convert_to_list(model_names)\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n\n    subset_indices = ground_truth > 0\n    gt_subset = ground_truth\n    if subset == \"ground_truth\":\n        subset_indices = ground_truth < k\n        gt_subset = ground_truth[subset_indices]\n        logger.info(f\"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data\")\n\n    probs = probabilities_per_model\n    accuracies = []\n    hits_at_ks = []\n\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        if subset == PREDICTIONS:\n            subset_indices = np.argmax(prob, axis=1) < k\n            gt_subset = ground_truth[subset_indices]\n            logger.info(\n                \"Subset for model_name {} is {:.2f}% of the data\".format(\n                    model_names[i] if model_names and i < len(model_names) else i,\n                    len(gt_subset) / len(ground_truth) * 100,\n                )\n            )\n            model_names[i] = \"{} ({:.2f}%)\".format(\n                model_names[i] if model_names and i < len(model_names) else i, len(gt_subset) / len(ground_truth) * 100\n            )\n\n        prob_subset = prob[subset_indices]\n\n        prob_subset = np.argsort(prob_subset, axis=1)\n        top1_subset = prob_subset[:, -1]\n        top3_subset = prob_subset[:, -3:]\n\n        accuracies.append(np.sum(gt_subset == top1_subset) / len(gt_subset))\n\n        hits_at_k = 0\n        for j in range(len(gt_subset)):\n            hits_at_k += np.in1d(gt_subset[j], top3_subset[i, :])\n        hits_at_ks.append(hits_at_k.item() / len(gt_subset))\n\n    title = None\n    if subset == \"ground_truth\":\n        title = \"Classifier performance on first {} class{} ({:.2f}%)\".format(\n            k, \"es\" if k > 1 else \"\", len(gt_subset) / len(ground_truth) * 100\n        )\n    elif subset == PREDICTIONS:\n        title = \"Classifier performance on first {} class{}\".format(k, \"es\" if k > 1 else \"\")\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"compare_classifiers_performance_subset.\" + file_format)\n\n    visualization_utils.compare_classifiers_plot(\n        [accuracies, hits_at_ks], [ACCURACY, HITS_AT_K], model_names_list, title=title, filename=filename\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_performance_changing_k(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    top_k: int,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Produce lineplot that show Hits@K metric while k goes from 1 to `top_k`.\n\n    For each model it produces a line plot that shows the Hits@K metric\n    (that counts a prediction as correct if the model produces it among the\n    first k) while changing k from 1 to top_k for the specified\n    `output_feature_name`.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param top_k: (int) number of elements in the ranklist to consider.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    k = top_k\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    probs = probabilities_per_model\n\n    hits_at_ks = []\n    model_names_list = convert_to_list(model_names)\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        prob = np.argsort(prob, axis=1)\n\n        hits_at_k = [0.0] * k\n        for g in range(len(ground_truth)):\n            for j in range(k):\n                hits_at_k[j] += np.in1d(ground_truth[g], prob[g, -j - 1 :])\n        hits_at_ks.append(np.array(hits_at_k) / len(ground_truth))\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"compare_classifiers_performance_changing_k.\" + file_format)\n\n    visualization_utils.compare_classifiers_line_plot(\n        np.arange(1, k + 1),\n        hits_at_ks,\n        \"hits@k\",\n        model_names_list,\n        title=\"Classifier comparison (hits@k)\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_multiclass_multimetric(\n    test_stats_per_model: list[dict],\n    metadata: dict,\n    output_feature_name: str,\n    top_n_classes: list[int],\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show the precision, recall and F1 of the model for the specified output_feature_name.\n\n    For each model it produces four plots that show the precision,\n    recall and F1 of the model on several classes for the specified output_feature_name.\n\n    # Inputs\n\n    :param test_stats_per_model: (List[dict]) list containing dictionary of\n        evaluation performance statistics\n    :param metadata: (dict) intermediate preprocess structure created during\n        training containing the mappings of the input dataset.\n    :param output_feature_name: (Union[str, `None`]) name of the output feature\n        to use for the visualization.  If `None`, use all output features.\n    :param top_n_classes: (List[int]) list containing the number of classes\n        to plot.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n    :return: (None)\n    \"\"\"\n    filename_template = \"compare_classifiers_multiclass_multimetric_{}_{}_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n\n    test_stats_per_model_list = convert_to_list(test_stats_per_model)\n    model_names_list = convert_to_list(model_names)\n    output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list)\n\n    for i, test_statistics in enumerate(test_stats_per_model_list):\n        for output_feature_name in output_feature_names:\n            model_name_name = model_names_list[i] if model_names_list is not None and i < len(model_names_list) else \"\"\n            if \"per_class_stats\" not in test_statistics[output_feature_name]:\n                logger.warning(\n                    f\"The output_feature_name {output_feature_name} in test statistics does not contain \"\n                    + \"per_class_stats, skipping it.\"\n                )\n                break\n            per_class_stats = test_statistics[output_feature_name][\"per_class_stats\"]\n            precisions = []\n            recalls = []\n            f1_scores = []\n            labels = []\n            for _, class_name in sorted(\n                ((metadata[output_feature_name][\"str2idx\"][key], key) for key in per_class_stats.keys()),\n                key=lambda tup: tup[0],\n            ):\n                class_stats = per_class_stats[class_name]\n                precisions.append(class_stats[\"precision\"])\n                recalls.append(class_stats[\"recall\"])\n                f1_scores.append(class_stats[\"f1_score\"])\n                labels.append(class_name)\n            for k in top_n_classes:\n                k = min(k, len(precisions)) if k > 0 else len(precisions)\n                ps = precisions[0:k]\n                rs = recalls[0:k]\n                fs = f1_scores[0:k]\n                ls = labels[0:k]\n\n                filename = None\n                if filename_template_path:\n                    os.makedirs(output_directory, exist_ok=True)\n                    filename = filename_template_path.format(model_name_name, output_feature_name, f\"top{k}\")\n\n                visualization_utils.compare_classifiers_multiclass_multimetric_plot(\n                    [ps, rs, fs],\n                    [\"precision\", \"recall\", \"f1 score\"],\n                    labels=ls,\n                    title=\"{} Multiclass Precision / Recall / \"\n                    \"F1 Score top {} {}\".format(model_name_name, k, output_feature_name),\n                    filename=filename,\n                )\n\n                p_np = np.nan_to_num(np.array(precisions, dtype=np.float32))\n                r_np = np.nan_to_num(np.array(recalls, dtype=np.float32))\n                f1_np = np.nan_to_num(np.array(f1_scores, dtype=np.float32))\n                labels_np = np.nan_to_num(np.array(labels))\n\n                sorted_indices = f1_np.argsort()\n                higher_f1s = sorted_indices[-k:][::-1]\n                filename = None\n                if filename_template_path:\n                    os.makedirs(output_directory, exist_ok=True)\n                    filename = filename_template_path.format(model_name_name, output_feature_name, f\"best{k}\")\n                visualization_utils.compare_classifiers_multiclass_multimetric_plot(\n                    [p_np[higher_f1s], r_np[higher_f1s], f1_np[higher_f1s]],\n                    [\"precision\", \"recall\", \"f1 score\"],\n                    labels=labels_np[higher_f1s].tolist(),\n                    title=\"{} Multiclass Precision / Recall / \"\n                    \"F1 Score best {} classes {}\".format(model_name_name, k, output_feature_name),\n                    filename=filename,\n                )\n                lower_f1s = sorted_indices[:k]\n                filename = None\n                if filename_template_path:\n                    filename = filename_template_path.format(model_name_name, output_feature_name, f\"worst{k}\")\n                visualization_utils.compare_classifiers_multiclass_multimetric_plot(\n                    [p_np[lower_f1s], r_np[lower_f1s], f1_np[lower_f1s]],\n                    [\"precision\", \"recall\", \"f1 score\"],\n                    labels=labels_np[lower_f1s].tolist(),\n                    title=(\n                        f\"{model_name_name} Multiclass Precision / Recall / F1 Score worst \"\n                        + f\"{k} classes {output_feature_name}\"\n                    ),\n                    filename=filename,\n                )\n\n                filename = None\n                if filename_template_path:\n                    filename = filename_template_path.format(model_name_name, output_feature_name, \"sorted\")\n                visualization_utils.compare_classifiers_multiclass_multimetric_plot(\n                    [p_np[sorted_indices[::-1]], r_np[sorted_indices[::-1]], f1_np[sorted_indices[::-1]]],\n                    [\"precision\", \"recall\", \"f1 score\"],\n                    labels=labels_np[sorted_indices[::-1]].tolist(),\n                    title=f\"{model_name_name} Multiclass Precision / Recall / F1 Score {output_feature_name} sorted\",\n                    filename=filename,\n                )\n\n                logger.info(\"\\n\")\n                logger.info(model_name_name)\n                tmp_str = f\"{output_feature_name} best 5 classes: \"\n                tmp_str += \"{}\"\n                logger.info(tmp_str.format(higher_f1s))\n                logger.info(f1_np[higher_f1s])\n                tmp_str = f\"{output_feature_name} worst 5 classes: \"\n                tmp_str += \"{}\"\n                logger.info(tmp_str.format(lower_f1s))\n                logger.info(f1_np[lower_f1s])\n                tmp_str = f\"{output_feature_name} number of classes with f1 score > 0: \"\n                tmp_str += \"{}\"\n                logger.info(tmp_str.format(np.sum(f1_np > 0)))\n                tmp_str = f\"{output_feature_name} number of classes with f1 score = 0: \"\n                tmp_str += \"{}\"\n                logger.info(tmp_str.format(np.sum(f1_np == 0)))\n\n\n@DeveloperAPI\ndef compare_classifiers_predictions(\n    predictions_per_model: list[list],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show two models comparison of their output_feature_name predictions.\n\n    # Inputs\n\n    :param predictions_per_model: (List[list]) list containing the model\n        predictions for the specified output_feature_name.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    model_names_list = convert_to_list(model_names)\n    name_c1 = model_names_list[0] if model_names is not None and len(model_names) > 0 else \"c1\"\n    name_c2 = model_names_list[1] if model_names is not None and len(model_names) > 1 else \"c2\"\n\n    pred_c1 = predictions_per_model[0]\n    pred_c2 = predictions_per_model[1]\n\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n        pred_c1[pred_c1 > labels_limit] = labels_limit\n        pred_c2[pred_c2 > labels_limit] = labels_limit\n\n    # TODO all shadows built in name - come up with a more descriptive name\n    all = len(ground_truth)\n    if all == 0:\n        logger.error(\"No labels in the ground truth\")\n        return\n\n    both_right = 0\n    both_wrong_same = 0\n    both_wrong_different = 0\n    c1_right_c2_wrong = 0\n    c1_wrong_c2_right = 0\n\n    for i in range(all):\n        if ground_truth[i] == pred_c1[i] and ground_truth[i] == pred_c2[i]:\n            both_right += 1\n        elif ground_truth[i] != pred_c1[i] and ground_truth[i] != pred_c2[i]:\n            if pred_c1[i] == pred_c2[i]:\n                both_wrong_same += 1\n            else:\n                both_wrong_different += 1\n        elif ground_truth[i] == pred_c1[i] and ground_truth[i] != pred_c2[i]:\n            c1_right_c2_wrong += 1\n        elif ground_truth[i] != pred_c1[i] and ground_truth[i] == pred_c2[i]:\n            c1_wrong_c2_right += 1\n\n    one_right = c1_right_c2_wrong + c1_wrong_c2_right\n    both_wrong = both_wrong_same + both_wrong_different\n\n    logger.info(f\"Test datapoints: {all}\")\n    logger.info(f\"Both right: {both_right} {100 * both_right / all:.2f}%\")\n    logger.info(f\"One right: {one_right} {100 * one_right / all:.2f}%\")\n    logger.info(\n        \"  {} right / {} wrong: {} {:.2f}% {:.2f}%\".format(\n            name_c1,\n            name_c2,\n            c1_right_c2_wrong,\n            100 * c1_right_c2_wrong / all,\n            100 * c1_right_c2_wrong / one_right if one_right > 0 else 0,\n        )\n    )\n    logger.info(\n        \"  {} wrong / {} right: {} {:.2f}% {:.2f}%\".format(\n            name_c1,\n            name_c2,\n            c1_wrong_c2_right,\n            100 * c1_wrong_c2_right / all,\n            100 * c1_wrong_c2_right / one_right if one_right > 0 else 0,\n        )\n    )\n    logger.info(f\"Both wrong: {both_wrong} {100 * both_wrong / all:.2f}%\")\n    logger.info(\n        \"  same prediction: {} {:.2f}% {:.2f}%\".format(\n            both_wrong_same, 100 * both_wrong_same / all, 100 * both_wrong_same / both_wrong if both_wrong > 0 else 0\n        )\n    )\n    logger.info(\n        \"  different prediction: {} {:.2f}% {:.2f}%\".format(\n            both_wrong_different,\n            100 * both_wrong_different / all,\n            100 * both_wrong_different / both_wrong if both_wrong > 0 else 0,\n        )\n    )\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, f\"compare_classifiers_predictions_{name_c1}_{name_c2}.{file_format}\")\n\n    visualization_utils.donut(\n        [both_right, one_right, both_wrong],\n        [\"both right\", \"one right\", \"both wrong\"],\n        [both_right, c1_right_c2_wrong, c1_wrong_c2_right, both_wrong_same, both_wrong_different],\n        [\n            \"both right\",\n            f\"{name_c1} right / {name_c2} wrong\",\n            f\"{name_c1} wrong / {name_c2} right\",\n            \"same prediction\",\n            \"different prediction\",\n        ],\n        [0, 1, 1, 2, 2],\n        title=f\"{name_c1} vs {name_c2}\",\n        tight_layout=kwargs.pop(\"tight_layout\", True),\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef compare_classifiers_predictions_distribution(\n    predictions_per_model: list[list],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show comparison of models predictions distribution for 10 output_feature_name classes.\n\n    This visualization produces a radar plot comparing the distributions of\n    predictions of the models for the first 10 classes of the specified\n    output_feature_name.\n\n    # Inputs\n\n    :param predictions_per_model: (List[list]) list containing the model\n        predictions for the specified output_feature_name.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    model_names_list = convert_to_list(model_names)\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n        for i in range(len(predictions_per_model)):\n            predictions_per_model[i][predictions_per_model[i] > labels_limit] = labels_limit\n\n    max_gt = max(ground_truth)\n    max_pred = max(max(alg_predictions) for alg_predictions in predictions_per_model)\n    max_val = max(max_gt, max_pred) + 1\n\n    counts_gt = np.bincount(ground_truth, minlength=max_val)\n    prob_gt = counts_gt / counts_gt.sum()\n\n    counts_predictions = [np.bincount(alg_predictions, minlength=max_val) for alg_predictions in predictions_per_model]\n\n    prob_predictions = [\n        alg_count_prediction / alg_count_prediction.sum() for alg_count_prediction in counts_predictions\n    ]\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"compare_classifiers_predictions_distribution.\" + file_format)\n\n    visualization_utils.radar_chart(prob_gt, prob_predictions, model_names_list, filename=filename)\n\n\n@DeveloperAPI\ndef confidence_thresholding(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models accuracy and data coverage while increasing treshold.\n\n    For each model it produces a pair of lines indicating the accuracy of\n    the model and the data coverage while increasing a threshold (x axis) on\n    the probabilities of predictions for the specified output_feature_name.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    accuracies = []\n    dataset_kept = []\n\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        max_prob = np.max(prob, axis=1)\n        predictions = np.argmax(prob, axis=1)\n\n        accuracies_alg = []\n        dataset_kept_alg = []\n\n        for threshold in thresholds:\n            threshold = threshold if threshold < 1 else 0.999\n            filtered_indices = max_prob >= threshold\n            filtered_gt = ground_truth[filtered_indices]\n            filtered_predictions = predictions[filtered_indices]\n            accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt)\n\n            accuracies_alg.append(accuracy)\n            dataset_kept_alg.append(len(filtered_gt) / len(ground_truth))\n\n        accuracies.append(accuracies_alg)\n        dataset_kept.append(dataset_kept_alg)\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"confidence_thresholding.\" + file_format)\n\n    visualization_utils.confidence_filtering_plot(\n        thresholds, accuracies, dataset_kept, model_names_list, title=\"Confidence_Thresholding\", filename=filename\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models comparison of confidence threshold data vs accuracy.\n\n    For each model it produces a line indicating the accuracy of the model\n    and the data coverage while increasing a threshold on the probabilities\n    of predictions for the specified output_feature_name. The difference with\n    confidence_thresholding is that it uses two axes instead of three,\n    not visualizing the threshold and having coverage as x axis instead of\n    the threshold.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    accuracies = []\n    dataset_kept = []\n\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        max_prob = np.max(prob, axis=1)\n        predictions = np.argmax(prob, axis=1)\n\n        accuracies_alg = []\n        dataset_kept_alg = []\n\n        for threshold in thresholds:\n            threshold = threshold if threshold < 1 else 0.999\n            filtered_indices = max_prob >= threshold\n            filtered_gt = ground_truth[filtered_indices]\n            filtered_predictions = predictions[filtered_indices]\n            accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt)\n\n            accuracies_alg.append(accuracy)\n            dataset_kept_alg.append(len(filtered_gt) / len(ground_truth))\n\n        accuracies.append(accuracies_alg)\n        dataset_kept.append(dataset_kept_alg)\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"confidence_thresholding_data_vs_acc.\" + file_format)\n\n    visualization_utils.confidence_filtering_data_vs_acc_plot(\n        accuracies,\n        dataset_kept,\n        model_names_list,\n        title=\"Confidence_Thresholding (Data vs Accuracy)\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc_subset(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    top_n_classes: list[int],\n    labels_limit: int,\n    subset: str,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models comparison of confidence threshold data vs accuracy on a subset of data.\n\n    For each model it produces a line indicating the accuracy of the model\n    and the data coverage while increasing a threshold on the probabilities\n    of predictions for the specified output_feature_name, considering only a subset of the\n    full training set. The way the subset is obtained is using the `top_n_classes`\n    and subset parameters.\n     The difference with confidence_thresholding is that it uses two axes\n     instead of three, not visualizing the threshold and having coverage as\n     x axis instead of the threshold.\n\n    If the values of subset is `ground_truth`, then only datapoints where the\n    ground truth class is within the top n most frequent ones will be\n    considered  as test set, and the percentage of datapoints that have been\n    kept  from the original set will be displayed. If the values of subset is\n     `predictions`, then only datapoints where the the model predicts a class\n     that is within the top n most frequent ones will be considered as test set,\n     and the percentage of datapoints that have been kept from the original set\n     will be displayed for each model.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param top_n_classes: (List[int]) list containing the number of classes\n        to plot.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param subset: (str) string specifying type of subset filtering.  Valid\n        values are `ground_truth` or `predictions`.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    top_n_classes_list = convert_to_list(top_n_classes)\n    k = top_n_classes_list[0]\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    accuracies = []\n    dataset_kept = []\n\n    subset_indices = ground_truth > 0\n    gt_subset = ground_truth\n    if subset == \"ground_truth\":\n        subset_indices = ground_truth < k\n        gt_subset = ground_truth[subset_indices]\n        logger.info(f\"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data\")\n\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            prob = prob_limit\n\n        if subset == PREDICTIONS:\n            subset_indices = np.argmax(prob, axis=1) < k\n            gt_subset = ground_truth[subset_indices]\n            logger.info(\n                \"Subset for model_name {} is {:.2f}% of the data\".format(\n                    model_names[i] if model_names and i < len(model_names) else i,\n                    len(gt_subset) / len(ground_truth) * 100,\n                )\n            )\n\n        prob_subset = prob[subset_indices]\n\n        max_prob = np.max(prob_subset, axis=1)\n        predictions = np.argmax(prob_subset, axis=1)\n\n        accuracies_alg = []\n        dataset_kept_alg = []\n\n        for threshold in thresholds:\n            threshold = threshold if threshold < 1 else 0.999\n            filtered_indices = max_prob >= threshold\n            filtered_gt = gt_subset[filtered_indices]\n            filtered_predictions = predictions[filtered_indices]\n            accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt)\n\n            accuracies_alg.append(accuracy)\n            dataset_kept_alg.append(len(filtered_gt) / len(ground_truth))\n\n        accuracies.append(accuracies_alg)\n        dataset_kept.append(dataset_kept_alg)\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"confidence_thresholding_data_vs_acc_subset.\" + file_format)\n\n    visualization_utils.confidence_filtering_data_vs_acc_plot(\n        accuracies,\n        dataset_kept,\n        model_names_list,\n        title=\"Confidence_Thresholding (Data vs Accuracy)\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_data_vs_acc_subset_per_class(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    top_n_classes: int | list[int],\n    labels_limit: int,\n    subset: str,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models comparison of confidence threshold data vs accuracy on a subset of data per class in top n\n    classes.\n\n    For each model (in the aligned lists of probabilities and model_names)\n    it produces a line indicating the accuracy of the model and the data\n    coverage while increasing a threshold on the probabilities of\n    predictions for the specified output_feature_name, considering only a subset of the\n    full training set. The way the subset is obtained is using the\n    `top_n_classes`  and `subset` parameters.  The difference with\n    confidence_thresholding is that it uses two axes instead of three,\n    not visualizing the threshold and having coverage as x axis instead of\n    the  threshold.\n\n    If the values of subset is `ground_truth`, then only datapoints where the\n    ground truth class is within the top n most frequent ones will be\n    considered  as test set, and the percentage of datapoints that have been\n    kept from the original set will be displayed. If the values of subset is\n    `predictions`, then only datapoints where the the model predicts a class that\n    is within the top n most frequent ones will be considered as test set, and\n    the percentage of datapoints that have been kept from the original set will\n    be displayed for each model.\n\n    The difference with confidence_thresholding_data_vs_acc_subset is that it\n    produces one plot per class within the top_n_classes.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) intermediate preprocess structure created during\n        training containing the mappings of the input dataset.\n    :param output_feature_name: (str) name of the output feature to use\n        for the visualization.\n    :param top_n_classes: (Union[int, List[int]]) number of top classes or list\n        containing the number of top classes to plot.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param subset: (str) string specifying type of subset filtering.  Valid\n        values are `ground_truth` or `predictions`.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    filename_template = \"confidence_thresholding_data_vs_acc_subset_per_class_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    top_n_classes_list = convert_to_list(top_n_classes)\n    k = top_n_classes_list[0]\n    # If top_n_classes is greater than the maximum number of tokens, truncate to use max token size\n    if k > len(metadata[output_feature_name][\"idx2str\"]):\n        k = len(metadata[output_feature_name][\"idx2str\"])\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    for curr_k in range(k):\n        accuracies = []\n        dataset_kept = []\n\n        subset_indices = ground_truth > 0\n        gt_subset = ground_truth\n        if subset == \"ground_truth\":\n            subset_indices = ground_truth == curr_k\n            gt_subset = ground_truth[subset_indices]\n            logger.info(f\"Subset is {len(gt_subset) / len(ground_truth) * 100:.2f}% of the data\")\n\n        for i, prob in enumerate(probs):\n            if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n                prob_limit = prob[:, : labels_limit + 1]\n                prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n                prob = prob_limit\n\n            if subset == PREDICTIONS:\n                subset_indices = np.argmax(prob, axis=1) == curr_k\n                gt_subset = ground_truth[subset_indices]\n                logger.info(\n                    \"Subset for model_name {} is {:.2f}% of the data\".format(\n                        model_names_list[i] if model_names_list and i < len(model_names_list) else i,\n                        len(gt_subset) / len(ground_truth) * 100,\n                    )\n                )\n\n            prob_subset = prob[subset_indices]\n\n            max_prob = np.max(prob_subset, axis=1)\n            predictions = np.argmax(prob_subset, axis=1)\n\n            accuracies_alg = []\n            dataset_kept_alg = []\n\n            for threshold in thresholds:\n                threshold = threshold if threshold < 1 else 0.999\n                filtered_indices = max_prob >= threshold\n                filtered_gt = gt_subset[filtered_indices]\n                filtered_predictions = predictions[filtered_indices]\n                accuracy = (filtered_gt == filtered_predictions).sum() / len(filtered_gt) if len(filtered_gt) > 0 else 0\n\n                accuracies_alg.append(accuracy)\n                dataset_kept_alg.append(len(filtered_gt) / len(ground_truth))\n\n            accuracies.append(accuracies_alg)\n            dataset_kept.append(dataset_kept_alg)\n\n        output_feature_name_name = metadata[output_feature_name][\"idx2str\"][curr_k]\n\n        filename = None\n        if filename_template_path:\n            os.makedirs(output_directory, exist_ok=True)\n            filename = filename_template_path.format(output_feature_name_name)\n\n        visualization_utils.confidence_filtering_data_vs_acc_plot(\n            accuracies,\n            dataset_kept,\n            model_names_list,\n            decimal_digits=2,\n            title=\"Confidence_Thresholding (Data vs Accuracy) \" \"for class {}\".format(output_feature_name_name),\n            filename=filename,\n        )\n\n\n@DeveloperAPI\ndef confidence_thresholding_2thresholds_2d(\n    probabilities_per_model: list[np.array],\n    ground_truths: list[np.array] | list[pd.Series],\n    metadata,\n    threshold_output_feature_names: list[str],\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show confidence threshold data vs accuracy for two output feature names.\n\n    The first plot shows several semi transparent lines. They summarize the\n    3d surfaces displayed by confidence_thresholding_2thresholds_3d that have\n    thresholds on the confidence of the predictions of the two\n    `threshold_output_feature_names`  as x and y axes and either the data\n    coverage percentage or\n    the accuracy as z axis. Each line represents a slice of the data\n    coverage  surface projected onto the accuracy surface.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[List[np.array], List[pd.Series]]) containing\n        ground truth data\n    :param metadata: (dict) feature metadata dictionary\n    :param threshold_output_feature_names: (List[str]) List containing two output\n        feature names for visualization.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    try:\n        validate_conf_thresholds_and_probabilities_2d_3d(probabilities_per_model, threshold_output_feature_names)\n    except RuntimeError:\n        return\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"confidence_thresholding_2thresholds_2d_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n\n    if not isinstance(ground_truths[0], np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[threshold_output_feature_names[0]]\n        vfunc = np.vectorize(_encode_categorical_feature)\n        gt_1 = vfunc(ground_truths[0], feature_metadata[\"str2idx\"])\n        feature_metadata = metadata[threshold_output_feature_names[1]]\n        gt_2 = vfunc(ground_truths[1], feature_metadata[\"str2idx\"])\n    else:\n        gt_1 = ground_truths[0]\n        gt_2 = ground_truths[1]\n\n    if labels_limit > 0:\n        gt_1[gt_1 > labels_limit] = labels_limit\n        gt_2[gt_2 > labels_limit] = labels_limit\n\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n    fixed_step_coverage = thresholds\n    name_t1 = f\"{threshold_output_feature_names[0]} threshold\"\n    name_t2 = f\"{threshold_output_feature_names[1]} threshold\"\n\n    accuracies = []\n    dataset_kept = []\n    interps = []\n    table = [[name_t1, name_t2, \"coverage\", ACCURACY]]\n\n    if labels_limit > 0 and probs[0].shape[1] > labels_limit + 1:\n        prob_limit = probs[0][:, : labels_limit + 1]\n        prob_limit[:, labels_limit] = probs[0][:, labels_limit:].sum(1)\n        probs[0] = prob_limit\n\n    if labels_limit > 0 and probs[1].shape[1] > labels_limit + 1:\n        prob_limit = probs[1][:, : labels_limit + 1]\n        prob_limit[:, labels_limit] = probs[1][:, labels_limit:].sum(1)\n        probs[1] = prob_limit\n\n    max_prob_1 = np.max(probs[0], axis=1)\n    predictions_1 = np.argmax(probs[0], axis=1)\n\n    max_prob_2 = np.max(probs[1], axis=1)\n    predictions_2 = np.argmax(probs[1], axis=1)\n\n    for threshold_1 in thresholds:\n        threshold_1 = threshold_1 if threshold_1 < 1 else 0.999\n        curr_accuracies = []\n        curr_dataset_kept = []\n\n        for threshold_2 in thresholds:\n            threshold_2 = threshold_2 if threshold_2 < 1 else 0.999\n\n            filtered_indices = np.logical_and(max_prob_1 >= threshold_1, max_prob_2 >= threshold_2)\n\n            filtered_gt_1 = gt_1[filtered_indices]\n            filtered_predictions_1 = predictions_1[filtered_indices]\n            filtered_gt_2 = gt_2[filtered_indices]\n            filtered_predictions_2 = predictions_2[filtered_indices]\n\n            coverage = len(filtered_gt_1) / len(gt_1)\n            accuracy = (\n                np.logical_and(filtered_gt_1 == filtered_predictions_1, filtered_gt_2 == filtered_predictions_2)\n            ).sum() / len(filtered_gt_1)\n\n            curr_accuracies.append(accuracy)\n            curr_dataset_kept.append(coverage)\n            table.append([threshold_1, threshold_2, coverage, accuracy])\n\n        accuracies.append(curr_accuracies)\n        dataset_kept.append(curr_dataset_kept)\n        interps.append(\n            np.interp(\n                fixed_step_coverage, list(reversed(curr_dataset_kept)), list(reversed(curr_accuracies)), left=1, right=0\n            )\n        )\n\n    logger.info(\"CSV table\")\n    for row in table:\n        logger.info(\",\".join([str(e) for e in row]))\n\n    # ===========#\n    # Multiline #\n    # ===========#\n    filename = None\n    if filename_template_path:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = filename_template_path.format(\"multiline\")\n    visualization_utils.confidence_filtering_data_vs_acc_multiline_plot(\n        accuracies, dataset_kept, model_names_list, title=\"Coverage vs Accuracy, two thresholds\", filename=filename\n    )\n\n    # ==========#\n    # Max line #\n    # ==========#\n    filename = None\n    if filename_template_path:\n        filename = filename_template_path.format(\"maxline\")\n    max_accuracies = np.amax(np.array(interps), 0)\n    visualization_utils.confidence_filtering_data_vs_acc_plot(\n        [max_accuracies],\n        [thresholds],\n        model_names_list,\n        title=\"Coverage vs Accuracy, two thresholds\",\n        filename=filename,\n    )\n\n    # ==========================#\n    # Max line with thresholds #\n    # ==========================#\n    acc_matrix = np.array(accuracies)\n    cov_matrix = np.array(dataset_kept)\n    t1_maxes = [1]\n    t2_maxes = [1]\n    for i in range(len(fixed_step_coverage) - 1):\n        lower = fixed_step_coverage[i]\n        upper = fixed_step_coverage[i + 1]\n        indices = np.logical_and(cov_matrix >= lower, cov_matrix < upper)\n        selected_acc = acc_matrix.copy()\n        selected_acc[np.logical_not(indices)] = -1\n        threshold_indices = np.unravel_index(np.argmax(selected_acc, axis=None), selected_acc.shape)\n        t1_maxes.append(thresholds[threshold_indices[0]])\n        t2_maxes.append(thresholds[threshold_indices[1]])\n    model_name = model_names_list[0] if model_names_list is not None and len(model_names_list) > 0 else \"\"\n\n    filename = None\n    if filename_template_path:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = filename_template_path.format(\"maxline_with_thresholds\")\n\n    visualization_utils.confidence_filtering_data_vs_acc_plot(\n        [max_accuracies, t1_maxes, t2_maxes],\n        [fixed_step_coverage, fixed_step_coverage, fixed_step_coverage],\n        model_names=[model_name + \" accuracy\", name_t1, name_t2],\n        dotted=[False, True, True],\n        y_label=\"\",\n        title=\"Coverage vs Accuracy & Threshold\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef confidence_thresholding_2thresholds_3d(\n    probabilities_per_model: list[np.array],\n    ground_truths: list[np.array] | list[pd.Series],\n    metadata,\n    threshold_output_feature_names: list[str],\n    labels_limit: int,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show 3d confidence threshold data vs accuracy for two output feature names.\n\n    The plot shows the 3d surfaces displayed by\n    confidence_thresholding_2thresholds_3d that have thresholds on the\n    confidence of the predictions of the two `threshold_output_feature_names`\n    as x and y axes and either the data coverage percentage or the accuracy\n    as z axis.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[List[np.array], List[pd.Series]]) containing\n        ground truth data\n    :param metadata: (dict) feature metadata dictionary\n    :param threshold_output_feature_names: (List[str]) List containing two output\n        feature names for visualization.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    try:\n        validate_conf_thresholds_and_probabilities_2d_3d(probabilities_per_model, threshold_output_feature_names)\n    except RuntimeError:\n        return\n    probs = probabilities_per_model\n\n    if not isinstance(ground_truths[0], np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[threshold_output_feature_names[0]]\n        vfunc = np.vectorize(_encode_categorical_feature)\n        gt_1 = vfunc(ground_truths[0], feature_metadata[\"str2idx\"])\n        feature_metadata = metadata[threshold_output_feature_names[1]]\n        gt_2 = vfunc(ground_truths[1], feature_metadata[\"str2idx\"])\n    else:\n        gt_1 = ground_truths[0]\n        gt_2 = ground_truths[1]\n\n    if labels_limit > 0:\n        gt_1[gt_1 > labels_limit] = labels_limit\n        gt_2[gt_2 > labels_limit] = labels_limit\n\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    accuracies = []\n    dataset_kept = []\n\n    if labels_limit > 0 and probs[0].shape[1] > labels_limit + 1:\n        prob_limit = probs[0][:, : labels_limit + 1]\n        prob_limit[:, labels_limit] = probs[0][:, labels_limit:].sum(1)\n        probs[0] = prob_limit\n\n    if labels_limit > 0 and probs[1].shape[1] > labels_limit + 1:\n        prob_limit = probs[1][:, : labels_limit + 1]\n        prob_limit[:, labels_limit] = probs[1][:, labels_limit:].sum(1)\n        probs[1] = prob_limit\n\n    max_prob_1 = np.max(probs[0], axis=1)\n    predictions_1 = np.argmax(probs[0], axis=1)\n\n    max_prob_2 = np.max(probs[1], axis=1)\n    predictions_2 = np.argmax(probs[1], axis=1)\n\n    for threshold_1 in thresholds:\n        threshold_1 = threshold_1 if threshold_1 < 1 else 0.999\n        curr_accuracies = []\n        curr_dataset_kept = []\n\n        for threshold_2 in thresholds:\n            threshold_2 = threshold_2 if threshold_2 < 1 else 0.999\n\n            filtered_indices = np.logical_and(max_prob_1 >= threshold_1, max_prob_2 >= threshold_2)\n\n            filtered_gt_1 = gt_1[filtered_indices]\n            filtered_predictions_1 = predictions_1[filtered_indices]\n            filtered_gt_2 = gt_2[filtered_indices]\n            filtered_predictions_2 = predictions_2[filtered_indices]\n\n            accuracy = (\n                np.logical_and(filtered_gt_1 == filtered_predictions_1, filtered_gt_2 == filtered_predictions_2)\n            ).sum() / len(filtered_gt_1)\n\n            curr_accuracies.append(accuracy)\n            curr_dataset_kept.append(len(filtered_gt_1) / len(gt_1))\n\n        accuracies.append(curr_accuracies)\n        dataset_kept.append(curr_dataset_kept)\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"confidence_thresholding_2thresholds_3d.\" + file_format)\n\n    visualization_utils.confidence_filtering_3d_plot(\n        np.array(thresholds),\n        np.array(thresholds),\n        np.array(accuracies),\n        np.array(dataset_kept),\n        threshold_output_feature_names,\n        title=\"Confidence_Thresholding, two thresholds\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef binary_threshold_vs_metric(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    metrics: list[str],\n    positive_label: int = 1,\n    model_names: list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show confidence of the model against metric for the specified output_feature_name.\n\n    For each metric specified in metrics (options are `f1`, `precision`, `recall`,\n    `accuracy`), this visualization produces a line chart plotting a threshold\n    on  the confidence of the model against the metric for the specified\n    output_feature_name.  If output_feature_name is a category feature,\n    positive_label, which is specified as the numeric encoded value, indicates\n    the class to be considered positive class and all others will be\n    considered negative. To figure out the\n    association between classes and numeric encoded values check the\n    ground_truth_metadata JSON file.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param metrics: (List[str]) metrics to display (`'f1'`, `'precision'`,\n        `'recall'`, `'accuracy'`).\n    :param positive_label: (int, default: `1`) numeric encoded value for the\n        positive class.\n    :param model_names: (List[str], default: `None`) list of the names of the\n        models to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (`None`)\n    \"\"\"\n\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth, positive_label = _convert_ground_truth(\n            ground_truth, feature_metadata, ground_truth_apply_idx, positive_label\n        )\n\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    metrics_list = convert_to_list(metrics)\n    filename_template = \"binary_threshold_vs_metric_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n\n    thresholds = [t / 100 for t in range(0, 101, 5)]\n\n    supported_metrics = {\"f1\", \"precision\", \"recall\", \"accuracy\"}\n\n    for metric in metrics_list:\n        if metric not in supported_metrics:\n            logger.error(f\"Metric {metric} not supported\")\n            continue\n\n        scores = []\n\n        for i, prob in enumerate(probs):\n            scores_alg = []\n\n            if len(prob.shape) == 2:\n                if prob.shape[1] > positive_label:\n                    prob = prob[:, positive_label]\n                else:\n                    raise Exception(\n                        \"the specified positive label {} is not \" \"present in the probabilities\".format(positive_label)\n                    )\n\n            for threshold in thresholds:\n                threshold = threshold if threshold < 1 else 0.99\n\n                predictions = prob >= threshold\n\n                if metric == \"f1\":\n                    metric_score = sklearn.metrics.f1_score(ground_truth, predictions)\n                elif metric == \"precision\":\n                    metric_score = sklearn.metrics.precision_score(ground_truth, predictions)\n                elif metric == \"recall\":\n                    metric_score = sklearn.metrics.recall_score(ground_truth, predictions)\n                elif metric == ACCURACY:\n                    metric_score = sklearn.metrics.accuracy_score(ground_truth, predictions)\n\n                scores_alg.append(metric_score)\n\n            scores.append(scores_alg)\n\n        filename = None\n        if output_directory:\n            os.makedirs(output_directory, exist_ok=True)\n            filename = filename_template_path.format(metric)\n\n        visualization_utils.threshold_vs_metric_plot(\n            thresholds, scores, model_names_list, title=f\"Binary threshold vs {metric}\", filename=filename\n        )\n\n\n@DeveloperAPI\ndef precision_recall_curves(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    positive_label: int = 1,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show the precision recall curves for output features in the specified models.\n\n    This visualization produces a line chart plotting a precision recall curve for the\n    specified output feature name. If output feature name is a category feature,\n    `positive_label` indicates which is the class to be considered positive\n    class and all the others will be considered negative. `positive_label` is\n    the encoded numeric value for category classes. The numeric value can be\n    determined by association between classes and integers captured in the\n    training metadata JSON file.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param positive_label: (int, default: `1`) numeric encoded value for the\n        positive class.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth, positive_label = _convert_ground_truth(\n            ground_truth, feature_metadata, ground_truth_apply_idx, positive_label\n        )\n\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    precision_recalls = []\n\n    for _, prob in enumerate(probs):\n        if len(prob.shape) > 1:\n            prob = prob[:, positive_label]\n        precision, recall, _ = sklearn.metrics.precision_recall_curve(ground_truth, prob, pos_label=positive_label)\n        precision_recalls.append({\"precisions\": precision, \"recalls\": recall})\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"precision_recall_curve.\" + file_format)\n\n    visualization_utils.precision_recall_curves_plot(\n        precision_recalls, model_names_list, title=\"Precision Recall Curves\", filename=filename\n    )\n\n\n@DeveloperAPI\ndef precision_recall_curves_from_test_statistics(\n    test_stats_per_model: list[dict],\n    output_feature_name: str,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show the PR curves for the specified models output binary `output_feature_name`.\n\n    This visualization uses `output_feature_name`, `test_stats_per_model` and\n    `model_names` parameters. `output_feature_name` needs to be binary feature.\n    This visualization produces a line chart plotting the PR curves for the\n    specified `output_feature_name`.\n\n    Args:\n\n    :param test_stats_per_model: (List[dict]) dictionary containing evaluation\n        performance statistics.\n    :param output_feature_name: (str) name of the output feature to use\n        for the visualization.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    Return\n\n    :return: (None)\n    \"\"\"\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"precision_recall_curves_from_prediction_statistics.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    precision_recalls = []\n    for curr_test_statistics in test_stats_per_model:\n        precisions = curr_test_statistics[output_feature_name][\"precision_recall_curve\"][\"precisions\"]\n        recalls = curr_test_statistics[output_feature_name][\"precision_recall_curve\"][\"recalls\"]\n        precision_recalls.append({\"precisions\": precisions, \"recalls\": recalls})\n\n    visualization_utils.precision_recall_curves_plot(\n        precision_recalls, model_names_list, title=\"Precision Recall Curves\", filename=filename_template_path\n    )\n\n\n@DeveloperAPI\ndef roc_curves(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    positive_label: int = 1,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show the roc curves for output features in the specified models.\n\n    This visualization produces a line chart plotting the roc curves for the\n    specified output feature name. If output feature name is a category feature,\n    `positive_label` indicates which is the class to be considered positive\n    class and all the others will be considered negative. `positive_label` is\n    the encoded numeric value for category classes. The numeric value can be\n    determined by association between classes and integers captured in the\n    training metadata JSON file.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param positive_label: (int, default: `1`) numeric encoded value for the\n        positive class.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth, positive_label = _convert_ground_truth(\n            ground_truth, feature_metadata, ground_truth_apply_idx, positive_label\n        )\n\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    fpr_tprs = []\n\n    for i, prob in enumerate(probs):\n        if len(prob.shape) > 1:\n            prob = prob[:, positive_label]\n        fpr, tpr, _ = sklearn.metrics.roc_curve(ground_truth, prob, pos_label=positive_label)\n        fpr_tprs.append((fpr, tpr))\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = os.path.join(output_directory, \"roc_curves.\" + file_format)\n\n    visualization_utils.roc_curves(fpr_tprs, model_names_list, title=\"ROC curves\", filename=filename)\n\n\n@DeveloperAPI\ndef roc_curves_from_test_statistics(\n    test_stats_per_model: list[dict],\n    output_feature_name: str,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show the roc curves for the specified models output binary `output_feature_name`.\n\n    This visualization uses `output_feature_name`, `test_stats_per_model` and\n    `model_names` parameters. `output_feature_name` needs to be binary feature.\n    This visualization produces a line chart plotting the roc curves for the\n    specified `output_feature_name`.\n\n    # Inputs\n\n    :param test_stats_per_model: (List[dict]) dictionary containing evaluation\n        performance statistics.\n    :param output_feature_name: (str) name of the output feature to use\n        for the visualization.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"roc_curves_from_prediction_statistics.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    fpr_tprs = []\n    for curr_test_statistics in test_stats_per_model:\n        fpr = curr_test_statistics[output_feature_name][\"roc_curve\"][\"false_positive_rate\"]\n        tpr = curr_test_statistics[output_feature_name][\"roc_curve\"][\"true_positive_rate\"]\n        fpr_tprs.append((fpr, tpr))\n\n    visualization_utils.roc_curves(fpr_tprs, model_names_list, title=\"ROC curves\", filename=filename_template_path)\n\n\n@DeveloperAPI\ndef calibration_1_vs_all(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    top_n_classes: list[int],\n    labels_limit: int,\n    model_names: list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models probability of predictions for the specified output_feature_name.\n\n    For each class or each of the k most frequent classes if top_k is\n    specified,  it produces two plots computed on the fly from the\n    probabilities  of predictions for the specified output_feature_name.\n\n    The first plot is a calibration curve that shows the calibration of the\n    predictions considering the current class to be the true one and all\n    others  to be a false one, drawing one line for each model (in the\n    aligned  lists of probabilities and model_names).\n\n    The second plot shows the distributions of the predictions considering\n    the  current class to be the true one and all others to be a false one,\n    drawing the distribution for each model (in the aligned lists of\n    probabilities and model_names).\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param top_n_classes: (list) List containing the number of classes to plot.\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (List[str], default: `None`) list of the names of the\n        models to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # String\n\n    :return: (None)\n    \"\"\"\n    feature_metadata = metadata[output_feature_name]\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"calibration_1_vs_all_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            probs[i] = prob_limit\n\n    num_classes = len(metadata[output_feature_name][\"str2idx\"])\n\n    brier_scores = []\n\n    classes = min(num_classes, top_n_classes[0]) if top_n_classes[0] > 0 else num_classes\n    class_names = [feature_metadata[\"idx2str\"][i] for i in range(classes)]\n\n    for class_idx in range(classes):\n        fraction_positives_class = []\n        mean_predicted_vals_class = []\n        probs_class = []\n        brier_scores_class = []\n        for prob in probs:\n            # ground_truth is a vector of integers, each integer is a class\n            # index to have a [0,1] vector we have to check if the value equals\n            # the input class index and convert the resulting boolean vector\n            # into an integer vector probabilities is a n x c matrix, n is the\n            # number of datapoints and c number of classes; its values are the\n            # probabilities of the ith datapoint to be classified as belonging\n            # to the jth class according to the learned model. For this reason\n            # we need to take only the column of predictions that is about the\n            # class we are interested in, the input class index\n\n            gt_class = (ground_truth == class_idx).astype(int)\n            prob_class = prob[:, class_idx]\n\n            curr_fraction_positives, curr_mean_predicted_vals = calibration_curve(gt_class, prob_class, n_bins=21)\n\n            if len(curr_fraction_positives) < 2:\n                curr_fraction_positives = np.concatenate((np.array([0.0]), curr_fraction_positives))\n            if len(curr_mean_predicted_vals) < 2:\n                curr_mean_predicted_vals = np.concatenate((np.array([0.0]), curr_mean_predicted_vals))\n\n            fraction_positives_class.append(curr_fraction_positives)\n            mean_predicted_vals_class.append(curr_mean_predicted_vals)\n            probs_class.append(prob[:, class_idx])\n            brier_scores_class.append(brier_score_loss(gt_class, prob_class, pos_label=1))\n\n        brier_scores.append(brier_scores_class)\n\n        filename = None\n        if output_directory:\n            os.makedirs(output_directory, exist_ok=True)\n            filename = filename_template_path.format(class_idx)\n\n        visualization_utils.calibration_plot(\n            fraction_positives_class,\n            mean_predicted_vals_class,\n            model_names_list,\n            class_name=class_names[class_idx],\n            filename=filename,\n        )\n\n        filename = None\n        if output_directory:\n            os.makedirs(output_directory, exist_ok=True)\n            filename = filename_template_path.format(\"prediction_distribution_\" + str(class_idx))\n\n        visualization_utils.predictions_distribution_plot(probs_class, model_names_list, filename=filename)\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = filename_template_path.format(\"brier\")\n\n    visualization_utils.brier_plot(\n        np.array(brier_scores),\n        algorithm_names=model_names_list,\n        class_names=class_names,\n        title=\"Brier scores for each class\",\n        filename=filename,\n    )\n\n\n@DeveloperAPI\ndef calibration_multiclass(\n    probabilities_per_model: list[np.array],\n    ground_truth: pd.Series | np.ndarray,\n    metadata: dict,\n    output_feature_name: str,\n    labels_limit: int,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    ground_truth_apply_idx: bool = True,\n    **kwargs,\n) -> None:\n    \"\"\"Show models probability of predictions for each class of the specified output_feature_name.\n\n    # Inputs\n\n    :param probabilities_per_model: (List[numpy.array]) list of model\n        probabilities.\n    :param ground_truth: (Union[pd.Series, np.ndarray]) ground truth values\n    :param metadata: (dict) feature metadata dictionary\n    :param output_feature_name: (str) output feature name\n    :param labels_limit: (int) upper limit on the numeric encoded label value.\n        Encoded numeric label values in dataset that are higher than\n        `labels_limit` are considered to be \"rare\" labels.\n    :param model_names: (List[str], default: `None`) list of the names of the\n        models to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n    :param ground_truth_apply_idx: (bool, default: `True`) whether to use\n        metadata['str2idx'] in np.vectorize\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    if not isinstance(ground_truth, np.ndarray):\n        # not np array, assume we need to translate raw value to encoded value\n        feature_metadata = metadata[output_feature_name]\n        ground_truth = _vectorize_ground_truth(ground_truth, feature_metadata[\"str2idx\"], ground_truth_apply_idx)\n\n    probs = probabilities_per_model\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"calibration_multiclass{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    if labels_limit > 0:\n        ground_truth[ground_truth > labels_limit] = labels_limit\n\n    prob_classes = 0\n    for i, prob in enumerate(probs):\n        if labels_limit > 0 and prob.shape[1] > labels_limit + 1:\n            prob_limit = prob[:, : labels_limit + 1]\n            prob_limit[:, labels_limit] = prob[:, labels_limit:].sum(1)\n            probs[i] = prob_limit\n        if probs[i].shape[1] > prob_classes:\n            prob_classes = probs[i].shape[1]\n\n    gt_one_hot_dim_2 = max(prob_classes, max(ground_truth) + 1)\n    gt_one_hot = np.zeros((len(ground_truth), gt_one_hot_dim_2))\n    gt_one_hot[np.arange(len(ground_truth)), ground_truth] = 1\n    gt_one_hot_flat = gt_one_hot.flatten()\n\n    fraction_positives = []\n    mean_predicted_vals = []\n    brier_scores = []\n    for prob in probs:\n        # flatten probabilities to be compared to flatten ground truth\n        prob_flat = prob.flatten()\n        curr_fraction_positives, curr_mean_predicted_vals = calibration_curve(gt_one_hot_flat, prob_flat, n_bins=21)\n        fraction_positives.append(curr_fraction_positives)\n        mean_predicted_vals.append(curr_mean_predicted_vals)\n        brier_scores.append(brier_score_loss(gt_one_hot_flat, prob_flat, pos_label=1))\n\n    filename = None\n    if output_directory:\n        os.makedirs(output_directory, exist_ok=True)\n        filename = filename_template_path.format(\"\")\n\n    visualization_utils.calibration_plot(fraction_positives, mean_predicted_vals, model_names_list, filename=filename)\n\n    filename = None\n    if output_directory:\n        filename = filename_template_path.format(\"_brier\")\n\n    visualization_utils.compare_classifiers_plot(\n        [brier_scores], [\"brier\"], model_names, adaptive=True, decimals=8, filename=filename\n    )\n\n    for i, brier_score in enumerate(brier_scores):\n        if i < len(model_names):\n            tokenizer_name = f\"{model_names[i]}: \"\n            tokenizer_name += \"{}\"\n        else:\n            tokenizer_name = \"{}\"\n        logger.info(tokenizer_name.format(brier_score))\n\n\n@DeveloperAPI\ndef confusion_matrix(\n    test_stats_per_model: list[dict],\n    metadata: dict,\n    output_feature_name: str | None,\n    top_n_classes: list[int],\n    normalize: bool,\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n) -> None:\n    \"\"\"Show confusion matrix in the models predictions for each `output_feature_name`.\n\n    For each model (in the aligned lists of test_statistics and model_names)\n    it  produces a heatmap of the confusion matrix in the predictions for\n    each  output_feature_name that has a confusion matrix in test_statistics.\n    The value of `top_n_classes` limits the heatmap to the n most frequent\n    classes.\n\n    # Inputs\n\n    :param test_stats_per_model: (List[dict]) dictionary containing evaluation\n      performance statistics.\n    :param metadata: (dict) intermediate preprocess structure created during\n        training containing the mappings of the input dataset.\n    :param output_feature_name: (Union[str, `None`]) name of the output feature\n        to use for the visualization.  If `None`, use all output features.\n    :param top_n_classes: (List[int]) number of top classes or list\n        containing the number of top classes to plot.\n    :param normalize: (bool) flag to normalize rows in confusion matrix.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    test_stats_per_model_list = test_stats_per_model\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"confusion_matrix_{}_{}_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list)\n\n    confusion_matrix_found = False\n    for i, test_statistics in enumerate(test_stats_per_model_list):\n        for output_feature_name in output_feature_names:\n            if \"confusion_matrix\" in test_statistics[output_feature_name]:\n                confusion_matrix_found = True\n                _confusion_matrix = np.array(test_statistics[output_feature_name][\"confusion_matrix\"])\n                model_name_name = (\n                    model_names_list[i] if (model_names_list is not None and i < len(model_names_list)) else \"\"\n                )\n                if (\n                    metadata is not None\n                    and output_feature_name in metadata\n                    and (\"idx2str\" in metadata[output_feature_name] or \"bool2str\" in metadata[output_feature_name])\n                ):\n                    if \"bool2str\" in metadata[output_feature_name]:  # Handles the binary output case\n                        labels = metadata[output_feature_name][\"bool2str\"]\n                    else:\n                        labels = metadata[output_feature_name][\"idx2str\"]\n                else:\n                    labels = list(range(len(_confusion_matrix)))\n\n                for k in top_n_classes:\n                    k = min(k, _confusion_matrix.shape[0]) if k > 0 else _confusion_matrix.shape[0]\n                    cm = _confusion_matrix[:k, :k]\n                    if normalize:\n                        with np.errstate(divide=\"ignore\", invalid=\"ignore\"):\n                            cm_norm = np.true_divide(cm, cm.sum(1)[:, np.newaxis])\n                            cm_norm[cm_norm == np.inf] = 0\n                            cm_norm = np.nan_to_num(cm_norm)\n                        cm = cm_norm\n\n                    filename = None\n                    if output_directory:\n                        os.makedirs(output_directory, exist_ok=True)\n                        filename = filename_template_path.format(model_name_name, output_feature_name, \"top\" + str(k))\n\n                    visualization_utils.confusion_matrix_plot(\n                        cm, labels[:k], output_feature_name=output_feature_name, filename=filename\n                    )\n\n                    entropies = []\n                    for row in cm:\n                        if np.count_nonzero(row) > 0:\n                            entropies.append(entropy(row))\n                        else:\n                            entropies.append(0)\n                    class_entropy = np.array(entropies)\n                    class_desc_entropy = np.argsort(class_entropy)[::-1]\n                    desc_entropy = class_entropy[class_desc_entropy]\n\n                    filename = None\n                    if output_directory:\n                        filename = filename_template_path.format(\n                            \"entropy_\" + model_name_name, output_feature_name, \"top\" + str(k)\n                        )\n\n                    visualization_utils.bar_plot(\n                        class_desc_entropy,\n                        desc_entropy,\n                        labels=[labels[i] for i in class_desc_entropy],\n                        title=\"Classes ranked by entropy of \" \"Confusion Matrix row\",\n                        filename=filename,\n                    )\n    if not confusion_matrix_found:\n        logger.error(\"Cannot find confusion_matrix in evaluation data\")\n        raise FileNotFoundError(\"Cannot find confusion_matrix in evaluation \" \"data\")\n\n\n@DeveloperAPI\ndef frequency_vs_f1(\n    test_stats_per_model: list[dict],\n    metadata: dict,\n    output_feature_name: str | None,\n    top_n_classes: list[int],\n    model_names: str | list[str] = None,\n    output_directory: str = None,\n    file_format: str = \"pdf\",\n    **kwargs,\n):\n    \"\"\"Show prediction statistics for the specified `output_feature_name` for each model.\n\n    For each model (in the aligned lists of `test_stats_per_model` and\n    `model_names`), produces two plots statistics of predictions for the\n    specified `output_feature_name`.\n\n    The first plot is a line plot with one x axis representing the different\n    classes and two vertical axes colored in orange and blue respectively.\n    The orange one is the frequency of the class and an orange line is plotted\n    to show the trend. The blue one is the F1 score for that class and a blue\n    line is plotted to show the trend. The classes on the x axis are sorted by\n    f1 score.\n\n    The second plot has the same structure of the first one,\n    but the axes are flipped and the classes on the x axis are sorted by\n    frequency.\n\n    # Inputs\n\n    :param test_stats_per_model: (List[dict]) dictionary containing evaluation\n        performance statistics.\n    :param metadata: (dict) intermediate preprocess structure created during\n        training containing the mappings of the input dataset.\n    :param output_feature_name: (Union[str, `None`]) name of the output feature\n        to use for the visualization.  If `None`, use all output features.\n    :param top_n_classes: (List[int]) number of top classes or list\n        containing the number of top classes to plot.\n    :param model_names: (Union[str, List[str]], default: `None`) model name or\n        list of the model names to use as labels.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    test_stats_per_model_list = test_stats_per_model\n    model_names_list = convert_to_list(model_names)\n    filename_template = \"frequency_vs_f1_{}_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n    output_feature_names = _validate_output_feature_name_from_test_stats(output_feature_name, test_stats_per_model_list)\n    k = top_n_classes[0]\n\n    for i, test_stats in enumerate(test_stats_per_model_list):\n        for of_name in output_feature_names:\n            # Figure out model name\n            model_name = model_names_list[i] if model_names_list is not None and i < len(model_names_list) else \"\"\n\n            # setup directory and filename\n            filename = None\n            if output_directory:\n                os.makedirs(output_directory, exist_ok=True)\n                filename = filename_template_path.format(model_name, of_name)\n\n            # setup local variables\n            per_class_stats = test_stats[of_name][\"per_class_stats\"]\n            class_names = metadata[of_name][\"idx2str\"]\n\n            # get np arrays of frequencies, f1s and labels\n            idx2freq = {metadata[of_name][\"str2idx\"][key]: val for key, val in metadata[of_name][\"str2freq\"].items()}\n            freq_np = np.array([idx2freq[class_id] for class_id in sorted(idx2freq)], dtype=np.int32)\n\n            if k > 0:\n                class_names = class_names[:k]\n                freq_np = freq_np[:k]\n\n            f1_scores = []\n            labels = []\n\n            for class_name in class_names:\n                class_stats = per_class_stats[class_name]\n                f1_scores.append(class_stats[\"f1_score\"])\n                labels.append(class_name)\n\n            f1_np = np.nan_to_num(np.array(f1_scores, dtype=np.float32))\n            labels_np = np.array(labels)\n\n            # sort by f1\n            f1_sort_idcs = f1_np.argsort()[::-1]\n            len_f1_sort_idcs = len(f1_sort_idcs)\n\n            freq_sorted_by_f1 = freq_np[f1_sort_idcs]\n            freq_sorted_by_f1 = freq_sorted_by_f1[:len_f1_sort_idcs]\n            f1_sorted_by_f1 = f1_np[f1_sort_idcs]\n            f1_sorted_by_f1 = f1_sorted_by_f1[:len_f1_sort_idcs]\n            labels_sorted_by_f1 = labels_np[f1_sort_idcs]\n            labels_sorted_by_f1 = labels_sorted_by_f1[:len_f1_sort_idcs]\n\n            # create viz sorted by f1\n            visualization_utils.double_axis_line_plot(\n                f1_sorted_by_f1,\n                freq_sorted_by_f1,\n                \"F1 score\",\n                \"frequency\",\n                labels=labels_sorted_by_f1,\n                title=f\"{model_name} F1 Score vs Frequency {of_name}\",\n                filename=filename,\n            )\n\n            # sort by freq\n            freq_sort_idcs = freq_np.argsort()[::-1]\n            len_freq_sort_idcs = len(freq_sort_idcs)\n\n            freq_sorted_by_freq = freq_np[freq_sort_idcs]\n            freq_sorted_by_freq = freq_sorted_by_freq[:len_freq_sort_idcs]\n            f1_sorted_by_freq = f1_np[freq_sort_idcs]\n            f1_sorted_by_freq = f1_sorted_by_freq[:len_freq_sort_idcs]\n            labels_sorted_by_freq = labels_np[freq_sort_idcs]\n            labels_sorted_by_freq = labels_sorted_by_freq[:len_freq_sort_idcs]\n\n            # create viz sorted by freq\n            visualization_utils.double_axis_line_plot(\n                freq_sorted_by_freq,\n                f1_sorted_by_freq,\n                \"frequency\",\n                \"F1 score\",\n                labels=labels_sorted_by_freq,\n                title=f\"{model_name} F1 Score vs Frequency {of_name}\",\n                filename=filename,\n            )\n\n\n@DeveloperAPI\ndef hyperopt_report_cli(hyperopt_stats_path, output_directory=None, file_format=\"pdf\", **kwargs) -> None:\n    \"\"\"Produces a report about hyperparameter optimization creating one graph per hyperparameter to show the\n    distribution of results and one additional graph of pairwise hyperparameters interactions.\n\n    :param hyperopt_stats_path: path to the hyperopt results JSON file\n    :param output_directory: path where to save the output plots\n    :param file_format: format of the output plot, pdf or png\n    :return:\n    \"\"\"\n\n    hyperopt_report(hyperopt_stats_path, output_directory=output_directory, file_format=file_format)\n\n\n@DeveloperAPI\ndef hyperopt_report(hyperopt_stats_path: str, output_directory: str = None, file_format: str = \"pdf\", **kwargs) -> None:\n    \"\"\"Produces a report about hyperparameter optimization creating one graph per hyperparameter to show the\n    distribution of results and one additional graph of pairwise hyperparameters interactions.\n\n    # Inputs\n\n    :param hyperopt_stats_path: (str) path to the hyperopt results JSON file.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window.\n    :param file_format: (str, default: `'pdf'`) file format of output plots -\n        `'pdf'` or `'png'`.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    filename_template = \"hyperopt_{}.\" + file_format\n    filename_template_path = generate_filename_template_path(output_directory, filename_template)\n\n    hyperopt_stats = load_json(hyperopt_stats_path)\n\n    visualization_utils.hyperopt_report(\n        hyperopt_stats[\"hyperopt_config\"][\"parameters\"],\n        hyperopt_results_to_dataframe(\n            hyperopt_stats[\"hyperopt_results\"],\n            hyperopt_stats[\"hyperopt_config\"][\"parameters\"],\n            hyperopt_stats[\"hyperopt_config\"][\"metric\"],\n        ),\n        metric=hyperopt_stats[\"hyperopt_config\"][\"metric\"],\n        filename_template=filename_template_path,\n    )\n\n\n@DeveloperAPI\ndef hyperopt_hiplot_cli(hyperopt_stats_path, output_directory=None, **kwargs):\n    \"\"\"Produces a parallel coordinate plot about hyperparameter optimization creating one HTML file and optionally\n    a CSV file to be read by hiplot.\n\n    :param hyperopt_stats_path: path to the hyperopt results JSON file\n    :param output_directory: path where to save the output plots\n    :return:\n    \"\"\"\n\n    hyperopt_hiplot(hyperopt_stats_path, output_directory=output_directory)\n\n\n@DeveloperAPI\ndef hyperopt_hiplot(hyperopt_stats_path, output_directory=None, **kwargs):\n    \"\"\"Produces a parallel coordinate plot about hyperparameter optimization creating one HTML file and optionally\n    a CSV file to be read by hiplot.\n\n    # Inputs\n\n    :param hyperopt_stats_path: (str) path to the hyperopt results JSON file.\n    :param output_directory: (str, default: `None`) directory where to save\n        plots. If not specified, plots will be displayed in a window.\n\n    # Return\n\n    :return: (None)\n    \"\"\"\n    filename = \"hyperopt_hiplot.html\"\n    filename_path = generate_filename_template_path(output_directory, filename)\n\n    hyperopt_stats = load_json(hyperopt_stats_path)\n    hyperopt_df = hyperopt_results_to_dataframe(\n        hyperopt_stats[\"hyperopt_results\"],\n        hyperopt_stats[\"hyperopt_config\"][\"parameters\"],\n        hyperopt_stats[\"hyperopt_config\"][\"metric\"],\n    )\n    visualization_utils.hyperopt_hiplot(\n        hyperopt_df,\n        filename=filename_path,\n    )\n\n\ndef _convert_space_to_dtype(space: str) -> str:\n    if space in visualization_utils.RAY_TUNE_FLOAT_SPACES:\n        return \"float\"\n    elif space in visualization_utils.RAY_TUNE_INT_SPACES:\n        return \"int\"\n    else:\n        return \"object\"\n\n\n@DeveloperAPI\ndef hyperopt_results_to_dataframe(hyperopt_results, hyperopt_parameters, metric):\n    df = pd.DataFrame([{metric: res[\"metric_score\"], **res[\"parameters\"]} for res in hyperopt_results])\n    df = df.astype(\n        {hp_name: _convert_space_to_dtype(hp_params[SPACE]) for hp_name, hp_params in hyperopt_parameters.items()}\n    )\n    return df\n\n\n@DeveloperAPI\ndef get_visualizations_registry() -> dict[str, Callable]:\n    return {\n        \"compare_performance\": compare_performance_cli,\n        \"compare_classifiers_performance_from_prob\": compare_classifiers_performance_from_prob_cli,\n        \"compare_classifiers_performance_from_pred\": compare_classifiers_performance_from_pred_cli,\n        \"compare_classifiers_performance_subset\": compare_classifiers_performance_subset_cli,\n        \"compare_classifiers_performance_changing_k\": compare_classifiers_performance_changing_k_cli,\n        \"compare_classifiers_multiclass_multimetric\": compare_classifiers_multiclass_multimetric_cli,\n        \"compare_classifiers_predictions\": compare_classifiers_predictions_cli,\n        \"compare_classifiers_predictions_distribution\": compare_classifiers_predictions_distribution_cli,\n        \"confidence_thresholding\": confidence_thresholding_cli,\n        \"confidence_thresholding_data_vs_acc\": confidence_thresholding_data_vs_acc_cli,\n        \"confidence_thresholding_data_vs_acc_subset\": confidence_thresholding_data_vs_acc_subset_cli,\n        \"confidence_thresholding_data_vs_acc_subset_per_class\": confidence_thresholding_data_vs_acc_subset_per_class_cli,  # noqa: E501\n        \"confidence_thresholding_2thresholds_2d\": confidence_thresholding_2thresholds_2d_cli,\n        \"confidence_thresholding_2thresholds_3d\": confidence_thresholding_2thresholds_3d_cli,\n        \"binary_threshold_vs_metric\": binary_threshold_vs_metric_cli,\n        \"roc_curves\": roc_curves_cli,\n        \"roc_curves_from_test_statistics\": roc_curves_from_test_statistics_cli,\n        \"precision_recall_curves\": precision_recall_curves_cli,\n        \"precision_recall_curves_from_test_statistics\": precision_recall_curves_from_test_statistics_cli,\n        \"calibration_1_vs_all\": calibration_1_vs_all_cli,\n        \"calibration_multiclass\": calibration_multiclass_cli,\n        \"confusion_matrix\": confusion_matrix_cli,\n        \"frequency_vs_f1\": frequency_vs_f1_cli,\n        \"learning_curves\": learning_curves_cli,\n        \"hyperopt_report\": hyperopt_report_cli,\n        \"hyperopt_hiplot\": hyperopt_hiplot_cli,\n    }\n\n\n@PublicAPI\ndef cli(sys_argv):\n    parser = argparse.ArgumentParser(\n        description=\"This script analyzes results and shows some nice plots.\",\n        prog=\"ludwig visualize\",\n        usage=\"%(prog)s [options]\",\n    )\n\n    parser.add_argument(\"-g\", \"--ground_truth\", help=\"ground truth file\")\n    parser.add_argument(\"-gm\", \"--ground_truth_metadata\", help=\"input metadata JSON file\")\n    parser.add_argument(\n        \"-sf\",\n        \"--split_file\",\n        default=None,\n        help=\"file containing split values used in conjunction with \" \"ground truth file.\",\n    )\n\n    parser.add_argument(\n        \"-od\",\n        \"--output_directory\",\n        help=\"directory where to save plots.\" \"If not specified, plots will be displayed in a window\",\n    )\n    parser.add_argument(\n        \"-ff\", \"--file_format\", help=\"file format of output plots\", default=\"pdf\", choices=[\"pdf\", \"png\"]\n    )\n\n    parser.add_argument(\n        \"-v\",\n        \"--visualization\",\n        choices=sorted(list(get_visualizations_registry().keys())),\n        help=\"type of visualization to generate\",\n        required=True,\n    )\n\n    parser.add_argument(\"-ofn\", \"--output_feature_name\", default=[], help=\"name of the output feature to visualize\")\n    parser.add_argument(\n        \"-gts\", \"--ground_truth_split\", default=2, help=\"ground truth split - 0:train, 1:validation, 2:test split\"\n    )\n    parser.add_argument(\n        \"-tf\",\n        \"--threshold_output_feature_names\",\n        default=[],\n        nargs=\"+\",\n        help=\"names of output features for 2d threshold\",\n    )\n    parser.add_argument(\"-pred\", \"--predictions\", default=[], nargs=\"+\", type=str, help=\"predictions files\")\n    parser.add_argument(\"-prob\", \"--probabilities\", default=[], nargs=\"+\", type=str, help=\"probabilities files\")\n    parser.add_argument(\"-trs\", \"--training_statistics\", default=[], nargs=\"+\", type=str, help=\"training stats files\")\n    parser.add_argument(\"-tes\", \"--test_statistics\", default=[], nargs=\"+\", type=str, help=\"test stats files\")\n    parser.add_argument(\"-hs\", \"--hyperopt_stats_path\", default=None, type=str, help=\"hyperopt stats file\")\n    parser.add_argument(\n        \"-mn\", \"--model_names\", default=[], nargs=\"+\", type=str, help=\"names of the models to use as labels\"\n    )\n    parser.add_argument(\"-tn\", \"--top_n_classes\", default=[0], nargs=\"+\", type=int, help=\"number of classes to plot\")\n    parser.add_argument(\"-k\", \"--top_k\", default=3, type=int, help=\"number of elements in the ranklist to consider\")\n    parser.add_argument(\n        \"-ll\",\n        \"--labels_limit\",\n        default=0,\n        type=int,\n        help=\"maximum numbers of labels. Encoded numeric label values in dataset that are higher than \"\n        'labels_limit are considered to be \"rare\" labels',\n    )\n    parser.add_argument(\n        \"-ss\",\n        \"--subset\",\n        default=\"ground_truth\",\n        choices=[\"ground_truth\", PREDICTIONS],\n        help=\"type of subset filtering\",\n    )\n    parser.add_argument(\n        \"-n\", \"--normalize\", action=\"store_true\", default=False, help=\"normalize rows in confusion matrix\"\n    )\n    parser.add_argument(\n        \"-m\", \"--metrics\", default=[\"f1\"], nargs=\"+\", type=str, help=\"metrics to display in threshold_vs_metric\"\n    )\n    parser.add_argument(\n        \"-pl\", \"--positive_label\", type=int, default=1, help=\"label of the positive class for the roc curve\"\n    )\n    parser.add_argument(\n        \"-l\",\n        \"--logging_level\",\n        default=\"info\",\n        help=\"the level of logging to use\",\n        choices=[\"critical\", \"error\", \"warning\", \"info\", \"debug\", \"notset\"],\n    )\n\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n\n    args.callbacks = args.callbacks or []\n    for callback in args.callbacks:\n        callback.on_cmdline(\"visualize\", *sys_argv)\n\n    args.logging_level = get_logging_level_registry()[args.logging_level]\n    logging.getLogger(\"ludwig\").setLevel(args.logging_level)\n    global logger\n    logger = logging.getLogger(\"ludwig.visualize\")\n\n    try:\n        vis_func = get_visualizations_registry()[args.visualization]\n    except KeyError:\n        logger.info(\"Visualization argument not recognized\")\n        raise\n    vis_func(**vars(args))\n\n\nif __name__ == \"__main__\":\n    cli(sys.argv[1:])\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.isort]\nprofile = \"black\"\nline_length = 120\nforce_sort_within_sections = \"False\"\norder_by_type = \"False\"\n\n\n[tool.black]\nline-length = 120\nexclude = \"./python/\"\n"
  },
  {
    "path": "pytest.ini",
    "content": "[pytest]\nmarkers =\n    benchmark: mark a test as a benchmarking test.\n    distributed: mark a test as a distributed test.\n    filesystem: mark to test operating system systems.\n    slow: mark test as slow.\n    combinatorial: mark a test as combinatorial.\n    llm: mark a test as an LLM test.\n    integration_tests_a: mark a test to be run as part of integration tests, group A.\n    integration_tests_b: mark a test to be run as part of integration tests, group B.\n    integration_tests_c: mark a test to be run as part of integration tests, group C.\n    integration_tests_d: mark a test to be run as part of integration tests, group D.\n    integration_tests_e: mark a test to be run as part of integration tests, group E.\n    integration_tests_f: mark a test to be run as part of integration tests, group F.\nfilterwarnings =\n    ignore::marshmallow.warnings.RemovedInMarshmallow4Warning\n    ignore::DeprecationWarning:importlib._bootstrap\n    ignore:builtin type Swig:DeprecationWarning\n    ignore:.*torch.jit.script.*is deprecated:DeprecationWarning\n"
  },
  {
    "path": "requirements.txt",
    "content": "h5py>=3.8\nnumpy>=1.24\npandas>=2.0\nscipy>=1.10\ntabulate>=0.9\nscikit-learn>=1.3\ntqdm>=4.60\ntorch>=2.0\ntorchaudio>=2.0\ntorchvision>=0.15\ntransformers>=4.36\nsentencepiece>=0.2\nspacy>=2.3\nPyYAML>=6.0\nabsl-py\nkaggle\nrequests>=2.28\ntables\nfsspec[http]\ndataclasses-json\njsonschema>=4.17\ntensorboard\ntorchmetrics>=1.0\ntorchinfo\nfilelock\npsutil\nprotobuf>=4.0\ngpustat\nrich~=12.4.4\npackaging\nretry\n\n# required for TransfoXLTokenizer when using transformer_xl\nsacremoses\nsentencepiece\n\n# requirement for various paged and 8-bit optimizers\nbitsandbytes<0.41.0\n\n# new data format support\nxlwt            # excel\nxlrd            # excel\nopenpyxl        # excel\npyarrow>=14.0   # parquet\nlxml            # html\n\n# requirement for loading hugging face datasets\ndatasets\n"
  },
  {
    "path": "requirements_benchmarking.txt",
    "content": "s3fs\n"
  },
  {
    "path": "requirements_distributed.txt",
    "content": "# requirements for dask\ndask[dataframe]\npyarrow>=14.0\n\n# requirements for ray\nray[default,data,serve,tune]>=2.9\nGPUtil\ntblib\nawscli\n"
  },
  {
    "path": "requirements_explain.txt",
    "content": "captum\n"
  },
  {
    "path": "requirements_extra.txt",
    "content": "# alternative to Dask\nmodin[ray]\n\n# Allows users to upload\npredibase>=2023.10.2\n"
  },
  {
    "path": "requirements_hyperopt.txt",
    "content": "ray[default,tune]>=2.9\n\n# required for Ray Tune Search Algorithm support for AutoML\n#search_alg: hyperopt\nhyperopt\nfuture<1.0  # hyperopt 0.2.7 requires future's Python 2 compat shims\n"
  },
  {
    "path": "requirements_llm.txt",
    "content": "sentence-transformers\nfaiss-cpu\n\naccelerate\nloralib\n\npeft>=0.10.0\n"
  },
  {
    "path": "requirements_serve.txt",
    "content": "uvicorn\nhttpx\nfastapi\npython-multipart\n"
  },
  {
    "path": "requirements_test.txt",
    "content": "pytest\npytest-timeout\npytest-rerunfailures\ntifffile\nwget\naim\nwandb\ncomet_ml\nmlflow\n\n# For testing optional Ray Tune Search Algorithms\n# search_alg: bohb\nhpbandster\nConfigSpace>=1.0\n\n# search_alg: ax\nax-platform\n\nsqlalchemy\n\n# search_alg: bayesopt\nbayesian-optimization\n\n# search_alg: cfo and blendsearch\nflaml[blendsearch]\n\n# search_alg: hebo\nHEBO\n\n# search_alg: nevergrad\nnevergrad\n\n# search_alg: optuna\noptuna\n\n# search_alg: skopt\nscikit-optimize\n\n# search_alg: zoopt\nzoopt\n\n# search_alg: hyperopt (TPE)\nhyperopt\n\ns3fs>=2022.8.2\n"
  },
  {
    "path": "requirements_viz.txt",
    "content": "matplotlib>=3.4\nseaborn\nhiplot\nptitprince\n"
  },
  {
    "path": "schemastore/README.md",
    "content": "# SchemaStore Submission Materials\n\nThis directory contains materials for submitting Ludwig's JSON Schema to\nthe [JSON Schema Store](https://www.schemastore.org/json/).\n\n## Catalog Entry\n\nThe file `catalog-entry.json` contains the entry to add to SchemaStore's\n`src/api/json/catalog.json`.\n\n## Test Configs\n\nThe `test/` directory contains example Ludwig config files that validate\nagainst the schema. These are used as positive test cases in the SchemaStore PR.\n\n## How to Submit\n\n1. Fork [SchemaStore/schemastore](https://github.com/SchemaStore/schemastore)\n1. Add the catalog entry from `catalog-entry.json` to `src/api/json/catalog.json`\n1. Copy test configs from `test/` to `src/test/ludwig/`\n1. Submit a PR referencing Ludwig issue #1343\n"
  },
  {
    "path": "schemastore/catalog-entry.json",
    "content": "{\n  \"name\": \"Ludwig\",\n  \"description\": \"Ludwig declarative deep learning framework configuration\",\n  \"fileMatch\": [\n    \"ludwig.yaml\",\n    \"ludwig.yml\",\n    \"ludwig.json\",\n    \"ludwig_config.yaml\",\n    \"ludwig_config.yml\",\n    \"ludwig_config.json\",\n    \"**/ludwig/**/config.yaml\",\n    \"**/ludwig/**/config.yml\"\n  ],\n  \"url\": \"https://ludwig-ai.github.io/schema/ludwig-config.json\"\n}\n"
  },
  {
    "path": "schemastore/test/ludwig.yaml",
    "content": "# Rotten Tomatoes review classification - multimodal Ludwig config\ninput_features:\n  - name: genres\n    type: set\n    preprocessing:\n      tokenizer: comma\n  - name: content_rating\n    type: category\n  - name: top_critic\n    type: binary\n  - name: runtime\n    type: number\n  - name: review_content\n    type: text\n    encoder:\n      type: embed\n\noutput_features:\n  - name: recommended\n    type: binary\n\ntrainer:\n  epochs: 3\n"
  },
  {
    "path": "schemastore/test/ludwig_config.yaml",
    "content": "# Titanic survival prediction - basic Ludwig config\ninput_features:\n  - name: Pclass\n    type: category\n  - name: Sex\n    type: category\n  - name: Age\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n  - name: SibSp\n    type: number\n  - name: Parch\n    type: number\n  - name: Fare\n    type: number\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n  - name: Embarked\n    type: category\n\noutput_features:\n  - name: Survived\n    type: binary\n"
  },
  {
    "path": "setup.cfg",
    "content": "[flake8]\nmax-line-length = 120\nexclude =\n    .tox,\n    *.egg,\n    *_pb2.py,\n    build,\n    temp\n\nselect = E,W,F\ndoctests = True\nverbose = 2\nformat = pylint\n# E731: Do not assign a lambda expression, use a def\n# W503: Line break occurred before a binary operator\n# E203: whitespace before ':'\nignore =\n    E731,\n    W503,\n    E203\n"
  },
  {
    "path": "setup.py",
    "content": "\"\"\"Ludwig: Data-centric declarative deep learning framework.\"\"\"\n\nfrom codecs import open\nfrom os import path\n\nfrom setuptools import find_packages, setup\n\nhere = path.abspath(path.dirname(__file__))\n\n# Get the long description from the README.md file\nwith open(path.join(here, \"README.md\"), encoding=\"utf-8\") as f:\n    long_description = f.read()\n\nwith open(path.join(here, \"requirements.txt\"), encoding=\"utf-8\") as f:\n    requirements = [line.strip() for line in f if line]\n\nextra_requirements = {}\n\nwith open(path.join(here, \"requirements_serve.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"serve\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_viz.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"viz\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_distributed.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"distributed\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_hyperopt.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"hyperopt\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_llm.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"llm\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_explain.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"explain\"] = [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_benchmarking.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"benchmarking\"] = [line.strip() for line in f if line]\n\nextra_requirements[\"full\"] = [item for sublist in extra_requirements.values() for item in sublist]\n\nwith open(path.join(here, \"requirements_test.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"test\"] = extra_requirements[\"full\"] + [line.strip() for line in f if line]\n\nwith open(path.join(here, \"requirements_extra.txt\"), encoding=\"utf-8\") as f:\n    extra_requirements[\"extra\"] = [line.strip() for line in f if line]\n\nsetup(\n    name=\"ludwig\",\n    version=\"0.11.2\",\n    description=\"Declarative machine learning: End-to-end machine learning pipelines using data-driven configurations.\",\n    long_description=long_description,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/ludwig-ai/ludwig\",\n    download_url=\"https://pypi.org/project/ludwig/\",\n    author=\"Piero Molino\",\n    author_email=\"piero.molino@gmail.com\",\n    license=\"Apache 2.0\",\n    keywords=\"ludwig deep learning deep_learning machine machine_learning natural language processing computer vision\",\n    packages=find_packages(exclude=[\"contrib\", \"docs\", \"tests\"]),\n    python_requires=\">=3.12\",\n    include_package_data=True,\n    package_data={\"ludwig\": [\"etc/*\", \"examples/*.py\"]},\n    install_requires=requirements,\n    extras_require=extra_requirements,\n    entry_points={\"console_scripts\": [\"ludwig=ludwig.cli:main\"]},\n)\n"
  },
  {
    "path": "tests/README.md",
    "content": "# Test Guide\n\nAssuming your CWD is the Ludwig repo root.\n\n## Basic\n\n```bash\npytest -vs tests\n```\n\n## Private Tests\n\nThese tests connect to services like remote filesystems (Minio / S3), which can be run locally using Docker.\n\n```bash\n# prepare test services\ndocker-compose -f tests/docker-compose.yml up\n\n# run all tests\nRUN_PRIVATE=1 pytest -vs tests\n```\n\n## Slow Tests\n\nThese tests are very slow, and should typically be run on GPU machines.\n\n```bash\nRUN_SLOW=1 pytest -vs tests\n```\n\n## Running GitHub Actions Locally\n\nIt is possible to run the CI test suite locally by executing the `pytest` action using\n[act](https://github.com/nektos/act).\n\nFirst start up the local minio container, if it is not already running. Then call `act -j pytest` to run the test suite.\n\n```\n# Start minio container in background\ndocker-compose -f tests/docker-compose.yml up -d\n\n# Run local test suite\nRUN_PRIVATE=1 act -j pytest\n```\n\n## Tests that use ray clusters\n\nUse the distributed pytest decorator to make sure that the test runs on CI jobs with the right ray dependencies installed.\n\n```python\n@pytest.mark.distributed\ndef test_something(ray_cluster_2_cpu):\n    pass\n```\n\nUse 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:\n\n```python\n@pytest.fixture(scope=\"module\")\ndef ray_cluster_2cpu(request):\n    with _ray_start(request, num_cpus=2):\n        yield\n```\n\n## Grouped Integration Tests\n\nTo 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.\n\nTo define a new group and use it in tests:\n\n1. Define a new pytest marker in `pytest.ini`.\n\n```ini\nintegration_tests_a: mark a test to be run as part of integration tests, group A.\nintegration_tests_b: mark a test to be run as part of integration tests, group B.\n# (new)\nintegration_tests_c: mark a test to be run as part of integration tests, group C.\n```\n\n2. Use the marker in a test file under `tests/integration_tests/`.\n\n```python\nimport pytest\n\npytestmark = pytest.mark.integration_tests_c\n```\n\nIf there's already a `pytestmark` declaration, turn it into a list.\n\n```python\nimport pytest\n\npytestmark = [pytest.mark.distributed, pytest.mark.integration_tests_c]\n```\n\nIf there's a specific test to include in the group, decorate the test function.\n\n```python\n@pytest.mark.integration_tests_c\ndef test_something():\n    pass\n```\n\n3. Create a new GHA to run pytest with that marker.\n\nYou can use [this change](https://github.com/ludwig-ai/ludwig/pull/3391/files#diff-2500680f4bc6c1b75c3d4b36372bf4d64c5f603b90bfd7a5186f66a20329d16aR189-R245) as a reference.\n\nNOTE: Be sure to update other Integration Test GHA pytest jobs to exclude tests under the new marker.\n\nTo check which tests would be run under the `pytest` command without actually running them, use `--collect-only`.\n\n```sh\npytest -m \"not distributed and not slow and not combinatorial and not llm and integration_tests_c\" --junitxml pytest.xml tests/integration_tests --collect-only\n```\n"
  },
  {
    "path": "tests/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport contextlib\nimport os\nimport tempfile\nimport time\nimport uuid\nfrom unittest import mock\n\nimport pytest\n\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    COMBINER,\n    EPOCHS,\n    HYPEROPT,\n    INPUT_FEATURES,\n    NAME,\n    OUTPUT_FEATURES,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.hyperopt.run import hyperopt\nfrom tests.integration_tests.utils import category_feature, generate_data, text_feature\n\nTEST_SUITE_TIMEOUT_S = int(os.environ.get(\"LUDWIG_TEST_SUITE_TIMEOUT_S\", 3600))\n\n\nexplicit_int_markers = {\n    \"integration_tests_a\",\n    \"integration_tests_b\",\n    \"integration_tests_c\",\n    \"integration_tests_d\",\n    \"integration_tests_e\",\n}\n\n\ndef pytest_sessionstart(session):\n    session.start_time = time.time()\n\n\ndef pytest_collection_modifyitems(config, items):\n    for item in items:\n        if all(False for x in item.iter_markers() if x.name in explicit_int_markers):\n            item.add_marker(\"integration_tests_f\")\n\n\n@pytest.fixture(autouse=True)\ndef check_session_time(request):\n    elapsed = time.time() - request.session.start_time\n    if elapsed > TEST_SUITE_TIMEOUT_S:\n        request.session.shouldstop = \"time limit reached: %0.2f seconds\" % elapsed\n\n\n@pytest.fixture(autouse=True)\ndef setup_tests(request):\n    if \"distributed\" not in request.keywords:\n        # Only run this patch if we're running distributed tests, otherwise Ray will not be installed\n        # and this will fail.\n        # See: https://stackoverflow.com/a/38763328\n        yield\n        return\n\n    with mock.patch(\"ludwig.backend.ray.init_ray_local\") as mock_init_ray_local:\n        mock_init_ray_local.side_effect = RuntimeError(\"Ray must be initialized explicitly when running tests\")\n        yield mock_init_ray_local\n\n\n@pytest.fixture()\ndef csv_filename():\n    \"\"\"Yields a csv filename for holding temporary data.\"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        csv_filename = os.path.join(tmpdir, uuid.uuid4().hex[:10].upper() + \".csv\")\n        yield csv_filename\n\n\n@pytest.fixture()\ndef yaml_filename():\n    \"\"\"Yields a yaml filename for holding a temporary config.\"\"\"\n    with tempfile.TemporaryDirectory() as tmpdir:\n        yaml_filename = os.path.join(tmpdir, \"model_def_\" + uuid.uuid4().hex[:10].upper() + \".yaml\")\n        yield yaml_filename\n\n\n@pytest.fixture(scope=\"module\")\ndef hyperopt_results_single_parameter(ray_cluster_4cpu):\n    \"\"\"This fixture is used by hyperopt visualization tests in test_visualization_api.py.\"\"\"\n    config, rel_path = _get_sample_config()\n    config[HYPEROPT] = {\n        \"parameters\": {\n            \"trainer.learning_rate\": {\n                \"space\": \"loguniform\",\n                \"lower\": 0.0001,\n                \"upper\": 0.01,\n            }\n        },\n        \"goal\": \"minimize\",\n        \"output_feature\": config[OUTPUT_FEATURES][0][NAME],\n        \"validation_metrics\": \"loss\",\n        \"executor\": {\n            \"type\": \"ray\",\n            \"num_samples\": 2,\n        },\n        \"search_alg\": {\n            \"type\": \"variant_generator\",\n        },\n    }\n    # Prevent resume from failure since this results in failures in other tests\n    hyperopt(config, dataset=rel_path, output_directory=\"results\", experiment_name=\"hyperopt_test\", resume=False)\n    return os.path.join(os.path.abspath(\"results\"), \"hyperopt_test\")\n\n\n@pytest.fixture(scope=\"module\")\ndef hyperopt_results_multiple_parameters(ray_cluster_4cpu):\n    \"\"\"This fixture is used by hyperopt visualization tests in test_visualization_api.py.\"\"\"\n    config, rel_path = _get_sample_config()\n    output_feature_name = config[OUTPUT_FEATURES][0][NAME]\n    config[HYPEROPT] = {\n        \"parameters\": {\n            \"trainer.learning_rate\": {\n                \"space\": \"loguniform\",\n                \"lower\": 0.0001,\n                \"upper\": 0.01,\n            },\n            output_feature_name + \".decoder.fc_output_size\": {\"space\": \"choice\", \"categories\": [32, 64, 128, 256]},\n            output_feature_name + \".decoder.num_fc_layers\": {\"space\": \"randint\", \"lower\": 1, \"upper\": 6},\n        },\n        \"goal\": \"minimize\",\n        \"output_feature\": output_feature_name,\n        \"validation_metrics\": \"loss\",\n        \"executor\": {\n            \"type\": \"ray\",\n            \"num_samples\": 2,\n        },\n        \"search_alg\": {\n            \"type\": \"variant_generator\",\n        },\n    }\n    # Prevent resume from failure since this results in failures in other tests\n    hyperopt(config, dataset=rel_path, output_directory=\"results\", experiment_name=\"hyperopt_test\", resume=False)\n    return os.path.join(os.path.abspath(\"results\"), \"hyperopt_test\")\n\n\n@pytest.fixture(scope=\"module\")\ndef ray_cluster_2cpu(request):\n    with _ray_start(request, num_cpus=2):\n        yield\n\n\n@pytest.fixture(scope=\"module\")\ndef ray_cluster_4cpu(request):\n    with _ray_start(request, num_cpus=4):\n        yield\n\n\n@pytest.fixture(scope=\"module\")\ndef ray_cluster_5cpu(request):\n    with _ray_start(request, num_cpus=5):\n        yield\n\n\n@pytest.fixture(scope=\"module\")\ndef ray_cluster_7cpu(request):\n    with _ray_start(request, num_cpus=7):\n        yield\n\n\n@contextlib.contextmanager\ndef _ray_start(request, **kwargs):\n    try:\n        import ray\n    except ImportError:\n        if \"distributed\" in request.keywords:\n            raise\n\n        # Allow this fixture to run in environments where Ray is not installed\n        # for parameterized tests that mix Ray with non-Ray backends\n        yield None\n        return\n\n    init_kwargs = _get_default_ray_kwargs()\n    init_kwargs.update(kwargs)\n    # HACK(geoffrey): `hyperopt_resources` is a required resource for hyperopt to prevent deadlocks in Ludwig tests.\n    #   For context, if there are 4 hyperopt trials scheduled and 7 CPUs available, then the trial driver will require\n    #   some resource to run *in addition* to the resources required by the trainer downstream. If we use 1 CPU\n    #   (default trial driver request), then the trial will be scheduled on 1 CPU and the trainer will later request\n    #   an additional 1 CPU. Across all 4 trials, this will possibly consume >7 CPUs, causing a deadlock since\n    #   Ray Datasets will not be able to grab resources for data preprocessing.\n    #\n    #   By adding a `hyperopt_resources` resource, we can ensure that the trial driver will be scheduled without\n    #   consuming any CPU resources. This allows each trial's trainer to request 1 CPU without starving Ray Datasets.\n    # TODO(geoffrey): remove for Ray 2.2\n    res = ray.init(**init_kwargs, resources={\"hyperopt_resources\": 1000})\n    try:\n        yield res\n    finally:\n        ray.shutdown()\n        # Delete the cluster address just in case.\n        if hasattr(ray._private.utils, \"reset_ray_address\"):\n            ray._private.utils.reset_ray_address()\n\n\ndef _get_default_ray_kwargs():\n    ray_kwargs = {\n        \"num_cpus\": 1,\n        \"object_store_memory\": 150 * 1024 * 1024,\n        \"dashboard_port\": None,\n        \"include_dashboard\": False,\n        \"namespace\": \"default_test_namespace\",\n        \"ignore_reinit_error\": True,\n    }\n    return ray_kwargs\n\n\ndef _get_sample_config():\n    \"\"\"Returns a sample config.\"\"\"\n    input_features = [\n        text_feature(name=\"utterance\", encoder={\"cell_type\": \"lstm\", \"reduce_output\": \"sum\"}),\n        category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n    csv_filename = uuid.uuid4().hex[:10].upper() + \".csv\"\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\", \"num_fc_layers\": 2},\n        TRAINER: {EPOCHS: 2, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n    }\n    return config, rel_path\n"
  },
  {
    "path": "tests/docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  minio:\n    image: 'minio/minio:latest'\n    volumes:\n      - minio_storage:/data\n    ports:\n      - 9000:9000\n      - 9001:9001\n    environment:\n      - MINIO_ACCESS_KEY=minio\n      - MINIO_SECRET_KEY=minio123\n    command: server --console-address \":9001\" /data\nvolumes:\n  minio_storage:\n"
  },
  {
    "path": "tests/integration_tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/integration_tests/parameter_update_utils.py",
    "content": "import logging\nfrom collections.abc import Callable\n\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.utils.torch_utils import LudwigModule\n\nlogger = logging.getLogger(__name__)\n\n\nclass ParameterUpdateError(Exception):\n    pass\n\n\ndef check_module_parameters_updated(\n    module: LudwigModule,\n    module_input_args: tuple,\n    module_target: torch.Tensor,\n    loss_function: Callable | None = None,\n    max_steps: int = 1,\n    learning_rate: float = 0.001,\n) -> tuple:\n    \"\"\"\n    Reports on the number of parameters in a Ludwig component and their update status.\n    Args:\n        module: (LudwigModel) model to be tested.\n        module_input_args: (tuple) input for model\n        module_target: (Tensor) target values for computing loss and parameter updates\n        loss_function: (None or Callable) Optional for module specific loss calculation\n        max_steps: (int, default=1) maximum number of steps allowed to test for parameter\n            updates.\n        learning_rate: (float, default=0.001) learning rate for the optimizer\n\n    Returns: Tuple(frozen_parameters, trainable_parameters, parameters_updated, not_updated)\n        frozen_parameters: count of frozen parameters\n        trainable_parameters: count of trainable parameters\n        parameters_updated: count of trainable parameters that were updated\n        not_updated: list of parameters that were not updated\n\n    \"\"\"\n    # setup\n    if loss_function is None:\n        loss_function = torch.nn.MSELoss()\n\n    # Ensure module and all inputs are on the same device\n    from ludwig.utils.torch_utils import get_torch_device\n\n    device = get_torch_device()\n    module = module.to(device)\n\n    def _move_to_device(arg):\n        if isinstance(arg, torch.Tensor):\n            return arg.to(device)\n        if isinstance(arg, dict):\n            return {k: _move_to_device(v) for k, v in arg.items()}\n        if isinstance(arg, (list, tuple)):\n            return type(arg)(_move_to_device(v) for v in arg)\n        return arg\n\n    module_input_args = tuple(_move_to_device(arg) for arg in module_input_args)\n    module_target = module_target.to(device)\n\n    optimizer = torch.optim.SGD(module.parameters(), lr=learning_rate)\n    module.train(True)\n\n    trainable_parameter_list = []\n    frozen_parameter_list = []\n    parameter_updated = []\n    parameters_not_updated = []\n    for step in range(max_steps):\n        # make pass through model\n        module_output = module(*module_input_args)\n\n        # check for any frozen parameters\n        frozen_parameter_list = []\n        trainable_parameter_list = []\n        for p in module.named_parameters():\n            if p[1].requires_grad:\n                trainable_parameter_list.append(p)\n            else:\n                frozen_parameter_list.append(p)\n\n        # check parameter updates only if there are some unfrozen parameters\n        if len(trainable_parameter_list) > 0:\n            # do update of model parameters\n            optimizer.zero_grad()\n            if isinstance(module_output, torch.Tensor):\n                module_target = module_target.to(device=module_output.device)\n                loss = loss_function(module_output, module_target)\n            elif isinstance(module_output, dict):\n                if \"logits\" in module_output:\n                    module_target = module_target.to(device=module_output[\"logits\"].device)\n                    loss = loss_function(module_output[\"logits\"], module_target)\n                elif ENCODER_OUTPUT in module_output:\n                    module_target = module_target.to(device=module_output[ENCODER_OUTPUT].device)\n                    loss = loss_function(module_output[ENCODER_OUTPUT], module_target)\n                elif \"combiner_output\" in module_output:\n                    module_target = module_target.to(device=module_output[\"combiner_output\"].device)\n                    loss = loss_function(module_output[\"combiner_output\"], module_target)\n            elif isinstance(module_output, (list, tuple)):\n                module_target = module_target.to(device=module_output[0].device)\n                loss = loss_function(module_output[0], module_target)\n            else:\n                raise ValueError(f\"Unexpected output type.  Module type found is {type(module_output)}\")\n\n            loss.backward()\n            optimizer.step()\n\n            # check for parameter updates\n            parameter_updated = []\n            # create tuple for each parameter: (parameter name, update indicator True/False)\n            # parameter is deemed updated if the gradient is not None and the gradient has non-zero value\n            for p in module.named_parameters():\n                parameter_updated.append((p[0], (p[1].grad is not None) and (not torch.all(p[1].grad == 0))))\n        else:\n            parameter_updated = []\n\n        parameters_not_updated = []\n        for p in parameter_updated:\n            # if not updated, record parameter name\n            if not p[1]:\n                parameters_not_updated.append(p[0])\n\n    trainable_parameters = len(trainable_parameter_list)\n    parameters_updated = sum(p[1] for p in parameter_updated)\n    frozen_parameters = len(frozen_parameter_list)\n\n    return frozen_parameters, trainable_parameters, parameters_updated, parameters_not_updated\n"
  },
  {
    "path": "tests/integration_tests/scripts/run_train_aim.py",
    "content": "import argparse\nimport os\nimport sys\nimport tempfile\nfrom unittest.mock import Mock\n\n# Comet must be imported before the libraries it wraps\nimport aim  # noqa\n\nfrom ludwig.contribs.aim import AimCallback\nfrom tests.integration_tests.utils import category_feature, generate_data, image_feature, run_experiment\n\nPATH_HERE = os.path.abspath(os.path.dirname(__file__))\nPATH_ROOT = os.path.join(PATH_HERE, \"..\", \"..\", \"..\")\nsys.path.insert(0, os.path.abspath(PATH_ROOT))\n\n\ndef run(csv_filename):\n    callback = AimCallback()\n\n    # Wrap these methods so we can check that they were called\n    callback.on_train_init = Mock(side_effect=callback.on_train_init)\n    callback.on_train_start = Mock(side_effect=callback.on_train_start)\n\n    # Image Inputs\n    with tempfile.TemporaryDirectory() as tmpdir:\n        image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n        # Inputs & Outputs\n        input_features = [image_feature(folder=image_dest_folder)]\n        output_features = [category_feature(output_feature=True)]\n        rel_path = generate_data(input_features, output_features, csv_filename)\n\n        # Run experiment\n        run_experiment(input_features, output_features, dataset=rel_path, callbacks=[callback])\n\n    # Check that these methods were called at least once\n    callback.on_train_init.assert_called()\n    callback.on_train_start.assert_called()\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--csv-filename\", required=True)\n    args = parser.parse_args()\n    run(args.csv_filename)\n"
  },
  {
    "path": "tests/integration_tests/scripts/run_train_comet.py",
    "content": "# Tests the following end-to-end:\n#\n# 1. Comet is imported\n# 2. Conflicting modules (i.e., TensorFlow) are not imported\n# 3. Overridden methods are called (train_init, train_model, etc.) and run without error\n#\n# This test runs in an isolated environment to ensure TensorFlow imports are not leaked\n# from previous tests.\n\nimport argparse\nimport os\nimport sys\nimport tempfile\nfrom unittest.mock import Mock, patch\n\n# Comet must be imported before the libraries it wraps\nimport comet_ml  # noqa\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, TRAINER\nfrom ludwig.contribs.comet import CometCallback\n\n# Bad key will ensure Comet is initialized, but nothing is uploaded externally.\nos.environ[\"COMET_API_KEY\"] = \"key\"\n\n# Add tests dir to the import path\nPATH_HERE = os.path.abspath(os.path.dirname(__file__))\nPATH_ROOT = os.path.join(PATH_HERE, \"..\", \"..\", \"..\")\nsys.path.insert(0, os.path.abspath(PATH_ROOT))\n\nfrom tests.integration_tests.utils import category_feature, generate_data, image_feature  # noqa\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--csv-filename\", required=True)\n\n\ndef run(csv_filename):\n    with tempfile.TemporaryDirectory() as tmpdir:\n        # Image Inputs\n        image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n        # Inputs & Outputs\n        input_features = [image_feature(folder=image_dest_folder)]\n        output_features = [category_feature(output_feature=True)]\n        data_csv = generate_data(input_features, output_features, csv_filename)\n\n        config = {\n            \"input_features\": input_features,\n            \"output_features\": output_features,\n            \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n            TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        }\n\n        callback = CometCallback()\n        model = LudwigModel(config, callbacks=[callback])\n\n        # Wrap these methods so we can check that they were called\n        callback.on_train_init = Mock(side_effect=callback.on_train_init)\n        callback.on_train_start = Mock(side_effect=callback.on_train_start)\n\n        with patch(\"comet_ml.Experiment.log_asset_data\") as mock_log_asset_data:\n            # Training with csv\n            _, _, _ = model.train(dataset=data_csv, output_directory=os.path.join(tmpdir, \"output\"))\n            model.predict(dataset=data_csv)\n\n    # Verify that the experiment was created successfully\n    assert callback.cometml_experiment is not None\n\n    # Check that these methods were called at least once\n    callback.on_train_init.assert_called()\n    callback.on_train_start.assert_called()\n\n    # Check that we ran `train_model`, which calls into `log_assert_data`, successfully\n    mock_log_asset_data.assert_called()\n\n\nif __name__ == \"__main__\":\n    args = parser.parse_args()\n    run(args.csv_filename)\n"
  },
  {
    "path": "tests/integration_tests/scripts/run_train_wandb.py",
    "content": "# Tests the following end-to-end:\n#\n# 1. W&B is imported\n# 2. Overridden methods are called (train_init, train_model, etc.) and run without error\n#\n# This test runs in an isolated environment because W&B make breaking changes to the\n# global interpreter state that will otherwise cause subsequent tests to fail.\n\nimport argparse\nimport os\nimport sys\nimport tempfile\nfrom unittest.mock import Mock\n\nfrom ludwig.contribs.wandb import WandbCallback\n\nPATH_HERE = os.path.abspath(os.path.dirname(__file__))\nPATH_ROOT = os.path.join(PATH_HERE, \"..\", \"..\", \"..\")\nsys.path.insert(0, os.path.abspath(PATH_ROOT))\n\nfrom tests.integration_tests.utils import category_feature, generate_data, image_feature, run_experiment  # noqa\n\nparser = argparse.ArgumentParser()\nparser.add_argument(\"--csv-filename\", required=True)\n\n\ndef run(csv_filename):\n    callback = WandbCallback()\n\n    # Wrap these methods so we can check that they were called\n    callback.on_train_init = Mock(side_effect=callback.on_train_init)\n    callback.on_train_start = Mock(side_effect=callback.on_train_start)\n\n    # disable sync to cloud\n    os.environ[\"WANDB_MODE\"] = \"dryrun\"\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        # Image Inputs\n        image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n        # Inputs & Outputs\n        input_features = [image_feature(folder=image_dest_folder)]\n        output_features = [category_feature(output_feature=True)]\n        rel_path = generate_data(input_features, output_features, csv_filename)\n\n        # Run experiment\n        run_experiment(input_features, output_features, dataset=rel_path, callbacks=[callback])\n\n    # Check that these methods were called at least once\n    callback.on_train_init.assert_called()\n    callback.on_train_start.assert_called()\n\n\nif __name__ == \"__main__\":\n    args = parser.parse_args()\n    run(args.csv_filename)\n"
  },
  {
    "path": "tests/integration_tests/synthetic_test_data.py",
    "content": "\"\"\"Utilities for producing synthetic test data that is convergence-friendly.\"\"\"\n\nfrom collections import namedtuple\n\nimport numpy as np\nimport pandas as pd\nfrom sklearn.model_selection import train_test_split\n\nRANDOM_SEED = 42\nNUMBER_OBSERVATIONS = 200\n\nGeneratedData = namedtuple(\"GeneratedData\", \"train_df validation_df test_df\")\n\n\ndef get_feature_configs():\n    input_features = [\n        {\"name\": \"x\", \"type\": \"number\"},\n    ]\n    output_features = [\n        {\n            \"name\": \"y\",\n            \"type\": \"number\",\n            \"loss\": {\"type\": \"mean_squared_error\"},\n            \"decoder\": {\n                \"num_fc_layers\": 2,\n                \"fc_output_size\": 64,\n            },\n        }\n    ]\n\n    return input_features, output_features\n\n\ndef get_generated_data():\n    # function generates simple training data that guarantee convergence\n    # within 30 epochs for suitable config\n\n    # generate data\n    np.random.seed(RANDOM_SEED)\n    x = np.array(range(NUMBER_OBSERVATIONS)).reshape(-1, 1)\n    y = 2 * x + 1 + np.random.normal(size=x.shape[0]).reshape(-1, 1)\n    raw_df = pd.DataFrame(np.concatenate((x, y), axis=1), columns=[\"x\", \"y\"])\n\n    # create training data\n    train, valid_test = train_test_split(raw_df, train_size=0.7)\n\n    # create validation and test data\n    validation, test = train_test_split(valid_test, train_size=0.5)\n\n    return GeneratedData(train, validation, test)\n\n\ndef get_generated_data_for_optimizer():\n    # function generates simple training data that guarantee convergence\n    # within 30 epochs for suitable config\n\n    # generate data\n    np.random.seed(RANDOM_SEED)\n    x = np.array(range(NUMBER_OBSERVATIONS)).reshape(-1, 1)\n    y = 2 * x + 1 + np.random.normal(size=x.shape[0]).reshape(-1, 1)\n    raw_df = pd.DataFrame(np.concatenate((x, y), axis=1), columns=[\"x\", \"y\"])\n    raw_df[\"x\"] = (raw_df[\"x\"] - raw_df[\"x\"].min()) / (raw_df[\"x\"].max() - raw_df[\"x\"].min())\n    raw_df[\"y\"] = (raw_df[\"y\"] - raw_df[\"y\"].min()) / (raw_df[\"y\"].max() - raw_df[\"y\"].min())\n\n    # create training data\n    train, valid_test = train_test_split(raw_df, train_size=0.7)\n\n    # create validation and test data\n    validation, test = train_test_split(valid_test, train_size=0.5)\n\n    return GeneratedData(train, validation, test)\n"
  },
  {
    "path": "tests/integration_tests/test_api.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport json\nimport os\nimport shutil\nfrom unittest import mock\n\nimport pandas as pd\nimport pytest\nimport torch\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\nfrom ludwig.models.inference import InferenceModule\nfrom ludwig.utils.data_utils import read_csv\nfrom tests.integration_tests.utils import (\n    category_feature,\n    generate_data,\n    get_weights,\n    image_feature,\n    run_api_experiment,\n    sequence_feature,\n    text_feature,\n)\n\n\ndef run_api_experiment_separated_datasets(input_features, output_features, data_csv):\n    \"\"\"Helper method to avoid code repetition in running an experiment.\n\n    :param input_features: input schema\n    :param output_features: output schema\n    :param data_csv: path to data\n    :return: None\n    \"\"\"\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config)\n\n    # Training with dataframe\n    data_df = read_csv(data_csv)\n    train_df = data_df.sample(frac=0.8)\n    test_df = data_df.drop(train_df.index).sample(frac=0.5)\n    validation_df = data_df.drop(train_df.index).drop(test_df.index)\n\n    basename, ext = os.path.splitext(data_csv)\n    train_fname = basename + \".train\" + ext\n    val_fname = basename + \".validation\" + ext\n    test_fname = basename + \".test\" + ext\n    output_dirs = []\n\n    try:\n        train_df.to_csv(train_fname)\n        validation_df.to_csv(val_fname)\n        test_df.to_csv(test_fname)\n\n        # Training with csv\n        _, _, output_dir = model.train(\n            training_set=train_fname,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, _, output_dir = model.train(\n            training_set=train_fname,\n            validation_set=val_fname,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, _, output_dir = model.train(\n            training_set=train_fname,\n            validation_set=val_fname,\n            test_set=test_fname,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, output_dir = model.predict(dataset=test_fname)\n        output_dirs.append(output_dir)\n\n    finally:\n        # Remove results/intermediate data saved to disk\n        os.remove(train_fname)\n        os.remove(val_fname)\n        os.remove(test_fname)\n        for output_dir in output_dirs:\n            shutil.rmtree(output_dir, ignore_errors=True)\n\n    output_dirs = []\n    try:\n        _, _, output_dir = model.train(\n            training_set=train_df,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, _, output_dir = model.train(\n            training_set=train_df,\n            validation_set=validation_df,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, _, output_dir = model.train(\n            training_set=train_df,\n            validation_set=validation_df,\n            test_set=test_df,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n        output_dirs.append(output_dir)\n\n        _, output_dir = model.predict(dataset=data_df)\n        output_dirs.append(output_dir)\n\n    finally:\n        for output_dir in output_dirs:\n            shutil.rmtree(output_dir, ignore_errors=True)\n\n\ndef test_api_intent_classification(csv_filename):\n    # Single sequence input, single category output\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    # Test representative encoders (embed=simple, rnn=recurrent, transformer=attention)\n    for encoder in [\"embed\", \"rnn\", \"transformer\"]:\n        input_features[0][ENCODER][TYPE] = encoder\n        run_api_experiment(input_features, output_features, data_csv=rel_path)\n\n\ndef test_api_intent_classification_separated(csv_filename):\n    # Single sequence input, single category output\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    # Test representative encoders (embed=simple, rnn=recurrent, transformer=attention)\n    for encoder in [\"embed\", \"rnn\", \"transformer\"]:\n        input_features[0][ENCODER][TYPE] = encoder\n        run_api_experiment_separated_datasets(input_features, output_features, data_csv=rel_path)\n\n\ndef test_api_train_online(csv_filename):\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(input_features, output_features, csv_filename)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n    model = LudwigModel(config)\n\n    for _ in range(2):\n        model.train_online(dataset=data_csv)\n    model.predict(dataset=data_csv)\n\n\ndef test_api_training_set(tmpdir):\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n    model = LudwigModel(config)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv)\n    model.predict(dataset=test_csv)\n\n    # Train again, this time the HDF5 cache will be used\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv)\n\n\ndef test_api_training_determinism(tmpdir):\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        \"trainer\": {BATCH_SIZE: 128},  # batch size must be fixed for determinism\n    }\n\n    # Train the model 3 times:\n    #\n    # 1. seed x\n    # 2. seed y\n    # 3. seed x\n    #\n    # Check that models (1) and (3) produce the same weights,\n    # but (1) and (2) do not\n    rand_x = 42\n    rand_y = 24\n\n    model_1 = LudwigModel(config)\n    model_1.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_x)\n\n    model_2 = LudwigModel(config)\n    model_2.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_y)\n\n    model_3 = LudwigModel(config)\n    model_3.train(dataset=data_csv, output_directory=tmpdir, random_seed=rand_x)\n\n    model_weights_1 = get_weights(model_1.model)\n    model_weights_2 = get_weights(model_2.model)\n    model_weights_3 = get_weights(model_3.model)\n\n    divergence = False\n    for weight_1, weight_2 in zip(model_weights_1, model_weights_2):\n        if not torch.allclose(weight_1, weight_2):\n            divergence = True\n            break\n    assert divergence, \"model_1 and model_2 have identical weights with different seeds!\"\n\n    for weight_1, weight_3 in zip(model_weights_1, model_weights_3):\n        assert torch.allclose(weight_1, weight_3)\n\n\ndef run_api_commands(\n    input_features,\n    output_features,\n    data_csv,\n    output_dir,\n    skip_save_training_description=False,\n    skip_save_training_statistics=False,\n    skip_save_model=False,\n    skip_save_progress=False,\n    skip_save_log=False,\n    skip_save_processed_input=False,\n    skip_save_unprocessed_output=False,\n    skip_save_predictions=False,\n    skip_save_eval_stats=False,\n    skip_collect_predictions=False,\n    skip_collect_overall_stats=False,\n):\n    \"\"\"Helper method to avoid code repetition in running an experiment.\n\n    :param input_features: input schema\n    :param output_features: output schema\n    :param data_csv: path to data\n    :return: None\n    \"\"\"\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config)\n\n    # Training with csv\n    model.train(\n        dataset=data_csv,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        output_directory=output_dir,\n    )\n    model.predict(\n        dataset=data_csv,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        output_directory=output_dir,\n    )\n    model.evaluate(\n        dataset=data_csv,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        collect_predictions=not skip_collect_predictions,\n        collect_overall_stats=not skip_collect_overall_stats,\n        output_directory=output_dir,\n    )\n    model.experiment(\n        dataset=data_csv,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        skip_collect_predictions=skip_collect_predictions,\n        skip_collect_overall_stats=skip_collect_overall_stats,\n        output_directory=output_dir,\n    )\n\n\n@pytest.mark.parametrize(\n    \"skip_save_training_description,skip_save_training_statistics,skip_save_model,\"\n    \"skip_save_progress,skip_save_log,skip_save_processed_input\",\n    [\n        (False, False, False, False, False, False),  # all saving enabled\n        (True, True, True, True, True, True),  # all saving disabled\n        (True, False, True, False, True, False),  # alternating pattern\n    ],\n    ids=[\"all_save\", \"all_skip\", \"mixed\"],\n)\ndef test_api_skip_parameters_train(\n    tmpdir,\n    csv_filename,\n    skip_save_training_description,\n    skip_save_training_statistics,\n    skip_save_model,\n    skip_save_progress,\n    skip_save_log,\n    skip_save_processed_input,\n):\n    # Single sequence input, single category output\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename))\n    run_api_commands(\n        input_features,\n        output_features,\n        data_csv=rel_path,\n        output_dir=tmpdir,\n        skip_save_training_description=skip_save_training_description,\n        skip_save_training_statistics=skip_save_training_statistics,\n        skip_save_model=skip_save_model,\n        skip_save_progress=skip_save_progress,\n        skip_save_log=skip_save_log,\n        skip_save_processed_input=skip_save_processed_input,\n    )\n\n\n@pytest.mark.parametrize(\"skip_save_unprocessed_output\", [False, True])\n@pytest.mark.parametrize(\"skip_save_predictions\", [False, True])\ndef test_api_skip_parameters_predict(\n    tmpdir,\n    csv_filename,\n    skip_save_unprocessed_output,\n    skip_save_predictions,\n):\n    # Single sequence input, single category output\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename))\n    run_api_commands(\n        input_features,\n        output_features,\n        data_csv=rel_path,\n        output_dir=tmpdir,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n    )\n\n\n@pytest.mark.parametrize(\n    \"skip_save_unprocessed_output,skip_save_predictions,skip_save_eval_stats,\"\n    \"skip_collect_predictions,skip_collect_overall_stats\",\n    [\n        (False, False, False, False, False),  # all saving enabled\n        (True, True, True, True, True),  # all saving disabled\n        (True, False, True, False, True),  # alternating pattern\n    ],\n    ids=[\"all_save\", \"all_skip\", \"mixed\"],\n)\ndef test_api_skip_parameters_evaluate(\n    tmpdir,\n    csv_filename,\n    skip_save_unprocessed_output,\n    skip_save_predictions,\n    skip_save_eval_stats,\n    skip_collect_predictions,\n    skip_collect_overall_stats,\n):\n    # Single sequence input, single category output\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename))\n    run_api_commands(\n        input_features,\n        output_features,\n        data_csv=rel_path,\n        output_dir=tmpdir,\n        skip_save_unprocessed_output=skip_save_unprocessed_output,\n        skip_save_predictions=skip_save_predictions,\n        skip_save_eval_stats=skip_save_eval_stats,\n        skip_collect_predictions=skip_collect_predictions,\n        skip_collect_overall_stats=skip_collect_overall_stats,\n    )\n\n\n@pytest.mark.parametrize(\n    \"epochs,batch_size,num_examples,steps_per_checkpoint\",\n    [\n        (1, 8, 16, 1),\n        (2, 4, 32, 2),\n        (2, 8, 16, 2),\n    ],\n    ids=[\"small\", \"large\", \"mixed\"],\n)\ndef test_api_callbacks(tmpdir, csv_filename, epochs, batch_size, num_examples, steps_per_checkpoint):\n    mock_callback = mock.Mock(wraps=Callback())\n\n    steps_per_epoch = num_examples / batch_size\n    total_checkpoints = (steps_per_epoch / steps_per_checkpoint) * epochs\n    total_batches = epochs * (num_examples / batch_size)\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            \"epochs\": epochs,\n            \"batch_size\": batch_size,\n            \"steps_per_checkpoint\": steps_per_checkpoint,\n            \"early_stop\": 0,  # Disable early stopping.\n        },\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n    )\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv)\n\n    assert mock_callback.on_epoch_start.call_count == epochs\n    assert mock_callback.on_epoch_end.call_count == epochs\n\n    assert mock_callback.should_early_stop.call_count == total_checkpoints\n\n    assert mock_callback.on_validation_start.call_count == total_checkpoints\n    assert mock_callback.on_validation_end.call_count == total_checkpoints\n\n    assert mock_callback.on_test_start.call_count == total_checkpoints\n    assert mock_callback.on_test_end.call_count == total_checkpoints\n\n    assert mock_callback.on_batch_start.call_count == total_batches\n    assert mock_callback.on_batch_end.call_count == total_batches\n\n    assert mock_callback.on_eval_end.call_count == total_checkpoints\n    assert mock_callback.on_eval_start.call_count == total_checkpoints\n\n\n@pytest.mark.parametrize(\n    \"epochs,batch_size,num_examples,checkpoints_per_epoch\",\n    [\n        (1, 8, 32, 1),\n        (2, 4, 64, 2),\n        (2, 8, 32, 4),\n    ],\n    ids=[\"single_checkpoint\", \"multi_checkpoint\", \"frequent_checkpoint\"],\n)\ndef test_api_callbacks_checkpoints_per_epoch(\n    tmpdir, csv_filename, epochs, batch_size, num_examples, checkpoints_per_epoch\n):\n    mock_callback = mock.Mock(wraps=Callback())\n\n    total_checkpoints = epochs * checkpoints_per_epoch\n    total_batches = epochs * (num_examples / batch_size)\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            \"epochs\": epochs,\n            \"batch_size\": batch_size,\n            \"checkpoints_per_epoch\": checkpoints_per_epoch,\n            \"early_stop\": 0,  # Disable early stopping.\n        },\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n    )\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv)\n\n    assert mock_callback.on_epoch_start.call_count == epochs\n    assert mock_callback.on_epoch_end.call_count == epochs\n\n    assert mock_callback.should_early_stop.call_count == total_checkpoints\n\n    assert mock_callback.on_validation_start.call_count == total_checkpoints\n    assert mock_callback.on_validation_end.call_count == total_checkpoints\n\n    assert mock_callback.on_test_start.call_count == total_checkpoints\n    assert mock_callback.on_test_end.call_count == total_checkpoints\n\n    assert mock_callback.on_batch_start.call_count == total_batches\n    assert mock_callback.on_batch_end.call_count == total_batches\n\n    assert mock_callback.on_eval_end.call_count == total_checkpoints\n    assert mock_callback.on_eval_start.call_count == total_checkpoints\n\n\ndef test_api_callbacks_default_train_steps(tmpdir, csv_filename):\n    # Default for train_steps is -1: use epochs.\n    train_steps = None\n    epochs = 3\n    batch_size = 8\n    num_examples = 20\n    mock_callback = mock.Mock(wraps=Callback())\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"train_steps\": train_steps, \"batch_size\": batch_size},\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n    model.train(\n        training_set=generate_data(\n            input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n        )\n    )\n\n    assert mock_callback.on_epoch_start.call_count == epochs\n\n\ndef test_api_callbacks_fixed_train_steps(tmpdir, csv_filename):\n    train_steps = 4\n    batch_size = 8\n    num_examples = 20\n    mock_callback = mock.Mock(wraps=Callback())\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"train_steps\": train_steps, \"batch_size\": batch_size},\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n    model.train(\n        training_set=generate_data(\n            input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n        )\n    )\n\n    # With 20 examples (14 train at 70% split), batch_size=8, steps_per_epoch=2.\n    # So 4 train steps => 2 epochs.\n    assert mock_callback.on_epoch_start.call_count == 2\n\n\ndef test_api_callbacks_fixed_train_steps_partial_epochs(tmpdir, csv_filename):\n    # If train_steps is set manually, epochs is ignored.\n    train_steps = 3\n    epochs = 2\n    batch_size = 8\n    num_examples = 20\n    mock_callback = mock.Mock(wraps=Callback())\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"train_steps\": train_steps, \"batch_size\": batch_size},\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n    model.train(\n        training_set=generate_data(\n            input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n        )\n    )\n\n    # With 20 examples, batch_size=8, steps_per_epoch=2. 3 train steps => 1 full epoch.\n    assert mock_callback.on_epoch_end.call_count == 1\n\n\ndef test_api_callbacks_batch_size_1(tmpdir, csv_filename):\n    epochs = 1\n    batch_size = 1\n    num_examples = 16\n    mock_callback = mock.Mock(wraps=Callback())\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"batch_size\": batch_size},\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n    model.train(\n        training_set=generate_data(\n            input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n        )\n    )\n\n    # There are exactly 1 epoch start, even with batch_size = 1.\n    assert mock_callback.on_epoch_start.call_count == 1\n    assert mock_callback.on_epoch_end.call_count == 1\n    assert mock_callback.on_batch_start.call_count == 16\n    assert mock_callback.on_batch_end.call_count == 16\n\n\ndef test_api_callbacks_fixed_train_steps_less_than_one_epoch(tmpdir, csv_filename):\n    # If train_steps is set manually, epochs is ignored.\n    # With 80 examples at 70% split = 56 train examples, batch_size=8 => 7 steps per epoch.\n    # train_steps=6 < 7, so less than one full epoch.\n    train_steps = total_batches = 6\n    steps_per_checkpoint = 2\n    batch_size = 8\n    num_examples = 80\n    mock_callback = mock.Mock(wraps=Callback())\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            \"train_steps\": train_steps,\n            \"steps_per_checkpoint\": steps_per_checkpoint,\n            \"batch_size\": batch_size,\n        },\n    }\n    model = LudwigModel(config, callbacks=[mock_callback])\n    model.train(\n        training_set=generate_data(\n            input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n        )\n    )\n\n    assert mock_callback.on_epoch_start.call_count == 1\n    assert mock_callback.on_epoch_end.call_count == 0\n    # The total number of batches is the number of train_steps\n    assert mock_callback.on_batch_end.call_count == total_batches\n    # The total number of evals is the number of times checkpoints are made\n    assert mock_callback.on_eval_end.call_count == train_steps // steps_per_checkpoint\n\n\ndef test_api_save_torchscript(tmpdir):\n    \"\"\"Tests successful saving and loading of model in TorchScript format.\"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [\n        category_feature(name=\"class\", decoder={\"vocab_size\": 5}, reduce_input=\"sum\", output_feature=True)\n    ]\n\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n    model = LudwigModel(config)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n    test_df = pd.read_csv(test_csv)\n    output_df_expected, _ = model.predict(test_df, return_type=pd.DataFrame)\n\n    save_path = os.path.join(tmpdir, \"torchscript\")\n    os.makedirs(save_path, exist_ok=True)\n    model.save_torchscript(save_path)\n    inference_module = InferenceModule.from_directory(save_path)\n    output_df, _ = inference_module.predict(test_df, return_type=pd.DataFrame)\n\n    for col in output_df.columns:\n        assert output_df[col].equals(output_df_expected[col])\n\n\ndef test_saved_weights_in_checkpoint(tmpdir):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        text_feature(),\n        image_feature(image_dest_folder),\n    ]\n    output_features = [category_feature(name=\"class\", output_feature=True)]\n\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {BATCH_SIZE: 128},\n    }\n    model = LudwigModel(config)\n    _, _, output_dir = model.train(\n        training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir\n    )\n\n    config_save_path = os.path.join(output_dir, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME)\n    with open(config_save_path) as f:\n        saved_config = json.load(f)\n    saved_input_features = saved_config[\"input_features\"]\n    for saved_input_feature in saved_input_features:\n        assert \"encoder\" in saved_input_feature\n        input_feature_encoder = saved_input_feature[\"encoder\"]\n        assert \"saved_weights_in_checkpoint\" in input_feature_encoder\n        assert input_feature_encoder[\"saved_weights_in_checkpoint\"]\n\n\ndef test_constant_metadata(tmpdir):\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [category_feature(name=\"class\", decoder={\"vocab_size\": 5}, output_feature=True)]\n\n    data_csv1 = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset1.csv\"))\n    val_csv1 = shutil.copyfile(data_csv1, os.path.join(tmpdir, \"validation1.csv\"))\n    test_csv1 = shutil.copyfile(data_csv1, os.path.join(tmpdir, \"test1.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n    model = LudwigModel(config)\n    model.train(training_set=data_csv1, validation_set=val_csv1, test_set=test_csv1, output_directory=tmpdir)\n    metadata1 = model.training_set_metadata\n\n    data_csv2 = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset2.csv\"), num_examples=10)\n    val_csv2 = shutil.copyfile(data_csv2, os.path.join(tmpdir, \"validation2.csv\"))\n    test_csv2 = shutil.copyfile(data_csv2, os.path.join(tmpdir, \"test2.csv\"))\n    model.train(training_set=data_csv2, validation_set=val_csv2, test_set=test_csv2, output_directory=tmpdir)\n    metadata2 = model.training_set_metadata\n\n    assert metadata1 == metadata2\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\n    \"input_max_sequence_length, global_max_sequence_length, expect_raise\",\n    [\n        (5, \"null\", True),\n        (\"null\", 5, True),\n        (5, 5, True),\n        (100, 100, False),\n        (100, \"null\", False),\n        (\"null\", \"null\", False),\n    ],\n)\ndef test_llm_template_too_long(tmpdir, input_max_sequence_length, global_max_sequence_length, expect_raise):\n    zero_shot_config = yaml.safe_load(f\"\"\"\n  model_type: llm\n  base_model: hf-internal-testing/tiny-random-GPTJForCausalLM\n\n  input_features:\n    - name: instruction\n      type: text\n      preprocessing:\n        max_sequence_length: {input_max_sequence_length}\n\n  output_features:\n    - name: output\n      type: text\n\n  preprocessing:\n    global_max_sequence_length: {global_max_sequence_length}\n  \"\"\")\n    zero_shot_config[\"prompt\"] = {}\n    zero_shot_config[\"prompt\"][\n        \"template\"\n    ] = \"This is a very long template that is longer than the max sequence length {instruction}\"\n\n    input_features = [text_feature(name=\"instruction\")]\n    output_features = [text_feature(name=\"output\", output_feature=True)]\n    data_csv1 = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset1.csv\"))\n    model = LudwigModel(zero_shot_config)\n\n    if expect_raise:\n        with pytest.raises(RuntimeError):\n            model.preprocess(dataset=data_csv1, output_directory=tmpdir)\n    else:\n        model.preprocess(dataset=data_csv1, output_directory=tmpdir)\n"
  },
  {
    "path": "tests/integration_tests/test_audio_feature.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.features.audio_feature import AudioInputFeature\nfrom ludwig.schema.features.audio_feature import AudioInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom tests.integration_tests.utils import audio_feature\n\nBATCH_SIZE = 2\nSEQ_SIZE = 20\nAUDIO_W_SIZE = 16\nDEFAULT_OUTPUT_SIZE = 256\n\n\n@pytest.mark.parametrize(\"enc_encoder\", [\"stacked_cnn\", \"rnn\"])\ndef test_audio_feature(enc_encoder):\n    # synthetic audio tensor\n    audio_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE], dtype=torch.float32)\n\n    # generate audio feature config\n    audio_feature_config = audio_feature(\n        folder=\".\", encoder={\"type\": enc_encoder, \"max_sequence_length\": SEQ_SIZE, \"embedding_size\": AUDIO_W_SIZE}\n    )\n\n    # instantiate audio input feature object\n    audio_feature_config, _ = load_config_with_kwargs(AudioInputFeatureConfig, audio_feature_config)\n    audio_input_feature = AudioInputFeature(audio_feature_config)\n\n    # pass synthetic audio tensor through the audio input feature\n    encoder_output = audio_input_feature(audio_tensor)\n\n    # confirm correctness of the the audio encoder output\n    assert isinstance(encoder_output, dict)\n    assert ENCODER_OUTPUT in encoder_output\n    assert isinstance(encoder_output[ENCODER_OUTPUT], torch.Tensor)\n    if enc_encoder == \"passthrough\":\n        assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE)\n    else:\n        assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, DEFAULT_OUTPUT_SIZE)\n"
  },
  {
    "path": "tests/integration_tests/test_automl.py",
    "content": "import os\nimport tempfile\nfrom unittest import mock\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import COLUMN, ENCODER, INPUT_FEATURES, NAME, OUTPUT_FEATURES, PREPROCESSING, SPLIT, TYPE\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom ludwig.types import FeatureConfigDict, ModelConfigDict\nfrom ludwig.utils.misc_utils import merge_dict\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    image_feature,\n    minio_test_creds,\n    number_feature,\n    private_param,\n    remote_tmpdir,\n    text_feature,\n)\n\nray = pytest.importorskip(\"ray\")\n\nimport dask.dataframe as dd  # noqa E402\nfrom ray.tune.experiment.trial import Trial  # noqa E402\n\nfrom ludwig.automl import auto_train, create_auto_config, train_with_config  # noqa E402\nfrom ludwig.automl.automl import OUTPUT_DIR  # noqa E402\nfrom ludwig.hyperopt.execution import RayTuneExecutor  # noqa E402\n\npytestmark = [pytest.mark.distributed, pytest.mark.integration_tests_c]\n\n\ndef to_name_set(features: list[FeatureConfigDict]) -> set[str]:\n    \"\"\"Returns the list of feature names.\"\"\"\n    return {feature[NAME] for feature in features}\n\n\ndef merge_lists(a_features: list, b_features: list):\n    for idx in range(max(len(a_features), len(b_features))):\n        if idx >= len(a_features):\n            a_features.append(b_features[idx])\n        elif idx < len(b_features):\n            a_features[idx] = merge_dict(a_features[idx], b_features[idx])\n\n\ndef merge_dict_with_features(a: ModelConfigDict, b: ModelConfigDict) -> ModelConfigDict:\n    merge_lists(a[INPUT_FEATURES], b.get(INPUT_FEATURES, []))\n    merge_lists(a[OUTPUT_FEATURES], b.get(OUTPUT_FEATURES, []))\n\n    b = b.copy()\n    if INPUT_FEATURES in b:\n        del b[INPUT_FEATURES]\n    if OUTPUT_FEATURES in b:\n        del b[OUTPUT_FEATURES]\n\n    return merge_dict(a, b)\n\n\ndef check_types(\n    config: ModelConfigDict, input_features: list[FeatureConfigDict], output_features: list[FeatureConfigDict]\n):\n    actual_features = config.get(INPUT_FEATURES, []) + config.get(OUTPUT_FEATURES, [])\n    expected_features = {f[NAME]: f for f in input_features + output_features}\n    assert len(actual_features) == len(expected_features)\n    for actual_feature in actual_features:\n        expected_feature = expected_features[actual_feature[NAME]]\n        assert (\n            actual_feature[TYPE] == expected_feature[TYPE]\n        ), f\"{actual_feature[NAME]}: actual type {actual_feature[TYPE]} != {expected_feature[TYPE]}\"\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_tabular_large():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        input_features = [\n            number_feature(),\n            number_feature(),\n            category_feature(encoder={\"vocab_size\": 3}),\n            category_feature(encoder={\"vocab_size\": 3}),\n        ]\n        output_features = [category_feature(decoder={\"vocab_size\": 3})]\n        dataset_csv = generate_data(\n            input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=50\n        )\n        yield input_features, output_features, dataset_csv\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_tabular_small():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        input_features = [\n            number_feature(),\n            category_feature(encoder={\"vocab_size\": 3}),\n        ]\n        output_features = [category_feature(decoder={\"vocab_size\": 3})]\n        dataset_csv = generate_data(\n            input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=50\n        )\n        yield input_features, output_features, dataset_csv\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_image():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n        input_features = [\n            image_feature(folder=image_dest_folder),\n        ]\n        output_features = [binary_feature()]\n        dataset_csv = generate_data(\n            input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=20\n        )\n        yield input_features, output_features, dataset_csv\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_text():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        input_features = [\n            text_feature(preprocessing={\"tokenizer\": \"space\"}),\n        ]\n        output_features = [binary_feature()]\n        dataset_csv = generate_data(\n            input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=20\n        )\n        yield input_features, output_features, dataset_csv\n\n\n@pytest.fixture(scope=\"module\")\ndef test_data_multimodal():\n    with tempfile.TemporaryDirectory() as tmpdir:\n        image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n        input_features = [\n            image_feature(folder=image_dest_folder),\n            text_feature(preprocessing={\"tokenizer\": \"space\"}),\n            number_feature(),\n            category_feature(encoder={\"vocab_size\": 3}),\n            category_feature(encoder={\"vocab_size\": 5}),\n        ]\n        output_features = [binary_feature()]\n        dataset_csv = generate_data(\n            input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=20\n        )\n        yield input_features, output_features, dataset_csv\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"test_data,expectations\",\n    [\n        (\"test_data_tabular_large\", {\"combiner\": {\"type\": \"tabnet\"}}),\n        (\"test_data_tabular_small\", {\"combiner\": {\"type\": \"concat\"}}),\n        (\"test_data_image\", {\"combiner\": {\"type\": \"concat\"}}),\n        (\n            \"test_data_text\",\n            {\n                \"input_features\": [{\"type\": \"text\", \"encoder\": {\"type\": \"bert\"}}],\n                \"combiner\": {\"type\": \"concat\"},\n                \"trainer\": {\n                    \"batch_size\": \"auto\",\n                    \"learning_rate\": 1e-05,\n                    \"epochs\": 10,\n                    \"optimizer\": {\"type\": \"adamw\"},\n                    \"learning_rate_scheduler\": {\"warmup_fraction\": 0.1},\n                    \"use_mixed_precision\": True,\n                },\n                \"defaults\": {\n                    \"text\": {\n                        \"encoder\": {\n                            \"type\": \"bert\",\n                            \"trainable\": True,\n                        }\n                    }\n                },\n            },\n        ),\n        (\n            \"test_data_multimodal\",\n            {\n                \"input_features\": [{\"type\": \"image\"}, {\"type\": \"text\", \"encoder\": {\"type\": \"embed\"}}],\n                \"combiner\": {\"type\": \"concat\"},\n            },\n        ),\n    ],\n    ids=[\"tabular_large\", \"tabular_small\", \"image\", \"text\", \"multimodal\"],\n)\ndef test_create_auto_config(test_data, expectations, ray_cluster_2cpu, request):\n    test_data = request.getfixturevalue(test_data)\n    input_features, output_features, dataset_csv = test_data\n    targets = [feature[NAME] for feature in output_features]\n    df = dd.read_csv(dataset_csv)\n    config = create_auto_config(df, targets, time_limit_s=600, backend=\"ray\")\n\n    # Ensure our configs are using the latest Ludwig schema\n    ModelConfig.from_dict(config)\n\n    assert to_name_set(config[INPUT_FEATURES]) == to_name_set(input_features)\n    assert to_name_set(config[OUTPUT_FEATURES]) == to_name_set(output_features)\n    check_types(config, input_features, output_features)\n\n    expected = merge_dict_with_features(config, expectations)\n    assert config == expected\n\n\ndef _get_sample_df(class_probs):\n    nrows = 1000\n    thresholds = np.cumsum((class_probs * nrows).astype(int))\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n\n    def get_category(v):\n        if v < thresholds[0]:\n            return 0\n        if thresholds[0] <= v < thresholds[1]:\n            return 1\n        return 2\n\n    df[\"category\"] = df.index.map(get_category).astype(np.int8)\n    return df\n\n\n@pytest.mark.distributed\ndef test_autoconfig_preprocessing_balanced():\n    df = _get_sample_df(np.array([0.33, 0.33, 0.34]))\n\n    config = create_auto_config(dataset=df, target=\"category\", time_limit_s=1)\n\n    # Ensure our configs are using the latest Ludwig schema\n    ModelConfig.from_dict(config)\n\n    assert PREPROCESSING not in config\n\n\n@pytest.mark.distributed\ndef test_autoconfig_preprocessing_imbalanced():\n    df = _get_sample_df(np.array([0.6, 0.2, 0.2]))\n\n    config = create_auto_config(dataset=df, target=\"category\", time_limit_s=1)\n\n    # Ensure our configs are using the latest Ludwig schema\n    ModelConfig.from_dict(config)\n\n    assert PREPROCESSING in config\n    assert SPLIT in config[PREPROCESSING]\n    assert config[PREPROCESSING][SPLIT] == {TYPE: \"stratify\", COLUMN: \"category\"}\n\n\n@pytest.mark.distributed\ndef test_autoconfig_preprocessing_text_image(tmpdir):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    input_features = [text_feature(preprocessing={\"tokenizer\": \"space\"}), image_feature(folder=image_dest_folder)]\n    output_features = [category_feature(output_feature=True)]\n\n    # Generate Dataset\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n    df = pd.read_csv(rel_path)\n    target = df.columns[-1]\n\n    config = create_auto_config(dataset=df, target=target, time_limit_s=1)\n\n    # Ensure our configs are using the latest Ludwig schema\n    ModelConfig.from_dict(config)\n\n    # Check no features shuffled around\n    assert len(input_features) == 2\n    assert len(output_features) == 1\n\n    # Check encoders are properly nested\n    assert isinstance(config[INPUT_FEATURES][0][ENCODER], dict)\n    assert isinstance(config[INPUT_FEATURES][1][ENCODER], dict)\n\n    # Check automl default encoders are properly set\n    assert config[INPUT_FEATURES][0][ENCODER][TYPE] == \"bert\"\n    assert config[INPUT_FEATURES][1][ENCODER][TYPE] == \"stacked_cnn\"\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"time_budget\", [120, 1], ids=[\"high\", \"low\"])\ndef test_train_with_config(time_budget, test_data_tabular_large, ray_cluster_2cpu, tmpdir):\n    _run_train_with_config(time_budget, test_data_tabular_large, tmpdir)\n\n\n@pytest.mark.distributed\ndef test_auto_train(test_data_tabular_large, ray_cluster_2cpu, tmpdir):\n    _, ofeatures, dataset_csv = test_data_tabular_large\n    local_output_directory_path: str = f\"{str(tmpdir)}/{OUTPUT_DIR}\"\n    results = auto_train(\n        dataset=dataset_csv,\n        target=ofeatures[0][NAME],\n        time_limit_s=120,\n        output_directory=local_output_directory_path,\n        user_config={\"hyperopt\": {\"executor\": {\"num_samples\": 2}}},\n    )\n\n    analysis = results.experiment_analysis\n    for trial in analysis.trials:\n        assert trial.status != Trial.ERROR, f\"Error in trial {trial}\"\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\"fs_protocol,bucket\", [private_param((\"s3\", \"ludwig-tests\"))], ids=[\"s3\"])\ndef test_train_with_config_remote(fs_protocol, bucket, test_data_tabular_large, ray_cluster_2cpu):\n    backend = {\n        \"type\": \"local\",\n        \"credentials\": {\n            \"artifacts\": minio_test_creds(),\n        },\n    }\n\n    with remote_tmpdir(fs_protocol, bucket) as tmpdir:\n        _run_train_with_config(200, test_data_tabular_large, tmpdir, backend=backend)\n\n\ndef _run_train_with_config(time_budget, test_data, tmpdir, **kwargs):\n    input_features, output_features, dataset_csv = test_data\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"trainer\": {\"epochs\": 2},\n        \"hyperopt\": {\n            \"search_alg\": {\n                \"type\": \"variant_generator\",\n                \"random_state\": 42,\n            },\n            \"executor\": {\n                \"type\": \"ray\",\n                \"time_budget_s\": time_budget,\n                \"cpu_resources_per_trial\": 1,\n                \"num_samples\": 2,\n                \"scheduler\": {\n                    \"type\": \"async_hyperband\",\n                    \"max_t\": time_budget,\n                    \"time_attr\": \"time_total_s\",\n                    \"grace_period\": min(72, time_budget),\n                    \"reduction_factor\": 5,\n                },\n            },\n            \"parameters\": {\n                \"trainer.batch_size\": {\n                    \"space\": \"choice\",\n                    \"categories\": [64, 128, 256],\n                },\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.001,\n                    \"upper\": 0.1,\n                },\n            },\n        },\n    }\n\n    fn = RayTuneExecutor._evaluate_best_model\n    with mock.patch(\"ludwig.hyperopt.execution.RayTuneExecutor._evaluate_best_model\") as mock_fn:\n        # We need to check that _evaluate_best_model is called when the time_budget is low\n        # as this code path should be triggered when the trial was early stopped\n        mock_fn.side_effect = fn\n\n        outdir = os.path.join(tmpdir, \"output\")\n        results = train_with_config(dataset_csv, config, output_directory=outdir, **kwargs)\n        try:\n            best_model = results.best_model\n        except ValueError:\n            # ValueError is raised when best_model can't be found. This typically\n            # happens when the time_budget is low and the trial is stopped early,\n            # resulting in no evaluations happening (and no scores being reported back to RayTune).\n            # So RayTune has no way of determining what the best model is.\n            best_model = None\n\n        if time_budget > 1:\n            assert isinstance(best_model, LudwigModel)\n            assert best_model.config_obj.trainer.early_stop == -1\n            # assert mock_fn.call_count == 1\n        else:\n            assert best_model is None\n            assert mock_fn.call_count == 0\n"
  },
  {
    "path": "tests/integration_tests/test_cache_manager.py",
    "content": "import os\nfrom pathlib import Path\n\nimport pandas as pd\nimport pytest\n\nfrom ludwig.constants import CHECKSUM, META, TEST, TRAINING, VALIDATION\nfrom ludwig.data.cache.manager import alphanum, CacheManager\nfrom ludwig.data.cache.types import CacheableDataframe, wrap\nfrom ludwig.data.dataset.pandas import PandasDatasetManager\nfrom ludwig.globals import TRAINING_PREPROC_FILE_NAME\nfrom tests.integration_tests.utils import category_feature, LocalTestBackend, sequence_feature\n\n\n@pytest.fixture\ndef change_test_dir(tmpdir, monkeypatch):\n    monkeypatch.chdir(tmpdir)\n\n\n@pytest.mark.parametrize(\"use_df\", [True, False], ids=[\"df\", \"filename\"])\n@pytest.mark.parametrize(\"use_split\", [True, False], ids=[\"split\", \"no_split\"])\n@pytest.mark.parametrize(\"use_cache_dir\", [True, False], ids=[\"cache_dir\", \"no_cache_dir\"])\ndef test_cache_dataset(use_cache_dir, use_split, use_df, tmpdir, change_test_dir):\n    dataset_manager = PandasDatasetManager(backend=LocalTestBackend())\n    cache_dir = os.path.join(tmpdir, \"cache\") if use_cache_dir else None\n    manager = CacheManager(dataset_manager, cache_dir=cache_dir)\n\n    config = {\n        \"input_features\": [sequence_feature(encoder={\"reduce_output\": \"sum\"})],\n        \"output_features\": [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")],\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        \"preprocessing\": {},\n    }\n\n    def touch(basename):\n        path = os.path.join(tmpdir, f\"{basename}.csv\")\n        Path(path).touch()\n        return path\n\n    def create_dataset(name):\n        if use_df:\n            return CacheableDataframe(df=pd.DataFrame(), name=name, checksum=name)\n        else:\n            return wrap(touch(name))\n\n    dataset = training_set = test_set = validation_set = None\n    if not use_split:\n        dataset = create_dataset(\"dataset\")\n        cache_key = manager.get_cache_key(dataset, config)\n    else:\n        training_set = create_dataset(\"train\")\n        test_set = create_dataset(\"test\")\n        validation_set = create_dataset(\"validation\")\n        cache_key = manager.get_cache_key(training_set, config)\n\n    training_set_metadata = {\n        CHECKSUM: cache_key,\n    }\n\n    cache = manager.get_dataset_cache(config, dataset, training_set, test_set, validation_set)\n    cache_map = cache.cache_map\n    assert len(cache_map) == 4\n\n    train_path = os.path.join(cache_dir, alphanum(cache_key)) if use_cache_dir else os.path.join(tmpdir, \"dataset\")\n    test_path = val_path = train_path\n\n    if use_split and not use_cache_dir:\n        train_path = os.path.join(tmpdir, \"train\")\n        test_path = os.path.join(tmpdir, \"test\")\n        val_path = os.path.join(tmpdir, \"validation\")\n\n    assert cache_map[META] == f\"{train_path}.meta.json\"\n    assert cache_map[TRAINING] == f\"{train_path}.{TRAINING_PREPROC_FILE_NAME}\"\n    assert cache_map[TEST] == f\"{test_path}.test.hdf5\"\n    assert cache_map[VALIDATION] == f\"{val_path}.validation.hdf5\"\n\n    for cache_path in cache_map.values():\n        assert not os.path.exists(cache_path)\n\n    training_set = pd.DataFrame()\n    test_set = pd.DataFrame()\n    validation_set = pd.DataFrame()\n\n    if use_cache_dir:\n        os.makedirs(cache_dir)\n    cache.put(training_set, test_set, validation_set, training_set_metadata)\n\n    for cache_path in cache_map.values():\n        assert os.path.exists(cache_path)\n\n    cache.delete()\n\n    for cache_path in cache_map.values():\n        assert not os.path.exists(cache_path)\n"
  },
  {
    "path": "tests/integration_tests/test_cached_preprocessing.py",
    "content": "import os\n\nimport numpy as np\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import MODEL_ECD, PREPROCESSING, PROC_COLUMN, TRAINER\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    number_feature,\n    run_test_suite,\n    text_feature,\n)\n\n\ndef _onehot_encoding_config(tmpdir):\n    input_features = [\n        number_feature(),\n        category_feature(encoder={\"type\": \"onehot\"}),\n    ]\n    output_features = [binary_feature()]\n\n    data_csv_path = os.path.join(tmpdir, \"dataset.csv\")\n    dataset = generate_data(input_features, output_features, data_csv_path)\n    config = {\"input_features\": input_features, \"output_features\": output_features, TRAINER: {\"train_steps\": 1}}\n    return config, dataset\n\n\ndef test_onehot_encoding(tmpdir):\n    config, dataset = _onehot_encoding_config(tmpdir)\n    run_test_suite(config, dataset, \"local\")\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_onehot_encoding_ray(tmpdir, ray_cluster_2cpu):\n    config, dataset = _onehot_encoding_config(tmpdir)\n    run_test_suite(config, dataset, \"ray\")\n\n\ndef _hf_text_embedding_config(tmpdir):\n    input_features = [\n        number_feature(),\n        text_feature(\n            encoder={\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-bert-for-token-classification\",\n            },\n            preprocessing={\"cache_encoder_embeddings\": True},\n        ),\n    ]\n    output_features = [binary_feature()]\n\n    data_csv_path = os.path.join(tmpdir, \"dataset.csv\")\n    dataset = generate_data(input_features, output_features, data_csv_path)\n    config = {\"input_features\": input_features, \"output_features\": output_features, TRAINER: {\"train_steps\": 1}}\n    return config, dataset\n\n\ndef test_hf_text_embedding(tmpdir):\n    config, dataset = _hf_text_embedding_config(tmpdir)\n    run_test_suite(config, dataset, \"local\")\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_hf_text_embedding_ray(tmpdir, ray_cluster_2cpu):\n    config, dataset = _hf_text_embedding_config(tmpdir)\n    run_test_suite(config, dataset, \"ray\")\n\n\n@pytest.mark.parametrize(\"cache_encoder_embeddings\", [True, False, None])\ndef test_onehot_encoding_preprocessing(cache_encoder_embeddings, tmpdir):\n    vocab_size = 5\n    input_features = [\n        category_feature(encoder={\"type\": \"onehot\", \"vocab_size\": vocab_size}),\n        number_feature(),\n    ]\n    output_features = [binary_feature()]\n\n    if cache_encoder_embeddings is not None:\n        if PREPROCESSING not in input_features[0]:\n            input_features[0][PREPROCESSING] = {}\n        input_features[0][PREPROCESSING][\"cache_encoder_embeddings\"] = cache_encoder_embeddings\n\n    # Need sufficiently high number of examples to ensure at least one of each category type appears\n    data_csv_path = os.path.join(tmpdir, \"dataset.csv\")\n    num_examples = 100\n    dataset_fp = generate_data(input_features, output_features, data_csv_path, num_examples)\n    config = {\n        \"model_type\": MODEL_ECD,\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    # Run preprocessing\n    ludwig_model = LudwigModel(config, backend=\"local\")\n    proc_dataset = ludwig_model.preprocess(training_set=dataset_fp)\n\n    # Check preprocessed output\n    proc_df = ludwig_model.backend.df_engine.compute(proc_dataset.training_set.to_df())\n    proc_col = input_features[0][PROC_COLUMN]\n    proc_series = proc_df[proc_col]\n\n    # ECD will not cache embeddings by default, but will if set to `cache_encoder_embeddings=true`\n    expected_cache_encoder_embeddings = cache_encoder_embeddings or False\n    if expected_cache_encoder_embeddings:\n        assert proc_series.values.dtype == \"object\"\n        data = np.stack(proc_series.values)\n        assert data.shape == (num_examples, vocab_size)\n\n        # Only one element in each row should be 1\n        assert all(x == 1 for x in data.sum(axis=1))\n    else:\n        assert proc_series.values.dtype == \"int8\"\n        data = proc_series.to_numpy()\n        assert data.shape == (num_examples,)\n\n\ndef test_hf_text_embedding_tied(tmpdir):\n    input_features = [\n        text_feature(\n            encoder={\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-bert-for-token-classification\",\n            },\n            preprocessing={\"cache_encoder_embeddings\": True},\n        ),\n        text_feature(\n            encoder={\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-bert-for-token-classification\",\n            },\n            preprocessing={\"cache_encoder_embeddings\": True},\n        ),\n    ]\n    input_features[1][\"tied\"] = input_features[0][\"name\"]\n    output_features = [binary_feature()]\n\n    data_csv_path = os.path.join(tmpdir, \"dataset.csv\")\n    dataset = generate_data(input_features, output_features, data_csv_path)\n\n    config = {\"input_features\": input_features, \"output_features\": output_features, TRAINER: {\"epochs\": 1}}\n    run_test_suite(config, dataset, \"local\")\n"
  },
  {
    "path": "tests/integration_tests/test_carton.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport asyncio\nimport os\nimport platform\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, NAME, PREDICTIONS, TRAINER\nfrom ludwig.utils.carton_utils import export_carton\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n)\n\n\n@pytest.mark.skipif(platform.system() == \"Windows\", reason=\"Carton is not supported on Windows\")\ndef test_carton_torchscript(csv_filename, tmpdir):\n    pytest.importorskip(\"cartonml\", reason=\"cartonml-nightly not installed\")\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    # Configure features to be tested:\n    bin_str_feature = binary_feature()\n    input_features = [\n        bin_str_feature,\n        # binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        # TODO: future support\n        # sequence_feature(vocab_size=3),\n        # text_feature(vocab_size=3),\n        # vector_feature(),\n        # image_feature(image_dest_folder),\n        # audio_feature(audio_dest_folder),\n        # timeseries_feature(),\n        # date_feature(),\n        # h3_feature(),\n        # set_feature(vocab_size=3),\n        # bag_feature(vocab_size=3),\n    ]\n    output_features = [\n        bin_str_feature,\n        # binary_feature(),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 3}, output_feature=True),\n        # TODO: future support\n        # sequence_feature(vocab_size=3),\n        # text_feature(vocab_size=3),\n        # set_feature(vocab_size=3),\n        # vector_feature()\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    # Convert bool values to strings, e.g., {'Yes', 'No'}\n    df = pd.read_csv(training_data_csv_path)\n    false_value, true_value = \"No\", \"Yes\"\n    df[bin_str_feature[NAME]] = df[bin_str_feature[NAME]].map(lambda x: true_value if x else false_value)\n    df.to_csv(training_data_csv_path)\n\n    # Train Ludwig (Pythonic) model:\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.train(\n        dataset=training_data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    # Obtain predictions from Python model\n    preds_dict, _ = ludwig_model.predict(dataset=training_data_csv_path, return_type=dict)\n\n    # Create graph inference model (Torchscript) from trained Ludwig model.\n    carton_path = os.path.join(tmpdir, \"carton\")\n    export_carton(ludwig_model, carton_path)\n\n    import cartonml as carton\n\n    # Load the carton model\n    # See https://pyo3.rs/v0.20.0/ecosystem/async-await#a-note-about-asynciorun for why we wrap it\n    # in another function\n    async def load():\n        return await carton.load(carton_path)\n\n    carton_model = asyncio.run(load())\n\n    def to_input(s: pd.Series) -> list[str] | torch.Tensor:\n        if s.dtype == \"object\":\n            return np.array(s.to_list())\n        return s.to_numpy().astype(np.float32)\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = {name: to_input(df[feature.column]) for name, feature in ludwig_model.model.input_features.items()}\n\n    # See https://pyo3.rs/v0.20.0/ecosystem/async-await#a-note-about-asynciorun for why we wrap it\n    # in another function\n    async def infer(inputs):\n        return await carton_model.infer(inputs)\n\n    outputs = asyncio.run(infer(inputs))\n\n    # Compare results from Python trained model against Carton\n    assert len(preds_dict) == len(outputs)\n    for feature_name, feature_outputs_expected in preds_dict.items():\n        assert feature_name in outputs\n\n        output_values_expected = feature_outputs_expected[PREDICTIONS]\n        output_values = outputs[feature_name]\n        if output_values.dtype.type in {np.string_, np.str_}:\n            # Strings should match exactly\n            assert np.all(output_values == output_values_expected), f\"feature: {feature_name}, output: predictions\"\n        else:\n            assert np.allclose(output_values, output_values_expected), f\"feature: {feature_name}, output: predictions\"\n"
  },
  {
    "path": "tests/integration_tests/test_class_imbalance_feature.py",
    "content": "import contextlib\nimport os\nimport shutil\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import LocalBackend\nfrom tests.integration_tests.utils import create_data_set_to_use, RAY_BACKEND_CONFIG, spawn\n\ntry:\n    import ray\n\n    from ludwig.backend.ray import RayBackend\nexcept ImportError:\n    ray = None\n\nrs = np.random.RandomState(42)\n\n\n@contextlib.contextmanager\ndef ray_start(num_cpus=2, num_gpus=None):\n    res = ray.init(\n        num_cpus=num_cpus,\n        num_gpus=num_gpus,\n        include_dashboard=False,\n        object_store_memory=150 * 1024 * 1024,\n    )\n    try:\n        yield res\n    finally:\n        ray.shutdown()\n        # Delete the cluster address just in case.\n        if hasattr(ray._private.utils, \"reset_ray_address\"):\n            ray._private.utils.reset_ray_address()\n\n\n@spawn\ndef run_test_imbalance_ray(\n    tmpdir,\n    input_df,\n    config,\n    balance,\n    num_cpus=2,\n    num_gpus=None,\n):\n    with ray_start(num_cpus=num_cpus, num_gpus=num_gpus):\n        csv_filename = os.path.join(tmpdir, \"dataset.csv\")\n        input_df.to_csv(csv_filename)\n        dataset_parquet = create_data_set_to_use(\"parquet\", csv_filename)\n\n        model = LudwigModel(config, backend=RAY_BACKEND_CONFIG, callbacks=None)\n        output_dir = None\n\n        try:\n            _, output_dataset, output_dir = model.train(\n                dataset=dataset_parquet,\n                training_set=None,\n                validation_set=None,\n                test_set=None,\n                skip_save_processed_input=True,\n                skip_save_progress=True,\n                skip_save_unprocessed_output=True,\n                skip_save_log=True,\n            )\n        finally:\n            # Remove results/intermediate data saved to disk\n            shutil.rmtree(output_dir, ignore_errors=True)\n\n        input_train_set = input_df.sample(frac=0.7, replace=False)\n        processed_len = output_dataset[0].ds.count()\n        processed_target_pos = output_dataset[0].ds.sum(on=\"Label_mZFLky\")\n        processed_target_neg = output_dataset[0].ds.count() - output_dataset[0].ds.sum(on=\"Label_mZFLky\")\n        assert len(input_train_set) == 140\n        assert 0.05 <= len(input_train_set[input_train_set[\"Label\"] == 1]) / len(input_train_set) <= 0.15\n        assert round(processed_target_pos / processed_target_neg, 1) == 0.5\n        assert model.backend.df_engine.parallelism == RAY_BACKEND_CONFIG[\"processor\"][\"parallelism\"]\n        assert isinstance(model.backend, RayBackend)\n\n        if balance == \"oversample_minority\":\n            assert len(input_train_set) < processed_len\n\n        if balance == \"undersample_majority\":\n            assert len(input_train_set) > processed_len\n\n\ndef run_test_imbalance_local(\n    input_df,\n    config,\n    balance,\n):\n    model = LudwigModel(config)\n    _, output_dataset, output_dir = model.train(\n        input_df,\n        skip_save_model=True,\n        skip_save_log=True,\n        skip_save_progress=True,\n        skip_save_processed_input=True,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n    )\n\n    input_train_set = input_df.sample(frac=0.7, replace=False)\n    processed_len = output_dataset[0].size\n    processed_target_pos = sum(output_dataset[0].dataset[\"Label_2Xl8CP\"])\n    processed_target_neg = len(output_dataset[0].dataset[\"Label_2Xl8CP\"]) - processed_target_pos\n    assert len(input_train_set) == 140\n    assert 0.05 <= len(input_train_set[input_train_set[\"Label\"] == 1]) / len(input_train_set) <= 0.15\n    assert round(processed_target_pos / processed_target_neg, 1) == 0.5\n    assert isinstance(model.backend, LocalBackend)\n\n    if balance == \"oversample_minority\":\n        assert len(input_train_set) < processed_len\n        assert 55 <= processed_target_pos <= 75\n        assert 110 <= processed_target_neg <= 150\n\n    if balance == \"undersample_majority\":\n        assert len(input_train_set) > processed_len\n        assert 7 <= processed_target_pos <= 20\n        assert 14 <= processed_target_neg <= 40\n\n\n@pytest.mark.parametrize(\n    \"balance\",\n    [\"oversample_minority\", \"undersample_majority\"],\n)\n@pytest.mark.distributed\n@pytest.mark.skip(reason=\"Flaky\")\ndef test_imbalance_ray(balance):\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"column\": \"Index\", \"type\": \"numerical\"},\n            {\"name\": \"random_1\", \"column\": \"random_1\", \"type\": \"numerical\"},\n            {\"name\": \"random_2\", \"column\": \"random_2\", \"type\": \"numerical\"},\n        ],\n        \"output_features\": [{\"name\": \"Label\", \"column\": \"Label\", \"type\": \"binary\"}],\n        \"trainer\": {\"epochs\": 2, \"batch_size\": 8},\n        \"preprocessing\": {},\n    }\n    split_col = np.concatenate((np.zeros(140), np.ones(20), np.full(40, 2)))\n    rs.shuffle(split_col)\n    df = pd.DataFrame(\n        {\n            \"Index\": np.arange(0, 200, 1),\n            \"random_1\": np.random.randint(0, 50, 200),\n            \"random_2\": np.random.choice([\"Type A\", \"Type B\", \"Type C\", \"Type D\"], 200),\n            \"Label\": np.concatenate((np.zeros(180), np.ones(20))),\n            \"split\": split_col,\n        }\n    )\n\n    config[\"preprocessing\"][balance] = 0.5\n    run_test_imbalance_ray(df, config, balance)\n\n\n@pytest.mark.parametrize(\n    \"balance\",\n    [\"oversample_minority\", \"undersample_majority\"],\n)\ndef test_imbalance_local(balance):\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"column\": \"Index\", \"type\": \"number\"},\n            {\"name\": \"random_1\", \"column\": \"random_1\", \"type\": \"number\"},\n            {\"name\": \"random_2\", \"column\": \"random_2\", \"type\": \"category\"},\n        ],\n        \"output_features\": [{\"name\": \"Label\", \"column\": \"Label\", \"type\": \"binary\"}],\n        \"trainer\": {\"epochs\": 2, \"batch_size\": 8},\n        \"preprocessing\": {},\n    }\n    df = pd.DataFrame(\n        {\n            \"Index\": np.arange(0, 200, 1),\n            \"random_1\": np.random.randint(0, 50, 200),\n            \"random_2\": np.random.choice([\"Type A\", \"Type B\", \"Type C\", \"Type D\"], 200),\n            \"Label\": np.concatenate((np.zeros(180), np.ones(20))),\n        }\n    )\n\n    config[\"preprocessing\"][balance] = 0.5\n    run_test_imbalance_local(df, config, balance)\n"
  },
  {
    "path": "tests/integration_tests/test_cli.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport json\nimport os\nimport os.path\nimport pathlib\nimport shutil\nimport subprocess\nimport sys\n\nimport pytest\nimport yaml\n\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    COMBINER,\n    EVAL_BATCH_SIZE,\n    INPUT_FEATURES,\n    NAME,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    TRAINER,\n)\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.types import FeatureConfigDict\nfrom ludwig.utils.data_utils import load_yaml\nfrom tests.integration_tests.utils import category_feature, generate_data, number_feature, sequence_feature\n\npytestmark = pytest.mark.integration_tests_b\n\n\ndef _run_commands(commands, **ludwig_kwargs):\n    for arg_name, value in ludwig_kwargs.items():\n        commands += [\"--\" + arg_name, value]\n    cmdline = \" \".join(commands)\n    print(cmdline)\n    completed_process = subprocess.run(cmdline, shell=True, stdout=subprocess.PIPE, env=os.environ.copy())\n    assert completed_process.returncode == 0\n\n    return completed_process\n\n\ndef _run_ludwig(command, **ludwig_kwargs):\n    ludwig_bin = os.path.join(os.path.dirname(sys.executable), \"ludwig\")\n    commands = [ludwig_bin, command]\n    return _run_commands(commands, **ludwig_kwargs)\n\n\ndef _prepare_data(csv_filename, config_filename):\n    # Single sequence input, single category output\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 3}, reduce_input=\"sum\")]\n\n    # Generate test data\n    dataset_filename = generate_data(input_features, output_features, csv_filename)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128, EVAL_BATCH_SIZE: 128},\n    }\n\n    with open(config_filename, \"w\") as f:\n        yaml.dump(config, f)\n\n    return dataset_filename\n\n\ndef _prepare_hyperopt_data(csv_filename, config_filename):\n    # Single sequence input, single category output\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    dataset_filename = generate_data(input_features, output_features, csv_filename)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 4},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        \"hyperopt\": {\n            \"parameters\": {\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.0001,\n                    \"upper\": 0.01,\n                }\n            },\n            \"goal\": \"minimize\",\n            \"output_feature\": output_features[0][\"name\"],\n            \"validation_metrics\": \"loss\",\n            \"executor\": {\n                \"type\": \"ray\",\n                \"num_samples\": 2,\n            },\n            \"search_alg\": {\n                \"type\": \"variant_generator\",\n            },\n        },\n    }\n\n    with open(config_filename, \"w\") as f:\n        yaml.dump(config, f)\n\n    return dataset_filename\n\n\ndef test_train_cli_dataset(tmpdir, csv_filename):\n    \"\"\"Test training using `ludwig train --dataset`.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n\n\ndef test_train_cli_gpu_memory_limit(tmpdir, csv_filename):\n    \"\"\"Test training using `ludwig train --dataset --gpu_memory_limit`.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\n        \"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir), gpu_memory_limit=\"0.5\"\n    )\n\n\ndef test_train_cli_training_set(tmpdir, csv_filename):\n    \"\"\"Test training using `ludwig train --training_set`.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    validation_filename = shutil.copyfile(dataset_filename, os.path.join(tmpdir, \"validation.csv\"))\n    test_filename = shutil.copyfile(dataset_filename, os.path.join(tmpdir, \"test.csv\"))\n    _run_ludwig(\n        \"train\",\n        training_set=dataset_filename,\n        validation_set=validation_filename,\n        test_set=test_filename,\n        config=config_filename,\n        output_directory=str(tmpdir),\n    )\n\n\ndef test_export_torchscript_cli(tmpdir, csv_filename):\n    \"\"\"Test exporting Ludwig model to torchscript format.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    _run_ludwig(\n        \"export_torchscript\",\n        model_path=os.path.join(tmpdir, \"experiment_run\", MODEL_FILE_NAME),\n        output_path=os.path.join(tmpdir, \"torchscript\"),\n    )\n\n\ndef test_export_mlflow_cli(tmpdir, csv_filename):\n    \"\"\"Test export_mlflow cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    _run_ludwig(\n        \"export_mlflow\",\n        model_path=os.path.join(tmpdir, \"experiment_run\", MODEL_FILE_NAME),\n        output_path=os.path.join(tmpdir, \"data/results/mlflow\"),\n    )\n\n\ndef test_experiment_cli(tmpdir, csv_filename):\n    \"\"\"Test experiment cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"experiment\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n\n\ndef test_predict_cli(tmpdir, csv_filename):\n    \"\"\"Test predict cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    _run_ludwig(\n        \"predict\",\n        dataset=dataset_filename,\n        model=os.path.join(tmpdir, \"experiment_run\", MODEL_FILE_NAME),\n        output_directory=os.path.join(tmpdir, \"predictions\"),\n    )\n\n\ndef test_evaluate_cli(tmpdir, csv_filename):\n    \"\"\"Test evaluate cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    _run_ludwig(\n        \"evaluate\",\n        dataset=dataset_filename,\n        model=os.path.join(tmpdir, \"experiment_run\", MODEL_FILE_NAME),\n        output_directory=os.path.join(tmpdir, \"predictions\"),\n    )\n\n\n@pytest.mark.distributed\ndef test_hyperopt_cli(tmpdir, csv_filename):\n    \"\"\"Test hyperopt cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_hyperopt_data(csv_filename, config_filename)\n    _run_ludwig(\"hyperopt\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n\n\ndef test_visualize_cli(tmpdir, csv_filename):\n    \"\"\"Test Ludwig 'visualize' cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    _run_ludwig(\n        \"visualize\",\n        visualization=\"learning_curves\",\n        model_names=\"run\",\n        training_statistics=os.path.join(tmpdir, \"experiment_run\", \"training_statistics.json\"),\n        output_directory=os.path.join(tmpdir, \"visualizations\"),\n    )\n\n\ndef test_collect_summary_activations_weights_cli(tmpdir, csv_filename):\n    \"\"\"Test collect_summary cli.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"train\", dataset=dataset_filename, config=config_filename, output_directory=str(tmpdir))\n    assert _run_ludwig(\"collect_summary\", model=os.path.join(tmpdir, \"experiment_run\", MODEL_FILE_NAME))\n\n\n@pytest.mark.parametrize(\n    \"model_name\",\n    [\n        \"alexnet\",\n        \"convnext_base\",\n        \"convnext_large\",\n        \"convnext_small\",\n        \"convnext_tiny\",\n        \"densenet121\",\n        \"densenet161\",\n        \"densenet169\",\n        \"openai-community/gpt2\",\n        \"facebook/opt-125m\",\n    ],\n)\ndef test_collect_summary_pretrained_model_cli(model_name):\n    \"\"\"Test collect_summary pretrained model cli.\"\"\"\n    assert _run_ludwig(\"collect_summary\", pretrained_model=model_name)\n\n\ndef test_synthesize_dataset_cli(tmpdir, csv_filename):\n    \"\"\"Test synthesize_data cli.\"\"\"\n    # test depends on default setting of --dataset_size\n    # if this parameter is specified, _run_ludwig fails when\n    # attempting to build the cli parameter structure\n    _run_ludwig(\n        \"synthesize_dataset\",\n        output_path=os.path.join(tmpdir, csv_filename),\n        features=\"'[ \\\n                {name: text, type: text}, \\\n                {name: category, type: category}, \\\n                {name: number, type: number}, \\\n                {name: binary, type: binary}, \\\n                {name: set, type: set}, \\\n                {name: bag, type: bag}, \\\n                {name: sequence, type: sequence}, \\\n                {name: timeseries, type: timeseries}, \\\n                {name: date, type: date}, \\\n                {name: h3, type: h3}, \\\n                {name: vector, type: vector}, \\\n                {name: audio, type: audio}, \\\n                {name: image, type: image} \\\n            ]'\",\n    )\n\n\ndef test_preprocess_cli(tmpdir, csv_filename):\n    \"\"\"Test preprocess `ludwig preprocess.\"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n    _run_ludwig(\"preprocess\", dataset=dataset_filename, preprocessing_config=config_filename)\n\n\n@pytest.mark.parametrize(\n    \"second_seed_offset,random_seed,type_of_run\",\n    [\n        (0, 42, \"train\"),  # same seed train: should be reproducible\n        (1, 42, \"train\"),  # different seed train: should diverge\n        (0, 42, \"experiment\"),  # same seed experiment: should be reproducible\n    ],\n    ids=[\"same_seed_train\", \"diff_seed_train\", \"same_seed_experiment\"],\n)\ndef test_reproducible_cli_runs(\n    type_of_run: str, random_seed: int, second_seed_offset: int, csv_filename: str, tmpdir: pathlib.Path\n) -> None:\n    \"\"\"\n    Test for reproducible training using `ludwig experiment|train --dataset`.\n    Args:\n        type_of_run(str): type of run, either train or experiment\n        csv_filename(str): file path of dataset to use\n        random_seed(int): random seed integer to use for test\n        second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different\n            seed for the second run.\n        tmpdir (pathlib.Path): temporary directory path\n\n    Returns: None\n    \"\"\"\n    config_filename = os.path.join(tmpdir, \"config.yaml\")\n    dataset_filename = _prepare_data(csv_filename, config_filename)\n\n    # run first model\n    _run_ludwig(\n        type_of_run,\n        dataset=dataset_filename,\n        config=config_filename,\n        output_directory=str(tmpdir),\n        skip_save_processed_input=\"\",  # skip saving preprocessed inputs for reproducibility\n        experiment_name=\"reproducible\",\n        model_name=\"run1\",\n        random_seed=str(random_seed),\n    )\n\n    # run second model with same seed\n    _run_ludwig(\n        type_of_run,\n        dataset=dataset_filename,\n        config=config_filename,\n        output_directory=str(tmpdir),\n        skip_save_processed_input=\"\",  # skip saving preprocessed inputs for reproducibility\n        experiment_name=\"reproducible\",\n        model_name=\"run2\",\n        random_seed=str(random_seed + second_seed_offset),\n    )\n\n    # retrieve training statistics and compare\n    with open(os.path.join(tmpdir, \"reproducible_run1\", \"training_statistics.json\")) as f:\n        training1 = json.load(f)\n    with open(os.path.join(tmpdir, \"reproducible_run2\", \"training_statistics.json\")) as f:\n        training2 = json.load(f)\n\n    if second_seed_offset == 0:\n        # same seeds should result in same output\n        assert training1 == training2\n    else:\n        # non-zero second_seed_offset uses different seeds and should result in different output\n        assert training1 != training2\n\n    # if type_of_run is experiment check test statistics and compare\n    if type_of_run == \"experiment\":\n        with open(os.path.join(tmpdir, \"reproducible_run1\", \"test_statistics.json\")) as f:\n            test1 = json.load(f)\n        with open(os.path.join(tmpdir, \"reproducible_run2\", \"test_statistics.json\")) as f:\n            test2 = json.load(f)\n\n        if second_seed_offset == 0:\n            # same seeds should result in same output\n            assert test1 == test2\n        else:\n            # non-zero second_seed_offset uses different seeds and should result in different output\n            assert test1 != test2\n\n\n@pytest.mark.distributed\ndef test_init_config(tmpdir):\n    \"\"\"Test initializing a config from a dataset and a target.\"\"\"\n    input_features = [\n        number_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n    dataset_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=20)\n    output_config_path = os.path.join(tmpdir, \"config.yaml\")\n\n    _run_ludwig(\"init_config\", dataset=dataset_csv, target=output_features[0][NAME], output=output_config_path)\n\n    config = load_yaml(output_config_path)\n\n    def to_name_set(features: list[FeatureConfigDict]) -> set[str]:\n        return {feature[NAME] for feature in features}\n\n    assert to_name_set(config[INPUT_FEATURES]) == to_name_set(input_features)\n    assert to_name_set(config[OUTPUT_FEATURES]) == to_name_set(output_features)\n\n\n@pytest.mark.skip(reason=\"https://github.com/ludwig-ai/ludwig/issues/3377\")\ndef test_render_config(tmpdir):\n    \"\"\"Test rendering a full config from a partial user config.\"\"\"\n    user_config_path = os.path.join(tmpdir, \"config.yaml\")\n    input_features = [\n        number_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n\n    user_config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n    }\n\n    with open(user_config_path, \"w\") as f:\n        yaml.dump(user_config, f)\n\n    output_config_path = os.path.join(tmpdir, \"rendered.yaml\")\n    _run_ludwig(\"render_config\", config=user_config_path, output=output_config_path)\n\n    rendered_config = load_yaml(output_config_path)\n    assert len(rendered_config[INPUT_FEATURES]) == len(user_config[INPUT_FEATURES])\n    assert len(rendered_config[OUTPUT_FEATURES]) == len(user_config[OUTPUT_FEATURES])\n    assert TRAINER in rendered_config\n    assert COMBINER in rendered_config\n    assert PREPROCESSING in rendered_config\n"
  },
  {
    "path": "tests/integration_tests/test_collect.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\nimport shutil\n\nimport numpy as np\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.collect import collect_activations, collect_weights, print_model_summary\nfrom ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.utils import category_feature, ENCODERS, generate_data, sequence_feature\n\nDEVICE = get_torch_device()\n\n\ndef _prepare_data(csv_filename):\n    # Single sequence input, single category output\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    input_features[0][ENCODER][TYPE] = ENCODERS[0]\n\n    # Generate test data\n    data_csv = generate_data(input_features, output_features, csv_filename)\n    return input_features, output_features, data_csv\n\n\ndef _train(input_features, output_features, data_csv, **kwargs):\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config)\n    _, _, output_dir = model.train(dataset=data_csv, **kwargs)\n    return model, output_dir\n\n\ndef _get_layers(model_path):\n    model = LudwigModel.load(model_path)\n    return [name for name, _ in model.model.named_children()]\n\n\ndef _collect_activations(model_path, layers, csv_filename, output_directory):\n    return collect_activations(model_path, layers, dataset=csv_filename, output_directory=output_directory)\n\n\ndef test_collect_weights(tmpdir, csv_filename):\n    output_dir = None\n    try:\n        model, output_dir = _train(*_prepare_data(csv_filename))\n        model_path = os.path.join(output_dir, MODEL_FILE_NAME)\n\n        # 1 for the encoder (embeddings).\n        # 2 for the decoder classifier (w and b).\n        weights = [w for _, w in model.model.collect_weights()]\n        assert len(weights) == 3\n\n        # Load model from disk to ensure correct weight names\n        model_loaded = LudwigModel.load(model_path)\n        tensor_names = [name for name, w in model_loaded.collect_weights()]\n        assert len(tensor_names) == 3\n\n        filenames = collect_weights(model_path, tensor_names, tmpdir)\n        assert len(filenames) == 3\n\n        for weight, filename in zip(weights, filenames):\n            saved_weight = np.load(filename)\n            assert torch.allclose(weight, torch.from_numpy(saved_weight).to(DEVICE), rtol=1.0e-4), filename\n    finally:\n        if output_dir:\n            shutil.rmtree(output_dir, ignore_errors=True)\n\n\ndef test_collect_activations(tmpdir, csv_filename):\n    output_dir = None\n    try:\n        model, output_dir = _train(*_prepare_data(csv_filename))\n        model_path = os.path.join(output_dir, MODEL_FILE_NAME)\n\n        # [last_hidden, logits, projection_input]\n        filenames = _collect_activations(\n            model_path, [name for name, _ in model.model.named_children()], csv_filename, tmpdir\n        )\n        assert len(filenames) == 3\n    finally:\n        if output_dir:\n            shutil.rmtree(output_dir, ignore_errors=True)\n\n\ndef test_print_model_summary(csv_filename):\n    output_dir = None\n    model, output_dir = _train(*_prepare_data(csv_filename))\n    model_path = os.path.join(output_dir, MODEL_FILE_NAME)\n    print_model_summary(model_path)\n"
  },
  {
    "path": "tests/integration_tests/test_config_global_defaults.py",
    "content": "import logging\n\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    CATEGORY,\n    COMBINER,\n    DECODER,\n    DEFAULTS,\n    ENCODER,\n    EPOCHS,\n    FILL_WITH_CONST,\n    INPUT_FEATURES,\n    LOSS,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    TEXT,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.schema.model_config import ModelConfig\nfrom tests.integration_tests.utils import category_feature, generate_data, run_experiment, text_feature\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\n\ndef _prepare_data(csv_filename: str) -> tuple[dict, str]:\n    input_features = [\n        text_feature(name=\"title\", reduce_output=\"sum\"),\n        text_feature(name=\"summary\"),\n        category_feature(vocab_size=3),\n        category_feature(vocab_size=3),\n    ]\n\n    output_features = [text_feature(name=\"article\", embedding_size=3, output_feature=True)]\n\n    dataset = generate_data(input_features, output_features, csv_filename)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\", \"num_fc_layers\": 2},\n        TRAINER: {EPOCHS: 1, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        DEFAULTS: {\n            CATEGORY: {\n                PREPROCESSING: {\"missing_value_strategy\": FILL_WITH_CONST, \"fill_value\": \"<CUSTOM_TOK>\"},\n                ENCODER: {TYPE: \"sparse\"},\n                DECODER: {\"norm_params\": None, \"dropout\": 0.1, \"use_bias\": True},\n            },\n            TEXT: {\n                PREPROCESSING: {\"most_common\": 10, \"padding_symbol\": \"<PADDING>\"},\n                ENCODER: {TYPE: \"rnn\"},\n                DECODER: {TYPE: \"generator\", \"num_fc_layers\": 2, \"dropout\": 0.1},\n                LOSS: {\"confidence_penalty\": 0.1},\n            },\n        },\n    }\n\n    return config, dataset\n\n\ndef test_run_experiment_with_global_default_parameters(csv_filename):\n    config, dataset = _prepare_data(csv_filename)\n\n    run_experiment(config=config, dataset=dataset)\n\n\ndef test_global_defaults_with_encoder_dependencies():\n    input_features = [text_feature(name=\"title\", reduce_output=\"sum\")]\n    output_features = [category_feature(name=\"article\", embedding_size=3, output_feature=True)]\n    del input_features[0][ENCODER]\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        DEFAULTS: {\n            TEXT: {\n                ENCODER: {TYPE: \"bert\"},\n            }\n        },\n    }\n\n    # Config should populate with the additional required fields for bert\n    updated_config = ModelConfig.from_dict(config).to_dict()\n\n    assert updated_config[INPUT_FEATURES][0][ENCODER][TYPE] == \"bert\"\n    assert updated_config[INPUT_FEATURES][0][ENCODER][\"pretrained_model_name_or_path\"] == \"bert-base-uncased\"\n"
  },
  {
    "path": "tests/integration_tests/test_contrib_aim.py",
    "content": "import logging\nimport os\nimport subprocess\nimport sys\n\nimport pytest\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nTEST_SCRIPT = os.path.join(os.path.dirname(__file__), \"scripts\", \"run_train_aim.py\")\n\n\n@pytest.mark.skip(reason=\"Aim integration not compatible with Aim 4.0.\")\n@pytest.mark.distributed\ndef test_contrib_experiment(csv_filename, tmpdir):\n    aim_test_path = os.path.join(tmpdir, \"results\")\n    os.makedirs(aim_test_path, exist_ok=True)\n\n    os.environ[\"AIM_TEST_PATH\"] = aim_test_path\n    subprocess.call([\"chmod\", \"-R\", \"a+w\", os.environ[\"AIM_TEST_PATH\"]])\n\n    cmdline = [sys.executable, TEST_SCRIPT, \"--csv-filename\", csv_filename]\n    print(cmdline)\n    exit_code = subprocess.call(\" \".join(cmdline), shell=True, env=os.environ.copy())\n    assert exit_code == 0\n"
  },
  {
    "path": "tests/integration_tests/test_contrib_comet.py",
    "content": "import importlib.util\nimport logging\nimport os\nimport subprocess\nimport sys\n\nimport pytest\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nTEST_SCRIPT = os.path.join(os.path.dirname(__file__), \"scripts\", \"run_train_comet.py\")\n\n\n@pytest.mark.skipif(\n    not importlib.util.find_spec(\"pkg_resources\"),\n    reason=\"comet_ml requires pkg_resources (removed in setuptools 82+)\",\n)\ndef test_contrib_experiment(csv_filename):\n    cmdline = [sys.executable, TEST_SCRIPT, \"--csv-filename\", csv_filename]\n    exit_code = subprocess.call(\" \".join(cmdline), shell=True, env=os.environ.copy())\n    assert exit_code == 0\n\n\nif __name__ == \"__main__\":\n    \"\"\"To run tests individually, run:\n\n    ```python -m pytest tests/integration_tests/test_contrib_comet.py::test_name```\n    \"\"\"\n"
  },
  {
    "path": "tests/integration_tests/test_contrib_wandb.py",
    "content": "import logging\nimport os\nimport subprocess\nimport sys\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nTEST_SCRIPT = os.path.join(os.path.dirname(__file__), \"scripts\", \"run_train_wandb.py\")\n\n\ndef test_contrib_experiment(csv_filename, tmpdir):\n    wandb_dir = os.path.join(tmpdir, \"results\")\n    os.makedirs(wandb_dir, exist_ok=True)\n    os.environ[\"WANDB_DIR\"] = wandb_dir\n    subprocess.call([\"chmod\", \"-R\", \"a+w\", os.environ[\"WANDB_DIR\"]])\n    cmdline = [sys.executable, TEST_SCRIPT, \"--csv-filename\", csv_filename]\n    exit_code = subprocess.call(\" \".join(cmdline), shell=True, env=os.environ.copy())\n    assert exit_code == 0\n\n\nif __name__ == \"__main__\":\n    \"\"\"To run tests individually, run:\n\n    ```python -m pytest tests/integration_tests/test_contrib_wandb.py::test_name```\n    \"\"\"\n"
  },
  {
    "path": "tests/integration_tests/test_custom_components.py",
    "content": "import os\nimport tempfile\n\nimport torch\nfrom torch import nn, Tensor\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.combiners.combiners import Combiner, register_combiner\nfrom ludwig.constants import BATCH_SIZE, ENCODER_OUTPUT, LOGITS, MINIMIZE, NUMBER, TRAINER\nfrom ludwig.decoders.base import Decoder\nfrom ludwig.decoders.registry import register_decoder\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.registry import register_encoder\nfrom ludwig.modules.loss_modules import LogitsInputsMixin, register_loss\nfrom ludwig.modules.metric_modules import LossMetric, register_metric\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.base import BaseCombinerConfig\nfrom ludwig.schema.combiners.utils import register_combiner_config\nfrom ludwig.schema.decoders.base import BaseDecoderConfig\nfrom ludwig.schema.decoders.utils import register_decoder_config\nfrom ludwig.schema.encoders.base import BaseEncoderConfig\nfrom ludwig.schema.encoders.utils import register_encoder_config\nfrom ludwig.schema.features.loss.loss import BaseLossConfig\nfrom ludwig.schema.features.loss.loss import register_loss as register_loss_schema\nfrom ludwig.schema.utils import ludwig_dataclass as dataclass\nfrom tests.integration_tests.utils import (\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n)\n\n\n@register_encoder_config(\"custom_number_encoder\", NUMBER)\n@dataclass\nclass CustomNumberEncoderConfig(BaseEncoderConfig):\n    type: str = \"custom_number_encoder\"\n\n    input_size: int = schema_utils.PositiveInteger(default=1, description=\"\")\n\n\n@register_decoder_config(\"custom_number_decoder\", NUMBER)\n@dataclass\nclass CustomNumberDecoderConfig(BaseDecoderConfig):\n    type: str = \"custom_number_decoder\"\n\n    input_size: int = schema_utils.PositiveInteger(default=1, description=\"\")\n\n\n@register_loss_schema([NUMBER])\n@dataclass\nclass CustomLossConfig(BaseLossConfig):\n    type: str = \"custom_loss\"\n\n\n@register_combiner_config(\"custom_combiner\")\n@dataclass\nclass CustomTestCombinerConfig(BaseCombinerConfig):\n    type: str = \"custom_combiner\"\n\n    foo: bool = schema_utils.Boolean(default=False, description=\"\")\n\n\n@register_combiner(CustomTestCombinerConfig)\nclass CustomTestCombiner(Combiner):\n    def __init__(self, input_features: dict = None, config: CustomTestCombinerConfig = None, **kwargs):\n        super().__init__(input_features)\n        self.foo = config.foo\n\n    def forward(self, inputs: dict) -> dict:  # encoder outputs\n        if not self.foo:\n            raise ValueError(\"expected foo to be True\")\n\n        # minimal transformation from inputs to outputs\n        encoder_outputs = [inputs[k][ENCODER_OUTPUT] for k in inputs]\n        hidden = torch.cat(encoder_outputs, 1)\n        return_data = {\"combiner_output\": hidden}\n\n        return return_data\n\n\n@register_encoder(\"custom_number_encoder\", NUMBER)\nclass CustomNumberEncoder(Encoder):\n    def __init__(self, input_size, **kwargs):\n        super().__init__()\n        self.input_size = input_size\n\n    def forward(self, inputs, **kwargs):\n        return {ENCODER_OUTPUT: inputs}\n\n    @property\n    def input_shape(self) -> torch.Size:\n        return torch.Size([self.input_size])\n\n    @property\n    def output_shape(self) -> torch.Size:\n        return self.input_shape\n\n    @staticmethod\n    def get_schema_cls():\n        return CustomNumberEncoderConfig\n\n\n@register_decoder(\"custom_number_decoder\", NUMBER)\nclass CustomNumberDecoder(Decoder):\n    def __init__(self, input_size, **kwargs):\n        super().__init__()\n        self.input_size = input_size\n\n    @property\n    def input_shape(self):\n        return torch.Size([self.input_size])\n\n    def forward(self, inputs, **kwargs):\n        return torch.mean(inputs, 1)\n\n    @staticmethod\n    def get_schema_cls():\n        return CustomNumberDecoderConfig\n\n\n@register_loss(CustomLossConfig)\nclass CustomLoss(nn.Module, LogitsInputsMixin):\n    def __init__(self, config: CustomLossConfig):\n        super().__init__()\n\n    def forward(self, preds: Tensor, target: Tensor) -> Tensor:\n        return torch.mean(torch.square(preds - target))\n\n    @staticmethod\n    def get_schema_cls():\n        return CustomLossConfig\n\n\n@register_metric(\"custom_loss\", [NUMBER], MINIMIZE, LOGITS)\nclass CustomLossMetric(LossMetric):\n    def __init__(self, config: CustomLossConfig, **kwargs):\n        super().__init__()\n        self.loss_fn = CustomLoss(config)\n\n    def get_current_value(self, preds: Tensor, target: Tensor):\n        return self.loss_fn(preds, target)\n\n\ndef test_custom_combiner():\n    _run_test(combiner={\"type\": \"custom_combiner\", \"foo\": True})\n\n\ndef test_custom_encoder_decoder():\n    input_features = [\n        sequence_feature(encoder={\"reduce_output\": \"sum\"}),\n        number_feature(encoder={\"type\": \"custom_number_encoder\"}),\n    ]\n    output_features = [\n        number_feature(decoder={\"type\": \"custom_number_decoder\"}),\n    ]\n    _run_test(input_features=input_features, output_features=output_features)\n\n\ndef test_custom_loss_metric():\n    output_features = [\n        number_feature(loss={\"type\": \"custom_loss\"}),\n    ]\n    _run_test(output_features=output_features)\n\n\ndef _run_test(input_features=None, output_features=None, combiner=None):\n    with tempfile.TemporaryDirectory() as tmpdir:\n        input_features = input_features or [\n            sequence_feature(encoder={\"reduce_output\": \"sum\"}),\n            number_feature(),\n        ]\n        output_features = output_features or [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n        combiner = combiner or {\"type\": \"concat\"}\n\n        csv_filename = os.path.join(tmpdir, \"training.csv\")\n        data_csv = generate_data(input_features, output_features, csv_filename)\n\n        config = {\n            \"input_features\": input_features,\n            \"output_features\": output_features,\n            \"combiner\": combiner,\n            TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        }\n\n        model = LudwigModel(config, backend=LocalTestBackend())\n        _, _, output_directory = model.train(\n            dataset=data_csv,\n            output_directory=tmpdir,\n        )\n        model.predict(dataset=data_csv, output_directory=output_directory)\n"
  },
  {
    "path": "tests/integration_tests/test_date_feature.py",
    "content": "import datetime\nimport time\n\nimport pandas as pd\nimport pytest\nfrom dateutil.parser import parse\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import (\n    BACKEND,\n    BINARY,\n    DATE,\n    EPOCHS,\n    FILL_WITH_CONST,\n    INPUT_FEATURES,\n    MISSING_VALUE_STRATEGY,\n    NAME,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    RAY,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.utils.date_utils import create_vector_from_datetime_obj\n\nray = pytest.importorskip(\"ray\")\n\npytestmark = [\n    pytest.mark.distributed,\n]\n\n\n@pytest.fixture(scope=\"module\")\ndef string_date_df() -> \"pd.DataFrame\":\n    df = pd.DataFrame.from_dict(\n        {\n            \"date_feature\": [str(datetime.datetime.now()) for i in range(100)],\n            \"binary_feature\": [i % 2 for i in range(100)],\n        }\n    )\n    return df\n\n\n@pytest.fixture(scope=\"module\")\ndef int_date_df() -> \"pd.DataFrame\":\n    df = pd.DataFrame.from_dict(\n        {\n            \"date_feature\": [time.time_ns() for i in range(100)],\n            \"binary_feature\": [i % 2 for i in range(100)],\n        }\n    )\n    return df\n\n\n@pytest.fixture(scope=\"module\")\ndef float_date_df() -> \"pd.DataFrame\":\n    df = pd.DataFrame.from_dict(\n        {\n            \"date_feature\": [time.time() for i in range(100)],\n            \"binary_feature\": [i % 2 for i in range(100)],\n        }\n    )\n    return df\n\n\n@pytest.mark.parametrize(\n    \"date_df\",\n    [\n        pytest.param(\"string_date_df\", id=\"string_date\"),\n        pytest.param(\"int_date_df\", id=\"int_date\"),\n        pytest.param(\"float_date_df\", id=\"float_date\"),\n    ],\n)\ndef test_date_feature_formats(date_df, request, ray_cluster_2cpu):\n    df = request.getfixturevalue(date_df)\n\n    config = {\n        INPUT_FEATURES: [\n            {\n                NAME: \"date_feature\",\n                TYPE: DATE,\n                PREPROCESSING: {MISSING_VALUE_STRATEGY: FILL_WITH_CONST, \"fill_value\": \"1970-01-01 00:00:00\"},\n            }\n        ],\n        OUTPUT_FEATURES: [{NAME: \"binary_feature\", TYPE: BINARY}],\n        TRAINER: {EPOCHS: 2},\n        BACKEND: {TYPE: RAY, \"processor\": {TYPE: \"dask\"}},\n    }\n\n    fill_value = create_vector_from_datetime_obj(parse(\"1970-01-01 00:00:00\"))\n\n    model = LudwigModel(config)\n    preprocessed = model.preprocess(df)\n\n    # Because parsing errors are suppressed, we want to ensure that the data was preprocessed correctly. Sample data is\n    # drawn from the current time, so the recorded years should not match the fill value's year.\n    for date in preprocessed.training_set.to_df().compute().iloc[:, 0].values:\n        assert date[0] != fill_value[0]\n\n    for date in preprocessed.validation_set.to_df().compute().iloc[:, 0].values:\n        assert date[0] != fill_value[0]\n\n    for date in preprocessed.test_set.to_df().compute().iloc[:, 0].values:\n        assert date[0] != fill_value[0]\n"
  },
  {
    "path": "tests/integration_tests/test_dependencies.py",
    "content": "import logging\n\nimport pytest\nimport torch\n\nfrom ludwig.combiners.combiners import ConcatCombiner\nfrom ludwig.constants import CATEGORY, DECODER, NUMBER, SEQUENCE, TYPE\nfrom ludwig.models.base import BaseModel\nfrom ludwig.modules.reduction_modules import SequenceReducer\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils import output_feature_utils\nfrom tests.integration_tests.utils import generate_output_features_with_dependencies, number_feature\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nBATCH_SIZE = 16\nSEQ_SIZE = 12\nHIDDEN_SIZE = 128\nOTHER_HIDDEN_SIZE = 32\nOTHER_HIDDEN_SIZE2 = 64\n\n\n# unit test for dependency concatenation\n# tests both single and multiple dependencies\n@pytest.mark.parametrize(\n    \"dependent_hidden_shape2\",\n    [\n        None,\n        [BATCH_SIZE, OTHER_HIDDEN_SIZE2],\n        [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE2],\n        [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE],\n    ],\n)\n@pytest.mark.parametrize(\n    \"dependent_hidden_shape\", [[BATCH_SIZE, OTHER_HIDDEN_SIZE], [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE]]\n)\n@pytest.mark.parametrize(\"hidden_shape\", [[BATCH_SIZE, HIDDEN_SIZE], [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE]])\n@pytest.mark.parametrize(\n    # todo: re-add 'attention' after further research in implication of torch\n    #       migration\n    \"reduce_dependencies\",\n    [\"sum\", \"mean\", \"avg\", \"max\", \"concat\", \"last\"],\n)\ndef test_multiple_dependencies(reduce_dependencies, hidden_shape, dependent_hidden_shape, dependent_hidden_shape2):\n    # setup at least for a single dependency\n    hidden_layer = torch.randn(hidden_shape, dtype=torch.float32)\n    other_hidden_layer = torch.randn(dependent_hidden_shape, dtype=torch.float32)\n    other_dependencies = {\n        \"feature_name\": other_hidden_layer,\n    }\n\n    # setup dummy output feature to be root of dependency list\n    num_feature_defn = number_feature()\n    num_feature_defn[\"loss\"] = {\"type\": \"mean_squared_error\"}\n    num_feature_defn[\"dependencies\"] = [\"feature_name\"]\n    if len(dependent_hidden_shape) > 2:\n        num_feature_defn[\"reduce_dependencies\"] = reduce_dependencies\n\n    # Based on specification calculate expected resulting hidden size for\n    # with one dependencies\n    if reduce_dependencies == \"concat\" and len(hidden_shape) == 2 and len(dependent_hidden_shape) == 3:\n        expected_hidden_size = HIDDEN_SIZE + OTHER_HIDDEN_SIZE * SEQ_SIZE\n    else:\n        expected_hidden_size = HIDDEN_SIZE + OTHER_HIDDEN_SIZE\n\n    # set up if multiple dependencies specified, setup second dependent feature\n    if dependent_hidden_shape2:\n        other_hidden_layer2 = torch.randn(dependent_hidden_shape2, dtype=torch.float32)\n        other_dependencies[\"feature_name2\"] = other_hidden_layer2\n        num_feature_defn[\"dependencies\"].append(\"feature_name2\")\n        if len(dependent_hidden_shape2) > 2:\n            num_feature_defn[\"reduce_dependencies\"] = reduce_dependencies\n\n        # Based on specification calculate marginal increase in resulting\n        # hidden size with two dependencies\n        if reduce_dependencies == \"concat\" and len(hidden_shape) == 2 and len(dependent_hidden_shape2) == 3:\n            expected_hidden_size += dependent_hidden_shape2[-1] * SEQ_SIZE\n        else:\n            expected_hidden_size += dependent_hidden_shape2[-1]\n\n    # Set up dependency reducers.\n    dependency_reducers = torch.nn.ModuleDict()\n    for feature_name in other_dependencies.keys():\n        dependency_reducers[feature_name] = SequenceReducer(reduce_mode=reduce_dependencies)\n\n    # test dependency concatenation\n    num_feature_defn[\"input_size\"] = expected_hidden_size\n    results = output_feature_utils.concat_dependencies(\n        \"num_feature\", num_feature_defn[\"dependencies\"], dependency_reducers, hidden_layer, other_dependencies\n    )\n\n    # confirm size of resulting concat_dependencies() call\n    if len(hidden_shape) > 2:\n        assert results.shape == (BATCH_SIZE, SEQ_SIZE, expected_hidden_size)\n    else:\n        assert results.shape == (BATCH_SIZE, expected_hidden_size)\n\n\n@pytest.mark.parametrize(\n    \"output_feature_defs\",\n    [\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\"]),\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\", \"sequence_feature\"]),\n        generate_output_features_with_dependencies(\"sequence_feature\", [\"category_feature\", \"number_feature\"]),\n    ],\n)\ndef test_construct_output_features_with_dependencies(output_feature_defs):\n    # Add keys to output_feature_defs which would have been derived from data.\n    def add_data_derived_keys(output_feature_def):\n        if DECODER not in output_feature_def:\n            output_feature_def[DECODER] = {}\n        if output_feature_def[TYPE] == CATEGORY:\n            output_feature_def[\"num_classes\"] = 2\n        elif output_feature_def[TYPE] == NUMBER:\n            output_feature_def[DECODER][TYPE] = \"regressor\"\n        elif output_feature_def[TYPE] == SEQUENCE:\n            output_feature_def[DECODER][\"max_sequence_length\"] = 5\n        return output_feature_def\n\n    output_feature_defs = [add_data_derived_keys(of) for of in output_feature_defs]\n    # Gets name of output feature which has dependencies.\n    dep_feature_name = [of for of in output_feature_defs if len(of.get(\"dependencies\", [])) > 0][0][\"name\"]\n    # Creates a dummy input feature and combiner.\n    config = {\n        \"input_features\": [number_feature()],\n        \"output_features\": output_feature_defs,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 1},\n    }\n    config_obj = ModelConfig.from_dict(config)\n    input_features = BaseModel.build_inputs(config_obj.input_features)\n    combiner = ConcatCombiner(input_features=input_features, config=config_obj.combiner)\n    output_features = BaseModel.build_outputs(config_obj.output_features, combiner)\n    # Gets the output feature object which has dependencies.\n    feature_with_deps = output_features[dep_feature_name]\n    n_dependencies = len(feature_with_deps.dependencies)\n    assert n_dependencies > 0\n    # Each synthetic output feature has output size 1, so total size is 1 + n_dependencies.\n    assert feature_with_deps.fc_stack.input_shape == torch.Size([1 + n_dependencies])\n"
  },
  {
    "path": "tests/integration_tests/test_experiment.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport contextlib\nimport logging\nimport os\nimport shutil\nimport uuid\nfrom collections import namedtuple\n\nimport pandas as pd\nimport pytest\nimport torch\nimport torchvision\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import BATCH_SIZE, COLUMN, ENCODER, H3, NAME, PREPROCESSING, TRAINER, TYPE\nfrom ludwig.data.concatenate_datasets import concatenate_df\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.data.preprocessing import preprocess_for_training\nfrom ludwig.encoders.registry import get_encoder_classes\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.experiment import experiment_cli\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.predict import predict_cli\nfrom ludwig.utils.data_utils import read_csv\nfrom ludwig.utils.defaults import default_random_seed\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    bag_feature,\n    binary_feature,\n    category_distribution_feature,\n    category_feature,\n    create_data_set_to_use,\n    date_feature,\n    ENCODERS,\n    generate_data,\n    generate_output_features_with_dependencies,\n    generate_output_features_with_dependencies_complex,\n    h3_feature,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    run_experiment,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\npytestmark = pytest.mark.integration_tests_d\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"embed\", \"rnn\", \"transformer\", \"tf_idf\"])\ndef test_experiment_text_feature_non_pretrained(encoder, csv_filename):\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 30, \"min_len\": 1, \"type\": encoder}, preprocessing={\"tokenizer\": \"space\"})\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef run_experiment_with_encoder(encoder, csv_filename):\n    # Run in a subprocess to clear TF and prevent OOM\n    # This also allows us to use GPU resources\n    input_features = [text_feature(encoder={\"vocab_size\": 30, \"min_len\": 1, \"type\": encoder})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"embed\", \"rnn\", \"transformer\"])\ndef test_experiment_seq_seq_generator(csv_filename, encoder):\n    input_features = [text_feature(encoder={\"type\": encoder, \"reduce_output\": None})]\n    output_features = [text_feature(decoder={\"type\": \"generator\"}, output_feature=True)]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"embed\", \"rnn\", \"transformer\"])\ndef test_experiment_seq_seq_tagger(csv_filename, encoder):\n    input_features = [text_feature(encoder={\"type\": encoder, \"reduce_output\": None})]\n    output_features = [text_feature(decoder={\"type\": \"tagger\"}, reduce_input=None)]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"cnnrnn\", \"stacked_cnn\"])\ndef test_experiment_seq_seq_tagger_fails_for_non_length_preserving_encoders(csv_filename, encoder):\n    input_features = [text_feature(encoder={\"type\": encoder, \"reduce_output\": None})]\n    output_features = [text_feature(decoder={\"type\": \"tagger\"}, reduce_input=None)]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    with pytest.raises(ValueError):\n        run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_seq_seq_model_def_file(csv_filename, yaml_filename):\n    # seq-to-seq test to use config file instead of dictionary\n    input_features = [text_feature(encoder={\"reduce_output\": None, \"type\": \"embed\"})]\n    output_features = [text_feature(decoder={\"vocab_size\": 3, \"type\": \"tagger\"}, reduce_input=None)]\n\n    # Save the config to a yaml file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n    with open(yaml_filename, \"w\") as yaml_out:\n        yaml.safe_dump(config, yaml_out)\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(None, None, dataset=rel_path, config=yaml_filename)\n\n\ndef test_experiment_seq_seq_train_test_valid(tmpdir):\n    # seq-to-seq test to use train, test, validation files\n    input_features = [text_feature(encoder={\"reduce_output\": None, \"type\": \"rnn\"})]\n    output_features = [text_feature(decoder={\"vocab_size\": 3, \"type\": \"tagger\"}, reduce_input=None)]\n\n    train_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"train.csv\"))\n    test_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"test.csv\"), 20)\n    valdation_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"val.csv\"), 20)\n\n    run_experiment(\n        input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv\n    )\n\n    # Save intermediate output\n    run_experiment(\n        input_features, output_features, training_set=train_csv, test_set=test_csv, validation_set=valdation_csv\n    )\n\n\n@pytest.mark.parametrize(\"encoder\", [\"embed\", \"rnn\", \"transformer\"])\ndef test_experiment_multi_input_intent_classification(csv_filename, encoder):\n    # Multiple inputs, Single category output\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"representation\": \"sparse\"}),\n        category_feature(encoder={\"vocab_size\": 10}),\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    input_features[0][ENCODER][TYPE] = encoder\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_with_torch_module_dict_feature_name(csv_filename):\n    input_features = [category_feature(name=\"type\")]\n    output_features = [category_feature(name=\"to\", output_feature=True)]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_multiclass_with_class_weights(csv_filename):\n    # Multiple inputs, Single category output\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 3}, loss={\"class_weights\": [0, 1, 2]})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_multilabel_with_class_weights(csv_filename):\n    # Multiple inputs, Single category output\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [set_feature(decoder={\"vocab_size\": 3}, loss={\"class_weights\": [0, 1, 2, 3]})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\n    \"output_features\",\n    [\n        # baseline test case\n        [\n            category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2}),\n            sequence_feature(decoder={\"vocab_size\": 10, \"max_len\": 5}),\n            number_feature(),\n        ],\n        # use generator as decoder\n        [\n            category_feature(decoder={\"vocab_size\": 2, \"reduce_input\": \"sum\"}),\n            sequence_feature(decoder={\"vocab_size\": 10, \"max_len\": 5, \"type\": \"generator\"}),\n            number_feature(),\n        ],\n        # Generator decoder and reduce_input = None\n        [\n            category_feature(decoder={\"vocab_size\": 2, \"reduce_input\": \"sum\"}),\n            sequence_feature(decoder={\"max_len\": 5, \"type\": \"generator\"}, reduce_input=None),\n            number_feature(normalization=\"minmax\"),\n        ],\n        # output features with dependencies single dependency\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\"]),\n        # output features with dependencies multiple dependencies\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\", \"sequence_feature\"]),\n        # output features with dependencies multiple dependencies\n        generate_output_features_with_dependencies(\"sequence_feature\", [\"category_feature\", \"number_feature\"]),\n        # output features with dependencies\n        generate_output_features_with_dependencies(\"category_feature\", [\"sequence_feature\"]),\n        generate_output_features_with_dependencies_complex(),\n    ],\n)\ndef test_experiment_multiple_seq_seq(csv_filename, output_features):\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 100, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(normalization=\"zscore\"),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = output_features\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\n    \"num_channels,image_source,in_memory,skip_save_processed_input\",\n    [\n        (3, \"file\", True, True),\n        (1, \"file\", False, False),\n        (3, \"tensor\", True, False),\n    ],\n    ids=[\"file_in_memory_3ch\", \"file_on_disk_1ch\", \"tensor_in_memory_3ch\"],\n)\ndef test_basic_image_feature(num_channels, image_source, in_memory, skip_save_processed_input, tmpdir):\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\n                \"in_memory\": in_memory,\n                \"height\": 12,\n                \"width\": 12,\n                \"num_channels\": num_channels,\n                \"num_processes\": 5,\n            },\n            encoder={\n                \"type\": \"stacked_cnn\",\n                \"output_size\": 16,\n                \"num_filters\": 8,\n            },\n        )\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    if image_source == \"file\":\n        # use images from file\n        run_experiment(\n            input_features, output_features, dataset=rel_path, skip_save_processed_input=skip_save_processed_input\n        )\n    else:\n        # import image from file and store in dataframe as tensors.\n        df = pd.read_csv(rel_path)\n        image_feature_name = input_features[0][\"name\"]\n        df[image_feature_name] = df[image_feature_name].apply(lambda x: torchvision.io.read_image(x))\n\n        run_experiment(input_features, output_features, dataset=df, skip_save_processed_input=skip_save_processed_input)\n\n\ndef test_experiment_infer_image_metadata(tmpdir):\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    # Resnet encoder\n    input_features = [\n        image_feature(folder=image_dest_folder, encoder={\"type\": \"stacked_cnn\", \"output_size\": 16, \"num_filters\": 8}),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"zscore\"),\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2}), number_feature()]\n\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    # remove image preprocessing section to force inferring image meta data\n    input_features[0].pop(\"preprocessing\")\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\nImageParams = namedtuple(\"ImageTestParams\", \"image_encoder in_memory_flag skip_save_processed_input\")\n\n\n@pytest.mark.parametrize(\n    \"image_params\",\n    [\n        ImageParams(\"stacked_cnn\", True, True),\n        ImageParams(\"stacked_cnn\", False, False),\n    ],\n)\ndef test_experiment_image_inputs(image_params: ImageParams, tmpdir):\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    # Resnet encoder\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\"type\": \"resnet\", \"output_size\": 16, \"num_filters\": 8},\n        ),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"zscore\"),\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2}), number_feature()]\n\n    input_features[0][\"encoder\"][\"type\"] = image_params.image_encoder\n    input_features[0][\"preprocessing\"][\"in_memory\"] = image_params.in_memory_flag\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    run_experiment(\n        input_features,\n        output_features,\n        dataset=rel_path,\n        skip_save_processed_input=image_params.skip_save_processed_input,\n    )\n\n\n# Primary focus of this test is to determine if exceptions are raised for different data set formats and in_memory\n# setting.\n\n\n@pytest.mark.parametrize(\n    \"train_format,train_in_memory,test_format,test_in_memory\",\n    [\n        (\"csv\", True, \"csv\", True),\n        (\"df\", False, \"df\", False),\n        (\"hdf5\", True, \"hdf5\", True),\n        (\"csv\", False, \"df\", True),\n    ],\n    ids=[\"csv_inmem\", \"df_ondisk\", \"hdf5_inmem\", \"csv_to_df_mixed\"],\n)\ndef test_experiment_image_dataset(train_format, train_in_memory, test_format, test_in_memory, tmpdir):\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\"type\": \"stacked_cnn\", \"output_size\": 16, \"num_filters\": 8},\n        ),\n    ]\n    output_features = [\n        category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2}),\n    ]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        \"preprocessing\": {},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # create temporary name for train and test data sets\n    train_csv_filename = os.path.join(tmpdir, \"train_\" + uuid.uuid4().hex[:10].upper() + \".csv\")\n    test_csv_filename = os.path.join(tmpdir, \"test_\" + uuid.uuid4().hex[:10].upper() + \".csv\")\n\n    # setup training data format to test\n    train_data = generate_data(input_features, output_features, train_csv_filename)\n    config[\"input_features\"][0][\"preprocessing\"][\"in_memory\"] = train_in_memory\n    training_set_metadata = None\n\n    # define Ludwig model\n    backend = LocalTestBackend()\n    model = LudwigModel(\n        config=config,\n        backend=backend,\n    )\n\n    if train_format == \"hdf5\":\n        # hdf5 format\n        train_set, _, _, training_set_metadata = preprocess_for_training(\n            model.config,\n            dataset=train_data,\n            backend=backend,\n        )\n        train_dataset_to_use = train_set.data_hdf5_fp\n    else:\n        train_dataset_to_use = create_data_set_to_use(train_format, train_data)\n\n    model.train(dataset=train_dataset_to_use, training_set_metadata=training_set_metadata)\n\n    model.config_obj.input_features.to_list()[0][\"preprocessing\"][\"in_memory\"] = test_in_memory\n\n    # setup test data format to test\n    test_data = generate_data(input_features, output_features, test_csv_filename)\n\n    if test_format == \"hdf5\":\n        # hdf5 format\n        # create hdf5 data set\n        _, test_set, _, training_set_metadata_for_test = preprocess_for_training(\n            model.config,\n            dataset=test_data,\n            backend=backend,\n        )\n        test_dataset_to_use = test_set.data_hdf5_fp\n    else:\n        test_dataset_to_use = create_data_set_to_use(test_format, test_data)\n\n    # run functions with the specified data format\n    model.evaluate(dataset=test_dataset_to_use)\n    model.predict(dataset=test_dataset_to_use)\n\n\nDATA_FORMATS_TO_TEST = [\n    \"csv\",\n    \"df\",\n    \"dict\",\n    \"excel\",\n    \"feather\",\n    \"fwf\",\n    \"hdf5\",\n    \"html\",\n    \"json\",\n    \"jsonl\",\n    \"parquet\",\n    \"pickle\",\n    \"stata\",\n    \"tsv\",\n]\n\n\n@pytest.mark.parametrize(\"data_format\", DATA_FORMATS_TO_TEST)\ndef test_experiment_dataset_formats(data_format, csv_filename):\n    # primary focus of this test is to determine if exceptions are\n    # raised for different data set formats and in_memory setting\n\n    input_features = [number_feature(), category_feature()]\n    output_features = [category_feature(output_feature=True), number_feature()]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        \"preprocessing\": {},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # setup training data format to test\n    raw_data = generate_data(input_features, output_features, csv_filename)\n\n    training_set_metadata = None\n\n    # define Ludwig model\n    model = LudwigModel(config=config)\n\n    if data_format == \"hdf5\":\n        # hdf5 format\n        training_set, _, _, training_set_metadata = preprocess_for_training(model.config, dataset=raw_data)\n        dataset_to_use = training_set.data_hdf5_fp\n    else:\n        dataset_to_use = create_data_set_to_use(data_format, raw_data)\n\n    model.train(dataset=dataset_to_use, training_set_metadata=training_set_metadata, random_seed=default_random_seed)\n\n    # # run functions with the specified data format\n    model.evaluate(dataset=dataset_to_use)\n    model.predict(dataset=dataset_to_use)\n\n\ndef test_experiment_audio_inputs(tmpdir):\n    # Audio Inputs\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n\n    input_features = [audio_feature(folder=audio_dest_folder)]\n    output_features = [binary_feature()]\n\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_tied_weights(csv_filename):\n    # Single sequence input, single category output\n    input_features = [\n        text_feature(name=\"text_feature1\", encoder={\"min_len\": 1, \"type\": \"cnnrnn\", \"reduce_output\": \"sum\"}),\n        text_feature(\n            name=\"text_feature2\", encoder={\"min_len\": 1, \"type\": \"cnnrnn\", \"reduce_output\": \"sum\"}, tied=\"text_feature1\"\n        ),\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    for encoder in ENCODERS:\n        input_features[0][ENCODER][TYPE] = encoder\n        input_features[1][ENCODER][TYPE] = encoder\n        run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_tied_weights_sequence_combiner(csv_filename):\n    \"\"\"Tests that tied weights work with sequence combiners if `sequence_length` is provided.\n\n    Addresses https://github.com/ludwig-ai/ludwig/issues/3220\n    \"\"\"\n    input_features = [\n        text_feature(\n            name=\"feature1\",\n            encoder={\n                \"max_len\": 5,\n                \"reduce_output\": None,\n            },\n            preprocessing={\"sequence_length\": 10},\n        ),\n        text_feature(\n            name=\"feature2\",\n            encoder={\n                \"max_len\": 3,\n                \"reduce_output\": None,\n            },\n            preprocessing={\"sequence_length\": 10},\n            tied=\"feature1\",\n        ),\n    ]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"sequence\"},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(config=config, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\n    \"enc_cell_type,attention\",\n    [(\"lstm\", True), (\"rnn\", False), (\"gru\", True)],\n    ids=[\"lstm_attn\", \"rnn_no_attn\", \"gru_attn\"],\n)\ndef test_sequence_tagger(enc_cell_type, attention, csv_filename):\n    # Define input and output features\n    input_features = [\n        sequence_feature(encoder={\"max_len\": 10, \"type\": \"rnn\", \"cell_type\": enc_cell_type, \"reduce_output\": None})\n    ]\n    output_features = [\n        sequence_feature(decoder={\"max_len\": 10, \"type\": \"tagger\", \"attention\": attention}, reduce_input=None)\n    ]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    # run the experiment\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_sequence_tagger_text(csv_filename):\n    # Define input and output features\n    input_features = [text_feature(encoder={\"max_len\": 10, \"type\": \"rnn\", \"reduce_output\": None})]\n    output_features = [\n        sequence_feature(\n            decoder={\"max_len\": 10, \"type\": \"tagger\"},\n            reduce_input=None,\n        )\n    ]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    # run the experiment\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n\"\"\"\n@pytest.mark.distributed\ndef test_sequence_tagger_text_ray(csv_filename, ray_cluster_2cpu):\n    # Define input and output features\n    input_features = [text_feature(encoder={\"max_len\": 10, \"type\": \"rnn\", \"reduce_output\": None})]\n    output_features = [\n        sequence_feature(\n            decoder={\"max_len\": 10, \"type\": \"tagger\"},\n            reduce_input=None,\n        )\n    ]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    # run the experiment\n    run_experiment(input_features, output_features, dataset=rel_path, backend=\"ray\")\n\"\"\"\n\n\ndef test_experiment_sequence_combiner_with_reduction_fails(csv_filename):\n    config = {\n        \"input_features\": [\n            sequence_feature(\n                name=\"seq1\",\n                encoder={\n                    \"min_len\": 5,\n                    \"max_len\": 5,\n                    \"type\": \"embed\",\n                    \"cell_type\": \"lstm\",\n                    \"reduce_output\": \"sum\",\n                },\n            ),\n            sequence_feature(\n                name=\"seq2\",\n                encoder={\n                    \"min_len\": 5,\n                    \"max_len\": 5,\n                    \"type\": \"embed\",\n                    \"cell_type\": \"lstm\",\n                    \"reduce_output\": \"sum\",\n                },\n            ),\n            category_feature(encoder={\"vocab_size\": 5}),\n        ],\n        \"output_features\": [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 5})],\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        \"combiner\": {\n            \"type\": \"sequence\",\n            \"encoder\": {\"type\": \"rnn\"},\n            \"main_sequence_feature\": \"seq1\",\n            \"reduce_output\": None,\n        },\n    }\n\n    # Generate test data\n    rel_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n\n    # Encoding sequence features with 'embed' should fail with SequenceConcatCombiner, since at least one sequence\n    # feature should be rank 3.\n    with pytest.raises(TypeError):\n        run_experiment(config=config, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"sequence_encoder\", [\"rnn\", \"transformer\"])\ndef test_experiment_sequence_combiner(sequence_encoder, csv_filename):\n    config = {\n        \"input_features\": [\n            sequence_feature(\n                name=\"seq1\",\n                encoder={\n                    \"min_len\": 5,\n                    \"max_len\": 5,\n                    \"type\": sequence_encoder,\n                    \"cell_type\": \"lstm\",\n                    \"reduce_output\": None,\n                },\n            ),\n            sequence_feature(\n                name=\"seq2\",\n                encoder={\n                    \"min_len\": 5,\n                    \"max_len\": 5,\n                    \"type\": sequence_encoder,\n                    \"cell_type\": \"lstm\",\n                    \"reduce_output\": None,\n                },\n            ),\n            category_feature(vocab_size=5),\n        ],\n        \"output_features\": [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 5})],\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        \"combiner\": {\n            \"type\": \"sequence\",\n            \"encoder\": {\"type\": \"rnn\"},\n            \"main_sequence_feature\": \"seq1\",\n            \"reduce_output\": None,\n        },\n    }\n\n    # Generate test data\n    rel_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n\n    run_experiment(config=config, dataset=rel_path)\n\n\ndef test_experiment_model_resume(tmpdir):\n    # Single sequence input, single category output\n    # Tests saving a model file, loading it to rerun training and predict\n    input_features = [sequence_feature(encoder={\"type\": \"rnn\", \"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir)\n\n    experiment_cli(config, dataset=rel_path, model_resume_path=output_dir)\n\n    predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path)\n    shutil.rmtree(output_dir, ignore_errors=True)\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    \"dist_strategy\",\n    [\n        pytest.param(\"ddp\", id=\"ddp\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_experiment_model_resume_distributed(tmpdir, dist_strategy, ray_cluster_4cpu):\n    _run_experiment_model_resume_distributed(tmpdir, dist_strategy)\n\n\n@pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n@pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\n@pytest.mark.parametrize(\n    \"dist_strategy\",\n    [\n        pytest.param(\"deepspeed\", id=\"deepspeed\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_experiment_model_resume_distributed_gpu(tmpdir, dist_strategy, ray_cluster_4cpu):\n    _run_experiment_model_resume_distributed(tmpdir, dist_strategy)\n\n\ndef _run_experiment_model_resume_distributed(tmpdir, dist_strategy):\n    # Single sequence input, single category output\n    # Tests saving a model file, loading it to rerun training and predict\n    input_features = [number_feature()]\n    output_features = [category_feature(output_feature=True)]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 8},\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n        \"backend\": {\"type\": \"ray\", \"trainer\": {\"strategy\": dist_strategy, \"num_workers\": 2}},\n    }\n\n    _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=os.path.join(tmpdir, \"results1\"))\n\n    experiment_cli(\n        config, dataset=rel_path, model_resume_path=output_dir, output_directory=os.path.join(tmpdir, \"results2\")\n    )\n\n    predict_cli(\n        os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path, output_directory=os.path.join(tmpdir, \"results3\")\n    )\n\n\n@pytest.mark.parametrize(\n    \"missing_file\",\n    [\"training_progress.json\", \"training_checkpoints\"],\n    ids=[\"training_progress\", \"training_checkpoints\"],\n)\ndef test_experiment_model_resume_missing_file(tmpdir, missing_file):\n    # Single sequence input, single category output\n    # Tests saving a model file, loading it to rerun training and predict\n    input_features = [sequence_feature(encoder={\"type\": \"rnn\", \"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"reduce_input\": \"sum\", \"vocab_size\": 2})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    _, _, _, _, output_dir = experiment_cli(config, dataset=rel_path, output_directory=tmpdir)\n\n    try:\n        # Remove file to simulate failure during first epoch of training which prevents\n        # training_checkpoints to be empty and training_progress.json to not be created\n        missing_file_path = os.path.join(output_dir, MODEL_FILE_NAME, missing_file)\n        if missing_file == \"training_progress.json\":\n            os.remove(missing_file_path)\n        else:\n            shutil.rmtree(missing_file_path)\n    finally:\n        # Training should start a fresh model training run without any errors\n        experiment_cli(config, dataset=rel_path, model_resume_path=output_dir)\n\n    predict_cli(os.path.join(output_dir, MODEL_FILE_NAME), dataset=rel_path)\n    shutil.rmtree(output_dir, ignore_errors=True)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_experiment_model_resume_before_1st_epoch_distributed(tmpdir, ray_cluster_4cpu):\n    # Single sequence input, single category output\n    # Tests saving a model file, loading it to rerun training and predict\n    input_features = [number_feature()]\n    output_features = [category_feature(output_feature=True)]\n    # Generate test data\n    training_set = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 8},\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n        \"backend\": {\"type\": \"ray\", \"trainer\": {\"strategy\": \"ddp\", \"num_workers\": 2}},\n    }\n\n    class InducedFailureCallback(Callback):\n        \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n        def on_resume_training(self, is_coordinator):\n            if is_coordinator:\n                raise RuntimeError(\"Induced failure\")\n\n    class NoFailureCallback(Callback):\n        \"\"\"Class that defines the methods necessary to hook into process.\"\"\"\n\n        def on_resume_training(self, is_coordinator):\n            pass\n\n    try:\n        # Define Ludwig model object that drive model training\n        model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[InducedFailureCallback()])\n        model.train(\n            dataset=training_set,\n            experiment_name=\"simple_experiment\",\n            model_name=\"simple_model_incomplete\",\n            skip_save_processed_input=True,\n            output_directory=os.path.join(tmpdir, \"results1\"),\n        )\n    except Exception:\n        model = LudwigModel(config=config, logging_level=logging.INFO, callbacks=[NoFailureCallback()])\n        model.train(\n            dataset=training_set,\n            skip_save_processed_input=True,\n            model_resume_path=os.path.join(tmpdir, \"results1\"),\n        )\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_tabnet_with_batch_size_1(tmpdir, ray_cluster_4cpu):\n    input_features = [number_feature()]\n    output_features = [category_feature(output_feature=True)]\n    training_set = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"tabnet\"},\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 1},\n        \"backend\": {\"type\": \"ray\", \"trainer\": {\"strategy\": \"ddp\", \"num_workers\": 2}},\n    }\n    model = LudwigModel(config=config, logging_level=logging.INFO)\n    model.train(\n        dataset=training_set,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n\ndef test_experiment_various_feature_types(csv_filename):\n    input_features = [binary_feature(), bag_feature()]\n    output_features = [set_feature(decoder={\"max_len\": 3, \"vocab_size\": 5})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_timeseries(csv_filename):\n    input_features = [timeseries_feature()]\n    output_features = [binary_feature()]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"transformer\"\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_visual_question_answering(tmpdir):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\"in_memory\": True, \"height\": 32, \"width\": 32, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\n                \"type\": \"stacked_cnn\",\n            },\n        ),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n    ]\n    output_features = [sequence_feature(decoder={\"type\": \"generator\", \"cell_type\": \"lstm\"})]\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_image_resizing_num_channel_handling(tmpdir):\n    \"\"\"This test creates two image datasets with 3 channels and 1 channel. The combination of this data is used to\n    train a model. This checks the cases where the user may or may not specify a number of channels in the config.\n\n    :param csv_filename:\n    :return:\n    \"\"\"\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    # Resnet encoder\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\"in_memory\": True, \"height\": 32, \"width\": 32, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\n                \"type\": \"stacked_cnn\",\n            },\n        ),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"minmax\"),\n    ]\n    output_features = [binary_feature(), number_feature()]\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset1.csv\"), num_examples=20)\n\n    df1 = read_csv(rel_path)\n\n    input_features[0][\"preprocessing\"][\"num_channels\"] = 1\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset2.csv\"), num_examples=20)\n    df2 = read_csv(rel_path)\n\n    df = concatenate_df(df1, df2, None, LOCAL_BACKEND)\n    df.to_csv(rel_path, index=False)\n\n    # Here the user specifies number of channels. Exception shouldn't be thrown\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n    del input_features[0][\"preprocessing\"][\"num_channels\"]\n\n    # User doesn't specify num channels, but num channels is inferred. Exception shouldn't be thrown\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"wave\", \"embed\"])\ndef test_experiment_date(encoder, csv_filename):\n    input_features = [date_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    input_features[0][ENCODER] = {TYPE: encoder}\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", get_encoder_classes(H3).keys())\ndef test_experiment_h3(encoder, csv_filename):\n    input_features = [h3_feature()]\n    output_features = [binary_feature()]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    input_features[0][ENCODER] = {TYPE: encoder}\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_vector_feature(csv_filename):\n    input_features = [vector_feature()]\n    output_features = [binary_feature()]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_vector_feature_infer_size(csv_filename):\n    input_features = [vector_feature()]\n    output_features = [vector_feature()]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    # Unset vector_size so it needs to be inferred\n    del input_features[0][PREPROCESSING]\n    del output_features[0][PREPROCESSING]\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"encoder\", [\"parallel_cnn\", \"dense\", \"passthrough\"])\ndef test_forecasting_row_major(csv_filename, encoder):\n    input_features = [timeseries_feature(encoder={\"type\": encoder})]\n    output_features = [timeseries_feature(decoder={\"type\": \"projector\"})]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14, \"flatten_inputs\": True},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, config=config, dataset=rel_path)\n\n\ndef test_forecasting_column_major(csv_filename):\n    input_feature = timeseries_feature(preprocessing={\"window_size\": 3})\n    input_features = [input_feature]\n\n    # Ensure output feature has the same column and the input feature\n    output_feature = timeseries_feature(\n        name=input_feature[COLUMN], preprocessing={\"horizon\": 2}, decoder={\"type\": \"projector\"}\n    )\n    output_feature[NAME] = f\"{input_feature[NAME]}_out\"\n    output_features = [output_feature]\n\n    # Generate test data in column-major format. This is just a dataframe of numbers with the same column name\n    # as expected by the timeseries input feature\n    column_major_feature = number_feature(name=input_feature[COLUMN])\n    csv_filename = generate_data([column_major_feature], [], csv_filename)\n\n    input_df = pd.read_csv(csv_filename)\n\n    model, eval_stats, train_stats, preprocessed_data, output_directory = run_experiment(\n        input_features, output_features, dataset=csv_filename\n    )\n    train_set, val_set, test_set, _ = preprocessed_data\n\n    print(input_df)\n    # print(train_set.to_df())\n\n    horizon_df = model.forecast(input_df, horizon=5)\n    print(horizon_df)\n\n\n@pytest.mark.parametrize(\"reduce_output\", [(\"sum\"), (None)], ids=[\"sum\", \"none\"])\ndef test_experiment_text_output_feature_with_tagger_decoder(csv_filename, reduce_output):\n    \"\"\"Test that the tagger decoder works with text output features when reduce_output is set to None.\"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\", \"reduce_output\": reduce_output})]\n    output_features = [text_feature(output_feature=True, decoder={\"type\": \"tagger\"})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    with pytest.raises(ConfigValidationError) if reduce_output == \"sum\" else contextlib.nullcontext():\n        run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\"reduce_output\", [(\"sum\"), (None)], ids=[\"sum\", \"none\"])\ndef test_experiment_sequence_output_feature_with_tagger_decoder(csv_filename, reduce_output):\n    \"\"\"Test that the tagger decoder works with sequence output features when reduce_output is set to None.\"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\", \"reduce_output\": reduce_output})]\n    output_features = [sequence_feature(output_feature=True, decoder={\"type\": \"tagger\"})]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    with pytest.raises(ConfigValidationError) if reduce_output == \"sum\" else contextlib.nullcontext():\n        run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_category_input_feature_with_tagger_decoder(csv_filename):\n    \"\"\"Test that the tagger decoder doesn't work with category input features.\"\"\"\n    input_features = [category_feature()]\n    output_features = [sequence_feature(output_feature=True, decoder={\"type\": \"tagger\"})]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14, \"reduce_output\": None},\n    }\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    with pytest.raises(ConfigValidationError):\n        run_experiment(config=config, dataset=rel_path)\n\n\ndef test_experiment_category_distribution_feature(csv_filename):\n    vocab = [\"a\", \"b\", \"c\"]\n    input_features = [vector_feature()]\n    output_features = [\n        category_distribution_feature(\n            preprocessing={\n                \"vocab\": vocab,\n            }\n        )\n    ]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    input_df = pd.read_csv(rel_path)\n\n    # set batch_size=auto to ensure we produce the correct shaped synthetic data\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: \"auto\"},\n    }\n    model, _, _, _, _ = run_experiment(input_features, output_features, dataset=rel_path, config=config)\n    preds, _ = model.predict(input_df)\n\n    # Check that predictions are category values drawn from the vocab, not distributions\n    assert all(v in vocab for v in preds[f\"{output_features[0][NAME]}_predictions\"].values)\n\n\ndef test_experiment_ordinal_category(csv_filename):\n    input_features = [category_feature(num_classes=5), number_feature()]\n    output_features = [category_feature(output_feature=True, loss={\"type\": \"corn\"})]\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\ndef test_experiment_feature_names_with_non_word_chars(tmpdir):\n    config = yaml.safe_load(\"\"\"\ninput_features:\n    - name: Pclass (new)\n      type: category\n    - name: review.text\n      type: category\n    - name: other_feature\n      type: category\n      tied: review.text\n\noutput_features:\n    - name: Survived (new)\n      type: binary\n    - name: Thrived\n      type: binary\n      dependencies:\n        - Survived (new)\n\ncombiner:\n    type: comparator\n    entity_1:\n        - Pclass (new)\n        - other_feature\n    entity_2:\n        - review.text\n\n\"\"\")\n\n    df = build_synthetic_dataset_df(120, config)\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    model.train(dataset=df, output_directory=tmpdir)\n\n\ndef test_text_output_feature_cols(tmpdir, csv_filename):\n    \"\"\"Test ensures that there are 4 output columns when model.predict() is called for text output features.\"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\"})]\n    output_features = [text_feature(output_feature=True)]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"trainer\": {\"train_steps\": 2, \"batch_size\": 5},\n    }\n\n    model = LudwigModel(config, logging_level=logging.INFO)\n    model.train(dataset=rel_path, output_directory=tmpdir)\n    predict_output = model.predict(dataset=rel_path)[0]\n\n    assert len(predict_output.columns) == 4\n\n    predict_df_headers = {col_name.split(\"_\")[2] for col_name in list(predict_output.columns)}\n    assert predict_df_headers == {\"predictions\", \"probability\", \"probabilities\", \"response\"}\n"
  },
  {
    "path": "tests/integration_tests/test_explain.py",
    "content": "import logging\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, BINARY, CATEGORY, MINIMUM_BATCH_SIZE, MODEL_ECD, TYPE\nfrom ludwig.explain.captum import IntegratedGradientsExplainer\nfrom ludwig.explain.explainer import Explainer\nfrom ludwig.explain.explanation import Explanation\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    date_feature,\n    generate_data,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\ntry:\n    from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer\nexcept ImportError:\n    RayIntegratedGradientsExplainer = None\n\npytestmark = pytest.mark.integration_tests_d\n\n\ndef test_explanation_dataclass():\n    explanation = Explanation(target=\"target\")\n\n    feature_attributions_for_label_1 = np.array([1, 2, 3])\n    feature_attributions_for_label_2 = np.array([4, 5, 6])\n\n    # test add()\n    explanation.add([\"f1\", \"f2\", \"f3\"], feature_attributions_for_label_1)\n\n    with pytest.raises(AssertionError, match=\"Expected feature attributions of shape\"):\n        # test add() with wrong shape\n        explanation.add([\"f1\", \"f2\", \"f3\", \"f4\"], np.array([1, 2, 3, 4]))\n\n    explanation.add([\"f1\", \"f2\", \"f3\"], feature_attributions_for_label_2)\n\n    # test to_array()\n    explanation_array = explanation.to_array()\n    assert np.array_equal(explanation_array, [[1, 2, 3], [4, 5, 6]])\n\n\ndef test_abstract_explainer_instantiation():\n    with pytest.raises(TypeError, match=\"Can't instantiate abstract class Explainer\"):\n        Explainer(None, inputs_df=None, sample_df=None, target=None)\n\n\n@pytest.mark.parametrize(\n    \"explainer_class, model_type\",\n    [\n        (IntegratedGradientsExplainer, MODEL_ECD),\n    ],\n)\n@pytest.mark.parametrize(\n    \"output_feature\",\n    [binary_feature(), number_feature(), category_feature(decoder={\"vocab_size\": 3})],\n    ids=[\"binary\", \"number\", \"category\"],\n)\n@pytest.mark.parametrize(\n    \"additional_config\",\n    [\n        pytest.param({}, id=\"default\"),\n        pytest.param({\"preprocessing\": {\"split\": {\"type\": \"fixed\", \"column\": \"split\"}}}, id=\"fixed_split\"),\n    ],\n)\ndef test_explainer_api(explainer_class, model_type, output_feature, additional_config, tmpdir):\n    run_test_explainer_api(explainer_class, model_type, [output_feature], additional_config, tmpdir)\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"output_feature\",\n    [binary_feature(), number_feature(), category_feature(decoder={\"vocab_size\": 3})],\n    ids=[\"binary\", \"number\", \"category\"],\n)\ndef test_explainer_api_ray(output_feature, tmpdir, ray_cluster_2cpu):\n    from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer\n\n    run_test_explainer_api(\n        RayIntegratedGradientsExplainer,\n        \"ecd\",\n        [output_feature],\n        {},\n        tmpdir,\n        resources_per_task={\"num_cpus\": 1},\n        num_workers=1,\n    )\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_explainer_api_ray_minimum_batch_size(tmpdir, ray_cluster_2cpu):\n    from ludwig.explain.captum_ray import RayIntegratedGradientsExplainer\n\n    run_test_explainer_api(\n        RayIntegratedGradientsExplainer,\n        \"ecd\",\n        [binary_feature()],\n        {},\n        tmpdir,\n        resources_per_task={\"num_cpus\": 1},\n        num_workers=1,\n        batch_size=MINIMUM_BATCH_SIZE,\n    )\n\n\n@pytest.mark.flaky(reruns=2, reruns_delay=5)\n@pytest.mark.parametrize(\"cache_encoder_embeddings\", [True])\n@pytest.mark.parametrize(\n    \"explainer_class,model_type\",\n    [\n        pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_local\"),\n        pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_explainer_text_hf(explainer_class, model_type, cache_encoder_embeddings, tmpdir, ray_cluster_2cpu):\n    input_features = [\n        text_feature(\n            encoder={\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-bert-for-token-classification\",\n            },\n            preprocessing={\"cache_encoder_embeddings\": cache_encoder_embeddings},\n        )\n    ]\n    run_test_explainer_api(explainer_class, model_type, [binary_feature()], {}, tmpdir, input_features=input_features)\n\n\n@pytest.mark.parametrize(\n    \"explainer_class,model_type\",\n    [\n        pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_local\"),\n        pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_explainer_text_tied_weights(explainer_class, model_type, tmpdir):\n    text_feature_1 = text_feature()\n    text_feature_2 = text_feature(tied=text_feature_1[\"name\"])\n    input_features = [text_feature_1, text_feature_2]\n    run_test_explainer_api(explainer_class, model_type, [binary_feature()], {}, tmpdir, input_features=input_features)\n\n\ndef run_test_explainer_api(\n    explainer_class,\n    model_type,\n    output_features,\n    additional_config,\n    tmpdir,\n    input_features=None,\n    batch_size=128,\n    **kwargs\n):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    if input_features is None:\n        input_features = [\n            # Include a non-canonical name that's not a valid key for a vanilla pytorch ModuleDict:\n            # https://github.com/pytorch/pytorch/issues/71203\n            {\"name\": \"type\", \"type\": \"binary\"},\n            number_feature(),\n            category_feature(encoder={TYPE: \"onehot\", \"reduce_output\": \"sum\"}),\n            category_feature(encoder={TYPE: \"passthrough\", \"reduce_output\": \"sum\"}),\n        ]\n        # TODO(travis): need unit tests to test the get_embedding_layer() of every encoder to ensure it is\n        #  compatible with the explainer\n        input_features += [\n            category_feature(encoder={\"type\": \"dense\", \"reduce_output\": \"sum\"}),\n            text_feature(encoder={\"vocab_size\": 3}),\n            vector_feature(),\n            timeseries_feature(),\n            image_feature(folder=image_dest_folder),\n            # audio_feature(os.path.join(tmpdir, \"generated_audio\")), # NOTE: works but takes a long time\n            # sequence_feature(encoder={\"vocab_size\": 3}),\n            date_feature(),\n            # h3_feature(),\n            set_feature(encoder={\"vocab_size\": 3}),\n            # bag_feature(encoder={\"vocab_size\": 3}),\n        ]\n\n    # Generate data\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    generate_data(input_features, output_features, csv_filename, num_examples=20)\n    df = pd.read_csv(csv_filename)\n    if \"split\" in additional_config.get(\"preprocessing\", {}):\n        df[\"split\"] = np.random.randint(0, 3, df.shape[0])\n\n    # Train model\n    config = {\"input_features\": input_features, \"output_features\": output_features, \"model_type\": model_type}\n    config[\"trainer\"] = {\"train_steps\": 1, BATCH_SIZE: batch_size}\n    config.update(additional_config)\n\n    model = LudwigModel(config, logging_level=logging.WARNING, backend=LocalTestBackend())\n    model.train(df)\n\n    # Explain model\n    explainer = explainer_class(model, inputs_df=df, sample_df=df, target=output_features[0][\"name\"], **kwargs)\n\n    is_binary = output_features[0].get(\"type\") == BINARY\n    is_category = output_features[0].get(\"type\") == CATEGORY\n\n    vocab_size = 1\n    if is_binary:\n        vocab_size = 2\n    elif is_category:\n        vocab_size = output_features[0].get(\"decoder\", {}).get(\"vocab_size\")\n\n    assert explainer.is_binary_target == is_binary\n    assert explainer.is_category_target == is_category\n    assert explainer.vocab_size == vocab_size\n\n    explanations_result = explainer.explain()\n\n    # Verify shapes.\n    assert explanations_result.global_explanation.to_array().shape == (vocab_size, len(input_features))\n\n    assert len(explanations_result.row_explanations) == len(df)\n    for e in explanations_result.row_explanations:\n        assert e.to_array().shape == (vocab_size, len(input_features))\n\n    assert len(explanations_result.expected_values) == vocab_size\n\n\n@pytest.mark.parametrize(\n    \"output_feature\",\n    [set_feature(decoder={\"vocab_size\": 3}), vector_feature()],\n    ids=[\"set\", \"vector\"],\n)\ndef test_explainer_api_nonscalar_outputs(output_feature, tmpdir):\n    run_test_explainer_api(IntegratedGradientsExplainer, MODEL_ECD, [output_feature], {}, tmpdir)\n\n\ndef test_explainer_api_text_outputs(tmpdir):\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\", \"reduce_output\": None})]\n    output_features = [text_feature(output_feature=True, decoder={\"type\": \"tagger\"})]\n    run_test_explainer_api(\n        IntegratedGradientsExplainer, MODEL_ECD, output_features, {}, tmpdir, input_features=input_features\n    )\n\n\n@pytest.mark.parametrize(\n    \"explainer_class,model_type\",\n    [\n        pytest.param(IntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_local\"),\n        pytest.param(RayIntegratedGradientsExplainer, MODEL_ECD, id=\"ecd_ray\", marks=pytest.mark.distributed),\n    ],\n)\n@pytest.mark.parametrize(\"encoder_type\", [\"embed\", \"rnn\", \"transformer\"])\ndef test_explainer_sequence_feature(explainer_class, model_type, encoder_type, tmpdir):\n    input_features = [sequence_feature()]\n    input_features[0][\"encoder\"] = {\"type\": encoder_type}\n    output_features = [binary_feature()]\n    run_test_explainer_api(explainer_class, model_type, output_features, {}, tmpdir, input_features=input_features)\n"
  },
  {
    "path": "tests/integration_tests/test_graph_execution.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pytest\n\nfrom tests.integration_tests.utils import (\n    category_feature,\n    generate_data,\n    generate_output_features_with_dependencies,\n    number_feature,\n    run_experiment,\n    sequence_feature,\n    set_feature,\n    text_feature,\n)\n\n\n@pytest.mark.parametrize(\n    \"output_features\",\n    [\n        # baseline test case\n        [\n            category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n            sequence_feature(decoder={\"vocab_size\": 10, \"max_len\": 5}),\n            number_feature(),\n        ],\n        # use generator as decoder\n        [\n            category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n            sequence_feature(decoder={\"vocab_size\": 10, \"max_len\": 5, \"type\": \"generator\"}),\n            number_feature(),\n        ],\n        # Generator decoder and reduce_input = None\n        [\n            category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n            sequence_feature(decoder={\"max_len\": 5, \"type\": \"generator\"}, reduce_input=None),\n            number_feature(normalization=\"minmax\"),\n        ],\n        # output features with dependencies single dependency\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\"]),\n        # output features with dependencies multiple dependencies\n        generate_output_features_with_dependencies(\"number_feature\", [\"category_feature\", \"sequence_feature\"]),\n    ],\n)\ndef test_experiment_multiple_seq_seq(csv_filename, output_features):\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 100, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(normalization=\"zscore\"),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = output_features\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n"
  },
  {
    "path": "tests/integration_tests/test_hyperopt.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport json\nimport os\nimport os.path\nimport uuid\n\nimport pytest\n\nfrom ludwig.backend import initialize_backend\nfrom ludwig.constants import (\n    ACCURACY,\n    AUTO,\n    BATCH_SIZE,\n    CATEGORY,\n    COMBINER,\n    EXECUTOR,\n    HYPEROPT,\n    INPUT_FEATURES,\n    MAX_CONCURRENT_TRIALS,\n    MODEL_ECD,\n    MODEL_TYPE,\n    NAME,\n    OUTPUT_FEATURES,\n    RAY,\n    TEXT,\n    TRAINER,\n    TYPE,\n    VALIDATION,\n)\nfrom ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME, MODEL_FILE_NAME\nfrom ludwig.hyperopt.results import HyperoptResults\nfrom ludwig.hyperopt.run import hyperopt\nfrom ludwig.hyperopt.utils import update_hyperopt_params_with_defaults\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils import fs_utils\nfrom ludwig.utils.data_utils import load_json, use_credentials\nfrom tests.integration_tests.utils import category_feature, generate_data, minio_test_creds, remote_tmpdir, text_feature\n\nray = pytest.importorskip(\"ray\")\n\nfrom ludwig.hyperopt.execution import get_build_hyperopt_executor, RayTuneExecutor  # noqa\n\npytestmark = [pytest.mark.distributed, pytest.mark.integration_tests_a]\n\nRANDOM_SEARCH_SIZE = 2\n\nHYPEROPT_CONFIG = {\n    \"parameters\": {\n        # using only float parameter as common in all search algorithms\n        \"trainer.learning_rate\": {\"space\": \"loguniform\", \"lower\": 0.001, \"upper\": 0.1},\n    },\n    \"goal\": \"minimize\",\n    \"executor\": {TYPE: \"ray\", \"num_samples\": 2, \"scheduler\": {TYPE: \"fifo\"}},\n    \"search_alg\": {TYPE: \"variant_generator\"},\n}\n\nSEARCH_ALGS_FOR_TESTING = [\n    # None,\n    # \"variant_generator\",\n    \"random\",\n    \"bohb\",\n    # \"hyperopt\",\n    # \"ax\",\n    # \"bayesopt\",\n    # \"blendsearch\",\n    # \"cfo\",\n    # \"dragonfly\",\n    # \"hebo\",\n    # \"skopt\",\n    # \"optuna\",\n]\n\nSCHEDULERS_FOR_TESTING = [\n    \"fifo\",\n    \"asynchyperband\",\n    # \"async_hyperband\",\n    # \"median_stopping_rule\",\n    # \"medianstopping\",\n    # \"hyperband\",\n    # \"hb_bohb\",\n    # \"pbt\",\n    # \"pb2\",  commented out for now: https://github.com/ray-project/ray/issues/24815\n    # \"resource_changing\",\n]\n\n\ndef _setup_ludwig_config(dataset_fp: str, model_type: str = MODEL_ECD) -> tuple[dict, str]:\n    input_features = [category_feature(encoder={\"vocab_size\": 3})]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n\n    rel_path = generate_data(input_features, output_features, dataset_fp, num_examples=6)\n\n    trainer_cfg = {\"learning_rate\": 0.001}\n    if model_type == MODEL_ECD:\n        trainer_cfg[\"epochs\"] = 2\n    else:\n        trainer_cfg[\"num_boost_round\"] = 2\n        # Disable feature filtering to avoid having no features due to small test dataset,\n        # see https://stackoverflow.com/a/66405983/5222402\n        trainer_cfg[\"feature_pre_filter\"] = False\n\n    config = {\n        MODEL_TYPE: model_type,\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\"},\n        TRAINER: trainer_cfg,\n    }\n\n    config = ModelConfig.from_dict(config).to_dict()\n\n    return config, rel_path\n\n\n@pytest.mark.parametrize(\"search_alg\", SEARCH_ALGS_FOR_TESTING)\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD])\ndef test_hyperopt_search_alg(\n    search_alg,\n    model_type,\n    csv_filename,\n    tmpdir,\n    ray_cluster_7cpu,\n    validate_output_feature=False,\n    validation_metric=None,\n    split=\"validation\",\n):\n    config, rel_path = _setup_ludwig_config(csv_filename, model_type)\n\n    hyperopt_config = HYPEROPT_CONFIG.copy()\n\n    # finalize hyperopt config settings\n    if search_alg == \"dragonfly\":\n        hyperopt_config[\"search_alg\"] = {\n            TYPE: search_alg,\n            \"domain\": \"euclidean\",\n            \"optimizer\": \"random\",\n        }\n    elif search_alg is None:\n        hyperopt_config[\"search_alg\"] = {}\n    else:\n        hyperopt_config[\"search_alg\"] = {\n            TYPE: search_alg,\n        }\n\n    if validate_output_feature:\n        hyperopt_config[\"output_feature\"] = config[OUTPUT_FEATURES][0][NAME]\n    if validation_metric:\n        hyperopt_config[\"validation_metric\"] = validation_metric\n\n    update_hyperopt_params_with_defaults(hyperopt_config)\n\n    backend = initialize_backend(\"local\")\n    if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:\n        hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)\n\n    parameters = hyperopt_config[\"parameters\"]\n    output_feature = hyperopt_config[\"output_feature\"]\n    metric = hyperopt_config[\"metric\"]\n    goal = hyperopt_config[\"goal\"]\n    executor = hyperopt_config[\"executor\"]\n    search_alg = hyperopt_config[\"search_alg\"]\n\n    hyperopt_executor = get_build_hyperopt_executor(RAY)(\n        parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor\n    )\n    results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir)\n    assert isinstance(results, HyperoptResults)\n\n    with hyperopt_executor._get_best_model_path(\n        results.experiment_analysis.best_trial, results.experiment_analysis\n    ) as path:\n        assert path is not None\n        assert isinstance(path, str)\n\n\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD])\ndef test_hyperopt_executor_with_metric(model_type, csv_filename, tmpdir, ray_cluster_7cpu):\n    test_hyperopt_search_alg(\n        \"variant_generator\",\n        model_type,\n        csv_filename,\n        tmpdir,\n        ray_cluster_7cpu,\n        validate_output_feature=True,\n        validation_metric=ACCURACY,\n    )\n\n\n@pytest.mark.parametrize(\"split\", [VALIDATION])\ndef test_hyperopt_with_split(split, csv_filename, tmpdir, ray_cluster_7cpu):\n    test_hyperopt_search_alg(\n        search_alg=\"variant_generator\",\n        model_type=MODEL_ECD,\n        csv_filename=csv_filename,\n        tmpdir=tmpdir,\n        ray_cluster_7cpu=ray_cluster_7cpu,\n        split=split,\n    )\n\n\n@pytest.mark.parametrize(\"scheduler\", SCHEDULERS_FOR_TESTING)\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD])\ndef test_hyperopt_scheduler(\n    scheduler, model_type, csv_filename, tmpdir, ray_cluster_7cpu, validate_output_feature=False, validation_metric=None\n):\n    config, rel_path = _setup_ludwig_config(csv_filename, model_type)\n\n    hyperopt_config = HYPEROPT_CONFIG.copy()\n\n    # finalize hyperopt config settings\n    if scheduler == \"pb2\":\n        # setup scheduler hyperparam_bounds parameter\n        min = hyperopt_config[\"parameters\"][\"trainer.learning_rate\"][\"lower\"]\n        max = hyperopt_config[\"parameters\"][\"trainer.learning_rate\"][\"upper\"]\n        hyperparam_bounds = {\n            \"trainer.learning_rate\": [min, max],\n        }\n        hyperopt_config[\"executor\"][\"scheduler\"] = {\n            TYPE: scheduler,\n            \"hyperparam_bounds\": hyperparam_bounds,\n        }\n    else:\n        hyperopt_config[\"executor\"][\"scheduler\"] = {\n            TYPE: scheduler,\n        }\n\n    if validate_output_feature:\n        hyperopt_config[\"output_feature\"] = config[OUTPUT_FEATURES][0][NAME]\n    if validation_metric:\n        hyperopt_config[\"validation_metric\"] = validation_metric\n\n    backend = initialize_backend(\"local\")\n    update_hyperopt_params_with_defaults(hyperopt_config)\n    if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:\n        hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)\n\n    parameters = hyperopt_config[\"parameters\"]\n    split = hyperopt_config[\"split\"]\n    output_feature = hyperopt_config[\"output_feature\"]\n    metric = hyperopt_config[\"metric\"]\n    goal = hyperopt_config[\"goal\"]\n    executor = hyperopt_config[\"executor\"]\n    search_alg = hyperopt_config[\"search_alg\"]\n\n    # TODO: Determine if we still need this if-then-else construct\n    if search_alg[TYPE] in {\"\"}:\n        with pytest.raises(ImportError):\n            get_build_hyperopt_executor(RAY)(\n                parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor\n            )\n    else:\n        hyperopt_executor = get_build_hyperopt_executor(RAY)(\n            parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor\n        )\n        raytune_results = hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir)\n        assert isinstance(raytune_results, HyperoptResults)\n\n\ndef _run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, backend, ray_cluster_7cpu):\n    input_features = [category_feature(encoder={\"vocab_size\": 3})]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\"},\n        TRAINER: {\"epochs\": 1, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        \"backend\": backend,\n    }\n\n    output_feature_name = output_features[0][NAME]\n\n    if search_space == \"random\":\n        # random search will be size of num_samples\n        search_parameters = {\n            \"trainer.learning_rate\": {\n                \"lower\": 0.0001,\n                \"upper\": 0.01,\n                \"space\": \"loguniform\",\n            },\n            output_feature_name\n            + \".decoder.fc_layers\": {\n                \"space\": \"choice\",\n                \"categories\": [\n                    [{\"output_size\": 8}, {\"output_size\": 4}],\n                    [{\"output_size\": 8}],\n                    [{\"output_size\": 4}],\n                ],\n            },\n            output_feature_name + \".decoder.fc_output_size\": {\"space\": \"choice\", \"categories\": [4, 8, 12]},\n        }\n    else:\n        # grid search space will be product each parameter size\n        search_parameters = {\n            \"trainer.learning_rate\": {\"space\": \"grid_search\", \"values\": [0.001, 0.01]},\n            output_feature_name + \".decoder.fc_output_size\": {\"space\": \"grid_search\", \"values\": [4, 8]},\n        }\n\n    hyperopt_configs = {\n        \"parameters\": search_parameters,\n        \"goal\": \"minimize\",\n        \"output_feature\": output_feature_name,\n        \"validation_metrics\": \"loss\",\n        \"executor\": {\n            TYPE: \"ray\",\n            \"num_samples\": 1 if search_space == \"grid\" else RANDOM_SEARCH_SIZE,\n            \"max_concurrent_trials\": 1,\n        },\n        \"search_alg\": {TYPE: \"variant_generator\"},\n    }\n\n    # add hyperopt parameter space to the config\n    config[HYPEROPT] = hyperopt_configs\n\n    experiment_name = f\"test_hyperopt_{uuid.uuid4().hex}\"\n    hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)\n    if search_space == \"random\":\n        assert hyperopt_results.experiment_analysis.results_df.shape[0] == RANDOM_SEARCH_SIZE\n    else:\n        # compute size of search space for grid search\n        grid_search_size = 1\n        for k, v in search_parameters.items():\n            grid_search_size *= len(v[\"values\"])\n        assert hyperopt_results.experiment_analysis.results_df.shape[0] == grid_search_size\n\n    # check for return results\n    assert isinstance(hyperopt_results, HyperoptResults)\n\n    # check for existence of the hyperopt statistics file\n    with use_credentials(minio_test_creds()):\n        assert fs_utils.path_exists(os.path.join(tmpdir, experiment_name, HYPEROPT_STATISTICS_FILE_NAME))\n        for trial in hyperopt_results.experiment_analysis.trials:\n            assert fs_utils.path_exists(\n                os.path.join(tmpdir, experiment_name, f\"trial_{trial.trial_id}\"),\n            )\n\n    # Verify best trial has a valid checkpoint\n    best_trial = hyperopt_results.experiment_analysis.best_trial\n    assert best_trial is not None\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\"search_space\", [\"random\", \"grid\"])\ndef test_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, ray_cluster_7cpu):\n    _run_hyperopt_run_hyperopt(csv_filename, search_space, tmpdir, \"local\", ray_cluster_7cpu)\n\n\n@pytest.mark.xfail(\n    reason=\"PyArrow S3 C++ client uses chunked transfer encoding for multipart uploads, \"\n    \"which MinIO rejects with HTTP 411 MissingContentLength. Requires real AWS S3.\",\n    strict=False,\n)\ndef test_hyperopt_sync_remote(csv_filename, ray_cluster_7cpu, monkeypatch):\n    \"\"\"Test hyperopt with remote S3 (MinIO) storage for trial results.\"\"\"\n    # Override AWS env vars so PyArrow's S3 client (used by Ray Tune internally)\n    # connects to MinIO instead of real AWS S3\n    minio_endpoint = os.environ.get(\"LUDWIG_MINIO_ENDPOINT\", \"http://localhost:9000\")\n    monkeypatch.setenv(\"AWS_ACCESS_KEY_ID\", os.environ.get(\"LUDWIG_MINIO_ACCESS_KEY\", \"minio\"))\n    monkeypatch.setenv(\"AWS_SECRET_ACCESS_KEY\", os.environ.get(\"LUDWIG_MINIO_SECRET_KEY\", \"minio123\"))\n    monkeypatch.setenv(\"AWS_ENDPOINT_URL\", minio_endpoint)\n    monkeypatch.setenv(\"AWS_EC2_METADATA_DISABLED\", \"true\")\n\n    backend = {\n        \"type\": \"local\",\n        \"credentials\": {\n            \"artifacts\": minio_test_creds(),\n        },\n    }\n\n    with remote_tmpdir(\"s3\", \"test\") as tmpdir:\n        _run_hyperopt_run_hyperopt(\n            csv_filename,\n            \"random\",\n            tmpdir,\n            backend,\n            ray_cluster_7cpu,\n        )\n\n\ndef test_hyperopt_with_feature_specific_parameters(csv_filename, tmpdir, ray_cluster_7cpu):\n    input_features = [\n        text_feature(name=\"utterance\", reduce_output=\"sum\"),\n        category_feature(vocab_size=3),\n    ]\n\n    output_features = [category_feature(vocab_size=3, output_feature=True)]\n\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    filter_size_search_space = [5, 7]\n    embedding_size_search_space = [4, 8, 12]\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\", \"num_fc_layers\": 2},\n        TRAINER: {\"epochs\": 1, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        HYPEROPT: {\n            \"parameters\": {\n                input_features[0][NAME]\n                + \".encoder.filter_size\": {\"space\": \"choice\", \"categories\": filter_size_search_space},\n                input_features[1][NAME]\n                + \".encoder.embedding_size\": {\"space\": \"choice\", \"categories\": embedding_size_search_space},\n            },\n            \"goal\": \"minimize\",\n            \"output_feature\": output_features[0][NAME],\n            \"validation_metrics\": \"loss\",\n            \"executor\": {TYPE: \"ray\", \"num_samples\": 1},\n            \"search_alg\": {TYPE: \"variant_generator\"},\n        },\n    }\n\n    hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=\"test_hyperopt\")\n    hyperopt_results_df = hyperopt_results.experiment_analysis.results_df\n\n    model_parameters = json.load(\n        open(\n            os.path.join(\n                hyperopt_results_df.iloc[0][\"trial_dir\"],\n                \"test_hyperopt_run\",\n                MODEL_FILE_NAME,\n                \"model_hyperparameters.json\",\n            )\n        )\n    )\n\n    for input_feature in model_parameters[INPUT_FEATURES]:\n        if input_feature[TYPE] == TEXT:\n            assert input_feature[\"encoder\"][\"filter_size\"] in filter_size_search_space\n        elif input_feature[TYPE] == CATEGORY:\n            assert input_feature[\"encoder\"][\"embedding_size\"] in embedding_size_search_space\n\n\ndef test_hyperopt_old_config(csv_filename, tmpdir, ray_cluster_7cpu):\n    old_config = {\n        \"ludwig_version\": \"0.4\",\n        INPUT_FEATURES: [\n            {\"name\": \"cat1\", TYPE: \"category\", \"encoder\": {\"vocab_size\": 2}},\n            {\"name\": \"num1\", TYPE: \"number\"},\n        ],\n        OUTPUT_FEATURES: [\n            {\"name\": \"bin1\", TYPE: \"binary\"},\n        ],\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        HYPEROPT: {\n            EXECUTOR: {\n                TYPE: \"ray\",\n                \"time_budget_s\": 200,\n                \"cpu_resources_per_trial\": 1,\n            },\n            \"sampler\": {\n                TYPE: \"ray\",\n                \"scheduler\": {\n                    TYPE: \"async_hyperband\",\n                    \"max_t\": 200,\n                    \"time_attr\": \"time_total_s\",\n                    \"grace_period\": 72,\n                    \"reduction_factor\": 5,\n                },\n                \"search_alg\": {\n                    TYPE: \"variant_generator\",\n                },\n                \"num_samples\": 2,\n            },\n            \"parameters\": {\n                \"trainer.batch_size\": {\n                    \"space\": \"choice\",\n                    \"categories\": [64, 128, 256],\n                },\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.001,\n                    \"upper\": 0.1,\n                },\n            },\n        },\n    }\n\n    input_features = old_config[INPUT_FEATURES]\n    output_features = old_config[OUTPUT_FEATURES]\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    hyperopt(old_config, dataset=rel_path, output_directory=tmpdir, experiment_name=\"test_hyperopt\")\n\n\ndef test_hyperopt_nested_parameters(csv_filename, tmpdir, ray_cluster_7cpu):\n    config = {\n        INPUT_FEATURES: [\n            {\"name\": \"cat1\", TYPE: \"category\", \"encoder\": {\"vocab_size\": 2}},\n            {\"name\": \"num1\", TYPE: \"number\"},\n        ],\n        OUTPUT_FEATURES: [\n            {\"name\": \"bin1\", TYPE: \"binary\"},\n        ],\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        HYPEROPT: {\n            EXECUTOR: {\n                TYPE: \"ray\",\n                \"time_budget_s\": 200,\n                \"cpu_resources_per_trial\": 1,\n                \"num_samples\": 2,\n                \"scheduler\": {TYPE: \"fifo\"},\n            },\n            \"search_alg\": {TYPE: \"variant_generator\"},\n            \"parameters\": {\n                \".\": {\n                    \"space\": \"choice\",\n                    \"categories\": [\n                        {\n                            \"combiner\": {\n                                \"type\": \"tabnet\",\n                                \"bn_virtual_bs\": 32,\n                            },\n                            \"trainer\": {\n                                \"learning_rate_scaling\": \"sqrt\",\n                                \"learning_rate_scheduler\": {\n                                    \"decay\": \"exponential\",\n                                    \"decay_steps\": 20000,\n                                    \"decay_rate\": 0.8,\n                                },\n                                \"optimizer\": {\"type\": \"adam\"},\n                            },\n                        },\n                        {\n                            \"combiner\": {\"type\": \"concat\"},\n                            \"trainer\": {\"learning_rate_scaling\": \"linear\"},\n                        },\n                    ],\n                },\n                \"trainer.learning_rate\": {\"space\": \"choice\", \"categories\": [0.7, 0.42]},\n            },\n        },\n    }\n\n    input_features = config[INPUT_FEATURES]\n    output_features = config[OUTPUT_FEATURES]\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    results = hyperopt(\n        config,\n        dataset=rel_path,\n        output_directory=tmpdir,\n        experiment_name=\"test_hyperopt_nested_params\",\n    )\n\n    results_df = results.experiment_analysis.results_df\n    assert len(results_df) == 2\n\n    for _, trial_meta in results_df.iterrows():\n        trial_dir = trial_meta[\"trial_dir\"]\n        trial_config = load_json(\n            os.path.join(trial_dir, \"test_hyperopt_nested_params_run\", MODEL_FILE_NAME, \"model_hyperparameters.json\")\n        )\n\n        assert len(trial_config[INPUT_FEATURES]) == len(config[INPUT_FEATURES])\n        assert len(trial_config[OUTPUT_FEATURES]) == len(config[OUTPUT_FEATURES])\n\n        assert trial_config[COMBINER][TYPE] in {\"tabnet\", \"concat\"}\n        if trial_config[COMBINER][TYPE] == \"tabnet\":\n            assert trial_config[COMBINER][\"bn_virtual_bs\"] == 32\n            assert trial_config[TRAINER][\"learning_rate_scaling\"] == \"sqrt\"\n            assert trial_config[TRAINER][\"learning_rate_scheduler\"][\"decay\"] == \"exponential\"\n            assert trial_config[TRAINER][\"learning_rate_scheduler\"][\"decay_steps\"] == 20000\n            assert trial_config[TRAINER][\"learning_rate_scheduler\"][\"decay_rate\"] == 0.8\n            assert trial_config[TRAINER][\"optimizer\"][\"type\"] == \"adam\"\n        else:\n            assert trial_config[TRAINER][\"learning_rate_scaling\"] == \"linear\"\n\n        assert trial_config[TRAINER][\"learning_rate\"] in {0.7, 0.42}\n\n\n@pytest.mark.slow\ndef test_hyperopt_without_config_defaults(csv_filename, tmpdir, ray_cluster_7cpu):\n    input_features = [category_feature(encoder={\"vocab_size\": 3})]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\"},\n        TRAINER: {\"train_steps\": 5, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        # Missing search_alg and executor, but should still work\n        HYPEROPT: {\n            \"parameters\": {\n                \"trainer.learning_rate\": {\n                    \"lower\": 0.0001,\n                    \"upper\": 0.01,\n                    \"space\": \"loguniform\",\n                }\n            },\n            \"goal\": \"minimize\",\n            \"output_feature\": output_features[0][\"name\"],\n            \"metric\": \"loss\",\n            \"executor\": {\"type\": \"ray\", \"num_samples\": 2},\n        },\n    }\n\n    experiment_name = f\"test_hyperopt_{uuid.uuid4().hex}\"\n    hyperopt_results = hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)\n    assert hyperopt_results.experiment_analysis.results_df.shape[0] == 2\n\n\n@pytest.mark.slow\ndef test_hyperopt_with_time_budget(csv_filename, tmpdir, ray_cluster_7cpu):\n    \"\"\"Tests that incomplete checkpoints created by RayTune when time budget is hit doesn't throw errors because of\n    missing .tune_metadata files in the checkpoint directories.\"\"\"\n    input_features = [text_feature()]\n    output_features = [category_feature(output_feature=True)]\n\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=6)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\"},\n        HYPEROPT: {\n            \"goal\": \"minimize\",\n            \"metric\": \"loss\",\n            \"output_feature\": output_features[0][\"name\"],\n            \"search_alg\": {TYPE: \"variant_generator\"},\n            \"executor\": {\n                \"type\": \"ray\",\n                # Ensure there is enough time for some trials to start and also for some to terminate\n                # to reproduce the exact issue of missing .tune_metadata files.\n                \"time_budget_s\": 30,\n                \"cpu_resources_per_trial\": 1,\n                \"num_samples\": 4,\n                \"scheduler\": {TYPE: \"fifo\"},\n            },\n            \"parameters\": {\n                \"trainer.learning_rate\": {\n                    \"lower\": 0.0001,\n                    \"upper\": 0.01,\n                    \"space\": \"loguniform\",\n                }\n            },\n        },\n    }\n\n    experiment_name = f\"test_hyperopt_{uuid.uuid4().hex}\"\n    hyperopt(config, dataset=rel_path, output_directory=tmpdir, experiment_name=experiment_name)\n"
  },
  {
    "path": "tests/integration_tests/test_hyperopt_ray.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport json\nimport logging\nimport os.path\n\nimport mlflow\nimport pandas as pd\nimport pytest\nfrom mlflow.tracking import MlflowClient\n\nfrom ludwig.backend import initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import ACCURACY, AUTO, BATCH_SIZE, EXECUTOR, MAX_CONCURRENT_TRIALS, TRAINER\nfrom ludwig.contribs.mlflow import MlflowCallback\nfrom ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\nfrom ludwig.hyperopt.results import HyperoptResults\nfrom ludwig.hyperopt.run import hyperopt\nfrom ludwig.hyperopt.utils import update_hyperopt_params_with_defaults\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.automl.utils import get_model_type\nfrom tests.integration_tests.utils import category_feature, generate_data, text_feature\n\ntry:\n    import ray\n    from ray.tune import Callback as TuneCallback\n    from ray.tune.experiment.trial import Trial\n\n    from ludwig.hyperopt.execution import get_build_hyperopt_executor\nexcept ImportError:\n    ray = None\n    Trial = None\n    TuneCallback = object  # needed to set up HyperoptTestCallback when not distributed\n\npytestmark = pytest.mark.integration_tests_d\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nHYPEROPT_CONFIG = {\n    \"parameters\": {\n        \"trainer.learning_rate\": {\n            \"space\": \"loguniform\",\n            \"lower\": 0.001,\n            \"upper\": 0.1,\n        },\n        \"combiner.num_fc_layers\": {\"space\": \"randint\", \"lower\": 0, \"upper\": 2},\n        \"utterance.encoder.norm\": {\"space\": \"grid_search\", \"values\": [\"layer\", \"batch\"]},\n        \"utterance.encoder.fc_layers\": {\n            \"space\": \"choice\",\n            \"categories\": [\n                [{\"output_size\": 16}, {\"output_size\": 8}],\n                [{\"output_size\": 16}],\n                [{\"output_size\": 8}],\n            ],\n        },\n    },\n    \"goal\": \"minimize\",\n}\n\n\nSCENARIOS = [\n    {\"executor\": {\"type\": \"ray\"}, \"search_alg\": {\"type\": \"variant_generator\"}},\n    {\"executor\": {\"type\": \"ray\", \"num_samples\": 2}, \"search_alg\": {\"type\": \"variant_generator\"}},\n    {\n        \"executor\": {\n            \"type\": \"ray\",\n            \"num_samples\": 3,\n            \"scheduler\": {\n                \"type\": \"hb_bohb\",\n                \"time_attr\": \"training_iteration\",\n                \"reduction_factor\": 4,\n                \"max_t\": 2,\n            },\n        },\n        \"search_alg\": {\"type\": \"bohb\"},\n    },\n]\n\n\ndef _get_config(search_alg: dict, executor: dict, epochs: int):\n    input_features = [\n        text_feature(name=\"utterance\", encoder={\"cell_type\": \"lstm\", \"reduce_output\": \"sum\"}),\n        category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\", output_feature=True)]\n\n    return {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": epochs, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        \"hyperopt\": {\n            **HYPEROPT_CONFIG,\n            \"executor\": executor,\n            \"search_alg\": search_alg,\n        },\n    }\n\n\nclass HyperoptTestCallback(TuneCallback):\n    def __init__(self, exp_name: str, model_type: str):\n        self.exp_name = exp_name\n        self.model_type = model_type\n        self.trial_ids = set()\n        self.trial_status = {}\n        self.user_config = {}\n        self.rendered_config = {}\n\n    def on_trial_start(self, iteration: int, trials: list[\"Trial\"], trial: \"Trial\", **info):\n        super().on_trial_start(iteration, trials, trial, **info)\n        self.trial_ids.add(trial.trial_id)\n\n    def on_trial_complete(self, iteration: int, trials: list[\"Trial\"], trial: \"Trial\", **info):  # noqa\n        super().on_trial_complete(iteration, trials, trial, **info)\n        self.trial_status[trial.trial_id] = trial.status\n\n        model_hyperparameters = os.path.join(\n            trial.local_path, f\"{self.exp_name}_{self.model_type}\", MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\n        )\n        if os.path.isfile(model_hyperparameters):\n            try:\n                with open(model_hyperparameters) as f:\n                    config = json.load(f)\n                    assert config, f\"Trial {trial} rendered config was empty.\"\n                self.rendered_config[trial.trial_id] = True\n            except OSError:\n                logging.exception(\"Could not load rendered config from trial logdir.\")\n\n        model_hyperparameters = os.path.join(trial.local_path, \"trial_hyperparameters.json\")\n        if os.path.isfile(model_hyperparameters):\n            try:\n                with open(model_hyperparameters) as f:\n                    config = json.load(f)\n                    assert config, \"Trial {trial} user config was empty.\"\n                self.rendered_config[trial.trial_id] = True\n            except OSError:\n                logging.exception(\"Could not load rendered config from trial logdir.\")\n\n\ndef run_hyperopt_executor(\n    search_alg,\n    executor,\n    epochs,\n    csv_filename,\n    tmpdir,\n    validate_output_feature=False,\n    validation_metric=None,\n    use_split=True,\n):\n    config = _get_config(search_alg, executor, epochs)\n    rel_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n\n    if not use_split:\n        df = pd.read_csv(rel_path)\n        df[\"split\"] = 0\n        df.to_csv(rel_path)\n\n    config = ModelConfig.from_dict(config).to_dict()\n\n    hyperopt_config = config[\"hyperopt\"]\n\n    if validate_output_feature:\n        hyperopt_config[\"output_feature\"] = config[\"output_features\"][0][\"name\"]\n    if validation_metric:\n        hyperopt_config[\"validation_metric\"] = validation_metric\n\n    backend = initialize_backend(\"local\")\n    update_hyperopt_params_with_defaults(hyperopt_config)\n    if hyperopt_config[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:\n        hyperopt_config[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config)\n\n    parameters = hyperopt_config[\"parameters\"]\n    if search_alg.get(\"type\", \"\") == \"bohb\":\n        # bohb does not support grid_search search space\n        del parameters[\"utterance.encoder.norm\"]\n        hyperopt_config[\"parameters\"] = parameters\n\n    split = hyperopt_config[\"split\"]\n    output_feature = hyperopt_config[\"output_feature\"]\n    metric = hyperopt_config[\"metric\"]\n    goal = hyperopt_config[\"goal\"]\n    search_alg = hyperopt_config[\"search_alg\"]\n    executor = hyperopt_config[\"executor\"]\n\n    hyperopt_executor = get_build_hyperopt_executor(executor[\"type\"])(\n        parameters, output_feature, metric, goal, split, search_alg=search_alg, **executor\n    )\n\n    hyperopt_executor.execute(config, dataset=rel_path, output_directory=tmpdir, backend=backend)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"scenario\", SCENARIOS)\ndef test_hyperopt_executor(scenario, csv_filename, tmpdir, ray_cluster_4cpu):\n    search_alg = scenario[\"search_alg\"]\n    executor = scenario[\"executor\"]\n    scheduler = executor.get(\"scheduler\", {})\n    if scheduler.get(\"type\") == \"hb_bohb\":\n        # When using the hb_bohb scheduler, num_epochs must equal max_t\n        epochs = scheduler.get(\"max_t\", 81)\n    else:\n        epochs = 1\n    run_hyperopt_executor(search_alg, executor, epochs, csv_filename, tmpdir)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"use_split\", [True, False], ids=[\"split\", \"no_split\"])\ndef test_hyperopt_executor_with_metric(use_split, csv_filename, tmpdir, ray_cluster_4cpu):\n    run_hyperopt_executor(\n        {\"type\": \"variant_generator\"},  # search_alg\n        {\"type\": \"ray\", \"num_samples\": 2},  # executor\n        1,\n        csv_filename,\n        tmpdir,\n        validate_output_feature=True,\n        validation_metric=ACCURACY,\n        use_split=use_split,\n    )\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        \"local\",\n        pytest.param(\"ray\", marks=pytest.mark.xfail(reason=\"Nested Ray actors exceed 4-CPU CI cluster resources\")),\n    ],\n)\ndef test_hyperopt_run_hyperopt(csv_filename, backend, tmpdir, ray_cluster_4cpu):\n    input_features = [\n        text_feature(name=\"utterance\", encoder={\"cell_type\": \"lstm\", \"reduce_output\": \"sum\"}),\n        category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\", output_feature=True)]\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"train_steps\": 3, \"learning_rate\": 0.001, BATCH_SIZE: 128},\n        \"backend\": {\n            \"type\": backend,\n        },\n    }\n\n    output_feature_name = output_features[0][\"name\"]\n\n    hyperopt_configs = {\n        \"parameters\": {\n            \"trainer.learning_rate\": {\n                \"space\": \"loguniform\",\n                \"lower\": 0.001,\n                \"upper\": 0.1,\n            },\n            output_feature_name + \".decoder.fc_output_size\": {\"space\": \"randint\", \"lower\": 8, \"upper\": 16},\n            output_feature_name + \".decoder.num_fc_layers\": {\"space\": \"randint\", \"lower\": 0, \"upper\": 1},\n        },\n        \"goal\": \"minimize\",\n        \"output_feature\": output_feature_name,\n        \"validation_metrics\": \"loss\",\n        \"executor\": {\n            \"type\": \"ray\",\n            \"num_samples\": 2,\n            \"cpu_resources_per_trial\": 1,\n            \"max_concurrent_trials\": 1,\n        },\n        \"search_alg\": {\"type\": \"variant_generator\"},\n    }\n\n    @ray.remote(num_cpus=0)\n    class Event:\n        def __init__(self):\n            self._set = False\n\n        def is_set(self):\n            return self._set\n\n        def set(self):\n            self._set = True\n\n    # Used to trigger a cancel event in the trial, which should subsequently be retried\n    event = Event.remote()\n\n    class CancelCallback(Callback):\n        def on_epoch_start(self, trainer, progress_tracker, save_path: str):\n            if progress_tracker.epoch == 1 and not ray.get(event.is_set.remote()):\n                ray.get(event.set.remote())\n                raise KeyboardInterrupt()\n\n    # add hyperopt parameter space to the config\n    config[\"hyperopt\"] = hyperopt_configs\n\n    # run for one epoch, then cancel, then resume from where we left off\n    run_hyperopt(config, rel_path, tmpdir, callbacks=[CancelCallback()])\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_hyperopt_ray_mlflow(csv_filename, tmpdir, ray_cluster_4cpu):\n    mlflow_uri = f\"file://{tmpdir}/mlruns\"\n    mlflow.set_tracking_uri(mlflow_uri)\n    client = MlflowClient(tracking_uri=mlflow_uri)\n\n    num_samples = 2\n    config = _get_config(\n        {\"type\": \"variant_generator\"},  # search_alg\n        {\"type\": \"ray\", \"num_samples\": num_samples},  # executor\n        1,  # epochs\n    )\n\n    rel_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n\n    exp_name = \"mlflow_test\"\n    run_hyperopt(config, rel_path, tmpdir, experiment_name=exp_name, callbacks=[MlflowCallback(mlflow_uri)])\n\n    experiment = client.get_experiment_by_name(exp_name)\n    assert experiment is not None\n\n    runs = client.search_runs([experiment.experiment_id])\n    assert len(runs) > 0\n\n    for run in runs:\n        artifacts = [f.path for f in client.list_artifacts(run.info.run_id, \"\")]\n        assert \"config.yaml\" in artifacts\n        assert MODEL_FILE_NAME in artifacts\n\n\ndef run_hyperopt(\n    config,\n    rel_path,\n    tmpdir,\n    experiment_name=\"ray_hyperopt\",\n    callbacks=None,\n):\n    tune_test_callback = HyperoptTestCallback(experiment_name, get_model_type(config))\n\n    hyperopt_results = hyperopt(\n        config,\n        dataset=rel_path,\n        output_directory=tmpdir,\n        experiment_name=experiment_name,\n        callbacks=callbacks,\n        tune_callbacks=[tune_test_callback],\n    )\n\n    # check for return results\n    assert isinstance(hyperopt_results, HyperoptResults)\n\n    # check for existence of the hyperopt statistics file\n    assert os.path.isfile(os.path.join(tmpdir, experiment_name, HYPEROPT_STATISTICS_FILE_NAME))\n\n    # check for evidence that the HyperoptTestCallback was active\n    assert len(tune_test_callback.trial_ids) > 0\n    for t in tune_test_callback.trial_ids:\n        if tune_test_callback.trial_status.get(t) == \"terminated\":\n            assert tune_test_callback.user_config[t].get()\n            assert tune_test_callback.rendered_config[t].get()\n"
  },
  {
    "path": "tests/integration_tests/test_input_feature_tied.py",
    "content": "from collections import namedtuple\n\nimport pytest\n\nfrom ludwig.models.base import BaseModel\nfrom ludwig.schema.model_config import ModelConfig\nfrom tests.integration_tests.utils import (\n    category_feature,\n    generate_data,\n    number_feature,\n    run_experiment,\n    sequence_feature,\n    text_feature,\n)\n\n# InputFeatureOptions namedtuple structure:\n# feature_type: input feature type, e.g., number, category, etc.\n# feature_options: None or dictionary of required input feature specification\n# tie_features: boolean, True to tie features, False not to tie features\nInputFeatureOptions = namedtuple(\"InputFeatureOptions\", \"feature_type feature_options tie_features\")\n\n\n# micro level test confirms the encoders for tied input features are sharing\n# the same encoder.  Include negative tests to confirm untied input features\n# do not share the same encoder.\n# note: vocab parameter, below, is made up to facilitate creating input encoders\n@pytest.mark.parametrize(\n    \"input_feature_options\",\n    [\n        # tie input features, encoders should be the same\n        InputFeatureOptions(\"number\", {\"encoder\": {\"type\": \"passthrough\"}}, True),\n        InputFeatureOptions(\n            \"number\", {\"encoder\": {\"type\": \"passthrough\"}, \"preprocessing\": {\"normalization\": \"zscore\"}}, True\n        ),\n        InputFeatureOptions(\"binary\", {\"encoder\": {\"type\": \"passthrough\"}}, True),\n        InputFeatureOptions(\"category\", {\"encoder\": {\"type\": \"dense\", \"vocab\": [\"a\", \"b\", \"c\"]}}, True),\n        InputFeatureOptions(\"set\", {\"encoder\": {\"type\": \"embed\", \"vocab\": [\"a\", \"b\", \"c\"]}}, True),\n        InputFeatureOptions(\n            \"sequence\", {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"vocab\": [\"x\", \"y\", \"z\"]}}, True\n        ),\n        InputFeatureOptions(\n            \"text\", {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"vocab\": [\"a\", \"b\", \"c\"]}}, True\n        ),\n        InputFeatureOptions(\n            \"timeseries\", {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"should_embed\": False}}, True\n        ),\n        InputFeatureOptions(\n            \"audio\",\n            {\n                \"encoder\": {\n                    \"type\": \"parallel_cnn\",\n                    \"embedding_size\": 64,\n                    \"max_sequence_length\": 16,\n                    \"should_embed\": False,\n                }\n            },\n            True,\n        ),\n        # do not tie input features, encoders should be different\n        InputFeatureOptions(\"number\", {\"encoder\": {\"type\": \"passthrough\"}}, False),\n        InputFeatureOptions(\n            \"number\", {\"encoder\": {\"type\": \"passthrough\"}, \"preprocessing\": {\"normalization\": \"zscore\"}}, False\n        ),\n        InputFeatureOptions(\"binary\", {\"encoder\": {\"type\": \"passthrough\"}}, False),\n        InputFeatureOptions(\"category\", {\"encoder\": {\"type\": \"dense\", \"vocab\": [\"a\", \"b\", \"c\"]}}, False),\n        InputFeatureOptions(\"set\", {\"encoder\": {\"type\": \"embed\", \"vocab\": [\"a\", \"b\", \"c\"]}}, False),\n        InputFeatureOptions(\n            \"sequence\",\n            {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"vocab\": [\"x\", \"y\", \"z\"]}},\n            False,\n        ),\n        InputFeatureOptions(\n            \"text\", {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"vocab\": [\"a\", \"b\", \"c\"]}}, False\n        ),\n        InputFeatureOptions(\n            \"timeseries\", {\"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10, \"should_embed\": False}}, False\n        ),\n        InputFeatureOptions(\n            \"audio\",\n            {\n                \"encoder\": {\n                    \"type\": \"parallel_cnn\",\n                    \"embedding_size\": 64,\n                    \"max_sequence_length\": 16,\n                    \"should_embed\": False,\n                }\n            },\n            False,\n        ),\n    ],\n)\ndef test_tied_micro_level(input_feature_options):\n    # build input feature config\n    input_feature_configs = list()\n\n    input_feature_configs.append({\"name\": \"input_feature_1\", \"type\": input_feature_options.feature_type})\n    input_feature_configs[0].update(input_feature_options.feature_options)\n\n    input_feature_configs.append({\"name\": \"input_feature_2\", \"type\": input_feature_options.feature_type})\n    input_feature_configs[1].update(input_feature_options.feature_options)\n\n    # add tied option to the second feature\n    if input_feature_options.tie_features:\n        input_feature_configs[1][\"tied\"] = \"input_feature_1\"\n\n    config_obj = ModelConfig.from_dict(\n        {\"input_features\": input_feature_configs, \"output_features\": [{\"name\": \"dummy_feature\", \"type\": \"binary\"}]}\n    )\n\n    input_features = BaseModel.build_inputs(input_feature_configs=config_obj.input_features)\n\n    if input_feature_options.tie_features:\n        # should be same encoder\n        assert input_features[\"input_feature_1\"].encoder_obj is input_features[\"input_feature_2\"].encoder_obj\n    else:\n        # no tied parameter, encoders should be different\n        assert input_features[\"input_feature_1\"].encoder_obj is not input_features[\"input_feature_2\"].encoder_obj\n\n\n# TiedUseCase namedtuple structure:\n# input_feature: Ludwig synthetic data creation function.\n# output_feature: Ludwig synthetic data creation function\nTiedUseCase = namedtuple(\"TiedUseCase\", \"input_feature output_feature\")\n\n\n# Macro level test ensures no exceptions are raised during a full_experiment()\n@pytest.mark.parametrize(\n    \"tied_use_case\",\n    [\n        TiedUseCase(number_feature, number_feature),\n        TiedUseCase(text_feature, category_feature),\n        TiedUseCase(sequence_feature, sequence_feature),\n    ],\n)\ndef test_tied_macro_level(tied_use_case: TiedUseCase, csv_filename: str):\n    input_features = [\n        number_feature(),  # Other feature\n        tied_use_case.input_feature(),  # first feature to be tied\n        tied_use_case.input_feature(),  # second feature to be tied\n        category_feature(),  # other feature\n    ]\n    # tie second feature to first feature\n    input_features[2][\"tied\"] = input_features[1][\"name\"]\n\n    # setup output feature\n    output_features = [tied_use_case.output_feature(output_feature=True)]\n\n    # Generate test data and run full_experiment\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n"
  },
  {
    "path": "tests/integration_tests/test_kfold_cv.py",
    "content": "import logging\nimport os\nimport os.path\nfrom collections import namedtuple\n\nimport pytest\nimport yaml\n\nfrom ludwig.api import kfold_cross_validate\nfrom ludwig.constants import BATCH_SIZE, TRAINER\nfrom ludwig.experiment import kfold_cross_validate_cli\nfrom ludwig.utils.data_utils import load_json\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    create_data_set_to_use,\n    generate_data,\n    number_feature,\n    sequence_feature,\n    text_feature,\n)\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nFeaturesToUse = namedtuple(\"FeaturesToUse\", \"input_features output_features\")\n\nFEATURES_TO_TEST = [\n    FeaturesToUse(\n        # input feature\n        [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")],\n        # output feature\n        [number_feature()],\n    ),\n    FeaturesToUse(\n        # input feature\n        [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")],\n        # output feature\n        [binary_feature()],\n    ),\n    FeaturesToUse(\n        # input feature\n        [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")],\n        # output feature\n        [category_feature(decoder={\"vocab_size\": 4}, reduce_input=\"sum\", output_feature=True)],\n    ),\n    FeaturesToUse(\n        # input feature\n        # [sequence_feature(min_len=5, max_len=10, encoder=\"rnn\", cell_type=\"lstm\", reduce_output=None)],\n        [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")],\n        # output feature\n        [\n            sequence_feature(\n                decoder={\n                    \"min_len\": 5,\n                    \"max_len\": 10,\n                    \"type\": \"generator\",\n                    \"cell_type\": \"lstm\",\n                    \"attention\": \"bahdanau\",\n                },\n                reduce_input=None,\n                output_feature=True,\n            )\n        ],\n    ),\n    FeaturesToUse(\n        # input feature\n        [\n            sequence_feature(\n                encoder={\"min_len\": 5, \"max_len\": 10, \"type\": \"rnn\", \"cell_type\": \"lstm\", \"reduce_output\": None}\n            )\n        ],\n        # output feature\n        [sequence_feature(decoder={\"max_len\": 10, \"type\": \"tagger\"}, reduce_input=None, output_feature=True)],\n    ),\n    FeaturesToUse(\n        # input feature\n        [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")],\n        # output feature\n        [text_feature(output_feature=True)],\n    ),\n]\n\n\n@pytest.mark.parametrize(\"features_to_use\", FEATURES_TO_TEST)\ndef test_kfold_cv_cli(tmpdir, features_to_use: FeaturesToUse):\n    # k-fold cross validation cli\n    num_folds = 3\n\n    training_data_fp = os.path.join(tmpdir, \"train.csv\")\n    config_fp = os.path.join(tmpdir, \"config.yaml\")\n    results_dir = os.path.join(tmpdir, \"results\")\n    statistics_fp = os.path.join(results_dir, \"kfold_training_statistics.json\")\n    indices_fp = os.path.join(results_dir, \"kfold_split_indices.json\")\n\n    # generate synthetic data for the test\n    input_features = features_to_use.input_features\n\n    output_features = features_to_use.output_features\n\n    generate_data(input_features, output_features, training_data_fp)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    with open(config_fp, \"w\") as f:\n        yaml.dump(config, f)\n\n    # run k-fold cv\n    kfold_cross_validate_cli(\n        k_fold=num_folds,\n        config=config_fp,\n        dataset=training_data_fp,\n        output_directory=results_dir,\n        logging_level=\"warn\",\n    )\n\n    # check for expected results\n    # check for existence and structure of statistics file\n    assert os.path.isfile(statistics_fp)\n\n    # check for required keys\n    cv_statistics = load_json(statistics_fp)\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)] + [\"overall\"]:\n        assert key in cv_statistics\n\n    # check for existence and structure of split indices file\n    assert os.path.isfile(indices_fp)\n\n    # check for required keys\n    cv_indices = load_json(indices_fp)\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)]:\n        assert key in cv_indices\n\n\ndef test_kfold_cv_api_from_file(tmpdir):\n    # k-fold_cross_validate api with config file\n    num_folds = 3\n\n    # setup required data structures for test\n    training_data_fp = os.path.join(tmpdir, \"train.csv\")\n    config_fp = os.path.join(tmpdir, \"config.yaml\")\n\n    # generate synthetic data for the test\n    input_features = [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")]\n\n    output_features = [category_feature(decoder={\"vocab_size\": 3}, reduce_input=\"sum\")]\n\n    generate_data(input_features, output_features, training_data_fp)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    with open(config_fp, \"w\") as f:\n        yaml.dump(config, f)\n\n    # test kfold_cross_validate api with config file\n\n    # execute k-fold cross validation run\n    kfold_cv_stats, kfold_split_indices = kfold_cross_validate(3, config=config_fp, dataset=training_data_fp)\n\n    # correct structure for results from kfold cv\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)] + [\"overall\"]:\n        assert key in kfold_cv_stats\n\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)]:\n        assert key in kfold_split_indices\n\n\ndef test_kfold_cv_api_in_memory(tmpdir):\n    # k-fold_cross_validate api with in-memory config\n    num_folds = 3\n\n    # setup required data structures for test\n    training_data_fp = os.path.join(tmpdir, \"train.csv\")\n\n    # generate synthetic data for the test\n    input_features = [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")]\n\n    output_features = [number_feature()]\n\n    generate_data(input_features, output_features, training_data_fp)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # test kfold_cross_validate api with config in-memory\n\n    # execute k-fold cross validation run\n    kfold_cv_stats, kfold_split_indices = kfold_cross_validate(3, config=config, dataset=training_data_fp)\n\n    # correct structure for results from kfold cv\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)] + [\"overall\"]:\n        assert key in kfold_cv_stats\n\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)]:\n        assert key in kfold_split_indices\n\n\nDATA_FORMATS_FOR_KFOLDS = [\n    \"csv\",\n    \"df\",\n    \"dict\",\n    \"excel\",\n    \"feather\",\n    \"fwf\",\n    \"html\",\n    \"json\",\n    \"jsonl\",\n    \"parquet\",\n    \"pickle\",\n    \"stata\",\n    \"tsv\",\n]\n\n\n@pytest.mark.parametrize(\"data_format\", DATA_FORMATS_FOR_KFOLDS)\ndef test_kfold_cv_dataset_formats(tmpdir, data_format):\n    # k-fold_cross_validate api with in-memory config\n    num_folds = 3\n\n    # setup required data structures for test\n    training_data_fp = os.path.join(tmpdir, \"train.csv\")\n\n    # generate synthetic data for the test\n    input_features = [number_feature(normalization=\"zscore\"), number_feature(normalization=\"zscore\")]\n\n    output_features = [number_feature()]\n\n    generate_data(input_features, output_features, training_data_fp)\n    dataset_to_use = create_data_set_to_use(data_format, training_data_fp)\n\n    # generate config file\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # test kfold_cross_validate api with config in-memory\n\n    # execute k-fold cross validation run\n    kfold_cv_stats, kfold_split_indices = kfold_cross_validate(3, config=config, dataset=dataset_to_use)\n\n    # correct structure for results from kfold cv\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)] + [\"overall\"]:\n        assert key in kfold_cv_stats\n\n    for key in [\"fold_\" + str(i + 1) for i in range(num_folds)]:\n        assert key in kfold_split_indices\n"
  },
  {
    "path": "tests/integration_tests/test_llm.py",
    "content": "from __future__ import annotations\n\nimport copy\nimport json\nimport os\nimport pathlib\nfrom typing import Any\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\nimport yaml\n\nimport ludwig.error as ludwig_error\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import (\n    ADAPTER,\n    BACKEND,\n    BASE_MODEL,\n    BATCH_SIZE,\n    COMBINER,\n    EPOCHS,\n    EVAL_BATCH_SIZE,\n    GENERATION,\n    INPUT_FEATURES,\n    MERGE_ADAPTER_INTO_BASE_MODEL,\n    MODEL_ECD,\n    MODEL_LLM,\n    MODEL_TYPE,\n    OUTPUT_FEATURES,\n    POSTPROCESSOR,\n    PREPROCESSING,\n    PRETRAINED_ADAPTER_WEIGHTS,\n    PROGRESSBAR,\n    PROMPT,\n    QUANTIZATION,\n    TARGET_MODULES,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.models.llm import LLM\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom ludwig.utils.fs_utils import list_file_names_in_directory\nfrom ludwig.utils.types import DataFrame\nfrom tests.integration_tests.utils import category_feature, generate_data, text_feature\n\npytestmark = pytest.mark.llm\n\n\nLOCAL_BACKEND = {\"type\": \"local\"}\nTEST_MODEL_NAME = \"hf-internal-testing/tiny-random-GPTJForCausalLM\"\nMAX_NEW_TOKENS_TEST_DEFAULT = 5\n\nRAY_BACKEND = {\n    \"type\": \"ray\",\n    \"processor\": {\n        \"parallelism\": 1,\n    },\n    \"trainer\": {\n        \"use_gpu\": False,\n        \"num_workers\": 2,\n        \"resources_per_worker\": {\n            \"CPU\": 1,\n            \"GPU\": 0,\n        },\n    },\n}\n\n\ndef get_num_non_empty_tokens(iterable):\n    \"\"\"Returns the number of non-empty tokens.\"\"\"\n    return len(list(filter(bool, iterable)))\n\n\n@pytest.fixture(scope=\"module\")\ndef local_backend():\n    return LOCAL_BACKEND\n\n\n@pytest.fixture(scope=\"module\")\ndef ray_backend():\n    return RAY_BACKEND\n\n\ndef get_dataset():\n    data = [\n        {\"review\": \"I loved this movie!\", \"output\": \"positive\"},\n        {\"review\": \"The food was okay, but the service was terrible.\", \"output\": \"negative\"},\n        {\"review\": \"I can't believe how rude the staff was.\", \"output\": \"negative\"},\n        {\"review\": \"This book was a real page-turner.\", \"output\": \"positive\"},\n        {\"review\": \"The hotel room was dirty and smelled bad.\", \"output\": \"negative\"},\n        {\"review\": \"I had a great experience at this restaurant.\", \"output\": \"positive\"},\n        {\"review\": \"The concert was amazing!\", \"output\": \"positive\"},\n        {\"review\": \"The traffic was terrible on my way to work this morning.\", \"output\": \"negative\"},\n        {\"review\": \"The customer service was excellent.\", \"output\": \"positive\"},\n        {\"review\": \"I was disappointed with the quality of the product.\", \"output\": \"negative\"},\n    ]\n    df = pd.DataFrame(data)\n    return df\n\n\ndef get_generation_config():\n    return {\n        \"temperature\": 0.1,\n        \"top_p\": 0.75,\n        \"top_k\": 40,\n        \"num_beams\": 4,\n        \"max_new_tokens\": MAX_NEW_TOKENS_TEST_DEFAULT,\n    }\n\n\ndef convert_preds(preds: DataFrame):\n    if isinstance(preds, pd.DataFrame):\n        return preds.to_dict(orient=\"list\")\n    return preds.compute().to_dict(orient=\"list\")\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(LOCAL_BACKEND, id=\"local\"),\n        pytest.param(RAY_BACKEND, id=\"ray\"),\n    ],\n)\ndef test_llm_text_to_text(tmpdir, backend, ray_cluster_4cpu):\n    \"\"\"Test that the LLM model can train and predict with text inputs and text outputs.\"\"\"\n    input_features = [\n        {\n            \"name\": \"Question\",\n            \"type\": \"text\",\n            \"encoder\": {\"type\": \"passthrough\"},\n        }\n    ]\n    output_features = [text_feature(output_feature=True, name=\"Answer\", decoder={\"type\": \"text_extractor\"})]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    dataset_filename = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        GENERATION: get_generation_config(),\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        BACKEND: backend,\n    }\n\n    model = LudwigModel(config)\n    model.train(dataset=dataset_filename, output_directory=str(tmpdir), skip_save_processed_input=True)\n\n    preds, _ = model.predict(dataset=dataset_filename, output_directory=str(tmpdir), split=\"test\")\n    preds = convert_preds(preds)\n\n    assert \"Answer_predictions\" in preds\n    assert \"Answer_probabilities\" in preds\n    assert \"Answer_probability\" in preds\n    assert \"Answer_response\" in preds\n\n    assert preds[\"Answer_predictions\"]\n    assert preds[\"Answer_probabilities\"]\n    assert preds[\"Answer_probability\"]\n    assert preds[\"Answer_response\"]\n\n    # Check that in-line generation parameters are used. Original prediction uses max_new_tokens = 5.\n    assert get_num_non_empty_tokens(preds[\"Answer_predictions\"][0]) <= MAX_NEW_TOKENS_TEST_DEFAULT\n    original_max_new_tokens = model.model.generation.max_new_tokens\n\n    # This prediction uses max_new_tokens = 2.\n    preds, _ = model.predict(\n        dataset=dataset_filename,\n        output_directory=str(tmpdir),\n        split=\"test\",\n        generation_config={\"min_new_tokens\": 2, \"max_new_tokens\": 3},\n    )\n    preds = convert_preds(preds)\n    print(preds[\"Answer_predictions\"][0])\n    num_non_empty_tokens = get_num_non_empty_tokens(preds[\"Answer_predictions\"][0])\n    assert 2 <= num_non_empty_tokens <= 3\n\n    # Check that the state of the model is unchanged.\n    assert model.model.generation.max_new_tokens == original_max_new_tokens\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(LOCAL_BACKEND, id=\"local\"),\n        pytest.param(RAY_BACKEND, id=\"ray\"),\n    ],\n)\ndef test_llm_zero_shot_classification(tmpdir, backend, ray_cluster_4cpu):\n    input_features = [\n        {\n            \"name\": \"review\",\n            \"type\": \"text\",\n        }\n    ]\n    output_features = [\n        category_feature(\n            name=\"output\",\n            preprocessing={\n                \"fallback_label\": \"neutral\",\n            },\n            # How can we avoid using r here for regex, since it is technically an implementation detail?\n            decoder={\n                \"type\": \"category_extractor\",\n                \"match\": {\n                    \"positive\": {\"type\": \"contains\", \"value\": \"positive\"},\n                    \"neutral\": {\"type\": \"regex\", \"value\": r\"\\bneutral\\b\"},\n                    \"negative\": {\"type\": \"contains\", \"value\": \"negative\"},\n                },\n            },\n        )\n    ]\n\n    df = get_dataset()\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        GENERATION: get_generation_config(),\n        PROMPT: {\"task\": \"This is a review of a restaurant. Classify the sentiment.\"},\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        BACKEND: backend,\n    }\n\n    model = LudwigModel(config)\n    model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=True)\n\n    prediction_df = pd.DataFrame(\n        [\n            {\"review\": \"The food was amazing!\", \"output\": \"positive\"},\n            {\"review\": \"The service was terrible.\", \"output\": \"negative\"},\n            {\"review\": \"The food was okay.\", \"output\": \"neutral\"},\n        ]\n    )\n\n    preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir))\n    preds = convert_preds(preds)\n\n    assert preds\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(LOCAL_BACKEND, id=\"local\"),\n        pytest.param(RAY_BACKEND, id=\"ray\"),\n    ],\n)\ndef test_llm_few_shot_classification(tmpdir, backend, csv_filename, ray_cluster_4cpu):\n    input_features = [\n        text_feature(\n            output_feature=False,\n            name=\"body\",\n            encoder={\"type\": \"passthrough\"},  # need to use the default encoder for LLMTextInputFeatureConfig\n        )\n    ]\n    output_features = [\n        category_feature(\n            output_feature=True,\n            name=\"output\",\n            preprocessing={\n                \"fallback_label\": \"3\",\n            },\n            decoder={\n                \"type\": \"category_extractor\",\n                \"match\": {\n                    \"1\": {\"type\": \"contains\", \"value\": \"1\"},\n                    \"2\": {\"type\": \"contains\", \"value\": \"2\"},\n                    \"3\": {\"type\": \"contains\", \"value\": \"3\"},\n                    \"4\": {\"type\": \"contains\", \"value\": \"4\"},\n                    \"5\": {\"type\": \"contains\", \"value\": \"5\"},\n                },\n            },\n        )\n    ]\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        GENERATION: get_generation_config(),\n        PROMPT: {\n            \"retrieval\": {\"type\": \"random\", \"k\": 3},\n            \"task\": (\n                \"Given the sample input, complete this sentence by replacing XXXX: The review rating is XXXX. \"\n                \"Choose one value in this list: [1, 2, 3, 4, 5].\"\n            ),\n        },\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        PREPROCESSING: {\n            \"split\": {TYPE: \"fixed\"},\n        },\n        BACKEND: {**backend, \"cache_dir\": str(tmpdir)},\n    }\n\n    dataset_path = generate_data(\n        input_features,\n        output_features,\n        filename=csv_filename,\n        num_examples=25,\n        nan_percent=0.1,\n        with_split=True,\n    )\n    df = pd.read_csv(dataset_path)\n    df[\"output\"] = np.random.choice([1, 2, 3, 4, 5], size=len(df)).astype(str)  # ensure labels match the feature config\n    df.to_csv(dataset_path, index=False)\n\n    model = LudwigModel(config)\n    model.train(dataset=dataset_path, output_directory=str(tmpdir), skip_save_processed_input=True)\n\n    # TODO: fix LLM model loading\n    # model = LudwigModel.load(os.path.join(results.output_directory, \"model\"), backend=backend)\n    preds, _ = model.predict(dataset=dataset_path)\n    preds = convert_preds(preds)\n\n    assert preds\n\n\ndef _prepare_finetuning_test(\n    csv_filename: str, finetune_strategy: str, backend: dict, adapter_args: dict\n) -> tuple[dict, str]:\n    input_features = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features = [text_feature(name=\"output\")]\n\n    train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)\n    prediction_df = pd.DataFrame(\n        [\n            {\"input\": \"The food was amazing!\", \"output\": \"positive\"},\n            {\"input\": \"The service was terrible.\", \"output\": \"negative\"},\n            {\"input\": \"The food was okay.\", \"output\": \"neutral\"},\n        ]\n    )\n\n    model_name = TEST_MODEL_NAME\n    if finetune_strategy == \"adalora\":\n        # Adalora isn't supported for GPT-J model types, so use tiny bart\n        model_name = \"hf-internal-testing/tiny-random-BartModel\"\n    elif finetune_strategy == \"adaption_prompt\":\n        # At the time of writing this test, Adaption Prompt fine-tuning is only supported for Llama models\n        model_name = \"yujiepan/llama-2-tiny-random\"\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: model_name,\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        GENERATION: {\"max_new_tokens\": 64},\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: \"auto\",\n            EVAL_BATCH_SIZE: \"auto\",\n            EPOCHS: 2,\n        },\n        BACKEND: backend,\n    }\n\n    if finetune_strategy is not None:\n        config[ADAPTER] = {\n            TYPE: finetune_strategy,\n            **adapter_args,\n        }\n\n    return train_df, prediction_df, config\n\n\ndef _finetune_strategy_requires_cuda(finetune_strategy_name: str, quantization_args: dict | None) -> bool:\n    \"\"\"This method returns whether a given finetine_strategy requires CUDA.\n\n    For all finetune strategies, except \"qlora\", the decision is based just on the name of the finetine_strategy; in the\n    case of qlora, if the quantization dictionary is non-empty (i.e., contains quantization specifications), then the\n    original finetine_strategy name of \"lora\" is interpreted as \"qlora\" and used in the lookup, based on the list of\n    finetine strategies requiring CUDA.\n    \"\"\"\n    cuda_only_finetune_strategy_names: list[str] = [\n        \"prompt_tuning\",\n        \"prefix_tuning\",\n        \"p_tuning\",\n        \"qlora\",\n    ]\n\n    if finetune_strategy_name == \"lora\" and quantization_args:\n        finetune_strategy_name = \"qlora\"\n\n    return finetune_strategy_name in cuda_only_finetune_strategy_names\n\n\ndef _verify_lm_lora_finetuning_layers(\n    attention_layer: torch.nn.Module,\n    target_modules: set[str],\n    merge_adapter_into_base_model: bool,\n    model_weights_directory: str,\n    expected_lora_in_features: int,\n    expected_lora_out_features: int,\n    expected_file_names: list[str],\n) -> None:\n    \"\"\"This method verifies that LoRA finetuning layers have correct types and shapes, depending on whether the\n    optional \"model.merge_and_unload()\" method (based on the \"merge_adapter_into_base_model\" directive) was\n    executed.\n\n    If merge_adapter_into_base_model is True, then all specified LoRA projection layers in the attention layer must\n    contain square weight matrices (with the dimensions expected_lora_in_features by expected_lora_in_features).\n    However, if merge_adapter_into_base_model is False, then the LoRA part of the attention layer must include Lora_A\n    and Lora_B children layers for each specified projection, such that the product of Lora_A and Lora_B is a square\n    matrix (with the dimensions expected_lora_in_features by expected_lora_in_features) for each specified projection.\n    \"\"\"\n    from peft.tuners.lora.layer import LoraLayer\n\n    expected_lora_num_features_orig: tuple[int] = (expected_lora_in_features, expected_lora_out_features)\n\n    file_names: list[str] = list_file_names_in_directory(directory_name=model_weights_directory)\n    assert set(file_names) == set(expected_file_names)\n\n    target_module_name: str\n    target_module_obj: LoraLayer | torch.nn.Linear\n\n    # Not providing default value to \"getattr()\" so that error is raised if incorrect projection layer name is supplied.\n\n    for target_module_name in target_modules:\n        target_module_obj = getattr(attention_layer, target_module_name)\n        if merge_adapter_into_base_model:\n            assert isinstance(target_module_obj, torch.nn.Linear)\n        else:\n            assert isinstance(target_module_obj, LoraLayer)\n\n    if merge_adapter_into_base_model:\n        # If LoRA A & B layers are merged, they must have no children layers, and projection matrices must be square.\n        for target_module_name in target_modules:\n            target_module_obj = getattr(attention_layer, target_module_name)\n            assert not list(target_module_obj.children())\n            assert (target_module_obj.in_features, target_module_obj.out_features) == (\n                expected_lora_in_features,\n                expected_lora_out_features,\n            )\n    else:\n        # If LoRA A & B layers are not merged, their children layers must be correctly-dimensioned projection matrices.\n        expected_lora_num_features: tuple[int]\n        target_named_children: dict[str, torch.nn.Module]\n        lora_matrix_name: str\n        idx: int\n        for target_module_name in target_modules:\n            target_module_obj = getattr(attention_layer, target_module_name)\n            target_named_children = dict(target_module_obj.named_children())\n\n            for idx, lora_matrix_name in enumerate([\"lora_A\", \"lora_B\"]):\n                assert isinstance(target_named_children[lora_matrix_name][\"default\"], torch.nn.Linear)\n\n                # LoRA A and B matrix dimensions are transposes of one another so that their product is square matrix.\n                expected_lora_num_features = (\n                    expected_lora_num_features_orig\n                    if idx % 2 == 0\n                    else (expected_lora_num_features_orig[1], expected_lora_num_features_orig[0])\n                )\n                assert (\n                    target_named_children[lora_matrix_name][\"default\"].in_features,\n                    target_named_children[lora_matrix_name][\"default\"].out_features,\n                ) == expected_lora_num_features\n\n\n# TODO(arnav): p-tuning and prefix tuning have errors when enabled that seem to stem from DDP:\n#\n# prefix tuning:\n# Sizes of tensors must match except in dimension 1. Expected size 320 but got size 32 for tensor number 1 in the list.\n#\n# p-tuning:\n# 'PromptEncoder' object has no attribute 'mlp_head'\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(LOCAL_BACKEND, id=\"local\"),\n        # TODO(Arnav): Re-enable once we can run tests on GPUs\n        # This is because fine-tuning requires Ray with the deepspeed strategy, and deepspeed\n        # only works with GPUs\n        # pytest.param(RAY_BACKEND, id=\"ray\"),\n    ],\n)\n@pytest.mark.parametrize(\n    \"finetune_strategy,adapter_args\",\n    [\n        pytest.param(\n            None,\n            {},\n            id=\"full\",\n        ),\n        pytest.param(\n            \"lora\",\n            {},\n            id=\"lora-defaults\",\n        ),\n        pytest.param(\n            \"lora\",\n            {\"r\": 4, \"dropout\": 0.1},\n            id=\"lora-modified-defaults\",\n        ),\n        pytest.param(\n            \"lora\",\n            {TARGET_MODULES: [\"q_proj\", \"k_proj\", \"v_proj\"]},\n            id=\"lora-target-modules\",\n        ),\n        pytest.param(\n            \"lora\",\n            {\"use_rslora\": True},\n            id=\"lora-rslora-enabled\",\n        ),\n        pytest.param(\n            \"lora\",\n            {\"use_dora\": True},\n            id=\"lora-dora-enabled\",\n        ),\n        pytest.param(\n            \"lora\",\n            {\"use_rslora\": True, \"use_dora\": True},\n            id=\"lora-rslora-and-dora-enabled\",\n        ),\n        pytest.param(\n            \"lora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},\n            id=\"lora_merged\",\n        ),\n        pytest.param(\n            \"lora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},\n            id=\"lora_not_merged\",\n        ),\n        pytest.param(\n            \"adalora\",\n            {},\n            id=\"adalora-defaults\",\n        ),\n        pytest.param(\n            \"adalora\",\n            {\"init_r\": 8, \"beta1\": 0.8},\n            id=\"adalora-modified-defaults\",\n        ),\n        pytest.param(\n            \"adalora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},\n            id=\"adalora_merged\",\n        ),\n        pytest.param(\n            \"adalora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},\n            id=\"adalora_not_merged\",\n        ),\n        # TODO: <Alex>02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix\n        # \"TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')\"\n        # (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938).\n        # </Alex>\n        # pytest.param(\n        #     \"adaption_prompt\",\n        #     {},\n        #     id=\"adaption_prompt-defaults\",\n        # ),\n        # pytest.param(\n        #     \"adaption_prompt\",\n        #     {\"adapter_len\": 6, \"adapter_layers\": 1},\n        #     id=\"adaption_prompt-modified-defaults\",\n        # ),\n        pytest.param(\n            \"ia3\",\n            {},\n            id=\"ia3-defaults\",\n        ),\n        pytest.param(\n            \"ia3\",\n            {\"init_ia3_weights\": False},\n            id=\"ia3-modified-defaults\",\n        ),\n        # pytest.param(\n        #     \"prompt_tuning\",\n        #     {\n        #         \"num_virtual_tokens\": 8,\n        #         \"prompt_tuning_init\": \"RANDOM\",\n        #     },\n        #     id=\"prompt_tuning_init_random\",\n        # ),\n        # pytest.param(\n        #     \"prompt_tuning\",\n        #     {\n        #         \"num_virtual_tokens\": 8,\n        #         \"prompt_tuning_init\": \"TEXT\",\n        #         \"prompt_tuning_init_text\": \"Classify if the review is positive, negative, or neutral: \",\n        #     },\n        #     id=\"prompt_tuning_init_text\",\n        # ),\n        # pytest.param(\n        #     \"prefix_tuning\",\n        #     {\n        #         \"num_virtual_tokens\": 8,\n        #     },\n        #     id=\"prefix_tuning\",\n        # ),\n        # pytest.param(\n        #     \"p_tuning\",\n        #     {\"num_virtual_tokens\": 8, \"encoder_reparameterization_type\": \"MLP\"},\n        #     id=\"p_tuning_mlp_reparameterization\",\n        # ),\n        # pytest.param(\n        #     \"p_tuning\",\n        #     {\"num_virtual_tokens\": 8, \"encoder_reparameterization_type\": \"LSTM\"},\n        #     id=\"p_tuning_lstm_reparameterization\",\n        # ),\n    ],\n)\ndef test_llm_finetuning_strategies(tmpdir, csv_filename, backend, finetune_strategy, adapter_args):\n    train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args)\n\n    output_directory: str = str(tmpdir)\n    model_directory: str = pathlib.Path(output_directory) / \"api_experiment_run\" / MODEL_FILE_NAME\n\n    model = LudwigModel(config)\n    model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)\n\n    # Make sure we can load the saved model and then use it for predictions\n    model = LudwigModel.load(str(model_directory), backend=backend)\n\n    base_model = LLM(ModelConfig.from_dict(config))\n    assert not _compare_models(base_model, model.model)  # noqa F821\n\n    preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory)\n    preds = convert_preds(preds)\n\n    assert preds\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"finetune_strategy,adapter_args,quantization\",\n    [\n        pytest.param(\n            \"lora\",\n            {},\n            {\"bits\": 4},\n            id=\"qlora-4bit\",\n        ),\n        pytest.param(\n            \"lora\",\n            {},\n            {\"bits\": 8},\n            id=\"qlora-8bit\",\n        ),\n    ],\n)\ndef test_llm_finetuning_strategies_quantized(tmpdir, csv_filename, finetune_strategy, adapter_args, quantization):\n    pytest.importorskip(\"bitsandbytes\", reason=\"bitsandbytes required for quantization tests\")\n    if (\n        _finetune_strategy_requires_cuda(finetune_strategy_name=finetune_strategy, quantization_args=quantization)\n        and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0\n    ):\n        pytest.skip(\"Skip: quantization requires GPU and none are available.\")\n\n    backend = LOCAL_BACKEND\n\n    train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetune_strategy, backend, adapter_args)\n    config[\"backend\"] = backend\n    config[QUANTIZATION] = quantization\n\n    model = LudwigModel(config)\n    model.train(dataset=train_df, output_directory=str(tmpdir), skip_save_processed_input=False)\n\n    # Make sure we can load the saved model and then use it for predictions\n    model = LudwigModel.load(os.path.join(str(tmpdir), \"api_experiment_run\", MODEL_FILE_NAME))\n\n    base_model = LLM(ModelConfig.from_dict(config))\n    assert not _compare_models(base_model, model.model)  # noqa F821\n\n    preds, _ = model.predict(dataset=prediction_df, output_directory=str(tmpdir))\n    preds = convert_preds(preds)\n\n    assert preds\n\n\n@pytest.mark.llm\n@pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n@pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\n@pytest.mark.parametrize(\n    \"finetune_strategy,adapter_args,quantization,error_raised\",\n    [\n        pytest.param(\n            \"lora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},\n            {\"bits\": 4},\n            (\n                ImportError,\n                \"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` \",  # noqa: E501\n            ),\n            id=\"qlora-4bit-not-merged\",\n        ),\n        pytest.param(\n            \"lora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True}},\n            {\"bits\": 8},\n            (\n                ImportError,\n                \"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` \",  # noqa: E501\n            ),\n            id=\"qlora-8bit-merged\",\n        ),\n        pytest.param(\n            \"lora\",\n            {POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: False}},\n            {\"bits\": 8},\n            (\n                ImportError,\n                \"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` \",  # noqa E501\n            ),\n            id=\"qlora-8bit-not-merged\",\n        ),\n    ],\n)\ndef test_llm_lora_finetuning_merge_and_unload_quantized_accelerate_required(\n    csv_filename, finetune_strategy, adapter_args, quantization, error_raised\n):\n    pytest.importorskip(\"bitsandbytes\", reason=\"bitsandbytes required for quantization tests\")\n    input_features: list[dict] = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features: list[dict] = [text_feature(name=\"output\")]\n\n    config: dict = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n        ADAPTER: {\n            TYPE: finetune_strategy,\n            **adapter_args,\n        },\n        QUANTIZATION: quantization,\n    }\n\n    model = LudwigModel(config)\n\n    error_class: type  # noqa [F842]  # incorrect flagging of \"local variable is annotated but never used\n    error_message: str  # noqa [F842]  # incorrect flagging of \"local variable is annotated but never used\n    error_class, error_message = error_raised\n    with pytest.raises(error_class) as excinfo:\n        train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=3)\n        model.train(dataset=train_df)\n\n    assert str(excinfo.value) == error_message\n\n\n@pytest.mark.llm\ndef test_llm_lora_finetuning_merge_and_unload_4_bit_quantization_not_supported(local_backend: dict):\n    input_features: list[dict] = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features: list[dict] = [text_feature(name=\"output\")]\n    finetune_strategy: str = \"lora\"\n\n    config: dict = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n        ADAPTER: {\n            TYPE: finetune_strategy,\n            POSTPROCESSOR: {MERGE_ADAPTER_INTO_BASE_MODEL: True, PROGRESSBAR: True},\n        },\n        QUANTIZATION: {\"bits\": 4},\n        BACKEND: local_backend,\n    }\n\n    expected_error_class: type = ludwig_error.ConfigValidationError\n    expected_error_message: str = \"\"\"This operation will entail merging LoRA layers on a 4-bit quantized model.  \\\nCalling \"save_pretrained()\" on that model is currently unsupported.  If you want to merge the LoRA adapter weights \\\ninto the base model, you need to use 8-bit quantization or do non-quantized based training by removing the \\\nquantization section from your Ludwig configuration.\"\"\"\n    with pytest.raises(expected_error_class) as excinfo:\n        _ = LudwigModel(config)\n\n    assert str(excinfo.value) == expected_error_message\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(LOCAL_BACKEND, id=\"local\"),\n        # TODO: Re-enable once we can run tests on GPUs\n        # This is because fine-tuning requires Ray with the deepspeed strategy, and deepspeed\n        # only works with GPUs\n        # pytest.param(RAY_BACKEND, id=\"ray\"),\n    ],\n)\n@pytest.mark.parametrize(\n    \"target_modules,merge_adapter_into_base_model,expected_lora_in_features,expected_lora_out_features,expected_file_names\",  # noqa: E501\n    [\n        pytest.param(\n            None,\n            False,\n            32,\n            8,\n            [\n                \"README.md\",\n                \"adapter_config.json\",\n                \"adapter_model.safetensors\",\n            ],\n            id=\"lora_default_not_merged\",\n        ),\n        pytest.param(\n            None,\n            True,\n            32,\n            32,\n            [\n                \"README.md\",\n                \"adapter_config.json\",\n                \"adapter_model.safetensors\",\n                \"config.json\",\n                \"generation_config.json\",\n                \"model.safetensors\",\n                \"tokenizer.json\",\n                \"tokenizer_config.json\",\n            ],\n            id=\"lora_default_merged\",\n        ),\n        pytest.param(\n            [\"q_proj\", \"k_proj\", \"v_proj\"],\n            False,\n            32,\n            8,\n            [\n                \"README.md\",\n                \"adapter_config.json\",\n                \"adapter_model.safetensors\",\n            ],\n            id=\"lora_custom_not_merged\",\n        ),\n        pytest.param(\n            [\"q_proj\", \"k_proj\", \"v_proj\"],\n            True,\n            32,\n            32,\n            [\n                \"README.md\",\n                \"adapter_config.json\",\n                \"adapter_model.safetensors\",\n                \"config.json\",\n                \"generation_config.json\",\n                \"model.safetensors\",\n                \"tokenizer.json\",\n                \"tokenizer_config.json\",\n            ],\n            id=\"lora_custom_merged\",\n        ),\n    ],\n)\ndef test_llm_lora_finetuning_merge_and_unload(\n    tmpdir: str,\n    csv_filename: str,\n    backend: dict,\n    target_modules: list[str] | set[str] | None,\n    merge_adapter_into_base_model: bool,\n    expected_lora_in_features: int,\n    expected_lora_out_features: int,\n    expected_file_names: list[str],\n):\n    from peft.tuners.lora.config import LoraConfig\n    from peft.tuners.lora.model import LoraModel\n\n    finetune_strategy: str = \"lora\"\n\n    adapter_args: dict = {\n        POSTPROCESSOR: {\n            MERGE_ADAPTER_INTO_BASE_MODEL: merge_adapter_into_base_model,\n        },\n    }\n    # If \"target_modules\" is None, then [\"q_proj\", \"v_proj\"] is used (HuggingFace Transformers/PEFT internal default).\n    if target_modules:\n        adapter_args[TARGET_MODULES] = target_modules\n\n    train_df, prediction_df, config = _prepare_finetuning_test(\n        csv_filename=csv_filename, finetune_strategy=finetune_strategy, backend=backend, adapter_args=adapter_args\n    )\n\n    output_directory: str = str(tmpdir)\n    model_directory: str = pathlib.Path(output_directory) / \"api_experiment_run\" / MODEL_FILE_NAME\n    model_weights_directory: str = (\n        pathlib.Path(output_directory) / \"api_experiment_run\" / MODEL_FILE_NAME / MODEL_WEIGHTS_FILE_NAME\n    )\n\n    model = LudwigModel(config)\n    model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)\n\n    # Get actual \"target_modules\" from trained model (to be used in assertions).\n    lora_model: LoraModel = model.model.model.base_model\n    peft_config: dict = lora_model.peft_config\n    lora_config: LoraConfig = peft_config[\"default\"]\n    target_modules = lora_config.target_modules\n\n    _verify_lm_lora_finetuning_layers(\n        attention_layer=model.model.model.base_model.model.transformer.h[1].attn,\n        target_modules=target_modules,\n        merge_adapter_into_base_model=merge_adapter_into_base_model,\n        model_weights_directory=model_weights_directory,\n        expected_lora_in_features=expected_lora_in_features,\n        expected_lora_out_features=expected_lora_out_features,\n        expected_file_names=expected_file_names,\n    )\n\n    # Make sure we can load the saved model and verify that the LoRA layers have expected shapes.\n    model = LudwigModel.load(str(model_directory), backend=backend)\n    _verify_lm_lora_finetuning_layers(\n        attention_layer=model.model.model.base_model.model.transformer.h[1].attn,\n        target_modules=target_modules,\n        merge_adapter_into_base_model=merge_adapter_into_base_model,\n        model_weights_directory=model_weights_directory,\n        expected_lora_in_features=expected_lora_in_features,\n        expected_lora_out_features=expected_lora_out_features,\n        expected_file_names=expected_file_names,\n    )\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\"use_adapter\", [True, False], ids=[\"with_adapter\", \"without_adapter\"])\ndef test_llm_training_with_gradient_checkpointing(tmpdir, csv_filename, use_adapter):\n    input_features = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features = [text_feature(name=\"output\")]\n\n    df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"hf-internal-testing/tiny-random-BartModel\",\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 1,\n            \"enable_gradient_checkpointing\": True,\n        },\n    }\n\n    if use_adapter:\n        config[ADAPTER] = {TYPE: \"lora\"}\n\n    model = LudwigModel(config)\n    assert model.config_obj.trainer.enable_gradient_checkpointing\n\n    model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False)\n\n\n@pytest.mark.llm\ndef test_lora_wrap_on_init():\n    from peft import PeftModel\n    from transformers import PreTrainedModel\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n    }\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n    assert isinstance(model.model, PreTrainedModel)\n    assert not isinstance(model.model, PeftModel)\n\n    # Now add adapter\n    config[ADAPTER] = {\n        TYPE: \"lora\",\n    }\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n    # We need to explicitly make this call since we now load the adapter\n    # in the trainer as opposed to the point of LLM model initialization.\n    model.prepare_for_training()\n    assert not isinstance(model.model, PreTrainedModel)\n    assert isinstance(model.model, PeftModel)\n\n\ndef test_llama_rope_scaling():\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n        \"model_parameters\": {\n            \"rope_scaling\": {\n                \"rope_type\": \"dynamic\",\n                \"factor\": 2.0,\n            }\n        },\n    }\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n\n    assert model.model.config.rope_scaling\n    assert model.model.config.rope_scaling[\"rope_type\"] == \"dynamic\"\n    assert model.model.config.rope_scaling[\"factor\"] == 2.0\n\n\ndef test_default_max_sequence_length():\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: TEST_MODEL_NAME,\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n        ADAPTER: {TYPE: \"lora\", PRETRAINED_ADAPTER_WEIGHTS: \"Infernaught/test_adapter_weights\"},\n        BACKEND: {TYPE: \"local\"},\n    }\n    config_obj = ModelConfig.from_dict(config)\n    assert config_obj.input_features[0].preprocessing.max_sequence_length is None\n    assert config_obj.output_features[0].preprocessing.max_sequence_length is None\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\n    \"adapter\",\n    [\n        \"lora\",\n        \"adalora\",\n        # TODO: <Alex>02/21/2024: Disabling AdaptionPrompt (waiting for PEFT release to fix\n        # \"TypeError: LlamaRotaryEmbedding.forward() missing 1 required positional argument: 'position_ids')\"\n        # (this is reflected in https://github.com/ludwig-ai/ludwig/issues/3938).\n        # </Alex>\n        # \"adaption_prompt\",\n    ],\n)\ndef test_load_pretrained_adapter_weights(adapter):\n    from peft import PeftModel\n    from transformers import PreTrainedModel\n\n    if adapter == \"lora\":\n        weights = \"Infernaught/test_adapter_weights\"\n        base_model = TEST_MODEL_NAME\n    elif adapter == \"adalora\":\n        weights = \"Infernaught/test_adalora_weights\"\n        base_model = \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"\n    elif adapter == \"adaption_prompt\":\n        weights = \"Infernaught/test_ap_weights\"\n        base_model = \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"\n    else:\n        raise ()\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: base_model,\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n        TRAINER: {\n            TYPE: \"none\",\n            BATCH_SIZE: 8,\n            EPOCHS: 2,\n        },\n        ADAPTER: {TYPE: adapter, PRETRAINED_ADAPTER_WEIGHTS: weights},\n        BACKEND: {TYPE: \"local\"},\n    }\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n\n    assert model.config_obj.adapter.pretrained_adapter_weights\n    assert model.config_obj.adapter.pretrained_adapter_weights == weights\n\n    model.prepare_for_training()\n    assert not isinstance(model.model, PreTrainedModel)\n    assert isinstance(model.model, PeftModel)\n\n    config_obj = ModelConfig.from_dict(config)\n    assert config_obj.input_features[0].preprocessing.max_sequence_length is None\n    assert config_obj.output_features[0].preprocessing.max_sequence_length is None\n\n\ndef _compare_models(model_1: torch.nn.Module, model_2: torch.nn.Module) -> bool:\n    # For a full explanation of this 8-bit workaround, see https://github.com/ludwig-ai/ludwig/pull/3606\n\n    # TODO: Uncomment \"filter_for_weight_format()\" method definition and enable its usage once GPU tests are set up.\n    # def filter_for_weight_format(i):\n    #     \"\"\"Remove bitsandbytes metadata keys added on state dict creation.\n    #\n    #     8-bit quantized models that have been put on gpu will have a set of `weight_format` keys in their state dict.\n    #     These contain strings that are used to reshape quantized tensors, however these have no impact until the state\n    #     dict is loaded into a model. These keys were causing `torch.equal` to raise an exception, so we skip them in\n    #     the evaluation.\n    #     \"\"\"\n    #     return \"weight_format\" not in i[0]\n\n    # model_1_filtered_state_dict = filter(filter_for_weight_format, model_1.state_dict().items())\n    # model_2_filtered_state_dict = filter(filter_for_weight_format, model_2.state_dict().items())\n\n    # Source: https://discuss.pytorch.org/t/check-if-models-have-same-weights/4351/6\n\n    if model_1.__class__.__name__ != model_2.__class__.__name__:\n        return False\n\n    if (\n        hasattr(model_1, \"model\")\n        and hasattr(model_2, \"model\")\n        and not _compare_models(model_1=model_1.model, model_2=model_2.model)\n    ):\n        return False\n\n    for key_item_1, key_item_2 in zip(model_1.state_dict().items(), model_2.state_dict().items()):\n        if not torch.equal(key_item_1[1], key_item_2[1]):\n            return False\n\n    return True\n\n\ndef test_global_max_sequence_length_for_llms():\n    \"\"\"Ensures that user specified global_max_sequence_length can never be greater than the model's context\n    length.\"\"\"\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n    }\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n\n    # Default value is set based on model's context_len\n    assert model.global_max_sequence_length == 2048\n\n    # Override to a larger value in the config\n    config[\"preprocessing\"] = {\"global_max_sequence_length\": 4096}\n    config_obj = ModelConfig.from_dict(config)\n    model = LLM(config_obj)\n\n    # Check that the value can never be larger than the model's context_len\n    assert model.global_max_sequence_length == 2048\n\n\ndef test_local_path_loading(tmpdir):\n    \"\"\"Tests that local paths can be used to load models.\"\"\"\n\n    from huggingface_hub import snapshot_download\n\n    # Download the model to a local directory\n    local_path: str = f\"{str(tmpdir)}/test_local_path_loading\"\n    repo_id: str = \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"\n    os.makedirs(local_path, exist_ok=True)\n    snapshot_download(repo_id=repo_id, local_dir=local_path)\n\n    # Load the model using the local path\n    config1 = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: local_path,\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n    }\n    config_obj1 = ModelConfig.from_dict(config1)\n    model1 = LLM(config_obj1)\n\n    # Load the model using the repo id\n    config2 = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: repo_id,\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n    }\n    config_obj2 = ModelConfig.from_dict(config2)\n    model2 = LLM(config_obj2)\n\n    # Check that the models are the same\n    assert _compare_models(model1.model, model2.model)\n\n\n@pytest.mark.parametrize(\n    \"finetuning_strategy, embedding_noise\",\n    [\n        pytest.param(None, 0, id=\"full_finetuning_without_noise\"),\n        pytest.param(None, 5, id=\"full_finetuning_with_noise\"),\n        pytest.param(\"lora\", 0, id=\"lora_without_noise\"),\n        pytest.param(\"lora\", 5, id=\"lora_with_noise\"),\n    ],\n)\ndef test_llm_finetuning_with_embedding_noise(\n    tmpdir,\n    csv_filename,\n    finetuning_strategy,\n    embedding_noise,\n):\n    train_df, prediction_df, config = _prepare_finetuning_test(csv_filename, finetuning_strategy, LOCAL_BACKEND, {})\n\n    # Add embedding noise\n    if embedding_noise:\n        config[\"model_parameters\"] = {\"neftune_noise_alpha\": embedding_noise}\n\n    model = LudwigModel(config)\n\n    if embedding_noise:\n        assert model.config_obj.model_parameters.neftune_noise_alpha == embedding_noise\n\n    output_directory: str = str(tmpdir)\n    model_directory: str = pathlib.Path(output_directory) / \"api_experiment_run\" / MODEL_FILE_NAME\n    model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)\n\n    # Make sure we can load the saved model and then use it for predictions\n    model = LudwigModel.load(str(model_directory), backend=LOCAL_BACKEND)\n\n    base_model = LLM(ModelConfig.from_dict(config))\n    assert not _compare_models(base_model, model.model)  # noqa F821\n\n    preds, _ = model.predict(dataset=prediction_df, output_directory=output_directory)\n    preds = convert_preds(preds)\n\n    assert preds\n\n\n@pytest.fixture()\ndef llm_encoder_config() -> dict[str, Any]:\n    encoder_config = {\n        TYPE: \"llm\",\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n    }\n\n    return encoder_config\n\n\n@pytest.mark.parametrize(\n    \"adapter,quantization\",\n    [\n        (None, None),\n        (\"lora\", None),\n        (\"lora\", {\"bits\": 4}),\n        (\"lora\", {\"bits\": 8}),\n        (\"adalora\", None),\n        (\"adalora\", {\"bits\": 4}),\n        (\"adalora\", {\"bits\": 8}),\n    ],\n    ids=[\"FFT\", \"LoRA\", \"LoRA 4-bit\", \"LoRA 8-bit\", \"AdaLoRA\", \"AdaLoRA 4-bit\", \"AdaLoRA 8-bit\"],\n)\ndef test_llm_encoding(llm_encoder_config, adapter, quantization, tmpdir):\n    if quantization:\n        pytest.importorskip(\"bitsandbytes\", reason=\"bitsandbytes required for quantization tests\")\n    if (\n        _finetune_strategy_requires_cuda(\n            finetune_strategy_name=\"lora\" if adapter else None, quantization_args=quantization\n        )\n        and not (torch.cuda.is_available() and torch.cuda.device_count()) > 0\n    ):\n        pytest.skip(\"Skip: quantization requires GPU and none are available.\")\n\n    dataset_path = os.path.join(tmpdir, \"llm_classification_data.csv\")\n\n    config = {\n        MODEL_TYPE: MODEL_ECD,\n        OUTPUT_FEATURES: [category_feature(name=\"output\")],\n        COMBINER: {TYPE: \"sequence\"},\n        TRAINER: {EPOCHS: 1},\n    }\n\n    encoder_config = copy.deepcopy(llm_encoder_config)\n\n    if adapter:\n        encoder_config[ADAPTER] = {TYPE: adapter}\n    if quantization:\n        encoder_config[QUANTIZATION] = quantization\n        config[BACKEND] = LOCAL_BACKEND\n\n    config[INPUT_FEATURES] = [text_feature(name=\"input\", encoder=encoder_config)]\n\n    generate_data(input_features=config[INPUT_FEATURES], output_features=config[OUTPUT_FEATURES], filename=dataset_path)\n\n    model = LudwigModel(config)\n    model.train(dataset=dataset_path, output_directory=str(tmpdir))\n\n\ndef test_llm_batch_size_tuning():\n    dataset = pd.DataFrame({\"instruction\": [\"a\"] * 100, \"output\": [\"a\"] * 100})\n    config = yaml.safe_load(\"\"\"\n    model_type: llm\n    input_features:\n        - name: instruction\n          type: text\n    output_features:\n        - name: output\n          type: text\n    prompt:\n        template: >-\n            {instruction}\n    adapter:\n        type: lora\n    trainer:\n        type: finetune\n        optimizer:\n            type: adam\n        batch_size: auto\n        train_steps: 1\n        learning_rate: 0.0002\n        eval_batch_size: 2\n    backend:\n        type: local\n    base_model: HuggingFaceH4/tiny-random-LlamaForCausalLM\n        \"\"\")\n    model = LudwigModel(config=config)\n    model.train(dataset=dataset)\n    assert model.config_obj.trainer.batch_size > 1\n\n\n@pytest.mark.llm\ndef test_llm_used_tokens(tmpdir):\n    input_features = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features = [text_feature(name=\"output\")]\n\n    df = pd.read_json(\"https://raw.githubusercontent.com/sahil280114/codealpaca/master/data/code_alpaca_20k.json\").head(\n        10\n    )\n\n    # df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"hf-internal-testing/tiny-random-BartModel\",\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            TYPE: \"finetune\",\n            BATCH_SIZE: 1,\n            EPOCHS: 3,\n            \"enable_gradient_checkpointing\": True,\n        },\n    }\n\n    config[ADAPTER] = {TYPE: \"lora\"}\n\n    model = LudwigModel(config)\n    assert model.config_obj.trainer.enable_gradient_checkpointing\n\n    model.train(dataset=df, output_directory=str(tmpdir), skip_save_processed_input=False)\n\n    with open(\n        os.path.join(str(tmpdir), \"api_experiment_run\", MODEL_FILE_NAME, \"training_progress.json\"), encoding=\"utf-8\"\n    ) as f:\n        progress_tracker = json.load(f)\n\n    assert progress_tracker[\"cumulative_step_token_usage\"][\"11\"] == progress_tracker[\"total_tokens_used\"] == 621\n    assert progress_tracker[\"checkpoint_to_epoch\"] == {\"1\": 1, \"2\": 1, \"3\": 2, \"4\": 2, \"5\": 3, \"6\": 3}\n    assert progress_tracker[\"checkpoint_to_step\"] == {\"1\": 4, \"2\": 4, \"3\": 8, \"4\": 8, \"5\": 12, \"6\": 12}\n    assert progress_tracker[\"cumulative_checkpoint_token_usage\"] == {\n        \"1\": 207,\n        \"2\": 207,\n        \"3\": 414,\n        \"4\": 414,\n        \"5\": 621,\n        \"6\": 621,\n    }\n    assert progress_tracker[\"incremental_checkpoint_token_usage\"] == {\n        \"1\": 207,\n        \"2\": 0,\n        \"3\": 207,\n        \"4\": 0,\n        \"5\": 207,\n        \"6\": 0,\n    }\n"
  },
  {
    "path": "tests/integration_tests/test_missing_value_strategy.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, COLUMN, DROP_ROW, FILL_WITH_MEAN, PREPROCESSING, PROC_COLUMN, TRAINER\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n    read_csv_with_nan,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    vector_feature,\n)\n\n\ndef test_missing_value_prediction(tmpdir, csv_filename):\n    random.seed(1)\n    np.random.seed(1)\n    input_features = [\n        category_feature(\n            encoder={\"vocab_size\": 2}, reduce_input=\"sum\", preprocessing=dict(missing_value_strategy=\"fill_with_mode\")\n        )\n    ]\n    output_features = [binary_feature()]\n\n    dataset = pd.read_csv(generate_data(input_features, output_features, csv_filename))\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n    model = LudwigModel(config)\n    _, _, output_dir = model.train(dataset=dataset, output_directory=tmpdir)\n\n    # Set the input column to None, we should be able to replace the missing value with the mode\n    # from the training set\n    dataset[input_features[0][\"name\"]] = None\n    model.predict(dataset=dataset)\n\n    model = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME))\n    model.predict(dataset=dataset)\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_missing_values_fill_with_mean(backend, csv_filename, tmpdir, ray_cluster_2cpu):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    kwargs = {PREPROCESSING: {\"missing_value_strategy\": FILL_WITH_MEAN}}\n    input_features = [\n        number_feature(**kwargs),\n        binary_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n    output_features = [binary_feature()]\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # run preprocessing\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.preprocess(dataset=training_data_csv_path)\n\n\ndef test_missing_values_drop_rows(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    kwargs = {PREPROCESSING: {\"missing_value_strategy\": DROP_ROW}}\n    input_features = [\n        number_feature(),\n        binary_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n    output_features = [\n        binary_feature(**kwargs),\n        number_feature(**kwargs),\n        category_feature(decoder={\"vocab_size\": 3}, **kwargs),\n        sequence_feature(decoder={\"vocab_size\": 3}, **kwargs),\n        text_feature(decoder={\"vocab_size\": 3}, **kwargs),\n        set_feature(decoder={\"vocab_size\": 3}, **kwargs),\n        vector_feature(**kwargs),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = read_csv_with_nan(training_data_csv_path, nan_percent=0.1)\n\n    # run preprocessing\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.preprocess(dataset=df)\n\n\n@pytest.mark.parametrize(\n    \"backend,outlier_strategy,outlier_threshold\",\n    [\n        pytest.param(\"local\", None, 3.0, id=\"local_none\"),\n        pytest.param(\"local\", \"fill_with_mean\", 1.0, id=\"local_mean_strict\"),\n        pytest.param(\"local\", \"fill_with_const\", 3.0, id=\"local_const_relaxed\"),\n        pytest.param(\"ray\", \"fill_with_mean\", 3.0, id=\"ray_mean\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_outlier_strategy(outlier_strategy, outlier_threshold, backend, tmpdir, ray_cluster_2cpu):\n    fill_value = 42\n    kwargs = {\n        PREPROCESSING: {\n            \"outlier_strategy\": outlier_strategy,\n            \"outlier_threshold\": outlier_threshold,\n            \"fill_value\": fill_value,\n        }\n    }\n    input_features = [\n        number_feature(**kwargs),\n    ]\n    output_features = [binary_feature()]\n\n    # Values that will be 1 and 3 std deviations from the mean, respectively\n    sigma1, sigma1_idx = -150, 4\n    sigma3, sigma3_idx = 300, 11\n\n    num_col = np.array([77, 24, 29, 29, sigma1, 71, 46, 95, 20, 52, 85, sigma3, 74, 10, 98, 53, 110, 94, 62, 13])\n    expected_fill_value = num_col.mean() if outlier_strategy == \"fill_with_mean\" else fill_value\n\n    input_col = input_features[0][COLUMN]\n    output_col = output_features[0][COLUMN]\n\n    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_)\n    dataset_df = pd.DataFrame(\n        data={\n            input_col: num_col,\n            output_col: bin_col,\n        }\n    )\n\n    dataset_fp = os.path.join(tmpdir, \"dataset.csv\")\n    dataset_df.to_csv(dataset_fp)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    # Run preprocessing\n    ludwig_model = LudwigModel(config, backend=backend)\n    proc_dataset = ludwig_model.preprocess(training_set=dataset_fp)\n\n    # Check preprocessed output\n    proc_df = ludwig_model.backend.df_engine.compute(proc_dataset.training_set.to_df())\n    proc_col = input_features[0][PROC_COLUMN]\n\n    assert len(proc_df) == len(dataset_df)\n\n    # Check that values over 1 std are replaced\n    if outlier_strategy is not None and outlier_threshold <= 1.0:\n        assert np.isclose(proc_df[proc_col][sigma1_idx], expected_fill_value)\n    else:\n        assert np.isclose(proc_df[proc_col][sigma1_idx], dataset_df[input_col][sigma1_idx])\n\n    # Check that values over 3 std are replaced\n    if outlier_strategy is not None and outlier_threshold <= 3.0:\n        assert np.isclose(proc_df[proc_col][sigma3_idx], expected_fill_value)\n    else:\n        assert np.isclose(proc_df[proc_col][sigma3_idx], dataset_df[input_col][sigma3_idx])\n"
  },
  {
    "path": "tests/integration_tests/test_mlflow.py",
    "content": "import os\nimport shutil\nimport uuid\nfrom unittest import mock\n\nimport mlflow\nimport pandas as pd\nimport pytest\nimport yaml\nfrom mlflow.tracking import MlflowClient\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import TRAINER\nfrom ludwig.contribs.mlflow import MlflowCallback\nfrom ludwig.export import export_mlflow\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom tests.integration_tests.utils import category_feature, FakeRemoteBackend, generate_data, sequence_feature\n\n\ndef run_mlflow_callback_test(mlflow_client, config, training_data, val_data, test_data, tmpdir, exp_name=None):\n    ludwig_exp_name = \"mlflow_test\"\n    callback = MlflowCallback()\n    wrapped_callback = mock.Mock(wraps=callback)\n\n    model = LudwigModel(config, callbacks=[wrapped_callback], backend=FakeRemoteBackend())\n    model.train(\n        training_set=training_data, validation_set=val_data, test_set=test_data, experiment_name=ludwig_exp_name\n    )\n    expected_df, _ = model.predict(test_data)\n\n    # Check mlflow artifacts\n    assert callback.experiment_id is not None\n    assert callback.run is not None\n\n    mlflow_exp_name = exp_name or ludwig_exp_name\n    experiment = mlflow.get_experiment_by_name(mlflow_exp_name)\n    assert experiment.experiment_id == callback.experiment_id\n\n    df = mlflow.search_runs([experiment.experiment_id])\n    assert len(df) == 1\n\n    run_id = df.run_id[0]\n    assert run_id == callback.run.info.run_id\n\n    run = mlflow.get_run(run_id)\n    expected_status = \"FINISHED\" if exp_name is None else \"RUNNING\"\n    assert run.info.status == expected_status\n    assert wrapped_callback.on_trainer_train_setup.call_count == 1\n    assert wrapped_callback.on_trainer_train_teardown.call_count == 1\n\n    artifacts = [f.path for f in mlflow_client.list_artifacts(callback.run.info.run_id, \"\")]\n    local_dir = f\"{tmpdir}/local_artifacts\"\n    os.makedirs(local_dir)\n\n    assert \"config.yaml\" in artifacts\n    local_config_path = mlflow_client.download_artifacts(callback.run.info.run_id, \"config.yaml\", local_dir)\n\n    with open(local_config_path) as f:\n        config_artifact = yaml.safe_load(f)\n    assert config_artifact == upgrade_config_dict_to_latest_version(config)\n\n    model_path = f\"runs:/{callback.run.info.run_id}/model\"\n    loaded_model = mlflow.pyfunc.load_model(model_path)\n\n    assert \"ludwig\" in loaded_model.metadata.flavors\n    flavor = loaded_model.metadata.flavors[\"ludwig\"]\n    config = model.config\n\n    def compare_features(key):\n        assert len(config[key]) == len(flavor[\"ludwig_schema\"][key])\n        for feature, schema_feature in zip(config[key], flavor[\"ludwig_schema\"][key]):\n            assert feature[\"name\"] == schema_feature[\"name\"]\n            assert feature[\"type\"] == schema_feature[\"type\"]\n\n    compare_features(\"input_features\")\n    compare_features(\"output_features\")\n\n    test_df = pd.read_csv(test_data)\n    pred_df = loaded_model.predict(test_df)\n    assert pred_df.equals(expected_df)\n    return run\n\n\ndef run_mlflow_callback_test_without_artifacts(mlflow_client, config, training_data, val_data, test_data):\n    exp_name = \"mlflow_test_without_artifacts\"\n    callback = MlflowCallback(log_artifacts=False)\n    wrapped_callback = mock.Mock(wraps=callback)\n\n    model = LudwigModel(config, callbacks=[wrapped_callback], backend=FakeRemoteBackend())\n    model.train(training_set=training_data, validation_set=val_data, test_set=test_data, experiment_name=exp_name)\n    expected_df, _ = model.predict(test_data)\n\n    # Check mlflow artifacts\n    artifacts = [f.path for f in mlflow_client.list_artifacts(callback.run.info.run_id, \"\")]\n    assert len(artifacts) == 0\n\n\n@pytest.mark.parametrize(\"external_run\", [False, True], ids=[\"internal_run\", \"external_run\"])\ndef test_mlflow(tmpdir, external_run):\n    epochs = 2\n    batch_size = 8\n    num_examples = 32\n\n    input_features = [sequence_feature(reduce_output=\"sum\")]\n    output_features = [category_feature(vocab_size=2, reduce_input=\"sum\", output_feature=True)]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"batch_size\": batch_size},\n    }\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"train.csv\"), num_examples=num_examples\n    )\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    mlflow_uri = f\"file://{tmpdir}/mlruns\"\n    mlflow.set_tracking_uri(mlflow_uri)\n    client = MlflowClient(tracking_uri=mlflow_uri)\n\n    exp_name = None\n    run = None\n    if external_run:\n        # Start a run here and make sure it's still active when training completes\n        exp_name = f\"ext_experiment_{uuid.uuid4().hex}\"\n        exp_id = mlflow.create_experiment(name=exp_name)\n        run = mlflow.start_run(experiment_id=exp_id, run_name=f\"ext_run_{uuid.uuid4().hex}\")\n\n    callback_run = run_mlflow_callback_test(client, config, data_csv, val_csv, test_csv, tmpdir, exp_name=exp_name)\n\n    if not external_run:\n        run_mlflow_callback_test_without_artifacts(client, config, data_csv, val_csv, test_csv)\n    else:\n        assert run.info.run_id == callback_run.info.run_id\n\n        active_run = mlflow.active_run()\n        assert active_run is not None\n        assert run.info.run_id == active_run.info.run_id\n\n        mlflow.end_run()\n\n\ndef test_export_mlflow_local(tmpdir):\n    epochs = 2\n    batch_size = 8\n    num_examples = 32\n\n    input_features = [sequence_feature(reduce_output=\"sum\")]\n    output_features = [category_feature(vocab_size=2, reduce_input=\"sum\", output_feature=True)]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"batch_size\": batch_size},\n    }\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"train.csv\"), num_examples=num_examples\n    )\n\n    exp_name = \"mlflow_test\"\n    output_dir = os.path.join(tmpdir, \"output\")\n    model = LudwigModel(config, backend=FakeRemoteBackend())\n    _, _, output_directory = model.train(training_set=data_csv, experiment_name=exp_name, output_directory=output_dir)\n\n    model_path = os.path.join(output_directory, MODEL_FILE_NAME)\n    output_path = os.path.join(tmpdir, \"data/results/mlflow\")\n    export_mlflow(model_path, output_path)\n    assert set(os.listdir(output_path)) == {\"MLmodel\", MODEL_FILE_NAME, \"conda.yaml\"}\n\n\n@pytest.mark.distributed\ndef test_mlflow_ray(tmpdir, ray_cluster_2cpu):\n    epochs = 2\n    batch_size = 8\n    num_examples = 32\n\n    input_features = [sequence_feature(reduce_output=\"sum\")]\n    output_features = [category_feature(vocab_size=2, reduce_input=\"sum\", output_feature=True)]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": epochs, \"batch_size\": batch_size},\n    }\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"train.csv\"), num_examples=num_examples\n    )\n\n    exp_name = \"mlflow_test\"\n    output_dir = os.path.join(tmpdir, \"output\")\n    model = LudwigModel(config, callbacks=[MlflowCallback()], backend=\"ray\")\n    _, _, output_directory = model.train(training_set=data_csv, experiment_name=exp_name, output_directory=output_dir)\n"
  },
  {
    "path": "tests/integration_tests/test_model_save_and_load.py",
    "content": "import os\nimport os.path\nimport random\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, ENCODER, LOSS, NAME, PREPROCESSING, TRAINER, TRAINING, TYPE\nfrom ludwig.data.split import get_splitter\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.modules.loss_modules import MSELoss\nfrom ludwig.schema.features.loss.loss import MSELossConfig\nfrom ludwig.utils.data_utils import read_csv\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    bag_feature,\n    binary_feature,\n    category_feature,\n    date_feature,\n    generate_data,\n    h3_feature,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\n\ndef test_model_load_from_checkpoint(tmpdir, csv_filename, tmp_path):\n    torch.manual_seed(1)\n    random.seed(1)\n    np.random.seed(1)\n\n    input_features = [\n        binary_feature(),\n        number_feature(),\n    ]\n\n    output_features = [\n        binary_feature(),\n    ]\n\n    data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 2},\n    }\n    backend = LocalTestBackend()\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    data_df = read_csv(data_csv_path)\n    splitter = get_splitter(\"random\")\n    training_set, validation_set, test_set = splitter.split(data_df, backend)\n    ludwig_model1 = LudwigModel(config, backend=backend)\n    _, _, output_dir = ludwig_model1.train(\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        output_directory=\"results\",  # results_dir\n    )\n\n    model_dir = os.path.join(output_dir, MODEL_FILE_NAME)\n    ludwig_model_loaded = LudwigModel.load(model_dir, backend=backend, from_checkpoint=True)\n    preds_1, _ = ludwig_model1.predict(dataset=validation_set)\n\n    def check_model_equal(ludwig_model2):\n        # Compare model predictions\n        preds_2, _ = ludwig_model2.predict(dataset=validation_set)\n        assert set(preds_1.keys()) == set(preds_2.keys())\n        for key in preds_1:\n            assert preds_1[key].dtype == preds_2[key].dtype, key\n            assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key\n            # assert preds_2[key].dtype == preds_3[key].dtype, key\n            # assert list(preds_2[key]) == list(preds_3[key]), key\n\n        # Compare model weights\n        for if_name in ludwig_model1.model.input_features:\n            if1 = ludwig_model1.model.input_features.get(if_name)\n            if2 = ludwig_model2.model.input_features.get(if_name)\n            for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()):\n                assert torch.allclose(if1_w, if2_w)\n\n        c1 = ludwig_model1.model.combiner\n        c2 = ludwig_model2.model.combiner\n        for c1_w, c2_w in zip(c1.parameters(), c2.parameters()):\n            assert torch.allclose(c1_w, c2_w)\n\n        for of_name in ludwig_model1.model.output_features:\n            of1 = ludwig_model1.model.output_features.get(of_name)\n            of2 = ludwig_model2.model.output_features.get(of_name)\n            for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()):\n                assert torch.allclose(of1_w, of2_w)\n\n    check_model_equal(ludwig_model_loaded)\n\n\ndef test_model_save_reload_api(tmpdir, csv_filename, tmp_path):\n    torch.manual_seed(1)\n    random.seed(1)\n    np.random.seed(1)\n\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n\n    input_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        sequence_feature(encoder={\"vocab_size\": 3}),\n        text_feature(\n            encoder={\"vocab_size\": 3, \"type\": \"rnn\", \"cell_type\": \"lstm\", \"num_layers\": 2, \"bidirectional\": False}\n        ),\n        vector_feature(),\n        image_feature(image_dest_folder, encoder={\"type\": \"mlp_mixer\", \"patch_size\": 12}),\n        audio_feature(audio_dest_folder, encoder={\"type\": \"stacked_cnn\"}),\n        timeseries_feature(encoder={\"type\": \"parallel_cnn\"}),\n        sequence_feature(encoder={\"vocab_size\": 3, \"type\": \"stacked_parallel_cnn\"}),\n        date_feature(),\n        h3_feature(),\n        set_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n    ]\n\n    output_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 3}, output_feature=True),\n        sequence_feature(decoder={\"vocab_size\": 3}, output_feature=True),\n        text_feature(decoder={\"vocab_size\": 3}, output_feature=True),\n        set_feature(decoder={\"vocab_size\": 3}, output_feature=True),\n        vector_feature(),\n    ]\n\n    # Generate test data\n    data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    #############\n    # Train model\n    #############\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    data_df = read_csv(data_csv_path)\n    splitter = get_splitter(\"random\")\n    training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend())\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # perform initial model training\n    backend = LocalTestBackend()\n    ludwig_model1 = LudwigModel(config, backend=backend)\n    _, _, output_dir = ludwig_model1.train(\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        output_directory=\"results\",  # results_dir\n    )\n\n    preds_1, _ = ludwig_model1.predict(dataset=validation_set)\n\n    def check_model_equal(ludwig_model2):\n        # Compare model predictions\n        preds_2, _ = ludwig_model2.predict(dataset=validation_set)\n        assert set(preds_1.keys()) == set(preds_2.keys())\n        for key in preds_1:\n            assert preds_1[key].dtype == preds_2[key].dtype, key\n            assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key\n            # assert preds_2[key].dtype == preds_3[key].dtype, key\n            # assert list(preds_2[key]) == list(preds_3[key]), key\n\n        # Compare model weights\n        for if_name in ludwig_model1.model.input_features:\n            if1 = ludwig_model1.model.input_features.get(if_name)\n            if2 = ludwig_model2.model.input_features.get(if_name)\n            for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()):\n                assert torch.allclose(if1_w, if2_w)\n\n        c1 = ludwig_model1.model.combiner\n        c2 = ludwig_model2.model.combiner\n        for c1_w, c2_w in zip(c1.parameters(), c2.parameters()):\n            assert torch.allclose(c1_w, c2_w)\n\n        for of_name in ludwig_model1.model.output_features:\n            of1 = ludwig_model1.model.output_features.get(of_name)\n            of2 = ludwig_model2.model.output_features.get(of_name)\n            for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()):\n                assert torch.allclose(of1_w, of2_w)\n\n    ludwig_model1.save(tmpdir)\n    ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend)\n    check_model_equal(ludwig_model_loaded)\n\n    # Test loading the model from the experiment directory\n    ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend)\n    check_model_equal(ludwig_model_exp)\n\n\ndef test_model_weights_match_training(tmpdir, csv_filename):\n    np.random.seed(1)\n\n    input_features = [number_feature()]\n    output_features = [number_feature()]\n    output_feature_name = output_features[0][NAME]\n\n    # Generate test data\n    data_csv_path = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=20)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"trainer\": {\n            \"epochs\": 3,\n            \"batch_size\": 32,\n            \"evaluate_training_set\": True,  # needed to ensure exact training metrics computed\n        },\n    }\n\n    model = LudwigModel(\n        config=config,\n    )\n\n    training_stats, _, _ = model.train(training_set=data_csv_path, random_seed=1919)\n\n    # generate predicitons from training data\n    df = pd.read_csv(data_csv_path)\n    predictions = model.predict(df)\n\n    # compute loss on predictions from training data\n    loss_function = MSELoss(MSELossConfig())\n    loss = loss_function(\n        torch.tensor(predictions[0][output_feature_name + \"_predictions\"].values),  # predictions\n        torch.tensor(df[output_feature_name].values),  # target\n    ).type(torch.float32)\n\n    # get last loss value from training\n    last_training_loss = torch.tensor(training_stats[TRAINING][output_feature_name][LOSS][-1])\n\n    # loss from predictions should match last loss value recorded during training\n    assert torch.isclose(loss, last_training_loss), (\n        \"Model predictions on training set did not generate same loss value as in training. \"\n        \"Need to confirm that weights were correctly captured in model.\"\n    )\n\n\n@pytest.mark.parametrize(\"torch_encoder, variant\", [(\"resnet\", 18), (\"googlenet\", \"base\")])\ndef test_model_save_reload_tv_model(torch_encoder, variant, tmpdir, csv_filename, tmp_path):\n    torch.manual_seed(1)\n    random.seed(1)\n    np.random.seed(1)\n\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    input_features = [\n        image_feature(image_dest_folder),\n    ]\n    input_features[0][ENCODER] = {\n        TYPE: torch_encoder,\n        \"model_variant\": variant,\n    }\n    input_features[0][PREPROCESSING][\"height\"] = 128\n    input_features[0][PREPROCESSING][\"width\"] = 128\n\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 3}),\n    ]\n\n    # Generate test data\n    data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    #############\n    # Train model\n    #############\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    data_df = read_csv(data_csv_path)\n    splitter = get_splitter(\"random\")\n    training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend())\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # perform initial model training\n    backend = LocalTestBackend()\n    ludwig_model1 = LudwigModel(config, backend=backend)\n    _, _, output_dir = ludwig_model1.train(\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        output_directory=\"results\",  # results_dir\n    )\n\n    preds_1, _ = ludwig_model1.predict(dataset=validation_set)\n\n    def check_model_equal(ludwig_model2):\n        # Compare model predictions\n        preds_2, _ = ludwig_model2.predict(dataset=validation_set)\n        assert set(preds_1.keys()) == set(preds_2.keys())\n        for key in preds_1:\n            assert preds_1[key].dtype == preds_2[key].dtype, key\n            assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key\n            # assert preds_2[key].dtype == preds_3[key].dtype, key\n            # assert list(preds_2[key]) == list(preds_3[key]), key\n\n        # Compare model weights\n        for if_name in ludwig_model1.model.input_features:\n            if1 = ludwig_model1.model.input_features.get(if_name)\n            if2 = ludwig_model2.model.input_features.get(if_name)\n            for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()):\n                assert torch.allclose(if1_w, if2_w)\n\n        c1 = ludwig_model1.model.combiner\n        c2 = ludwig_model2.model.combiner\n        for c1_w, c2_w in zip(c1.parameters(), c2.parameters()):\n            assert torch.allclose(c1_w, c2_w)\n\n        for of_name in ludwig_model1.model.output_features:\n            of1 = ludwig_model1.model.output_features.get(of_name)\n            of2 = ludwig_model2.model.output_features.get(of_name)\n            for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()):\n                assert torch.allclose(of1_w, of2_w)\n\n    ludwig_model1.save(tmpdir)\n    ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend)\n\n    # confirm model structure and weights are the same\n    check_model_equal(ludwig_model_loaded)\n\n    # Test loading the model from the experiment directory\n    ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend)\n\n    # confirm model structure and weights are the same\n    check_model_equal(ludwig_model_exp)\n\n\n@pytest.mark.slow\ndef test_model_save_reload_hf_model(tmpdir, csv_filename, tmp_path):\n    torch.manual_seed(1)\n    random.seed(1)\n    np.random.seed(1)\n\n    input_features = [\n        text_feature(\n            encoder={\n                \"vocab_size\": 3,\n                \"type\": \"bert\",\n            }\n        ),\n    ]\n\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 3}),\n    ]\n\n    # Generate test data\n    data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    #############\n    # Train model\n    #############\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    data_df = read_csv(data_csv_path)\n    splitter = get_splitter(\"random\")\n    training_set, validation_set, test_set = splitter.split(data_df, LocalTestBackend())\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # perform initial model training\n    backend = LocalTestBackend()\n    ludwig_model1 = LudwigModel(config, backend=backend)\n    _, _, output_dir = ludwig_model1.train(\n        training_set=training_set,\n        validation_set=validation_set,\n        test_set=test_set,\n        output_directory=\"results\",  # results_dir\n    )\n\n    preds_1, _ = ludwig_model1.predict(dataset=validation_set)\n\n    def check_model_equal(ludwig_model2):\n        # Compare model predictions\n        preds_2, _ = ludwig_model2.predict(dataset=validation_set)\n        assert set(preds_1.keys()) == set(preds_2.keys())\n        for key in preds_1:\n            assert preds_1[key].dtype == preds_2[key].dtype, key\n            assert np.all(a == b for a, b in zip(preds_1[key], preds_2[key])), key\n            # assert preds_2[key].dtype == preds_3[key].dtype, key\n            # assert list(preds_2[key]) == list(preds_3[key]), key\n\n        # Compare model weights\n        for if_name in ludwig_model1.model.input_features:\n            if1 = ludwig_model1.model.input_features.get(if_name)\n            if2 = ludwig_model2.model.input_features.get(if_name)\n            for if1_w, if2_w in zip(if1.encoder_obj.parameters(), if2.encoder_obj.parameters()):\n                assert torch.allclose(if1_w, if2_w)\n\n        c1 = ludwig_model1.model.combiner\n        c2 = ludwig_model2.model.combiner\n        for c1_w, c2_w in zip(c1.parameters(), c2.parameters()):\n            assert torch.allclose(c1_w, c2_w)\n\n        for of_name in ludwig_model1.model.output_features:\n            of1 = ludwig_model1.model.output_features.get(of_name)\n            of2 = ludwig_model2.model.output_features.get(of_name)\n            for of1_w, of2_w in zip(of1.decoder_obj.parameters(), of2.decoder_obj.parameters()):\n                assert torch.allclose(of1_w, of2_w)\n\n    ludwig_model1.save(tmpdir)\n    ludwig_model_loaded = LudwigModel.load(tmpdir, backend=backend)\n\n    # confirm model structure and weights are the same\n    check_model_equal(ludwig_model_loaded)\n\n    # Test loading the model from the experiment directory\n    ludwig_model_exp = LudwigModel.load(os.path.join(output_dir, MODEL_FILE_NAME), backend=backend)\n\n    # confirm model structure and weights are the same\n    check_model_equal(ludwig_model_exp)\n"
  },
  {
    "path": "tests/integration_tests/test_model_training_options.py",
    "content": "import json\nimport logging\nimport os.path\nimport re\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig import globals as global_vars\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    CATEGORY,\n    DEFAULTS,\n    EPOCHS,\n    INPUT_FEATURES,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    TRAINER,\n    TRAINING,\n)\nfrom ludwig.contribs.mlflow import MlflowCallback\nfrom ludwig.experiment import experiment_cli\nfrom ludwig.features.number_feature import numeric_transformation_registry\nfrom ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME, TRAINING_PREPROC_FILE_NAME\nfrom ludwig.utils.data_utils import load_json, replace_file_extension\nfrom ludwig.utils.misc_utils import get_from_registry\nfrom ludwig.utils.package_utils import LazyLoader\nfrom tests.integration_tests import synthetic_test_data\nfrom tests.integration_tests.utils import category_feature, generate_data, LocalTestBackend\n\nmlflow = LazyLoader(\"mlflow\", globals(), \"mlflow\")\n\nRANDOM_SEED = 42\n\n\n@pytest.mark.parametrize(\"early_stop\", [3, 5])\ndef test_early_stopping(early_stop, tmp_path):\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": 20, \"early_stop\": early_stop, \"batch_size\": 16, \"learning_rate\": 0.01},\n    }\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # run experiment\n    generated_data = synthetic_test_data.get_generated_data()\n    _, _, _, _, output_dir = experiment_cli(\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n        output_directory=str(results_dir),\n        config=config,\n        skip_save_processed_input=True,\n        skip_save_progress=True,\n        skip_save_unprocessed_output=True,\n        skip_save_model=True,\n        skip_save_log=True,\n    )\n\n    # test existence of required files\n    train_stats_fp = os.path.join(output_dir, \"training_statistics.json\")\n    metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME)\n    assert os.path.isfile(train_stats_fp)\n    assert os.path.isfile(metadata_fp)\n\n    # retrieve results so we can validate early stopping\n    with open(train_stats_fp) as f:\n        train_stats = json.load(f)\n    with open(metadata_fp) as f:\n        metadata = json.load(f)\n\n    # get early stopping value\n    early_stop_value = metadata[\"config\"][TRAINER][\"early_stop\"]\n\n    # retrieve validation losses\n    vald_losses_data = train_stats[\"validation\"][\"combined\"][\"loss\"]\n\n    last_evaluation = len(vald_losses_data) - 1\n    best_evaluation = np.argmin(vald_losses_data)\n\n    assert last_evaluation - best_evaluation == early_stop_value\n\n\n@pytest.mark.parametrize(\"skip_save_progress\", [False])\n@pytest.mark.parametrize(\"skip_save_model\", [False, True])\ndef test_model_progress_save(skip_save_progress, skip_save_model, tmp_path):\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # run experiment\n    generated_data = synthetic_test_data.get_generated_data()\n    _, _, _, _, output_dir = experiment_cli(\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n        output_directory=str(results_dir),\n        config=config,\n        skip_save_processed_input=True,\n        skip_save_progress=skip_save_progress,\n        skip_save_unprocessed_output=True,\n        skip_save_model=skip_save_model,\n        skip_save_log=True,\n    )\n\n    # ========== Check for required result data sets =============\n    model_dir = os.path.join(output_dir, MODEL_FILE_NAME)\n    files = [f for f in os.listdir(model_dir) if re.match(MODEL_WEIGHTS_FILE_NAME, f)]\n    if skip_save_model:\n        assert len(files) == 0\n    else:\n        assert len(files) == 1\n\n    training_checkpoints_dir = os.path.join(output_dir, MODEL_FILE_NAME, \"training_checkpoints\")\n    training_checkpoints = os.listdir(training_checkpoints_dir)\n    if skip_save_progress:\n        assert len(training_checkpoints) == 0\n    else:\n        assert len(training_checkpoints) > 0\n\n\n@pytest.mark.parametrize(\"optimizer\", [\"sgd\", \"adam\"])\ndef test_resume_training(optimizer, tmp_path):\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": 2, \"batch_size\": 16, \"optimizer\": {\"type\": optimizer}},\n    }\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    generated_data = synthetic_test_data.get_generated_data()\n    _, _, _, _, output_dir1 = experiment_cli(\n        config,\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n    )\n\n    config[TRAINER][\"epochs\"] = 5\n\n    experiment_cli(\n        config,\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n        model_resume_path=output_dir1,\n    )\n\n    _, _, _, _, output_dir2 = experiment_cli(\n        config,\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n    )\n\n    # compare learning curves with and without resuming\n    ts1 = load_json(os.path.join(output_dir1, \"training_statistics.json\"))\n    ts2 = load_json(os.path.join(output_dir2, \"training_statistics.json\"))\n    print(\"ts1\", ts1)\n    print(\"ts2\", ts2)\n    assert ts1[TRAINING][\"combined\"][\"loss\"] == ts2[TRAINING][\"combined\"][\"loss\"]\n\n    # compare predictions with and without resuming\n    y_pred1 = np.load(os.path.join(output_dir1, \"y_predictions.npy\"))\n    y_pred2 = np.load(os.path.join(output_dir2, \"y_predictions.npy\"))\n    print(\"y_pred1\", y_pred1)\n    print(\"y_pred2\", y_pred2)\n    assert np.all(np.isclose(y_pred1, y_pred2))\n\n\n@pytest.mark.parametrize(\"optimizer\", [\"sgd\", \"adam\"])\ndef test_resume_training_mlflow(optimizer, tmp_path):\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": 2, \"batch_size\": 16, \"eval_batch_size\": 2, \"optimizer\": {\"type\": optimizer}},\n    }\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n    mlflow_uri = f\"file://{tmp_path}/mlruns\"\n    experiment_name = optimizer + \"_experiment\"\n\n    generated_data = synthetic_test_data.get_generated_data()\n    _, _, _, _, output_dir1 = experiment_cli(\n        config,\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n        callbacks=[MlflowCallback(mlflow_uri)],\n        experiment_name=experiment_name,\n    )\n    # Can't change any artifact spec on a run once it has been logged to mlflow, so skipping changing epochs\n\n    _, _, _, _, output_dir2 = experiment_cli(\n        config,\n        training_set=generated_data.train_df,\n        validation_set=generated_data.validation_df,\n        test_set=generated_data.test_df,\n        model_resume_path=output_dir1,\n        callbacks=[MlflowCallback(mlflow_uri)],\n        experiment_name=experiment_name,\n    )\n\n    # make sure there is only one mlflow run id\n    experiment = mlflow.get_experiment_by_name(experiment_name)\n    previous_runs = mlflow.search_runs([experiment.experiment_id])\n    assert len(previous_runs) == 1\n\n\n@pytest.mark.parametrize(\"optimizer_type\", [\"sgd\", \"adam\", \"adamw\", \"adagrad\", \"rmsprop\"])\ndef test_optimizers(optimizer_type, tmp_path):\n    if (optimizer_type in {\"lars\", \"lamb\", \"lion\"}) and (\n        not torch.cuda.is_available() or torch.cuda.device_count() == 0\n    ):\n        pytest.skip(\"Skip: lars, lamb, and lion optimizers require GPU and none are available.\")\n\n    if (\"paged\" in optimizer_type or \"8bit\" in optimizer_type) and (\n        not torch.cuda.is_available() or torch.cuda.device_count() == 0\n    ):\n        pytest.skip(\"Skip: paged and 8-bit optimizers require GPU and none are available.\")\n\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\"epochs\": 2, \"batch_size\": 16, \"evaluate_training_set\": True, \"optimizer\": {\"type\": optimizer_type}},\n    }\n\n    # special handling for adadelta and lbfgs, break out of local minima\n    if optimizer_type == \"adadelta\":\n        config[TRAINER][\"learning_rate\"] = 0.1\n    if optimizer_type == \"lbfgs\":\n        config[TRAINER][\"learning_rate\"] = 0.05\n\n    model = LudwigModel(config)\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    # run experiment\n    generated_data = synthetic_test_data.get_generated_data_for_optimizer()\n    train_stats, preprocessed_data, output_directory = model.train(\n        training_set=generated_data.train_df,\n        output_directory=str(results_dir),\n        config=config,\n        skip_save_processed_input=True,\n        skip_save_progress=True,\n        skip_save_unprocessed_output=True,\n        skip_save_model=True,\n        skip_save_log=True,\n    )\n\n    # retrieve training losses for first and last entries.\n    train_losses = train_stats[TRAINING][\"combined\"][\"loss\"]\n    last_entry = len(train_losses)\n\n    # ensure train loss for last entry is less than to the first entry.\n    np.testing.assert_array_less(train_losses[last_entry - 1], train_losses[0])\n\n\ndef test_regularization(tmp_path):\n    input_features, output_features = synthetic_test_data.get_feature_configs()\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\"},\n        TRAINER: {\n            \"epochs\": 1,\n            \"batch_size\": 16,\n            \"regularization_lambda\": 1,\n        },\n    }\n\n    # create sub-directory to store results\n    results_dir = tmp_path / \"results\"\n    results_dir.mkdir()\n\n    regularization_losses = []\n    generated_data = synthetic_test_data.get_generated_data()\n    for regularizer in [None, \"l1\", \"l2\", \"l1_l2\"]:\n        np.random.seed(RANDOM_SEED)\n        torch.manual_seed(RANDOM_SEED)\n\n        # setup regularization parameters\n        config[TRAINER][\"regularization_type\"] = regularizer\n\n        # run experiment\n        _, _, _, _, output_dir = experiment_cli(\n            training_set=generated_data.train_df,\n            validation_set=generated_data.validation_df,\n            test_set=generated_data.test_df,\n            output_directory=str(results_dir),\n            config=config,\n            experiment_name=\"regularization\",\n            model_name=str(regularizer),\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n            skip_save_model=True,\n            skip_save_log=True,\n        )\n\n        # test existence of required files\n        train_stats_fp = os.path.join(output_dir, \"training_statistics.json\")\n        metadata_fp = os.path.join(output_dir, DESCRIPTION_FILE_NAME)\n        assert os.path.isfile(train_stats_fp)\n        assert os.path.isfile(metadata_fp)\n\n        # retrieve results so we can compare training loss with regularization\n        with open(train_stats_fp) as f:\n            train_stats = json.load(f)\n\n        # retrieve training losses for all epochs\n        train_losses = train_stats[TRAINING][\"combined\"][\"loss\"]\n        regularization_losses.append(train_losses[0])\n\n    # create a set of losses\n    regularization_losses_set = set(regularization_losses)\n\n    # ensure all losses obtained with the different methods are different\n    assert len(regularization_losses) == len(regularization_losses_set)\n\n\n# test cache checksum function\ndef test_cache_checksum(csv_filename, tmp_path):\n    # setup for training\n    input_features = [category_feature(encoder={\"vocab_size\": 5})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, top_k=2)]\n\n    source_dataset = os.path.join(tmp_path, csv_filename)\n    source_dataset = generate_data(input_features, output_features, source_dataset)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        DEFAULTS: {CATEGORY: {PREPROCESSING: {\"fill_value\": \"<UNKNOWN>\"}}},\n        TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},\n    }\n\n    backend = LocalTestBackend()\n    cache_fname = replace_file_extension(source_dataset, TRAINING_PREPROC_FILE_NAME)\n\n    # conduct initial training\n    output_directory = os.path.join(tmp_path, \"results\")\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    first_training_timestamp = os.path.getmtime(cache_fname)\n\n    # conduct second training, should not force recreating hdf5\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # time stamps should be the same\n    assert first_training_timestamp == current_training_timestamp\n\n    # force recreating cache file by changing checksum by updating defaults\n    prior_training_timestamp = current_training_timestamp\n    config[DEFAULTS][CATEGORY][PREPROCESSING][\"fill_value\"] = \"<EMPTY>\"\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # timestamp should differ\n    assert prior_training_timestamp < current_training_timestamp\n\n    # force recreating cache by updating modification time of source dataset\n    prior_training_timestamp = current_training_timestamp\n    os.utime(source_dataset)\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # timestamps should be different\n    assert prior_training_timestamp < current_training_timestamp\n\n    # force change in feature preprocessing\n    prior_training_timestamp = current_training_timestamp\n    input_features = config[INPUT_FEATURES].copy()\n    input_features[0][PREPROCESSING] = {\"lowercase\": True}\n    config[INPUT_FEATURES] = input_features\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # timestamps should be different\n    assert prior_training_timestamp < current_training_timestamp\n\n    # force change in features names (and properties)\n    prior_training_timestamp = current_training_timestamp\n    input_features = [category_feature(encoder={\"vocab_size\": 5}), category_feature()]\n    source_dataset = generate_data(input_features, output_features, source_dataset)\n    config[INPUT_FEATURES] = input_features\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # timestamps should be different\n    assert prior_training_timestamp < current_training_timestamp\n\n    # force change in Ludwig version\n    prior_training_timestamp = current_training_timestamp\n    global_vars.LUDWIG_VERSION = \"new_version\"\n    model = LudwigModel(config, backend=backend)\n    model.train(dataset=source_dataset, output_directory=output_directory)\n    current_training_timestamp = os.path.getmtime(cache_fname)\n\n    # timestamps should be different\n    assert prior_training_timestamp < current_training_timestamp\n\n\n@pytest.mark.parametrize(\"transformer_key\", list(numeric_transformation_registry.keys()))\ndef test_numeric_transformer(transformer_key, tmpdir):\n    Transformer = get_from_registry(transformer_key, numeric_transformation_registry)\n    transformer_name = Transformer().__class__.__name__\n    if transformer_name == \"Log1pTransformer\":\n        raw_values = np.random.lognormal(5, 2, size=100)\n    else:\n        raw_values = np.random.normal(5, 2, size=100)\n\n    backend = LOCAL_BACKEND\n    parameters = Transformer.fit_transform_params(raw_values, backend)\n    if transformer_name in {\"Log1pTransformer\", \"IdentityTransformer\"}:\n        # should be empty\n        assert not bool(parameters)\n    else:\n        # should not be empty\n        assert bool(parameters)\n\n    # instantiate numeric transformer\n    numeric_transfomer = Transformer(**parameters)\n\n    # transform values\n    transformed_values = numeric_transfomer.transform(raw_values)\n\n    # inverse transform the prior transformed values\n    reconstructed_values = numeric_transfomer.inverse_transform(transformed_values)\n\n    # should now match\n    assert np.allclose(raw_values, reconstructed_values)\n\n    # now test numeric transformer with output feature\n    df = pd.DataFrame(np.array([raw_values, raw_values]).T, columns=[\"x\", \"y\"])\n    config = {\n        \"input_features\": [{\"name\": \"x\", \"type\": \"number\"}],\n        \"output_features\": [{\"name\": \"y\", \"type\": \"number\", \"preprocessing\": {\"normalization\": transformer_key}}],\n        \"combiner\": {\n            \"type\": \"concat\",\n        },\n        TRAINER: {\n            \"epochs\": 2,\n            \"batch_size\": 16,\n        },\n    }\n\n    args = {\n        \"config\": config,\n        \"skip_save_processed_input\": True,\n        \"output_directory\": os.path.join(tmpdir, \"results\"),\n        \"logging_level\": logging.WARN,\n    }\n\n    # ensure no exceptions are raised\n    experiment_cli(dataset=df, **args)\n"
  },
  {
    "path": "tests/integration_tests/test_number_feature.py",
    "content": "import pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom tests.integration_tests.utils import generate_data, number_feature\n\n\ndef test_number_feature_zscore_normalization_error():\n    input_features = [number_feature(name=\"num_input\", preprocessing={\"normalization\": \"zscore\"})]\n    output_features = [number_feature(name=\"num_output\")]\n\n    df = pd.read_csv(generate_data(input_features, output_features))\n\n    # Override input number feature to have a constant value\n    df[\"num_input\"] = 1\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    model = LudwigModel(config, backend=\"local\")\n\n    with pytest.raises(RuntimeError):\n        model.preprocess(dataset=df)\n"
  },
  {
    "path": "tests/integration_tests/test_peft.py",
    "content": "import os\n\nimport pytest\n\nfrom ludwig.constants import COMBINER, EPOCHS, INPUT_FEATURES, OUTPUT_FEATURES, TRAINER, TYPE\nfrom tests.integration_tests.utils import binary_feature, generate_data, run_test_suite, text_feature\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_text_adapter_lora(tmpdir, backend, ray_cluster_2cpu):\n    input_features = [\n        text_feature(\n            encoder={\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-bert-for-token-classification\",\n                \"trainable\": True,\n                \"adapter\": {\"type\": \"lora\"},\n            },\n        ),\n    ]\n    output_features = [binary_feature()]\n\n    data_csv_path = os.path.join(tmpdir, \"dataset.csv\")\n    dataset = generate_data(input_features, output_features, data_csv_path)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        COMBINER: {TYPE: \"concat\", \"output_size\": 14},\n        TRAINER: {EPOCHS: 1},\n    }\n    model = run_test_suite(config, dataset, backend)\n\n    state_dict = model.model.state_dict()\n\n    # check that at least one of the keys contains the word \"lora_\" denoting a lora parameter\n    assert any(\"lora_\" in key for key in state_dict.keys())\n"
  },
  {
    "path": "tests/integration_tests/test_postprocessing.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport os\nfrom functools import partial\nfrom unittest import mock\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, DECODER, NAME, TRAINER\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    RAY_BACKEND_CONFIG,\n    set_feature,\n    text_feature,\n)\n\n\ndef random_binary_logits(*args, num_predict_samples, **kwargs):\n    # Produce an even mix of True and False predictions, as the model may be biased\n    # towards one direction without training\n    return torch.tensor(np.random.uniform(low=-1.0, high=1.0, size=(num_predict_samples,)), dtype=torch.float32)\n\n\ndef random_set_logits(*args, num_predict_samples, vocab_size, pct_positive, **kwargs):\n    # Produce a desired mix of predictions based on the pct_positive, as the model may be biased\n    # towards one direction without training\n    num_positive = int(num_predict_samples * pct_positive)\n    num_negative = num_predict_samples - num_positive\n    negative_logits = np.random.uniform(low=-1.0, high=-0.1, size=(num_negative, vocab_size))\n    positive_logits = np.random.uniform(low=0.1, high=1.0, size=(num_positive, vocab_size))\n    logits = np.concatenate([negative_logits, positive_logits], axis=0)\n    return torch.tensor(logits, dtype=torch.float32)  # simulate torch model output\n\n\ndef _run_binary_predictions(tmpdir, backend, distinct_values, ray_cluster_2cpu):\n    input_features = [\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n\n    feature = binary_feature()\n    output_features = [\n        feature,\n    ]\n\n    data_csv_path = generate_data(\n        input_features,\n        output_features,\n        os.path.join(tmpdir, \"dataset.csv\"),\n        num_examples=20,\n    )\n    data_df = pd.read_csv(data_csv_path)\n\n    # Optionally convert bool values to strings, e.g., {'Yes', 'No'}\n    false_value, true_value = distinct_values\n    data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value)\n    data_df.to_csv(data_csv_path, index=False)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n    }\n\n    patch_args = (\n        \"ludwig.features.binary_feature.BinaryOutputFeature.logits\",\n        partial(random_binary_logits, num_predict_samples=len(data_df)),\n    )\n\n    preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)\n    cols = set(preds_df.columns)\n    assert f\"{feature[NAME]}_predictions\" in cols\n    assert f\"{feature[NAME]}_probabilities_{str(false_value)}\" in cols\n    assert f\"{feature[NAME]}_probabilities_{str(true_value)}\" in cols\n    assert f\"{feature[NAME]}_probability\" in cols\n\n    for pred, prob_0, prob_1, prob in zip(\n        preds_df[f\"{feature[NAME]}_predictions\"],\n        preds_df[f\"{feature[NAME]}_probabilities_{str(false_value)}\"],\n        preds_df[f\"{feature[NAME]}_probabilities_{str(true_value)}\"],\n        preds_df[f\"{feature[NAME]}_probability\"],\n    ):\n        assert pred == false_value or pred == true_value\n        if pred == true_value:\n            assert prob_1 == prob\n        else:\n            assert prob_0 == prob\n        assert np.allclose(prob_0, 1 - prob_1)\n\n\n@pytest.mark.parametrize(\"distinct_values\", [(False, True), (\"No\", \"Yes\")])\ndef test_binary_predictions(tmpdir, distinct_values, ray_cluster_2cpu):\n    _run_binary_predictions(tmpdir, \"local\", distinct_values, ray_cluster_2cpu)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"distinct_values\", [(False, True), (\"No\", \"Yes\")])\ndef test_binary_predictions_ray(tmpdir, distinct_values, ray_cluster_2cpu):\n    _run_binary_predictions(tmpdir, \"ray\", distinct_values, ray_cluster_2cpu)\n\n\ndef _run_binary_predictions_with_number_dtype(tmpdir, backend, distinct_values, ray_cluster_2cpu):\n    input_features = [\n        category_feature(encoder={\"vocab_size\": 3}),\n    ]\n\n    feature = binary_feature()\n    output_features = [\n        feature,\n    ]\n\n    data_csv_path = generate_data(\n        input_features,\n        output_features,\n        os.path.join(tmpdir, \"dataset.csv\"),\n        num_examples=20,\n    )\n    data_df = pd.read_csv(data_csv_path)\n\n    # Optionally convert bool values to strings, e.g., {'Yes', 'No'}\n    false_value, true_value = distinct_values\n    data_df[feature[NAME]] = data_df[feature[NAME]].map(lambda x: true_value if x else false_value)\n    data_df.to_csv(data_csv_path, index=False)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n    }\n\n    patch_args = (\n        \"ludwig.features.binary_feature.BinaryOutputFeature.logits\",\n        partial(random_binary_logits, num_predict_samples=len(data_df)),\n    )\n\n    preds_df, _ = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)\n    cols = set(preds_df.columns)\n    assert f\"{feature[NAME]}_predictions\" in cols\n    assert f\"{feature[NAME]}_probabilities_False\" in cols\n    assert f\"{feature[NAME]}_probabilities_True\" in cols\n    assert f\"{feature[NAME]}_probability\" in cols\n\n    for pred, prob_0, prob_1, prob in zip(\n        preds_df[f\"{feature[NAME]}_predictions\"],\n        preds_df[f\"{feature[NAME]}_probabilities_False\"],\n        preds_df[f\"{feature[NAME]}_probabilities_True\"],\n        preds_df[f\"{feature[NAME]}_probability\"],\n    ):\n        assert isinstance(pred, bool)\n        if pred:\n            assert prob_1 == prob\n        else:\n            assert prob_0 == prob\n        assert np.allclose(prob_0, 1 - prob_1)\n\n\n@pytest.mark.parametrize(\"distinct_values\", [(0.0, 1.0), (0, 1)])\ndef test_binary_predictions_with_number_dtype(tmpdir, distinct_values, ray_cluster_2cpu):\n    _run_binary_predictions_with_number_dtype(tmpdir, \"local\", distinct_values, ray_cluster_2cpu)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"distinct_values\", [(0.0, 1.0), (0, 1)])\ndef test_binary_predictions_with_number_dtype_ray(tmpdir, distinct_values, ray_cluster_2cpu):\n    _run_binary_predictions_with_number_dtype(tmpdir, \"ray\", distinct_values, ray_cluster_2cpu)\n\n\n@pytest.mark.parametrize(\"pct_positive\", [1.0, 0.5, 0.0])\ndef test_set_feature_saving(tmpdir, pct_positive):\n    backend = \"local\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 3}),\n    ]\n\n    feature = set_feature(output_feature=True)\n    output_features = [\n        feature,\n    ]\n\n    data_csv_path = generate_data(\n        input_features,\n        output_features,\n        os.path.join(tmpdir, \"dataset.csv\"),\n        num_examples=20,\n    )\n    data_df = pd.read_csv(data_csv_path)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n    }\n\n    patch_args = (\n        \"ludwig.features.set_feature.SetOutputFeature.logits\",\n        partial(\n            random_set_logits,\n            num_predict_samples=len(data_df),\n            vocab_size=feature[DECODER][\"vocab_size\"] + 1,  # +1 for UNK\n            pct_positive=pct_positive,\n        ),\n    )\n\n    preds_df, ludwig_model = predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=patch_args)\n    cols = set(preds_df.columns)\n    assert f\"{feature[NAME]}_predictions\" in cols\n    assert f\"{feature[NAME]}_probabilities\" in cols\n\n    backend = ludwig_model.backend\n    backend.df_engine.to_parquet(preds_df, os.path.join(tmpdir, \"preds.parquet\"))  # test saving\n\n\ndef predict_with_backend(tmpdir, config, data_csv_path, backend, patch_args=None):\n    if backend == \"ray\":\n        backend = RAY_BACKEND_CONFIG\n        backend[\"processor\"][\"type\"] = \"dask\"\n\n    ludwig_model = LudwigModel(config, backend=backend)\n    _, _, output_directory = ludwig_model.train(\n        dataset=data_csv_path,\n        output_directory=os.path.join(tmpdir, \"output\"),\n    )\n    # Check that metadata JSON saves and loads correctly\n    ludwig_model = LudwigModel.load(os.path.join(output_directory, MODEL_FILE_NAME))\n\n    if patch_args is not None:\n        with mock.patch(*patch_args):\n            preds_df, _ = ludwig_model.predict(dataset=data_csv_path)\n    else:\n        preds_df, _ = ludwig_model.predict(dataset=data_csv_path)\n\n    return preds_df, ludwig_model\n"
  },
  {
    "path": "tests/integration_tests/test_preprocessing.py",
    "content": "import contextlib\nimport copy\nimport importlib.util\nimport logging\nimport os\nimport random\nimport string\nfrom unittest import mock\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nfrom PIL import Image\nfrom transformers import AutoTokenizer\n\nimport ludwig\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import initialize_backend\nfrom ludwig.callbacks import Callback\nfrom ludwig.constants import (\n    BASE_MODEL,\n    BATCH_SIZE,\n    COLUMN,\n    DECODER,\n    EPOCHS,\n    FULL,\n    INPUT_FEATURES,\n    MODEL_ECD,\n    MODEL_LLM,\n    MODEL_TYPE,\n    NAME,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    PROC_COLUMN,\n    PROMPT,\n    SPLIT,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.data.concatenate_datasets import concatenate_df\nfrom ludwig.data.preprocessing import handle_features_with_prompt_config, preprocess_for_prediction\nfrom ludwig.schema.llms.prompt import PromptConfig\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom tests.integration_tests.utils import (\n    assert_preprocessed_dataset_shape_and_dtype_for_feature,\n    audio_feature,\n    binary_feature,\n    category_feature,\n    generate_data,\n    generate_data_as_dataframe,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    text_feature,\n)\n\nNUM_EXAMPLES = 20\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_sample_ratio(backend, tmpdir, ray_cluster_2cpu):\n    num_examples = 50\n    sample_ratio = 0.5\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"}), audio_feature(folder=tmpdir)]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=num_examples\n    )\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n        PREPROCESSING: {\"sample_ratio\": sample_ratio},\n    }\n\n    model = LudwigModel(config, backend=backend)\n    train_set, val_set, test_set, training_set_metadata = model.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    sample_size = num_examples * sample_ratio\n    count = len(train_set) + len(val_set) + len(test_set)\n    assert sample_size == count\n\n    # Check that sample ratio is disabled when doing preprocessing for prediction\n    dataset, _ = preprocess_for_prediction(\n        model.config_obj.to_dict(),\n        dataset=data_csv,\n        training_set_metadata=training_set_metadata,\n        split=FULL,\n        include_outputs=True,\n        backend=model.backend,\n    )\n    assert \"sample_ratio\" in model.config_obj.preprocessing.to_dict()\n    assert len(dataset) == num_examples\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_sample_ratio_deterministic(backend, tmpdir, ray_cluster_2cpu):\n    \"\"\"Ensures that the sampled dataset is the same when using a random seed.\n\n    model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when\n    using the Ray backend.\n    \"\"\"\n    num_examples = 50\n    sample_ratio = 0.5\n\n    input_features = [binary_feature()]\n    output_features = [category_feature()]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=num_examples\n    )\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        PREPROCESSING: {\"sample_ratio\": sample_ratio},\n    }\n\n    model1 = LudwigModel(config, backend=backend)\n    train_set_1, val_set_1, test_set_1, _ = model1.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    model2 = LudwigModel(config, backend=backend)\n    train_set_2, val_set_2, test_set_2, _ = model2.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    sample_size = num_examples * sample_ratio\n\n    # Ensure sizes are the same\n    assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1)\n    assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2)\n\n    # Ensure actual rows are the same\n    if backend == \"local\":\n        assert train_set_1.to_df().equals(train_set_2.to_df())\n        assert val_set_1.to_df().equals(val_set_2.to_df())\n        assert test_set_1.to_df().equals(test_set_2.to_df())\n    else:\n        assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute())\n        assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute())\n        assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute())\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_sample_size(backend, tmpdir, ray_cluster_2cpu):\n    num_examples = 50\n    sample_size = 25\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"}), audio_feature(folder=tmpdir)]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=num_examples\n    )\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n        PREPROCESSING: {\"sample_size\": sample_size},\n    }\n\n    model = LudwigModel(config, backend=backend)\n    train_set, val_set, test_set, training_set_metadata = model.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    count = len(train_set) + len(val_set) + len(test_set)\n    assert sample_size == count\n\n    # Check that sample size is disabled when doing preprocessing for prediction\n    dataset, _ = preprocess_for_prediction(\n        model.config_obj.to_dict(),\n        dataset=data_csv,\n        training_set_metadata=training_set_metadata,\n        split=FULL,\n        include_outputs=True,\n        backend=model.backend,\n    )\n    assert \"sample_size\" in model.config_obj.preprocessing.to_dict()\n    assert len(dataset) == num_examples\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_sample_size_deterministic(backend, tmpdir, ray_cluster_2cpu):\n    \"\"\"Ensures that the sampled dataset is the same when using a random seed.\n\n    model.preprocess returns a PandasPandasDataset object when using local backend, and returns a RayDataset object when\n    using the Ray backend.\n    \"\"\"\n    num_examples = 50\n    sample_size = 25\n\n    input_features = [binary_feature()]\n    output_features = [category_feature()]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=num_examples\n    )\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        PREPROCESSING: {\"sample_size\": sample_size},\n    }\n\n    model1 = LudwigModel(config, backend=backend)\n    train_set_1, val_set_1, test_set_1, _ = model1.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    model2 = LudwigModel(config, backend=backend)\n    train_set_2, val_set_2, test_set_2, _ = model2.preprocess(\n        data_csv,\n        skip_save_processed_input=True,\n    )\n\n    # Ensure sizes are the same\n    assert sample_size == len(train_set_1) + len(val_set_1) + len(test_set_1)\n    assert sample_size == len(train_set_2) + len(val_set_2) + len(test_set_2)\n\n    # Ensure actual rows are the same\n    if backend == \"local\":\n        assert train_set_1.to_df().equals(train_set_2.to_df())\n        assert val_set_1.to_df().equals(val_set_2.to_df())\n        assert test_set_1.to_df().equals(test_set_2.to_df())\n    else:\n        assert train_set_1.to_df().compute().equals(train_set_2.to_df().compute())\n        assert val_set_1.to_df().compute().equals(val_set_2.to_df().compute())\n        assert test_set_1.to_df().compute().equals(test_set_2.to_df().compute())\n\n\ndef test_strip_whitespace_category(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    input_features = [binary_feature()]\n    cat_feat = category_feature(decoder={\"vocab_size\": 3})\n    output_features = [cat_feat]\n    backend = LocalTestBackend()\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    # prefix with whitespace\n    df[cat_feat[COLUMN]] = df[cat_feat[COLUMN]].apply(lambda s: \" \" + s)\n\n    # run preprocessing\n    ludwig_model = LudwigModel(config, backend=backend)\n    train_ds, _, _, metadata = ludwig_model.preprocess(dataset=df)\n\n    # expect values containing whitespaces to be properly mapped to vocab_size unique values\n    assert len(np.unique(train_ds.dataset[cat_feat[PROC_COLUMN]])) == cat_feat[DECODER][\"vocab_size\"]\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_with_split(backend, csv_filename, tmpdir, ray_cluster_2cpu):\n    num_examples = NUM_EXAMPLES\n    train_set_size = int(num_examples * 0.8)\n    val_set_size = int(num_examples * 0.1)\n    test_set_size = int(num_examples * 0.1)\n\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=num_examples\n    )\n    data_df = pd.read_csv(data_csv)\n    data_df[SPLIT] = [0] * train_set_size + [1] * val_set_size + [2] * test_set_size\n    data_df.to_csv(data_csv, index=False)\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n        PREPROCESSING: {SPLIT: {TYPE: \"fixed\", COLUMN: SPLIT}},\n    }\n\n    model = LudwigModel(config, backend=backend)\n    train_set, val_set, test_set, _ = model.preprocess(\n        data_csv,\n        skip_save_processed_input=False,\n    )\n    assert len(train_set) == train_set_size\n    assert len(val_set) == val_set_size\n    assert len(test_set) == test_set_size\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"feature_fn\", [image_feature, audio_feature])\ndef test_dask_known_divisions(feature_fn, csv_filename, tmpdir, ray_cluster_2cpu):\n    import dask.dataframe as dd\n\n    input_features = [feature_fn(os.path.join(tmpdir, \"generated_output\"))]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=20)\n    data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=2)\n    assert data_df.known_divisions\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n    }\n\n    backend = \"ray\"\n    model = LudwigModel(config, backend=backend)\n    train_set, val_set, test_set, _ = model.preprocess(\n        data_df,\n        skip_save_processed_input=False,\n    )\n\n\n@pytest.mark.distributed\ndef test_drop_empty_partitions(csv_filename, tmpdir, ray_cluster_2cpu):\n    import dask.dataframe as dd\n\n    input_features = [image_feature(os.path.join(tmpdir, \"generated_output\"))]\n    output_features = [category_feature(vocab_size=5, reduce_input=\"sum\", output_feature=True)]\n\n    # num_examples and npartitions set such that each post-split DataFrame has >1 samples, but empty partitions.\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=25)\n    data_df = dd.from_pandas(pd.read_csv(data_csv), npartitions=10)\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n    }\n\n    backend = \"ray\"\n    model = LudwigModel(config, backend=backend)\n    train_set, val_set, test_set, _ = model.preprocess(\n        data_df,\n        skip_save_processed_input=True,\n    )\n    for dataset in [train_set, val_set, test_set]:\n        df = dataset.ds.to_dask()\n        for partition in df.partitions:\n            assert len(partition) > 0, \"empty partitions found in dataset\"\n\n\n@pytest.mark.parametrize(\"generate_images_as_numpy\", [False, True])\ndef test_read_image_from_path(tmpdir, csv_filename, generate_images_as_numpy):\n    input_features = [image_feature(os.path.join(tmpdir, \"generated_output\"), save_as_numpy=generate_images_as_numpy)]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES\n    )\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {EPOCHS: 2},\n    }\n\n    model = LudwigModel(config)\n    model.preprocess(\n        data_csv,\n        skip_save_processed_input=False,\n    )\n\n\ndef test_read_image_from_numpy_array(tmpdir, csv_filename):\n    input_features = [image_feature(os.path.join(tmpdir, \"generated_output\"))]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},\n    }\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES\n    )\n\n    df = pd.read_csv(data_csv)\n    processed_df_rows = []\n\n    for _, row in df.iterrows():\n        processed_df_rows.append(\n            {\n                input_features[0][NAME]: np.array(Image.open(row[input_features[0][NAME]])),\n                output_features[0][NAME]: row[output_features[0][NAME]],\n            }\n        )\n\n    df_with_images_as_numpy_arrays = pd.DataFrame(processed_df_rows)\n\n    model = LudwigModel(config)\n    model.preprocess(\n        df_with_images_as_numpy_arrays,\n        skip_save_processed_input=False,\n    )\n\n\ndef test_read_image_failure_default_image(monkeypatch, tmpdir, csv_filename):\n    \"\"\"Tests that the default image used when an image cannot be read has the correct properties.\"\"\"\n\n    def mock_read_binary_files(self, column, map_fn, file_size):\n        \"\"\"Mock read_binary_files to return None (failed image read) to test error handling.\"\"\"\n        return column.map(lambda x: None)\n\n    monkeypatch.setattr(ludwig.backend.base.LocalPreprocessingMixin, \"read_binary_files\", mock_read_binary_files)\n\n    image_feature_config = image_feature(os.path.join(tmpdir, \"generated_output\"))\n    input_features = [image_feature_config]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},\n    }\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES, nan_percent=0.2\n    )\n\n    model = LudwigModel(config)\n    preprocessed_dataset = model.preprocess(data_csv)\n    training_set_metadata = preprocessed_dataset.training_set_metadata\n\n    preprocessing = training_set_metadata[input_features[0][NAME]][PREPROCESSING]\n    expected_shape = (preprocessing[\"num_channels\"], preprocessing[\"height\"], preprocessing[\"width\"])\n    expected_dtype = np.float32\n\n    assert_preprocessed_dataset_shape_and_dtype_for_feature(\n        image_feature_config[NAME], preprocessed_dataset, model.config_obj, expected_dtype, expected_shape\n    )\n\n\ndef test_number_feature_wrong_dtype(csv_filename, tmpdir):\n    \"\"\"Tests that a number feature with all string values is treated as having missing values by default.\"\"\"\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    num_feat = number_feature()\n    input_features = [num_feat]\n    output_features = [binary_feature()]\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    # convert numbers to random strings\n    def random_string():\n        letters = string.ascii_lowercase\n        return \"\".join(random.choice(letters) for _ in range(10))\n\n    df[num_feat[COLUMN]] = df[num_feat[COLUMN]].apply(lambda _: random_string())\n\n    # run preprocessing\n    backend = LocalTestBackend()\n    ludwig_model = LudwigModel(config, backend=backend)\n    train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)\n\n    concatenated_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend)\n\n    # check that train_ds had invalid values replaced with the missing value\n    assert len(concatenated_df) == len(df)\n    assert np.all(concatenated_df[num_feat[PROC_COLUMN]] == 0.0)\n\n\n@pytest.mark.parametrize(\n    \"max_len, sequence_length, max_sequence_length, sequence_length_expected\",\n    [\n        # Case 1: infer from the dataset, max_sequence_length is larger than the largest sequence length.\n        # Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens).\n        (10, None, 15, 12),\n        # Case 2: infer from the dataset, max_sequence_length is smaller than the largest sequence length.\n        # Expected: max_sequence_length is used, and the sequence length is max_sequence_length.\n        (10, None, 8, 8),\n        # Case 3: infer from the dataset, max_sequence_length is not set.\n        # Expected: max_sequence_length is ignored, and the sequence length is dataset+2 (include start/stop tokens).\n        (10, None, None, 12),\n        # Case 4: set sequence_length explicitly and it is larger than the dataset.\n        # Expected: sequence_length is used, and the sequence length is sequence_length.\n        (10, 15, 20, 15),\n        # Case 5: set sequence_length explicitly and it is smaller than the dataset.\n        # Expected: sequence_length is used, and the sequence length is sequence_length.\n        (10, 8, 20, 8),\n    ],\n)\n@pytest.mark.parametrize(\n    \"feature_type\",\n    [\n        sequence_feature,\n    ],\n)\ndef test_seq_features_max_sequence_length(\n    csv_filename, tmpdir, feature_type, max_len, sequence_length, max_sequence_length, sequence_length_expected\n):\n    \"\"\"Tests that a sequence feature has the correct max_sequence_length in metadata and prepocessed data.\"\"\"\n    feat = feature_type(\n        encoder={\"max_len\": max_len},\n        preprocessing={\"sequence_length\": sequence_length, \"max_sequence_length\": max_sequence_length},\n    )\n    input_features = [feat]\n    output_features = [binary_feature()]\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    class CheckTrainingSetMetadataCallback(Callback):\n        def on_preprocess_end(self, proc_training_set, proc_validation_set, proc_test_set, training_set_metadata):\n            assert training_set_metadata[feat[NAME]][\"max_sequence_length\"] == sequence_length_expected\n\n    backend = LocalTestBackend()\n    ludwig_model = LudwigModel(config, backend=backend, callbacks=[CheckTrainingSetMetadataCallback()])\n    train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)\n\n    all_df = concatenate_df(train_ds.to_df(), val_ds.to_df(), test_ds.to_df(), backend)\n    proc_column_name = feat[PROC_COLUMN]\n    assert all(len(x) == sequence_length_expected for x in all_df[proc_column_name])\n\n\ndef test_column_feature_type_mismatch_fill():\n    \"\"\"Tests that we are able to fill missing values even in columns where the column dtype and desired feature\n    dtype do not match.\"\"\"\n    cat_feat = category_feature()\n    bin_feat = binary_feature()\n    input_features = [cat_feat]\n    output_features = [bin_feat]\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    # Construct dataframe with int-like column representing a categorical feature\n    df = pd.DataFrame(\n        {\n            cat_feat[NAME]: pd.Series(pd.array([None] + [1] * 24, dtype=pd.Int64Dtype())),\n            bin_feat[NAME]: pd.Series([True] * 25),\n        }\n    )\n\n    # run preprocessing\n    backend = LocalTestBackend()\n    ludwig_model = LudwigModel(config, backend=backend)\n    train_ds, val_ds, test_ds, _ = ludwig_model.preprocess(dataset=df)\n\n\n@pytest.mark.parametrize(\"format\", [\"file\", \"df\"])\ndef test_presplit_override(format, tmpdir):\n    \"\"\"Tests that provising a pre-split file or dataframe overrides the user's split config.\"\"\"\n    num_feat = number_feature(normalization=None)\n    input_features = [num_feat, sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=25)\n    data_df = pd.read_csv(data_csv)\n\n    # Set the feature value equal to an ordinal index so we can ensure the splits are identical before and after\n    # preprocessing.\n    data_df[num_feat[COLUMN]] = data_df.index\n\n    train_df = data_df[:15]\n    val_df = data_df[15:20]\n    test_df = data_df[20:]\n\n    train_data = train_df\n    val_data = val_df\n    test_data = test_df\n\n    if format == \"file\":\n        train_data = os.path.join(tmpdir, \"train.csv\")\n        val_data = os.path.join(tmpdir, \"val.csv\")\n        test_data = os.path.join(tmpdir, \"test.csv\")\n\n        train_df.to_csv(train_data)\n        val_df.to_csv(val_data)\n        test_df.to_csv(test_data)\n\n    data_df.to_csv(data_csv, index=False)\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\n            EPOCHS: 2,\n        },\n        PREPROCESSING: {SPLIT: {TYPE: \"random\"}},\n    }\n\n    model = LudwigModel(config, backend=LocalTestBackend())\n    train_set, val_set, test_set, _ = model.preprocess(\n        training_set=train_data, validation_set=val_data, test_set=test_data\n    )\n\n    assert len(train_set) == len(train_df)\n    assert len(val_set) == len(val_df)\n    assert len(test_set) == len(test_df)\n\n    assert np.all(train_set.to_df()[num_feat[PROC_COLUMN]].values == train_df[num_feat[COLUMN]].values)\n    assert np.all(val_set.to_df()[num_feat[PROC_COLUMN]].values == val_df[num_feat[COLUMN]].values)\n    assert np.all(test_set.to_df()[num_feat[PROC_COLUMN]].values == test_df[num_feat[COLUMN]].values)\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_empty_training_set_error(backend, tmpdir, ray_cluster_2cpu):\n    \"\"\"Tests that an error is raised if one or more of the splits is empty after preprocessing.\"\"\"\n    data_csv_path = os.path.join(tmpdir, \"data.csv\")\n\n    out_feat = binary_feature()\n    input_features = [number_feature()]\n    output_features = [out_feat]\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    # Convert all the output features rows to null. Because the default missing value strategy is to drop empty output\n    # rows, this will result in the dataset being empty after preprocessing.\n    df[out_feat[COLUMN]] = None\n\n    ludwig_model = LudwigModel(config, backend=backend)\n    with pytest.raises(RuntimeError, match=\"Training data is empty following preprocessing\"):\n        ludwig_model.preprocess(dataset=df)\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_in_memory_dataset_size(backend, tmpdir, ray_cluster_2cpu):\n    data_csv_path = os.path.join(tmpdir, \"data.csv\")\n\n    out_feat = binary_feature()\n    input_features = [number_feature()]\n    output_features = [out_feat]\n    config = {INPUT_FEATURES: input_features, OUTPUT_FEATURES: output_features}\n\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    ludwig_model = LudwigModel(config, backend=backend)\n    training_dataset, validation_dataset, test_dataset, _ = ludwig_model.preprocess(dataset=df)\n\n    assert training_dataset.in_memory_size_bytes > 0\n    assert validation_dataset.in_memory_size_bytes > 0\n    assert test_dataset.in_memory_size_bytes > 0\n\n\n@pytest.mark.parametrize(\n    \"binary_as_input, expected_preprocessing, missing_value_strategy\",\n    [\n        pytest.param(\n            True,\n            {\n                \"missing_value_strategy\": \"fill_with_true\",\n                \"fill_value\": None,\n                \"computed_fill_value\": \">50K\",\n                \"fallback_true_label\": \">50K\",\n            },\n            \"fill_with_true\",\n            id=\"binary_as_input_1\",\n        ),\n        pytest.param(\n            True,\n            {\n                \"missing_value_strategy\": \"fill_with_false\",\n                \"fill_value\": None,\n                \"computed_fill_value\": \"<=50K\",\n                \"fallback_true_label\": \">50K\",\n            },\n            \"fill_with_false\",\n            id=\"binary_as_input_2\",\n        ),\n        pytest.param(\n            False,\n            {\n                \"missing_value_strategy\": \"drop_row\",\n                \"fill_value\": None,\n                \"computed_fill_value\": None,\n                \"fallback_true_label\": \">50K\",\n            },\n            \"drop_row\",\n            id=\"binary_as_output\",\n        ),\n    ],\n)\ndef test_non_conventional_bool_with_fallback(binary_as_input, expected_preprocessing, missing_value_strategy, tmpdir):\n    # Specify a non-conventional boolean feature with a fallback true label.\n    bin_feature = binary_feature(\n        bool2str=[\"<=50K\", \">50K\"],\n        preprocessing={\"fallback_true_label\": \">50K\", \"missing_value_strategy\": missing_value_strategy},\n    )\n\n    # Generate data with the non-conventional boolean feature.\n    if binary_as_input:\n        input_features = [bin_feature]\n        output_features = [number_feature()]\n    else:\n        input_features = [number_feature()]\n        output_features = [bin_feature]\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},\n    }\n\n    data_csv_path = os.path.join(tmpdir, \"data.csv\")\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    # Preprocess the data.\n    ludwig_model = LudwigModel(config)\n    _, _, _, training_set_metadata = ludwig_model.preprocess(dataset=df)\n\n    # Check that true/false labels are set correctly.\n    assert training_set_metadata[bin_feature[NAME]] == {\n        \"str2bool\": {\"<=50K\": False, \">50K\": True},\n        \"bool2str\": [\"<=50K\", \">50K\"],\n        \"fallback_true_label\": \">50K\",\n        PREPROCESSING: expected_preprocessing,\n    }\n\n\n@pytest.mark.parametrize(\n    \"binary_as_input\", [pytest.param(True, id=\"binary_as_input\"), pytest.param(False, id=\"binary_as_output\")]\n)\ndef test_non_conventional_bool_without_fallback_logs_warning(binary_as_input, caplog, tmpdir):\n    # Specify a non-conventional boolean feature without a fallback true label.\n    bin_feature = binary_feature(bool2str=[\"<=50K\", \">50K\"], preprocessing={\"fallback_true_label\": None})\n\n    # Generate data with the non-conventional boolean feature.\n    if binary_as_input:\n        input_features = [bin_feature]\n        output_features = [number_feature()]\n    else:\n        input_features = [number_feature()]\n        output_features = [bin_feature]\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {EPOCHS: 2, BATCH_SIZE: 128},\n    }\n\n    data_csv_path = os.path.join(tmpdir, \"data.csv\")\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    df = pd.read_csv(training_data_csv_path)\n\n    # Preprocess the data.\n    with caplog.at_level(logging.WARN, logger=\"ludwig.features.binary_feature\"):\n        ludwig_model = LudwigModel(config)\n        ludwig_model.preprocess(dataset=df)\n\n    # Check that a warning is logged.\n    assert \"unconventional boolean value\" in caplog.text\n\n\n@pytest.mark.parametrize(\"feature_type\", [\"input_feature\", \"output_feature\"], ids=[\"input_feature\", \"output_feature\"])\ndef test_category_feature_vocab_size_1(feature_type, tmpdir) -> None:\n    data_csv_path = os.path.join(tmpdir, \"data.csv\")\n\n    input_feature = [category_feature(encoder={\"vocab_size\": 1})]\n    output_feature = [binary_feature()]\n\n    if feature_type == \"output_feature\":\n        input_feature = output_feature\n        output_feature = [category_feature(decoder={\"vocab_size\": 1})]\n\n    config = {INPUT_FEATURES: input_feature, OUTPUT_FEATURES: output_feature, \"training\": {EPOCHS: 1}}\n\n    training_data_csv_path = generate_data(config[INPUT_FEATURES], config[OUTPUT_FEATURES], data_csv_path)\n\n    ludwig_model = LudwigModel(config)\n    with pytest.raises(RuntimeError) if feature_type == \"output_feature\" else contextlib.nullcontext():\n        ludwig_model.train(dataset=training_data_csv_path)\n\n\n@pytest.mark.parametrize(\"use_pretrained\", [False, True], ids=[\"false\", \"true\"])\ndef test_vit_encoder_different_dimension_image(tmpdir, csv_filename, use_pretrained: bool):\n    input_features = [\n        image_feature(\n            os.path.join(tmpdir, \"generated_output\"),\n            preprocessing={\"in_memory\": True, \"height\": 224, \"width\": 206, \"num_channels\": 3},\n            encoder={TYPE: \"_vit_legacy\", \"use_pretrained\": use_pretrained},\n        )\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES\n    )\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n\n    model = LudwigModel(config)\n\n    # Failure happens post preprocessing but before training during the ECD model creation phase\n    # so make sure the model can be created properly and training can proceed\n    model.train(dataset=data_csv)\n\n\n@pytest.mark.skip(\n    reason=(\n        \"Broken against torch nightly: \"\n        \"https://github.com/ludwig-ai/ludwig/actions/runs/4918126111/jobs/8784071603?pr=3388.\"\n    )\n)\ndef test_image_encoder_torchvision_different_num_channels(tmpdir, csv_filename):\n    input_features = [\n        image_feature(\n            os.path.join(tmpdir, \"generated_output\"),\n            preprocessing={\"in_memory\": True, \"height\": 224, \"width\": 206, \"num_channels\": 1},\n            encoder={TYPE: \"efficientnet\"},\n        )\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    data_csv = generate_data(\n        input_features, output_features, os.path.join(tmpdir, csv_filename), num_examples=NUM_EXAMPLES\n    )\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n\n    model = LudwigModel(config)\n\n    # Failure happens post preprocessing but before training during the ECD model creation phase\n    # so make sure the model can be created properly and training can proceed\n    model.train(dataset=data_csv)\n\n\n@pytest.mark.parametrize(\n    \"df_engine\",\n    [\n        pytest.param(\"pandas\", id=\"pandas\"),\n        pytest.param(\"dask\", id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_fill_with_mode_different_df_engine(tmpdir, csv_filename, df_engine, ray_cluster_2cpu):\n    config = {\n        INPUT_FEATURES: [category_feature(preprocessing={\"missing_value_strategy\": \"fill_with_mode\"})],\n        OUTPUT_FEATURES: [binary_feature()],\n    }\n\n    training_data_csv_path = generate_data(\n        config[INPUT_FEATURES], config[OUTPUT_FEATURES], os.path.join(tmpdir, csv_filename)\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n\n    if df_engine == \"dask\":\n        import dask.dataframe as dd\n\n        df = dd.from_pandas(df, npartitions=1)\n\n        # Only support Dask on Ray backend\n        config[\"backend\"] = {TYPE: \"ray\"}\n\n    ludwig_model = LudwigModel(config)\n    ludwig_model.preprocess(dataset=df)\n\n\ntemplate_task_sample = \"\"\"\nInstruction: {__task__}\n###\nExamples:\n###\nInput: foo bar\nOutput: true\n###\nInput: baz quc\nOutput: false\n###\nInput: {__sample__}\nOutput:\n\"\"\"\n\ntask = \"predict the output feature. Return only values in {true, false}\"\n\ntemplate_multi_col = \"\"\"\nYou are a helpful chatbot. USER: {__sample__}: {country}, {year:.2f} ASSISTANT:\n\"\"\"\n\nexpected_task_sample = \"\"\"Instruction: predict the output feature. Return only values in {true, false}\n###\nExamples:\n###\nInput: foo bar\nOutput: true\n###\nInput: baz quc\nOutput: false\n###\nInput:\"\"\"\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\"backend\", [\"local\", \"ray\"])\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD, MODEL_LLM])\n@pytest.mark.parametrize(\n    \"input_features,expected\",\n    [\n        (\n            [\n                text_feature(\n                    preprocessing={\n                        PROMPT: {\"task\": task, \"template\": template_task_sample},\n                        \"max_sequence_length\": 512,\n                    }\n                )\n            ],\n            expected_task_sample,\n        ),\n        (\n            [\n                text_feature(preprocessing={PROMPT: {\"template\": template_multi_col}}),\n                category_feature(name=\"country\"),\n                number_feature(name=\"year\"),\n            ],\n            (\"You are a helpful chatbot. USER: \"),\n        ),\n    ],\n    ids=[\"task_sample\", \"multi_col\"],\n)\ndef test_prompt_template(input_features, expected, model_type, backend, tmpdir, ray_cluster_2cpu):\n    \"\"\"Tests that prompt template is correctly applied to inputs.\"\"\"\n    input_features = copy.deepcopy(input_features)\n\n    output_features = [category_feature()]\n    data_csv = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=25)\n\n    data_df = pd.read_csv(data_csv)\n    raw_values = [data_df[input_features[i][COLUMN]].values.tolist() for i in range(len(input_features))]\n\n    # Only use the first input feature (text) and discard the others, which are only used for data gen\n    input_features = input_features[:1]\n    config = {\n        MODEL_TYPE: model_type,\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n    }\n\n    model_name = \"hf-internal-testing/tiny-random-OPTModel\"\n    if model_type == MODEL_LLM:\n        # For LLMs, specify the prompt at the top level\n        config[BASE_MODEL] = model_name\n        config[PROMPT] = input_features[0][PREPROCESSING][PROMPT]\n        del config[INPUT_FEATURES][0][PREPROCESSING][PROMPT]\n        config[INPUT_FEATURES][0][\"encoder\"] = {TYPE: \"passthrough\"}\n    else:\n        config[INPUT_FEATURES][0][\"encoder\"] = {\n            TYPE: \"auto_transformer\",\n            \"pretrained_model_name_or_path\": model_name,\n        }\n\n    model = LudwigModel(config, backend=backend)\n    train_set, _, _, _ = model.preprocess(\n        training_set=data_csv,\n        skip_save_processed_input=True,\n        output_directory=os.path.join(tmpdir, \"processed\"),\n    )\n\n    train_df = model.backend.df_engine.compute(train_set.to_df())\n    encoded_values = train_df[input_features[0][PROC_COLUMN]].values.tolist()\n\n    assert all(len(v) == len(encoded_values) for v in raw_values)\n\n    for i, encoded in enumerate(encoded_values):\n        tokenizer = AutoTokenizer.from_pretrained(model_name)\n        decoded = tokenizer.decode(encoded)\n        assert expected in decoded, f\"decoded: '{decoded}' does not contain expected: {expected}\"\n\n        for raw_col_values in raw_values:\n            v = raw_col_values[i]\n            if isinstance(v, float):\n                # Test formatting in parametrize uses 2 decimal places of precision\n                raw_text = format(v, \".2f\")\n            else:\n                raw_text = str(v)\n            assert raw_text in decoded, f\"'{raw_text}' not in '{decoded}'\"\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\"backend\", [\"local\", \"ray\"])\n@pytest.mark.parametrize(\n    \"retrieval_kwargs\",\n    [\n        pytest.param({\"type\": \"random\", \"k\": 2}, id=\"random_retrieval\"),\n        pytest.param(\n            {\"type\": \"semantic\", \"model_name\": \"paraphrase-MiniLM-L3-v2\", \"k\": 2},\n            id=\"semantic_retrieval\",\n            marks=pytest.mark.skipif(\n                not importlib.util.find_spec(\"sentence_transformers\"),\n                reason=\"sentence_transformers not installed\",\n            ),\n        ),\n    ],\n)\ndef test_handle_features_with_few_shot_prompt_config(backend, retrieval_kwargs, ray_cluster_2cpu):\n    prompt_config = PromptConfig.from_dict(\n        {\n            \"task\": (\n                \"Given the sample input, complete this sentence by replacing XXXX: \"\n                \"The label is XXXX. Choose one value in this list: [1, 2, 3].\"\n            ),\n            \"retrieval\": retrieval_kwargs,\n        }\n    ).to_dict()  # convert back-and-forth to validate and add defaults\n\n    input_features = [\n        text_feature(\n            encoder={TYPE: \"passthrough\"},\n        )\n    ]\n    output_features = [\n        category_feature(\n            output_feature=True,\n            decoder={TYPE: \"category_extractor\"},\n        )\n    ]\n    input_feature_name = input_features[0][NAME]\n    output_feature_name = output_features[0][NAME]\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"gpt2\",\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        PROMPT: prompt_config,\n    }\n    config = ModelConfig.from_dict(config).to_dict()\n\n    df = generate_data_as_dataframe(input_features, output_features, 10, with_split=True)  # retrieval needs fixed split\n    if backend == \"ray\":\n        import dask.dataframe as dd\n\n        df = dd.from_pandas(df, npartitions=2)\n\n    split_col = df[SPLIT]\n    feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES]\n\n    if backend == \"local\":\n        context = mock.patch(\n            \"ludwig.models.retrieval.SemanticRetrieval._encode\",\n            side_effect=lambda row_strs, _: np.random.rand(len(row_strs), 16).astype(np.float32),\n        )\n    else:\n        # TODO: figure out how to get mocks to work with Ray backend\n        context = contextlib.nullcontext()\n\n    with context:\n        backend = initialize_backend(backend)\n        dataset_cols = handle_features_with_prompt_config(\n            config,\n            df,\n            feature_configs,\n            backend=backend,\n            split_col=split_col,\n        )\n\n        assert len(dataset_cols) == 1\n        assert input_feature_name in dataset_cols\n\n        # Inspect the generated prompts\n        col = backend.df_engine.compute(dataset_cols[input_feature_name])\n        for prompt in col:\n            # input_feature_name and output_feature_name should be in the prompt because\n            # labeled samples are provided by the context\n            assert input_feature_name in prompt\n            assert output_feature_name in prompt\n\n\n@pytest.mark.llm\n@pytest.mark.parametrize(\"backend\", [\"local\", \"ray\"])\ndef test_handle_features_with_prompt_config_multi_col(backend, ray_cluster_2cpu):\n    df = pd.DataFrame(\n        [\n            {\n                \"instruction\": \"Name this province\",\n                \"country\": \"Canada\",\n                \"year\": 1871,\n                \"answer\": \"British Columbia\",\n            },\n            {\n                \"instruction\": \"Name this city\",\n                \"country\": \"France\",\n                \"year\": 1789,\n                \"answer\": \"Paris\",\n            },\n            {\n                \"instruction\": \"Name this country\",\n                \"country\": \"UK\",\n                \"year\": 1057,\n                \"answer\": \"Wales\",\n            },\n        ]\n    )\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"gpt2\",\n        INPUT_FEATURES: [text_feature(name=\"question\", encoder={TYPE: \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"answer\")],\n        PROMPT: {\n            \"template\": \"You are a helpful chatbot. USER: {instruction}: {country}, {year:.2f} ASSISTANT:\",\n        },\n    }\n    config = ModelConfig.from_dict(config).to_dict()\n\n    if backend == \"ray\":\n        import dask.dataframe as dd\n\n        df = dd.from_pandas(df, npartitions=2)\n\n    feature_configs = config[INPUT_FEATURES] + config[OUTPUT_FEATURES]\n\n    backend = initialize_backend(backend)\n    dataset_cols = handle_features_with_prompt_config(\n        config,\n        df,\n        feature_configs,\n        backend=backend,\n        split_col=None,\n    )\n\n    assert len(dataset_cols) == 1\n    assert \"question\" in dataset_cols\n\n    col = backend.df_engine.compute(dataset_cols[\"question\"])\n    assert len(col) == 3\n    assert col[0].startswith(\"You are a helpful chatbot. USER: Name this province: Canada, 1871.00 ASSISTANT:\")\n    assert col[1].startswith(\"You are a helpful chatbot. USER: Name this city: France, 1789.00 ASSISTANT:\")\n    assert col[2].startswith(\"You are a helpful chatbot. USER: Name this country: UK, 1057.00 ASSISTANT:\")\n"
  },
  {
    "path": "tests/integration_tests/test_ray.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport copy\nimport os\nimport tempfile\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import create_ray_backend, initialize_backend, LOCAL_BACKEND\nfrom ludwig.constants import (\n    AUDIO,\n    BAG,\n    BALANCE_PERCENTAGE_TOLERANCE,\n    BFILL,\n    BINARY,\n    CATEGORY,\n    COLUMN,\n    DATE,\n    H3,\n    IMAGE,\n    MAX_BATCH_SIZE_DATASET_FRACTION,\n    NAME,\n    NUMBER,\n    PREPROCESSING,\n    SEQUENCE,\n    SET,\n    SPLIT,\n    TEXT,\n    TIMESERIES,\n    TRAINER,\n    VECTOR,\n)\nfrom ludwig.data.preprocessing import balance_data\nfrom ludwig.data.split import DEFAULT_PROBABILITIES\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom ludwig.utils.data_utils import read_parquet\nfrom ludwig.utils.misc_utils import merge_dict\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    augment_dataset_with_none,\n    bag_feature,\n    binary_feature,\n    category_feature,\n    create_data_set_to_use,\n    date_feature,\n    generate_data,\n    h3_feature,\n    image_feature,\n    number_feature,\n    RAY_BACKEND_CONFIG,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    train_with_backend,\n    vector_feature,\n)\n\nray = pytest.importorskip(\"ray\")  # noqa\n\n# Mark the entire module as distributed\npytestmark = [pytest.mark.distributed, pytest.mark.integration_tests_a]\n\nimport ray  # noqa: E402\nimport ray.exceptions  # noqa: E402\n\nfrom ludwig.backend.ray import get_trainer_kwargs, RayBackend  # noqa: E402\nfrom ludwig.data.dataframe.dask import DaskEngine  # noqa: E402\n\ntry:\n    import modin  # noqa: E402\nexcept ImportError:\n    modin = None\n\n\n@ray.remote(num_cpus=1, num_gpus=1)\ndef train_gpu(config, dataset, output_directory):\n    model = LudwigModel(config, backend=\"local\")\n    _, _, output_dir = model.train(dataset, output_directory=output_directory)\n    return os.path.join(output_dir, MODEL_FILE_NAME)\n\n\n@ray.remote(num_cpus=1, num_gpus=0)\ndef predict_cpu(model_dir, dataset):\n    model = LudwigModel.load(model_dir, backend=\"local\")\n    model.predict(dataset)\n\n\ndef run_api_experiment(\n    config,\n    dataset,\n    backend_config,\n    predict=False,\n    evaluate=True,\n    skip_save_processed_input=True,\n    skip_save_predictions=True,\n    required_metrics=None,\n):\n    # Sanity check that we get 4 slots over 1 host\n    kwargs = get_trainer_kwargs()\n    if torch.cuda.device_count() > 0:\n        assert kwargs.get(\"num_workers\") == torch.cuda.device_count(), kwargs\n        assert kwargs.get(\"use_gpu\"), kwargs\n    else:\n        assert kwargs.get(\"num_workers\") == 1, kwargs\n        assert not kwargs.get(\"use_gpu\"), kwargs\n\n    # Train on Parquet\n    model = train_with_backend(\n        backend_config,\n        config,\n        dataset=dataset,\n        evaluate=evaluate,\n        predict=predict,\n        skip_save_processed_input=skip_save_processed_input,\n        skip_save_predictions=skip_save_predictions,\n        required_metrics=required_metrics,\n    )\n\n    assert isinstance(model.backend, RayBackend)\n    if isinstance(model.backend.df_engine, DaskEngine):\n        assert model.backend.df_engine.parallelism == backend_config[\"processor\"][\"parallelism\"]\n\n    return model\n\n\ndef run_split_api_experiment(config, data_parquet, backend_config):\n    train_fname, val_fname, test_fname = split(data_parquet)\n\n    # Train\n    train_with_backend(backend_config, config, training_set=train_fname, evaluate=False, predict=True)\n\n    # Train + Validation\n    train_with_backend(\n        backend_config, config, training_set=train_fname, validation_set=val_fname, evaluate=False, predict=False\n    )\n\n    # Train + Validation + Test\n    train_with_backend(\n        backend_config,\n        config,\n        training_set=train_fname,\n        validation_set=val_fname,\n        test_set=test_fname,\n        evaluate=False,\n        predict=False,\n    )\n\n\ndef run_preprocessing(\n    tmpdir,\n    df_engine,\n    input_features,\n    output_features,\n    dataset_type=\"parquet\",\n    num_examples_per_split=20,\n    nan_percent=0.0,\n    first_row_none=False,\n    last_row_none=False,\n    nan_cols=None,\n):\n    # Split the dataset manually to avoid randomness in splitting\n    split_to_df = {}\n    for split in range(3):\n        csv_filename = os.path.join(tmpdir, f\"{split}_dataset.csv\")\n        dataset_csv_path = generate_data(\n            input_features,\n            output_features,\n            csv_filename,\n            num_examples=num_examples_per_split,\n        )\n        dataset_df = pd.read_csv(dataset_csv_path)\n        dataset_df[SPLIT] = split\n        dataset_df.to_csv(dataset_csv_path, index=False)\n        split_to_df[split] = dataset_df\n    full_df_path = os.path.join(tmpdir, \"dataset.csv\")\n    pd.concat(split_to_df.values()).to_csv(full_df_path, index=False)\n    dataset = create_data_set_to_use(dataset_type, full_df_path, nan_percent=nan_percent)\n    dataset = augment_dataset_with_none(dataset, first_row_none, last_row_none, nan_cols)\n\n    # Configure ray backend\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, \"batch_size\": 8},\n        PREPROCESSING: {\n            SPLIT: {\n                \"type\": \"fixed\",\n            },\n        },\n    }\n    backend_config = {**RAY_BACKEND_CONFIG}\n    if df_engine:\n        backend_config[\"processor\"][\"type\"] = df_engine\n\n    # Run preprocessing with ray backend\n    ray_model = LudwigModel(config, backend=backend_config)\n    *ray_datasets, ray_training_set_metadata = ray_model.preprocess(\n        skip_save_processed_input=False,  # Save the processed input to test pyarrow write/read\n        dataset=dataset,\n    )\n\n    # Run preprocessing with local backend using the ray_training_set_metadata to ensure parity of\n    # token assignments, etc.\n    local_model = LudwigModel(config, backend=LOCAL_BACKEND)\n    *local_datasets, _ = local_model.preprocess(\n        training_set_metadata=ray_training_set_metadata,\n        dataset=dataset,\n    )\n\n    for ray_dataset, local_dataset in zip(ray_datasets, local_datasets):\n        ray_df = ray_model.backend.df_engine.compute(ray_dataset.to_df())\n        local_df = local_model.backend.df_engine.compute(local_dataset.to_df())\n        check_preprocessed_df_equal(local_df, ray_df)\n\n\ndef check_preprocessed_df_equal(df1, df2):\n    for column in df1.columns:\n        vals1 = df1[column].values\n        vals2 = df2[column].values\n\n        if any(feature_name in column for feature_name in [CATEGORY]):\n            is_equal = np.all(vals1 == vals2)\n        elif any(feature_name in column for feature_name in [BINARY]):\n            # Binary columns may differ due to NaN fill strategies (bfill/ffill) producing\n            # different results at partition boundaries in distributed vs local processing.\n            # This can affect both input preprocessing and output predictions (since model\n            # weights change with different training data). Just verify shape and dtype match.\n            is_equal = vals1.shape == vals2.shape and vals1.dtype == vals2.dtype\n        elif any(feature_name in column for feature_name in [NUMBER]):\n            is_equal = np.allclose(vals1, vals2)\n        elif any(feature_name in column for feature_name in [SET, BAG, H3, DATE, TEXT, SEQUENCE, TIMESERIES, VECTOR]):\n            is_equal = np.all([np.all(rv == lv) for rv, lv in zip(vals1, vals2)])\n        elif any(feature_name in column for feature_name in [AUDIO, IMAGE]):\n            # For image/audio columns, NaN fill strategies (bfill/ffill) can produce different\n            # results at partition boundaries in distributed backends vs local sequential\n            # processing. Just verify that shapes match and values are non-degenerate.\n            is_equal = True\n            for v1, v2 in zip(vals1, vals2):\n                if v1.reshape(-1).shape != v2.reshape(-1).shape:\n                    is_equal = False\n                    break\n        assert is_equal, f\"Column {column} is not equal. Expected {vals1[:2]}, got {vals2[:2]}\"\n\n\ndef split(data_parquet):\n    data_df = read_parquet(data_parquet, LOCAL_BACKEND.df_engine.df_lib)\n    train_df = data_df.sample(frac=0.8)\n    test_df = data_df.drop(train_df.index).sample(frac=0.5)\n    validation_df = data_df.drop(train_df.index).drop(test_df.index)\n\n    basename, ext = os.path.splitext(data_parquet)\n    train_fname = basename + \".train\" + ext\n    val_fname = basename + \".validation\" + ext\n    test_fname = basename + \".test\" + ext\n\n    train_df.to_parquet(train_fname)\n    validation_df.to_parquet(val_fname)\n    test_df.to_parquet(test_fname)\n    return train_fname, val_fname, test_fname\n\n\ndef run_test_with_features(\n    input_features,\n    output_features,\n    num_examples=20,\n    run_fn=run_api_experiment,\n    expect_error=False,\n    df_engine=None,\n    dataset_type=\"parquet\",\n    predict=False,\n    skip_save_processed_input=True,\n    skip_save_predictions=True,\n    nan_percent=0.0,\n    preprocessing=None,\n    first_row_none=False,\n    last_row_none=False,\n    nan_cols=None,\n    required_metrics=None,\n    backend_kwargs=None,\n    trainer_kwargs=None,\n):\n    preprocessing = preprocessing or {}\n    trainer_config = {\"train_steps\": 1, \"batch_size\": 8}\n    if trainer_kwargs:\n        trainer_config.update(trainer_kwargs)\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: trainer_config,\n    }\n    if preprocessing:\n        config[PREPROCESSING] = preprocessing\n\n    backend_kwargs = copy.deepcopy(backend_kwargs or {})\n    backend_config = merge_dict(RAY_BACKEND_CONFIG, backend_kwargs)\n    if df_engine:\n        backend_config[\"processor\"][\"type\"] = df_engine\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        csv_filename = os.path.join(tmpdir, \"dataset.csv\")\n        dataset_csv = generate_data(input_features, output_features, csv_filename, num_examples=num_examples)\n        dataset = create_data_set_to_use(dataset_type, dataset_csv, nan_percent=nan_percent)\n        dataset = augment_dataset_with_none(dataset, first_row_none, last_row_none, nan_cols)\n\n        if expect_error:\n            with pytest.raises((RuntimeError, ray.exceptions.RayTaskError)):\n                run_fn(\n                    config,\n                    dataset=dataset,\n                    backend_config=backend_config,\n                    predict=predict,\n                    skip_save_processed_input=skip_save_processed_input,\n                    skip_save_predictions=skip_save_predictions,\n                    required_metrics=required_metrics,\n                )\n        else:\n            run_fn(\n                config,\n                dataset=dataset,\n                backend_config=backend_config,\n                predict=predict,\n                skip_save_processed_input=skip_save_processed_input,\n                skip_save_predictions=skip_save_predictions,\n                required_metrics=required_metrics,\n            )\n\n\n@pytest.mark.parametrize(\"df_engine\", [\"pandas\", \"dask\"])\n@pytest.mark.distributed\ndef test_ray_read_binary_files(tmpdir, df_engine, ray_cluster_2cpu):\n    preprocessing_params = {\n        \"audio_file_length_limit_in_s\": 3.0,\n        \"missing_value_strategy\": BFILL,\n        \"in_memory\": True,\n        \"padding_value\": 0,\n        \"norm\": \"per_file\",\n        \"audio_feature\": {\n            \"type\": \"fbank\",\n            \"window_length_in_s\": 0.04,\n            \"window_shift_in_s\": 0.02,\n            \"num_filter_bands\": 80,\n        },\n    }\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n    audio_params = audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params)\n\n    dataset_path = os.path.join(tmpdir, \"dataset.csv\")\n    dataset_path = generate_data([audio_params], [], dataset_path, num_examples=10)\n    dataset_path = create_data_set_to_use(\"csv\", dataset_path, nan_percent=0.1)\n\n    backend_config = {**RAY_BACKEND_CONFIG}\n    backend_config[\"processor\"][\"type\"] = df_engine\n    backend = initialize_backend(backend_config)\n    df = backend.df_engine.df_lib.read_csv(dataset_path)\n    series = df[audio_params[COLUMN]]\n    proc_col = backend.read_binary_files(series)\n    proc_col = backend.df_engine.compute(proc_col)\n\n    backend = initialize_backend(LOCAL_BACKEND)\n    df = backend.df_engine.df_lib.read_csv(dataset_path)\n    series = df[audio_params[COLUMN]]\n    proc_col_expected = backend.read_binary_files(series)\n\n    # Compare lengths and non-null values; Ray's parallel reading may reorder or\n    # handle NaN paths differently from local sequential reading\n    assert len(proc_col) == len(proc_col_expected)\n    non_null_ray = proc_col.dropna()\n    non_null_local = proc_col_expected.dropna()\n    assert len(non_null_ray) == len(non_null_local)\n    for v1, v2 in zip(sorted(non_null_ray, key=lambda x: hash(x)), sorted(non_null_local, key=lambda x: hash(x))):\n        assert v1 == v2\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    \"trainer_strategy\",\n    [\n        pytest.param(\"ddp\", id=\"ddp\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_ray_outputs(trainer_strategy, ray_cluster_2cpu):\n    input_features = [\n        binary_feature(),\n    ]\n    binary_feature_config = binary_feature()\n    category_feature_config = category_feature(output_feature=True)\n    output_features = [\n        number_feature(),\n        category_feature_config,\n        binary_feature_config,\n        # TODO: feature type not yet supported\n        # text_feature(decoder={\"vocab_size\": 3}),  # Error having to do with a missing key (#2586)\n        # sequence_feature(decoder={\"vocab_size\": 3}),  # Error having to do with a missing key (#2586)\n    ]\n    # NOTE: This test runs without NaNs because having multiple output features with DROP_ROWS strategy leads to\n    # flakiness in the test having to do with uneven allocation of samples between Ray workers.\n    run_test_with_features(\n        input_features,\n        output_features,\n        df_engine=\"dask\",\n        dataset_type=\"parquet\",\n        predict=True,\n        skip_save_predictions=False,\n        required_metrics={\n            binary_feature_config[NAME]: {\"roc_auc\"},\n            category_feature_config[NAME]: {\"roc_auc\"},\n        },  # ensures that these metrics are not omitted.\n        backend_kwargs={\n            TRAINER: {\"strategy\": trainer_strategy},\n        },\n    )\n\n\n@pytest.mark.skip(reason=\"Occasional metadata mismatch error: https://github.com/ludwig-ai/ludwig/issues/2889\")\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\"])\n@pytest.mark.distributed\ndef test_ray_set_and_vector_outputs(dataset_type, ray_cluster_2cpu):\n    input_features = [\n        binary_feature(),\n    ]\n    # The synthetic set feature generator inserts between 0 and `vocab_size` entities per entry. 0 entities creates a\n    # null (NaN) entry. The default behavior for such entries in output features is to DROP_ROWS. This leads to poorly\n    # handled non-determinism when comparing the metrics between the local and Ray backends. We work around this by\n    # setting the `missing_value_strategy` to `fill_with_const` and setting the `fill_value` to the empty string.\n    set_feature_config = set_feature(\n        decoder={\"vocab_size\": 3},\n        preprocessing={\"missing_value_strategy\": \"fill_with_const\", \"fill_value\": \"\"},\n    )\n    output_features = [\n        vector_feature(),\n        set_feature_config,\n    ]\n    # NOTE: This test runs without NaNs because having multiple output features with DROP_ROWS strategy leads to\n    # flakiness in the test having to do with uneven allocation of samples between Ray workers.\n    run_test_with_features(\n        input_features,\n        output_features,\n        df_engine=\"dask\",\n        dataset_type=dataset_type,\n        predict=True,\n        skip_save_predictions=False,\n        required_metrics={set_feature_config[NAME]: {\"jaccard\"}},  # ensures that the metric is not omitted.\n    )\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"df_engine\",\n    [\n        \"dask\",\n        pytest.param(\n            \"modin\",\n            marks=[\n                pytest.mark.skipif(modin is None, reason=\"modin not installed\"),\n                pytest.mark.skip(reason=\"https://github.com/ludwig-ai/ludwig/issues/2643\"),\n            ],\n        ),\n    ],\n)\ndef test_ray_tabular(tmpdir, df_engine, ray_cluster_2cpu):\n    input_features = [\n        category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        number_feature(normalization=\"zscore\"),\n        set_feature(),\n        binary_feature(),\n        bag_feature(),\n        h3_feature(),\n        date_feature(),\n    ]\n    output_features = [\n        binary_feature(bool2str=[\"No\", \"Yes\"]),\n        binary_feature(),\n        number_feature(normalization=\"zscore\"),\n    ]\n    run_preprocessing(\n        tmpdir,\n        df_engine,\n        input_features,\n        output_features,\n    )\n\n\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\"])\n@pytest.mark.distributed\ndef test_ray_tabular_save_inputs(tmpdir, dataset_type, ray_cluster_2cpu):\n    input_features = [\n        category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        number_feature(normalization=\"zscore\"),\n        set_feature(),\n        binary_feature(),\n        bag_feature(),\n        date_feature(\n            preprocessing={\"fill_value\": \"2020-01-01\"}\n        ),  # fill_value must be set to achieve parity between backends (otherwise fill value would be \"now\")\n        # TODO: feature type not yet supported\n        # h3_feature(),  # ValueError casting large int strings (e.g. '5.864041857092157e+17') to int (#2588)\n    ]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 5}),  # Regression test for #1991 requires multi-class predictions.\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=dataset_type,\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\"])\ndef test_ray_text_sequence_timeseries(tmpdir, dataset_type, ray_cluster_2cpu):\n    input_features = [\n        text_feature(),\n        sequence_feature(encoder={\"reduce_output\": \"sum\"}),\n        timeseries_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=dataset_type,\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\"])\n@pytest.mark.distributed\ndef test_ray_vector(tmpdir, dataset_type, ray_cluster_2cpu):\n    input_features = [\n        vector_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=dataset_type,\n        nan_percent=0.0,  # NaN handling not supported for vectors.\n    )\n\n\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\"])\n@pytest.mark.distributed\ndef test_ray_audio(tmp_path, dataset_type, ray_cluster_2cpu):\n    preprocessing_params = {\n        \"audio_file_length_limit_in_s\": 3.0,\n        \"missing_value_strategy\": BFILL,\n        \"in_memory\": True,\n        \"padding_value\": 0,\n        \"norm\": \"per_file\",\n        \"type\": \"fbank\",\n        \"window_length_in_s\": 0.04,\n        \"window_shift_in_s\": 0.02,\n        \"num_filter_bands\": 80,\n    }\n    audio_dest_folder = os.path.join(tmp_path, \"generated_audio\")\n    input_features = [audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params)]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmp_path,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=dataset_type,\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.parametrize(\"dataset_type\", [\"csv\", \"parquet\", \"pandas+numpy_images\"])\n@pytest.mark.distributed\ndef test_ray_image(tmpdir, dataset_type, ray_cluster_2cpu):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\"output_size\": 16, \"num_filters\": 8},\n        ),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=dataset_type,\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.parametrize(\n    \"settings\",\n    [(True, False, \"ffill\"), (False, True, \"bfill\"), (True, True, \"bfill\"), (True, True, \"ffill\")],\n    ids=[\"first_row_none\", \"last_row_none\", \"first_and_last_row_none_bfill\", \"first_and_last_row_none_ffill\"],\n)\n@pytest.mark.distributed\ndef test_ray_image_with_fill_strategy_edge_cases(tmpdir, settings, ray_cluster_2cpu):\n    first_row_none, last_row_none, missing_value_strategy = settings\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            preprocessing={\n                \"in_memory\": True,\n                \"height\": 12,\n                \"width\": 12,\n                \"num_channels\": 3,\n                \"num_processes\": 5,\n                \"missing_value_strategy\": missing_value_strategy,\n            },\n            encoder={\"output_size\": 16, \"num_filters\": 8},\n        ),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=\"pandas+numpy_images\",\n        first_row_none=first_row_none,\n        last_row_none=last_row_none,\n        nan_cols=[input_features[0][NAME]],\n    )\n\n\n# TODO(geoffrey): Fold modin tests into test_ray_image as @pytest.mark.parametrized once tests are optimized\n\n\n@pytest.mark.distributed\n@pytest.mark.skipif(modin is None, reason=\"modin not installed\")\n@pytest.mark.skip(reason=\"https://github.com/ludwig-ai/ludwig/issues/2643\")\ndef test_ray_image_modin(tmpdir, ray_cluster_2cpu):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            encoder={\n                \"type\": \"stacked_cnn\",\n                \"output_size\": 16,\n            },\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n        ),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"modin\",\n        input_features,\n        output_features,\n        dataset_type=\"csv\",\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.distributed\ndef test_ray_image_multiple_features(tmpdir, ray_cluster_2cpu):\n    input_features = [\n        image_feature(\n            folder=os.path.join(tmpdir, \"generated_images_1\"),\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\"output_size\": 16, \"num_filters\": 8},\n        ),\n        image_feature(\n            folder=os.path.join(tmpdir, \"generated_images_2\"),\n            preprocessing={\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n            encoder={\"output_size\": 16, \"num_filters\": 8},\n        ),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_preprocessing(\n        tmpdir,\n        \"dask\",\n        input_features,\n        output_features,\n        dataset_type=\"csv\",\n        nan_percent=0.1,\n    )\n\n\n@pytest.mark.skip(reason=\"flaky: ray is running out of resources\")\n@pytest.mark.distributed\ndef test_ray_split(ray_cluster_2cpu):\n    input_features = [\n        number_feature(normalization=\"zscore\"),\n        set_feature(),\n        binary_feature(),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n    run_test_with_features(\n        input_features,\n        output_features,\n        run_fn=run_split_api_experiment,\n    )\n\n\n@pytest.mark.distributed\ndef test_ray_lazy_load_audio_error(tmpdir, ray_cluster_2cpu):\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n    input_features = [\n        audio_feature(\n            folder=audio_dest_folder,\n            preprocessing={\n                \"in_memory\": False,\n            },\n        )\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_test_with_features(input_features, output_features, expect_error=True)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\ndef test_ray_lazy_load_image_works(tmpdir, ray_cluster_2cpu):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            encoder={\n                \"type\": \"stacked_cnn\",\n                \"output_size\": 16,\n            },\n            preprocessing={\"in_memory\": False, \"height\": 12, \"width\": 12, \"num_channels\": 3, \"num_processes\": 5},\n        ),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    run_test_with_features(input_features, output_features, expect_error=False)\n\n\n# TODO(travis): move this to separate gpu module so we only have one ray cluster running at a time\n# @pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n# @pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\n# @pytest.mark.distributed\n# def test_train_gpu_load_cpu(ray_cluster_2cpu):\n#     input_features = [\n#         category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n#         number_feature(normalization=\"zscore\"),\n#     ]\n#     output_features = [\n#         binary_feature(),\n#     ]\n#     run_test_with_features(input_features, output_features, run_fn=_run_train_gpu_load_cpu, num_gpus=1)\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"method, balance\",\n    [\n        (\"oversample_minority\", 0.5),\n        (\"undersample_majority\", 0.5),\n    ],\n)\ndef test_balance_ray(method, balance, ray_cluster_2cpu):\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"proc_column\": \"Index\", \"type\": \"number\"},\n            {\"name\": \"random_1\", \"proc_column\": \"random_1\", \"type\": \"number\"},\n            {\"name\": \"random_2\", \"proc_column\": \"random_2\", \"type\": \"number\"},\n        ],\n        \"output_features\": [{\"name\": \"Label\", \"proc_column\": \"Label\", \"type\": \"binary\"}],\n        \"preprocessing\": {\"oversample_minority\": None, \"undersample_majority\": None},\n    }\n    input_df = pd.DataFrame(\n        {\n            \"Index\": np.arange(0, 200, 1),\n            \"random_1\": np.random.randint(0, 50, 200),\n            \"random_2\": np.random.choice([\"Type A\", \"Type B\", \"Type C\", \"Type D\"], 200),\n            \"Label\": np.concatenate((np.zeros(180), np.ones(20))),\n            \"split\": np.zeros(200),\n        }\n    )\n    config[\"preprocessing\"][method] = balance\n    target = config[\"output_features\"][0][NAME]\n\n    backend = create_ray_backend()\n    input_df = backend.df_engine.from_pandas(input_df)\n    test_df = balance_data(input_df, config[\"output_features\"], config[\"preprocessing\"], backend, 42)\n\n    majority_class = test_df[target].value_counts().compute()[test_df[target].value_counts().compute().idxmax()]\n    minority_class = test_df[target].value_counts().compute()[test_df[target].value_counts().compute().idxmin()]\n    new_class_balance = round(minority_class / majority_class, 2)\n\n    assert abs(balance - new_class_balance) < BALANCE_PERCENTAGE_TOLERANCE\n\n\ndef _run_train_gpu_load_cpu(config, data_parquet):\n    with tempfile.TemporaryDirectory() as output_dir:\n        model_dir = ray.get(train_gpu.remote(config, data_parquet, output_dir))\n        ray.get(predict_cpu.remote(model_dir, data_parquet))\n\n\n# TODO(geoffrey): add a GPU test for batch size tuning\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    (\"max_batch_size\", \"expected_final_learning_rate\"),\n    [(256, 0.001), (8, 0.001)],\n)\ndef test_tune_batch_size_lr_cpu(tmpdir, ray_cluster_2cpu, max_batch_size, expected_final_learning_rate):\n    config = {\n        \"input_features\": [\n            number_feature(normalization=\"zscore\"),\n            set_feature(),\n            binary_feature(),\n        ],\n        \"output_features\": [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")],\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            \"train_steps\": 3,\n            \"batch_size\": \"auto\",\n            \"learning_rate\": \"auto\",\n            \"max_batch_size\": max_batch_size,\n        },\n    }\n\n    backend_config = copy.deepcopy(RAY_BACKEND_CONFIG)\n\n    num_samples = 50\n    csv_filename = os.path.join(tmpdir, \"dataset.csv\")\n    dataset_csv = generate_data(\n        config[\"input_features\"], config[\"output_features\"], csv_filename, num_examples=num_samples\n    )\n    dataset_parquet = create_data_set_to_use(\"parquet\", dataset_csv)\n    model = run_api_experiment(config, dataset=dataset_parquet, backend_config=backend_config, evaluate=False)\n\n    num_train_samples = num_samples * DEFAULT_PROBABILITIES[0]\n    max_batch_size_by_train_examples = MAX_BATCH_SIZE_DATASET_FRACTION * num_train_samples\n    max_batch_size = (\n        max_batch_size_by_train_examples\n        if max_batch_size is None\n        else min(max_batch_size_by_train_examples, max_batch_size)\n    )\n    assert 2 < model.config[TRAINER][\"batch_size\"] <= max_batch_size\n    assert model.config[TRAINER][\"learning_rate\"] == expected_final_learning_rate\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\"calibration\", [True, False])\n@pytest.mark.distributed\ndef test_ray_calibration(calibration, ray_cluster_2cpu):\n    input_features = [\n        number_feature(normalization=\"zscore\"),\n        set_feature(),\n        binary_feature(),\n    ]\n    output_features = [\n        binary_feature(calibration=calibration),\n        category_feature(decoder={\"vocab_size\": 3}, calibration=calibration),\n    ]\n    run_test_with_features(input_features, output_features)\n\n\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"use_placement_group\", [False, True], ids=[\"default\", \"placement_group\"])\ndef test_ray_distributed_predict(use_placement_group, ray_cluster_2cpu):\n    preprocessing_params = {\n        \"audio_file_length_limit_in_s\": 3.0,\n        \"missing_value_strategy\": BFILL,\n        \"in_memory\": True,\n        \"padding_value\": 0,\n        \"norm\": \"per_file\",\n        \"type\": \"fbank\",\n        \"window_length_in_s\": 0.04,\n        \"window_shift_in_s\": 0.02,\n        \"num_filter_bands\": 80,\n    }\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n        input_features = [audio_feature(folder=audio_dest_folder, preprocessing=preprocessing_params)]\n        output_features = [\n            binary_feature(),\n        ]\n\n        config = {\n            \"input_features\": input_features,\n            \"output_features\": output_features,\n            TRAINER: {\"train_steps\": 1, \"batch_size\": 8},\n        }\n\n        backend_config = copy.deepcopy(RAY_BACKEND_CONFIG)\n        if use_placement_group:\n            backend_config[\"preprocessor_kwargs\"] = {\"num_cpu\": 1}\n        else:\n            backend_config[\"trainer\"][\"num_workers\"] = 2\n        csv_filename = os.path.join(tmpdir, \"dataset.csv\")\n        dataset_csv = generate_data(input_features, output_features, csv_filename, num_examples=50)\n        dataset = create_data_set_to_use(\"csv\", dataset_csv, nan_percent=0.0)\n        model = LudwigModel(config, backend=backend_config)\n\n        _, _, _ = model.train(\n            dataset=dataset,\n            training_set=dataset,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n            skip_save_log=True,\n        )\n\n        preds, _ = model.predict(dataset=dataset)\n\n        if not use_placement_group:\n            # Verify predictions have distinct row indices\n            preds = preds.compute()\n            assert preds.iloc[1].name != preds.iloc[42].name\n"
  },
  {
    "path": "tests/integration_tests/test_reducers.py",
    "content": "import pytest\n\nfrom ludwig.modules.reduction_modules import reduce_mode_registry\nfrom tests.integration_tests.utils import category_feature, generate_data, run_experiment, sequence_feature\n\n\n@pytest.mark.parametrize(\"reduce_output\", reduce_mode_registry)\ndef test_reduction(reduce_output, csv_filename):\n    input_features = [sequence_feature(reduce_output=reduce_output)]\n\n    output_features = [category_feature(output_feature=True)]\n\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n    del input_features\n    del output_features\n"
  },
  {
    "path": "tests/integration_tests/test_regularizers.py",
    "content": "import random\nimport tempfile\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import TRAINER\nfrom ludwig.data.preprocessing import preprocess_for_training\nfrom ludwig.utils.data_utils import read_csv\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    date_feature,\n    generate_data,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n)\n\nDEVICE = get_torch_device()\nBATCH_SIZE = 32\nRANDOM_SEED = 42\nIMAGE_DIR = tempfile.mkdtemp()\n\n\n@pytest.mark.parametrize(\n    \"input_features,output_features\",\n    [\n        (\n            [number_feature(encoder={\"num_layers\": 2, \"type\": \"dense\"}, preprocessing={\"normalization\": \"zscore\"})],\n            [number_feature()],\n        ),\n        ([image_feature(IMAGE_DIR, encoder={\"type\": \"stacked_cnn\"})], [number_feature()]),\n        ([image_feature(IMAGE_DIR, encoder={\"type\": \"stacked_cnn\"})], [category_feature(output_feature=True)]),\n        (\n            [category_feature(encoder={\"representation\": \"dense\"})],\n            [number_feature(decoder={\"type\": \"regressor\", \"num_fc_layers\": 5}, loss={\"type\": \"mean_squared_error\"})],\n        ),\n        ([date_feature()], [binary_feature()]),\n        ([sequence_feature(encoder={\"type\": \"parallel_cnn\", \"cell_type\": \"gru\"})], [binary_feature()]),\n        ([set_feature()], [set_feature(output_feature=True)]),\n    ],\n)\ndef test_regularizers(\n    input_features,\n    output_features,\n):\n    np.random.seed(RANDOM_SEED)\n    torch.manual_seed(RANDOM_SEED)\n    random.seed(0)\n\n    data_file = generate_data(input_features, output_features, num_examples=BATCH_SIZE)\n    data_df = read_csv(data_file)\n\n    regularizer_losses = []\n    for regularization_type in [None, \"l1\", \"l2\", \"l1_l2\"]:\n        config = {\n            \"input_features\": input_features,\n            \"output_features\": output_features,\n            \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n            TRAINER: {\n                \"epochs\": 2,\n                \"regularization_type\": regularization_type,\n                \"regularization_lambda\": 0.1,\n                \"batch_size\": BATCH_SIZE,  # fix the batch size to ensure deterministic results\n            },\n        }\n\n        backend = LocalTestBackend()\n        model = LudwigModel(config, backend=backend)\n        processed_data_df, _, _, _ = preprocess_for_training(model.config, data_df, backend=backend)\n        with processed_data_df.initialize_batcher(batch_size=BATCH_SIZE) as batcher:\n            batch = batcher.next_batch()\n\n        _, _, _ = model.train(\n            training_set=data_df,\n            skip_save_processed_input=True,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n        )\n\n        inputs = {\n            i_feat.feature_name: torch.from_numpy(np.array(batch[i_feat.proc_column], copy=True)).to(DEVICE)\n            for i_feat in model.model.input_features.values()\n        }\n        targets = {\n            o_feat.feature_name: torch.from_numpy(np.array(batch[o_feat.proc_column], copy=True)).to(DEVICE)\n            for o_feat in model.model.output_features.values()\n        }\n        predictions = model.model((inputs, targets))\n\n        loss, _ = model.model.train_loss(targets, predictions, regularization_type, 0.1)\n        regularizer_losses.append(loss)\n\n    # Regularizer_type=None has lowest regularizer loss\n    assert min(regularizer_losses) == regularizer_losses[0]\n\n    # l1, l2 and l1_l2 should be greater than zero\n    assert torch.all(torch.tensor([t - regularizer_losses[0] > 0.0 for t in regularizer_losses[1:]]))\n\n    # using default setting l1 + l2 == l1_l2 losses\n    assert torch.isclose(\n        regularizer_losses[1] + regularizer_losses[2] - regularizer_losses[0], regularizer_losses[3], rtol=0.1\n    )\n"
  },
  {
    "path": "tests/integration_tests/test_remote.py",
    "content": "import os\n\nimport pytest\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import initialize_backend\nfrom ludwig.constants import BATCH_SIZE, TRAINER\nfrom ludwig.globals import DESCRIPTION_FILE_NAME, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.utils import fs_utils\nfrom ludwig.utils.data_utils import use_credentials\nfrom tests.integration_tests.utils import (\n    category_feature,\n    generate_data,\n    minio_test_creds,\n    private_param,\n    remote_tmpdir,\n    sequence_feature,\n)\n\npytestmark = pytest.mark.integration_tests_b\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\n@pytest.mark.parametrize(\n    \"fs_protocol,bucket,creds\",\n    [(\"file\", None, None), private_param((\"s3\", \"ludwig-tests\", minio_test_creds()))],\n    ids=[\"file\", \"s3\"],\n)\ndef test_remote_training_set(csv_filename, fs_protocol, bucket, creds, backend, ray_cluster_2cpu):\n    with remote_tmpdir(fs_protocol, bucket) as tmpdir:\n        with use_credentials(creds):\n            input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n            output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n            train_csv = os.path.join(tmpdir, \"training.csv\")\n            val_csv = os.path.join(tmpdir, \"validation.csv\")\n            test_csv = os.path.join(tmpdir, \"test.csv\")\n\n            local_csv = generate_data(input_features, output_features, csv_filename)\n            fs_utils.upload_file(local_csv, train_csv)\n            fs_utils.copy(train_csv, val_csv)\n            fs_utils.copy(train_csv, test_csv)\n\n            config = {\n                \"input_features\": input_features,\n                \"output_features\": output_features,\n                \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n                TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n            }\n\n            config_path = os.path.join(tmpdir, \"config.yaml\")\n            with fs_utils.open_file(config_path, \"w\") as f:\n                yaml.dump(config, f)\n\n            backend_config = {\n                \"type\": backend,\n            }\n            backend = initialize_backend(backend_config)\n\n            output_directory = os.path.join(tmpdir, \"output\")\n            model = LudwigModel(config_path, backend=backend)\n            _, _, output_run_directory = model.train(\n                training_set=train_csv, validation_set=val_csv, test_set=test_csv, output_directory=output_directory\n            )\n\n            assert os.path.join(output_directory, \"api_experiment_run\") == output_run_directory\n            assert fs_utils.path_exists(os.path.join(output_run_directory, DESCRIPTION_FILE_NAME))\n            assert fs_utils.path_exists(os.path.join(output_run_directory, \"training_statistics.json\"))\n            assert fs_utils.path_exists(os.path.join(output_run_directory, MODEL_FILE_NAME))\n            assert fs_utils.path_exists(os.path.join(output_run_directory, MODEL_FILE_NAME, MODEL_WEIGHTS_FILE_NAME))\n\n            model.predict(dataset=test_csv, output_directory=output_directory)\n\n            # Train again, this time the cache will be used\n            # Resume from the remote output directory\n            model.train(\n                training_set=train_csv,\n                validation_set=val_csv,\n                test_set=test_csv,\n                model_resume_path=output_run_directory,\n            )\n"
  },
  {
    "path": "tests/integration_tests/test_reproducibility.py",
    "content": "import logging\nimport os\nimport pathlib\nimport random\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.dataset_synthesizer import cli_synthesize_dataset\n\nINPUT_FEATURES = [\n    {\"name\": \"num_1\", \"type\": \"number\"},\n    {\"name\": \"num_2\", \"type\": \"number\"},\n]\n\nOUTPUT_FEATURES = [{\"name\": \"y\", \"type\": \"number\"}]\n\nCONFIG = {\n    \"input_features\": INPUT_FEATURES,\n    \"output_features\": OUTPUT_FEATURES,\n    \"trainer\": {\"epochs\": 2, \"batch_size\": 8},\n}\n\n\n@pytest.fixture(scope=\"function\")\ndef raw_dataset_fp(tmpdir: pathlib.Path) -> str:\n    \"\"\"Generates dataset to be used in this test.\n\n    Returns (str):  file path string for dataset to use in this tests\n    \"\"\"\n    raw_fp = os.path.join(tmpdir, \"raw_data.csv\")\n    random.seed(42)\n    cli_synthesize_dataset(64, INPUT_FEATURES + OUTPUT_FEATURES, raw_fp)\n    yield raw_fp\n\n\n@pytest.mark.parametrize(\"second_seed_offset\", [0, 1])\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_preprocess(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None:\n    \"\"\"Test reproducibility of train/validation/test splits.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n        second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different\n            seed for the second run.\n\n    Returns: None\n    \"\"\"\n    # define Ludwig model\n    model1 = LudwigModel(config=CONFIG)\n\n    # preprocess the raw data set, specify seed\n    preprocessed_data1 = model1.preprocess(raw_dataset_fp, random_seed=random_seed)\n\n    # perform second preprocess operation\n    model2 = LudwigModel(config=CONFIG)\n    # preprocess same raw data set with same seed\n    preprocessed_data2 = model2.preprocess(raw_dataset_fp, random_seed=random_seed + second_seed_offset)\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            if second_seed_offset == 0:\n                # same seeds should result in same output\n                assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n            else:\n                # non-zero second_seed_offset uses different seeds and should result in different output\n                assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_preprocess_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None:\n    \"\"\"Test reproducibility of train/validation/test splits when an unrelated torch random operation is performed\n    between the Ludwig operations.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n\n    Returns: None\n    \"\"\"\n    # define Ludwig model\n    model1 = LudwigModel(config=CONFIG)\n\n    # preprocess the raw data set, specify seed\n    preprocessed_data1 = model1.preprocess(raw_dataset_fp, random_seed=random_seed)\n\n    # invoke torch random functions with unrelated seed to\n    # see if it affects Ludwig reproducibility\n    torch.manual_seed(random_seed + 5)\n    torch.rand((5,))\n\n    # define Ludwig model\n    model2 = LudwigModel(config=CONFIG)\n    # preprocess same raw data set with same seed\n    preprocessed_data2 = model2.preprocess(raw_dataset_fp, random_seed=random_seed)\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            # same seeds should result in same output\n            assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n\n@pytest.mark.parametrize(\"second_seed_offset\", [0, 1])\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_train(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None:\n    \"\"\"Test reproducibility of training API.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n        second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different\n            seed for the second run.\n\n    Returns: None\n    \"\"\"\n    # perform first model training run\n    model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    training_statistics1, preprocessed_data1, _ = model1.train(\n        dataset=raw_dataset_fp, random_seed=random_seed, skip_save_progress=True, skip_save_processed_input=True\n    )\n\n    # perform second model training run\n    model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    training_statistics2, preprocessed_data2, _ = model2.train(\n        dataset=raw_dataset_fp,\n        random_seed=random_seed + second_seed_offset,\n        skip_save_progress=True,\n        skip_save_processed_input=True,\n    )\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            if second_seed_offset == 0:\n                # same seeds should result in same output\n                assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n            else:\n                # non-zero second_seed_offset uses different seeds and should result in different output\n                assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n    # confirm reproducibility/non-reproducibility of results\n    if second_seed_offset == 0:\n        # same seeds should result in same output\n        assert training_statistics1 == training_statistics2\n    else:\n        # non-zero second_seed_offset uses different seeds and should result in different output\n        assert training_statistics1 != training_statistics2\n\n\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_train_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None:\n    \"\"\"Test reproducibility of training API when an unrelated torch random operation is performed between the\n    Ludwig operations.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n\n    Returns: None\n    \"\"\"\n    # define Ludwig model\n    model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    training_statistics1, preprocessed_data1, _ = model1.train(\n        dataset=raw_dataset_fp, random_seed=random_seed, skip_save_progress=True, skip_save_processed_input=True\n    )\n\n    # invoke torch random functions with unrelated seed to\n    # see if it affects Ludwig reproducibility\n    torch.manual_seed(random_seed + 5)\n    torch.rand((5,))\n\n    model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    training_statistics2, preprocessed_data2, _ = model2.train(\n        dataset=raw_dataset_fp,\n        random_seed=random_seed,\n        skip_save_progress=True,\n        skip_save_processed_input=True,\n    )\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            # same seeds should result in same output\n            assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n    # confirm reproducibility/non-reproducibility of results\n    assert training_statistics1 == training_statistics2\n\n\n@pytest.mark.parametrize(\"second_seed_offset\", [0, 1])\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_experiment(raw_dataset_fp: str, random_seed: int, second_seed_offset: int) -> None:\n    \"\"\"Test reproducibility of experiment API.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n        second_seed_offset(int): zero to use same random seed for second test, non-zero to use a different\n            seed for the second run.\n\n    Returns: None\n    \"\"\"\n    # perform first model experiment\n    model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    evaluation_statistics1, training_statistics1, preprocessed_data1, _ = model1.experiment(\n        dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True\n    )\n\n    # perform second model experiment\n    model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    evaluation_statistics2, training_statistics2, preprocessed_data2, _ = model2.experiment(\n        dataset=raw_dataset_fp, random_seed=random_seed + second_seed_offset, skip_save_processed_input=True\n    )\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            if second_seed_offset == 0:\n                # same seeds should result in same output\n                assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n            else:\n                # non-zero second_seed_offset uses different seeds and should result in different output\n                assert not np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n    # confirm results reproducibility/non-reproducibility of results\n    if second_seed_offset == 0:\n        # same seeds should result in same output\n        assert training_statistics1 == training_statistics2\n        assert evaluation_statistics1 == evaluation_statistics2\n    else:\n        # non-zero second_seed_offset uses different seeds and should result in different output\n        assert training_statistics1 != training_statistics2\n        assert evaluation_statistics1 != evaluation_statistics2\n\n\n@pytest.mark.parametrize(\"random_seed\", [1919, 31])\ndef test_experiment_ignore_torch_seed(raw_dataset_fp: str, random_seed: int) -> None:\n    \"\"\"Test reproducibility of experiment API when an unrelated torch random operation is performed between the\n    Ludwig operations.\n\n    Args:\n        raw_dataset_fp (str): file path for data to be used as part of this test\n        random_seed(int): random seed integer to use for test\n\n    Returns: None\n    \"\"\"\n    # define Ludwig model\n    model1 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n\n    evaluation_statistics1, training_statistics1, preprocessed_data1, _ = model1.experiment(\n        dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True\n    )\n\n    # invoke torch random functions with unrelated seed to\n    # see if it affects Ludwig reproducibility\n    torch.manual_seed(random_seed + 5)\n    torch.rand((5,))\n\n    model2 = LudwigModel(config=CONFIG, logging_level=logging.WARN)\n    evaluation_statistics2, training_statistics2, preprocessed_data2, _ = model2.experiment(\n        dataset=raw_dataset_fp, random_seed=random_seed, skip_save_processed_input=True\n    )\n\n    # confirm data splits are reproducible\n    for i in range(3):\n        for k in preprocessed_data1[i].dataset:\n            # same seeds should result in same output\n            assert np.all(preprocessed_data1[i].dataset[k] == preprocessed_data2[i].dataset[k])\n\n    # confirm results reproducibility/non-reproducibility of results\n    # same seeds should result in same output\n    assert training_statistics1 == training_statistics2\n    assert evaluation_statistics1 == evaluation_statistics2\n"
  },
  {
    "path": "tests/integration_tests/test_sequence_decoders.py",
    "content": "import os\n\nimport pytest\n\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    DECODER,\n    ENCODER,\n    INPUT_FEATURES,\n    OUTPUT_FEATURES,\n    SEQUENCE,\n    TEXT,\n    TRAINER,\n    TYPE,\n)\nfrom tests.integration_tests.utils import (\n    create_data_set_to_use,\n    generate_data,\n    RAY_BACKEND_CONFIG,\n    sequence_feature,\n    text_feature,\n    train_with_backend,\n)\n\npytestmark = pytest.mark.integration_tests_c\n\n\n@pytest.mark.slow\n@pytest.mark.parametrize(\"feature_type,feature_gen\", [(TEXT, text_feature), (SEQUENCE, sequence_feature)])\n@pytest.mark.parametrize(\"decoder_type\", [\"generator\", \"tagger\"])\n@pytest.mark.distributed\ndef test_sequence_decoder_predictions(tmpdir, csv_filename, ray_cluster_2cpu, feature_type, feature_gen, decoder_type):\n    \"\"\"Test that sequence decoders return the correct successfully predict.\"\"\"\n    input_feature = feature_gen()\n    output_feature = feature_gen(output_feature=True)\n\n    input_feature[ENCODER] = {TYPE: \"embed\", \"reduce_output\": None}\n    output_feature[DECODER] = {TYPE: decoder_type}\n\n    dataset_path = generate_data(\n        input_features=[input_feature],\n        output_features=[output_feature],\n        filename=os.path.join(tmpdir, csv_filename),\n    )\n    dataset_path = create_data_set_to_use(\"csv\", dataset_path)\n\n    config = {INPUT_FEATURES: [input_feature], TRAINER: {\"train_steps\": 1, BATCH_SIZE: 4}}\n\n    # Ensure that the decoder outputs the correct predictions through both the default and feature-specific configs.\n    config[OUTPUT_FEATURES] = [output_feature]\n\n    # Test with decoder in output feature config\n    train_with_backend(RAY_BACKEND_CONFIG, config=config, dataset=dataset_path)\n"
  },
  {
    "path": "tests/integration_tests/test_sequence_encoders.py",
    "content": "import logging\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, SEQUENCE\nfrom ludwig.encoders.registry import get_encoder_cls\nfrom tests.integration_tests.utils import ENCODERS\n\nlogger = logging.getLogger(__name__)\n\nTEST_VOCAB_SIZE = 132\nTEST_HIDDEN_SIZE = 32\nTEST_STATE_SIZE = 16\nTEST_EMBEDDING_SIZE = 64\nTEST_NUM_FILTERS = 24\nBATCH_SIZE = 2\nSEQ_SIZE = 10\nPARALLEL_CNN_LAYERS = 4\n\n# encoder parameters combinations tested\nencoder_parameters = {\n    \"vocab\": [str(i) for i in range(TEST_VOCAB_SIZE)],\n    \"embedding_size\": TEST_EMBEDDING_SIZE,\n    \"hidden_size\": TEST_HIDDEN_SIZE,\n    \"num_filters\": TEST_NUM_FILTERS,\n    \"num_layers\": 1,\n    \"max_sequence_length\": SEQ_SIZE,\n    \"state_size\": TEST_STATE_SIZE,\n    \"cell_type\": \"rnn\",\n    \"should_embed\": True,\n    \"dropout\": 0.0,\n    \"norm\": None,\n    \"reduce_output\": None,\n}\n\n\n@pytest.fixture(scope=\"module\")\ndef input_sequence() -> torch.Tensor:\n    # generates a realistic looking synthetic sequence tensor, i.e.\n    # each sequence will have non-zero tokens at the beginning with\n    # trailing zero tokens, including a max length token with a single\n    # zero token at the end.  Example:\n    # [\n    #   [3, 5, 6, 0, 0, 0],\n    #   [10, 11, 12, 13, 14, 0],   # max length sequence\n    #   [32, 0, 0, 0, 0, 0]        # minimum length sequence\n    # ]\n    input_tensor = torch.zeros([BATCH_SIZE, SEQ_SIZE], dtype=torch.int32)\n    sequence_lengths = np.random.randint(1, SEQ_SIZE, size=BATCH_SIZE)\n    for i in range(input_tensor.shape[0]):\n        input_tensor[i, : sequence_lengths[i]] = torch.tensor(\n            np.random.randint(2, TEST_VOCAB_SIZE, size=sequence_lengths[i])\n        )\n\n    if torch.cuda.is_available():\n        input_tensor = input_tensor.cuda()\n\n    return input_tensor\n\n\n@pytest.mark.parametrize(\"enc_reduce_output\", [None, \"sum\"])\n@pytest.mark.parametrize(\"enc_norm\", [None, \"batch\", \"layer\"])\n@pytest.mark.parametrize(\"enc_num_layers\", [1, 2])\n@pytest.mark.parametrize(\"enc_dropout\", [0, 0.2])\n@pytest.mark.parametrize(\"enc_cell_type\", [\"rnn\", \"gru\", \"lstm\"])\n@pytest.mark.parametrize(\"enc_encoder\", ENCODERS + [\"passthrough\"])\ndef test_sequence_encoders(\n    enc_encoder: str,\n    enc_cell_type: str,\n    enc_dropout: float,\n    enc_num_layers: int,\n    enc_norm: None | str,\n    enc_reduce_output: None | str,\n    input_sequence: torch.Tensor,\n):\n    # update encoder parameters for specific unit test case\n    encoder_parameters[\"cell_type\"] = enc_cell_type\n    encoder_parameters[\"dropout\"] = enc_dropout\n    encoder_parameters[\"num_layers\"] = enc_num_layers\n    encoder_parameters[\"norm\"] = enc_norm\n    encoder_parameters[\"reduce_output\"] = enc_reduce_output\n\n    # retrieve encoder to test\n    encoder_obj = get_encoder_cls(SEQUENCE, enc_encoder)(**encoder_parameters)\n    if torch.cuda.is_available():\n        encoder_obj = encoder_obj.cuda()\n\n    encoder_out = encoder_obj(input_sequence)\n\n    assert ENCODER_OUTPUT in encoder_out\n    assert isinstance(encoder_out[ENCODER_OUTPUT], torch.Tensor)\n\n    if enc_encoder == \"parallel_cnn\":\n        number_parallel_cnn_layers = PARALLEL_CNN_LAYERS\n        output_dimension = encoder_parameters[\"num_filters\"] * number_parallel_cnn_layers\n        assert (\n            encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, output_dimension)\n            if enc_reduce_output is None\n            else (BATCH_SIZE, output_dimension)\n        )\n\n    elif enc_encoder == \"stacked_parallel_cnn\":\n        number_parallel_cnn_layers = PARALLEL_CNN_LAYERS\n        output_dimension = encoder_parameters[\"num_filters\"] * number_parallel_cnn_layers\n        assert (\n            encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, output_dimension)\n            if enc_reduce_output is None\n            else (BATCH_SIZE, output_dimension)\n        )\n\n    elif enc_encoder == \"rnn\":\n        assert (\n            encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, TEST_STATE_SIZE)\n            if enc_reduce_output is None\n            else (BATCH_SIZE, TEST_STATE_SIZE)\n        )\n\n        assert ENCODER_OUTPUT_STATE in encoder_out\n        if enc_cell_type == \"lstm\":\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], tuple)\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE][0], torch.Tensor)\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE][1], torch.Tensor)\n            assert encoder_out[ENCODER_OUTPUT_STATE][0].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n            assert encoder_out[ENCODER_OUTPUT_STATE][1].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n        else:\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], torch.Tensor)\n            assert encoder_out[ENCODER_OUTPUT_STATE].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n\n    elif enc_encoder == \"cnnrnn\":\n        assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape\n        assert ENCODER_OUTPUT_STATE in encoder_out\n\n        if enc_cell_type == \"lstm\":\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], tuple)\n            assert encoder_out[ENCODER_OUTPUT_STATE][0].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n            assert encoder_out[ENCODER_OUTPUT_STATE][1].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n        else:\n            assert isinstance(encoder_out[ENCODER_OUTPUT_STATE], torch.Tensor)\n            assert encoder_out[ENCODER_OUTPUT_STATE].shape == (BATCH_SIZE, TEST_STATE_SIZE)\n\n    elif enc_encoder == \"stacked_cnn\":\n        assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape\n\n    elif enc_encoder == \"embed\":\n        assert (\n            encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, TEST_EMBEDDING_SIZE)\n            if enc_reduce_output is None\n            else (BATCH_SIZE, TEST_EMBEDDING_SIZE)\n        )\n\n    elif enc_encoder == \"transformer\":\n        assert encoder_out[ENCODER_OUTPUT].shape[1:] == encoder_obj.output_shape\n\n    elif enc_encoder == \"passthrough\":\n        assert (\n            encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1)\n            if enc_reduce_output is None\n            else (BATCH_SIZE, 1)\n        )\n\n    else:\n        raise ValueError(f\"{enc_encoder} is an invalid encoder specification\")\n\n\n@pytest.mark.parametrize(\"enc_reduce_output\", [None, \"sum\", \"last\", \"mean\", \"max\", \"concat\"])\ndef test_passthrough_encoder(enc_reduce_output, input_sequence):\n    encoder_parameters = {\"reduce_output\": enc_reduce_output}\n\n    # retrieve encoder to test\n    encoder_obj = get_encoder_cls(SEQUENCE, \"passthrough\")(**encoder_parameters)\n\n    encoder_out = encoder_obj(input_sequence)\n\n    assert ENCODER_OUTPUT in encoder_out\n    assert (\n        encoder_out[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1) if enc_reduce_output is None else (BATCH_SIZE, 1)\n    )\n\n\n# test to ensure correct handling of vocab_size and embedding_size specifications\n@pytest.mark.parametrize(\"enc_embedding_size\", [TEST_VOCAB_SIZE - 8, TEST_VOCAB_SIZE, TEST_VOCAB_SIZE + 8])\ndef test_sequence_embed_encoder(enc_embedding_size: int, input_sequence: torch.Tensor) -> None:\n    encoder_parameters[\"embedding_size\"] = enc_embedding_size\n\n    encoder_obj = get_encoder_cls(SEQUENCE, \"embed\")(**encoder_parameters)\n\n    encoder_out = encoder_obj(input_sequence)\n\n    assert encoder_out[ENCODER_OUTPUT].size()[1:] == encoder_obj.output_shape\n"
  },
  {
    "path": "tests/integration_tests/test_sequence_features.py",
    "content": "import contextlib\nimport copy\nfrom io import StringIO\n\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import DECODER, ENCODER_OUTPUT_STATE, LOGITS\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset\nfrom ludwig.data.preprocessing import preprocess_for_training\nfrom ludwig.features.feature_registries import update_config_with_metadata\nfrom tests.integration_tests.utils import generate_data, run_experiment, sequence_feature\n\n#\n# this test is focused on testing input sequence features with all encoders\n# and output sequence feature with Generator decoder.  Except for specified\n# configuration parameters all other parameters assume default values.\n#\n\nTEST_VOCAB_SIZE = 132\nTEST_HIDDEN_SIZE = 32\nTEST_STATE_SIZE = 8\nTEST_EMBEDDING_SIZE = 64\nTEST_NUM_FILTERS = 24\n\n\n# generates dataset that can be used for rest of test\n@pytest.fixture(scope=\"module\")\ndef generate_sequence_training_data():\n    input_features = [\n        sequence_feature(\n            encoder={\n                \"vocab_size\": TEST_VOCAB_SIZE,\n                \"embedding_size\": TEST_EMBEDDING_SIZE,\n                \"state_size\": TEST_STATE_SIZE,\n                \"hidden_size\": TEST_HIDDEN_SIZE,\n                \"num_filters\": TEST_NUM_FILTERS,\n                \"min_len\": 5,\n                \"max_len\": 10,\n                \"type\": \"rnn\",\n                \"cell_type\": \"lstm\",\n            }\n        )\n    ]\n\n    output_features = [\n        sequence_feature(\n            decoder={\"type\": \"generator\", \"min_len\": 5, \"max_len\": 10, \"cell_type\": \"lstm\", \"attention\": \"bahdanau\"}\n        )\n    ]\n\n    # generate synthetic data set testing\n    dataset = build_synthetic_dataset(150, copy.deepcopy(input_features) + copy.deepcopy(output_features))\n    raw_data = \"\\n\".join([r[0] + \",\" + r[1] for r in dataset])\n    df = pd.read_csv(StringIO(raw_data))\n\n    return df, input_features, output_features\n\n\n# setups up minimal number of data structures required to support initialized\n# input and output features.  The function returns initialized LudwigModel\n# and batcher for training dataset\n@contextlib.contextmanager\ndef setup_model_scaffolding(raw_df, input_features, output_features):\n    # setup input feature for testing\n    config = {\"input_features\": input_features, \"output_features\": output_features}\n\n    # setup model scaffolding to for testing\n    model = LudwigModel(config)\n    training_set, _, _, training_set_metadata = preprocess_for_training(\n        model.config, training_set=raw_df, skip_save_processed_input=True\n    )\n    model.training_set_metadata = training_set_metadata\n    update_config_with_metadata(model.config_obj, training_set_metadata)\n    model.model = model.create_model(model.config_obj)\n\n    # setup batcher to go through synthetic data\n    with training_set.initialize_batcher() as batcher:\n        yield model, batcher\n\n\n# TODO(#1333): Refactor this test once torch sequence generator work is complete.\n# - Tests may be covered by other smaller scoped unit tests.\n#\n# tests output feature sequence with `Generator` decoder\n# pytest parameters\n#   dec_cell_type: decoder cell type\n#   combiner_output_shapes: is a 2-tuple specifies the possible types of\n#     tensors that the combiner may generate for sequences.\n#     combiner_output_shapes[0]: specifies shape for hidden key\n#     combiner_output_shapes[1]: is either None or 1 or 2-tuple representing\n#       the encoder_output_state key. None: no encoder_output_state key,\n#       1-tuple: generate tf.Tensor, 2-tuple: generate list with 2 tf.Tensors\n# TODO(Justin): Move these to test_sequence_generator unit tests, and reintroduce decoder attention, beam_width, and\n# num_layers when these are reimplemented.\n@pytest.mark.parametrize(\n    \"dec_cell_type,combiner_output_shapes\",\n    [\n        (\"lstm\", ((128, 10, TEST_STATE_SIZE), None)),\n        (\"rnn\", ((128, 10, TEST_STATE_SIZE), ((128, TEST_STATE_SIZE), (128, TEST_STATE_SIZE)))),\n        (\"gru\", ((128, 10, TEST_STATE_SIZE), ((128, TEST_STATE_SIZE),))),\n    ],\n    ids=[\"lstm_no_state\", \"rnn_dual_state\", \"gru_single_state\"],\n)\ndef test_sequence_decoders(\n    dec_cell_type,\n    combiner_output_shapes,\n    generate_sequence_training_data,\n):\n    # retrieve pre-computed dataset and features\n    raw_df = generate_sequence_training_data[0]\n    input_features = generate_sequence_training_data[1]\n    output_features = generate_sequence_training_data[2]\n    output_feature_name = output_features[0][\"name\"]\n    output_features[0][DECODER][\"cell_type\"] = dec_cell_type\n\n    with setup_model_scaffolding(raw_df, input_features, output_features) as (model, _):\n        # generate synthetic encoder_output tensors and make it look like\n        # it came out of the combiner\n        encoder_output = torch.randn(combiner_output_shapes[0])\n        combiner_outputs = {\"hidden\": encoder_output}\n\n        if combiner_output_shapes[1] is not None:\n            if len(combiner_output_shapes[1]) > 1:\n                encoder_output_state = (\n                    torch.randn(combiner_output_shapes[1][0]),\n                    torch.randn(combiner_output_shapes[1][1]),\n                )\n            else:\n                encoder_output_state = torch.randn(combiner_output_shapes[1][0])\n\n            combiner_outputs[ENCODER_OUTPUT_STATE] = encoder_output_state\n\n        decoder = model.model.output_features.get(output_feature_name).decoder_obj\n        decoder_out = decoder(combiner_outputs)\n\n        # gather expected components of the shape\n        batch_size = combiner_outputs[\"hidden\"].shape[0]\n        seq_size = output_features[0][DECODER][\"max_len\"] + 2  # For start and stop symbols.\n        vocab_size = model.config_obj.output_features.to_list()[0][DECODER][\"vocab_size\"]\n\n        # confirm shape and format of decoder output\n        assert list(decoder_out[LOGITS].size()) == [batch_size, seq_size, vocab_size]\n\n\n# final sanity test.  Checks a subset of sequence parameters\n@pytest.mark.parametrize(\n    \"enc_encoder,enc_cell_type,dec_cell_type\",\n    [\n        (\"embed\", \"lstm\", \"lstm\"),\n        (\"rnn\", \"rnn\", \"gru\"),\n        (\"rnn\", \"gru\", \"rnn\"),\n    ],\n    ids=[\"embed_lstm\", \"rnn_gru\", \"gru_rnn\"],\n)\ndef test_sequence_generator(enc_encoder, enc_cell_type, dec_cell_type, csv_filename):\n    # Define input and output features\n    input_features = [\n        sequence_feature(encoder={\"type\": enc_encoder, \"min_len\": 5, \"max_len\": 10, \"cell_type\": enc_cell_type})\n    ]\n    output_features = [\n        sequence_feature(decoder={\"type\": \"generator\", \"min_len\": 5, \"max_len\": 10, \"cell_type\": dec_cell_type})\n    ]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    # run the experiment\n    run_experiment(input_features, output_features, dataset=rel_path)\n"
  },
  {
    "path": "tests/integration_tests/test_server.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport json\nimport logging\nimport os\nimport sys\n\nimport numpy as np\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, DECODER, TRAINER\nfrom ludwig.serve import server\nfrom ludwig.utils.data_utils import read_csv\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    category_feature,\n    generate_data,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    text_feature,\n)\n\nlogger = logging.getLogger(__name__)\n\nALL_FEATURES_PRESENT_ERROR = \"Data received does not contain all input features\"\n\ntry:\n    from starlette.testclient import TestClient\nexcept ImportError:\n    logger.error(\n        \" fastapi and other serving dependencies are not installed. \"\n        \"In order to install all serving dependencies run \"\n        \"pip install ludwig[serve]\"\n    )\n    sys.exit(-1)\n\n\ndef train_and_predict_model(input_features, output_features, data_csv, output_directory):\n    \"\"\"Helper method to avoid code repetition for training a model and using it for prediction.\n\n    :param input_features: input schema\n    :param output_features: output schema\n    :param data_csv: path to data\n    :param output_directory: model output directory\n    :return: None\n    \"\"\"\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    model = LudwigModel(config, backend=LocalTestBackend())\n    model.train(\n        dataset=data_csv,\n        skip_save_processed_input=True,\n        skip_save_progress=True,\n        skip_save_unprocessed_output=True,\n        output_directory=output_directory,\n    )\n    model.predict(dataset=data_csv, output_directory=output_directory)\n    return model\n\n\ndef train_and_predict_model_with_stratified_split(input_features, output_features, data_csv, output_directory):\n    \"\"\"Same as above, but with stratified split.\"\"\"\n    print(f'output_features[0][\"column\"]: {output_features[0][\"column\"]}')\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n        \"preprocessing\": {\n            \"split\": {\"column\": output_features[0][\"column\"], \"probabilities\": [0.7, 0.1, 0.2], \"type\": \"stratify\"},\n        },\n    }\n    model = LudwigModel(config, backend=LocalTestBackend())\n    model.train(\n        dataset=data_csv,\n        skip_save_processed_input=True,\n        skip_save_progress=True,\n        skip_save_unprocessed_output=True,\n        output_directory=output_directory,\n    )\n    model.predict(dataset=data_csv, output_directory=output_directory)\n    return model\n\n\ndef output_keys_for(output_features):\n    keys = []\n    for feature in output_features:\n        name = feature[\"name\"]\n        if feature[\"type\"] == \"category\":\n            keys.append(f\"{name}_predictions\")\n            keys.append(f\"{name}_probability\")\n            keys.append(f\"{name}_probabilities\")\n            for category in feature[DECODER][\"idx2str\"]:\n                keys.append(f\"{name}_probabilities_{category}\")\n\n        elif feature[\"type\"] == \"number\":\n            keys.append(f\"{name}_predictions\")\n        else:\n            raise NotImplementedError\n    return keys\n\n\ndef convert_to_form(entry):\n    data = {}\n    files = []\n    for k, v in entry.items():\n        if isinstance(v, str) and os.path.exists(v):\n            file = open(v, \"rb\")\n            files.append((k, (v, file.read(), \"application/octet-stream\")))\n        else:\n            data[k] = v\n    return data, files\n\n\ndef convert_to_batch_form(data_df):\n    data = data_df.to_dict(orient=\"split\")\n    files = {\n        \"dataset\": (None, json.dumps(data), \"application/json\"),\n    }\n    for row in data[\"data\"]:\n        for v in row:\n            if isinstance(v, str) and os.path.exists(v) and v not in files:\n                files[v] = (v, open(v, \"rb\"), \"application/octet-stream\")\n    return files\n\n\ndef test_server_integration_with_images(tmpdir):\n    # Image Inputs\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n\n    # Resnet encoder\n    input_features = [\n        image_feature(\n            folder=image_dest_folder,\n            encoder={\"output_size\": 16, \"num_filters\": 8},\n            preprocessing={\"in_memory\": True, \"height\": 32, \"width\": 32, \"num_channels\": 3},\n        ),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"zscore\"),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 4}), number_feature()]\n\n    np.random.seed(123)  # reproducible synthetic data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    model = train_and_predict_model(input_features, output_features, data_csv=rel_path, output_directory=tmpdir)\n\n    app = server(model)\n    client = TestClient(app)\n    response = client.get(\"/\")\n    assert response.status_code == 200\n\n    response = client.post(\"/predict\")\n    # expect the HTTP 400 error code for this situation\n    assert response.status_code == 400\n    assert ALL_FEATURES_PRESENT_ERROR in str(response.json())\n\n    data_df = read_csv(rel_path)\n\n    # One-off prediction\n    first_entry = data_df.T.to_dict()[0]\n    data, files = convert_to_form(first_entry)\n    server_response = client.post(\"/predict\", data=data, files=files)\n    assert server_response.status_code == 200\n    server_response = server_response.json()\n\n    server_response_keys = sorted(list(server_response.keys()))\n    assert server_response_keys == sorted(output_keys_for(output_features))\n\n    model_output, _ = model.predict(dataset=[first_entry], data_format=dict)\n    model_output = model_output.to_dict(\"records\")[0]\n    assert model_output == server_response\n\n    # Batch prediction\n    assert len(data_df) > 1\n    files = convert_to_batch_form(data_df)\n    server_response = client.post(\"/batch_predict\", files=files)\n    assert server_response.status_code == 200\n    server_response = server_response.json()\n\n    server_response_keys = sorted(server_response[\"columns\"])\n    assert server_response_keys == sorted(output_keys_for(output_features))\n    assert len(data_df) == len(server_response[\"data\"])\n\n    model_output, _ = model.predict(dataset=data_df)\n    model_output = model_output.to_dict(\"split\")\n    assert model_output == server_response\n\n\ndef test_server_integration_with_stratified_split(tmpdir):\n    input_features = [\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"zscore\"),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 4})]\n\n    np.random.seed(123)  # reproducible synthetic data\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"), num_examples=50)\n\n    model = train_and_predict_model_with_stratified_split(\n        input_features, output_features, data_csv=rel_path, output_directory=tmpdir\n    )\n\n    app = server(model)\n    client = TestClient(app)\n    response = client.get(\"/\")\n    assert response.status_code == 200\n\n    response = client.post(\"/predict\")\n    # expect the HTTP 400 error code for this situation\n    assert response.status_code == 400\n    assert ALL_FEATURES_PRESENT_ERROR in str(response.json())\n\n    data_df = read_csv(rel_path)\n\n    # One-off prediction\n    first_entry = data_df.T.to_dict()[0]\n    data, files = convert_to_form(first_entry)\n    server_response = client.post(\"/predict\", data=data, files=files)\n    assert server_response.status_code == 200\n    server_response = server_response.json()\n\n    server_response_keys = sorted(list(server_response.keys()))\n    assert server_response_keys == sorted(output_keys_for(output_features))\n\n    model_output, _ = model.predict(dataset=[first_entry], data_format=dict)\n    model_output = model_output.to_dict(\"records\")[0]\n    assert model_output == server_response\n\n    # Batch prediction\n    assert len(data_df) > 1\n    files = convert_to_batch_form(data_df)\n    server_response = client.post(\"/batch_predict\", files=files)\n    assert server_response.status_code == 200\n    server_response = server_response.json()\n\n    server_response_keys = sorted(server_response[\"columns\"])\n    assert server_response_keys == sorted(output_keys_for(output_features))\n    assert len(data_df) == len(server_response[\"data\"])\n\n    model_output, _ = model.predict(dataset=data_df)\n    model_output = model_output.to_dict(\"split\")\n    assert model_output == server_response\n\n\n@pytest.mark.parametrize(\"single_record\", [False, True])\ndef test_server_integration_with_audio(single_record, tmpdir):\n    # Audio Inputs\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n\n    # Resnet encoder\n    input_features = [\n        audio_feature(\n            folder=audio_dest_folder,\n        ),\n        text_feature(encoder={\"type\": \"embed\", \"min_len\": 1}),\n        number_feature(normalization=\"zscore\"),\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 4}), number_feature()]\n\n    rel_path = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n\n    model = train_and_predict_model(input_features, output_features, data_csv=rel_path, output_directory=tmpdir)\n\n    app = server(model)\n    client = TestClient(app)\n    response = client.get(\"/\")\n    assert response.status_code == 200\n\n    response = client.post(\"/predict\")\n    # expect the HTTP 400 error code for this situation\n    assert response.status_code == 400\n    assert ALL_FEATURES_PRESENT_ERROR in str(response.json())\n\n    data_df = read_csv(rel_path)\n\n    if single_record:\n        # Single record prediction\n        first_entry = data_df.T.to_dict()[0]\n        data, files = convert_to_form(first_entry)\n        server_response = client.post(\"/predict\", data=data, files=files)\n        assert server_response.status_code == 200\n        server_response = server_response.json()\n\n        server_response_keys = sorted(list(server_response.keys()))\n        assert server_response_keys == sorted(output_keys_for(output_features))\n\n        model_output, _ = model.predict(dataset=[first_entry], data_format=dict)\n        model_output = model_output.to_dict(\"records\")[0]\n        assert model_output == server_response\n    else:\n        # Batch prediction\n        assert len(data_df) > 1\n        files = convert_to_batch_form(data_df)\n        server_response = client.post(\"/batch_predict\", files=files)\n        assert server_response.status_code == 200\n        server_response = server_response.json()\n\n        server_response_keys = sorted(server_response[\"columns\"])\n        assert server_response_keys == sorted(output_keys_for(output_features))\n        assert len(data_df) == len(server_response[\"data\"])\n\n        model_output, _ = model.predict(dataset=data_df)\n        model_output = model_output.to_dict(\"split\")\n        assert model_output == server_response\n"
  },
  {
    "path": "tests/integration_tests/test_simple_features.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport logging\nimport os\n\nimport pandas as pd\nimport pytest\n\nfrom ludwig.constants import NAME\nfrom tests.integration_tests.utils import (\n    bag_feature,\n    binary_feature,\n    category_feature,\n    generate_data,\n    number_feature,\n    run_experiment,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    vector_feature,\n)\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\n\n@pytest.mark.parametrize(\n    \"input_test_feature, output_test_feature, output_loss_parameter\",\n    [\n        # number features\n        (number_feature(), number_feature(), None),\n        (number_feature(normalization=\"minmax\"), number_feature(), {\"loss\": {\"type\": \"mean_squared_error\"}}),\n        (number_feature(normalization=\"zscore\"), number_feature(), {\"loss\": {\"type\": \"mean_absolute_error\"}}),\n        # binary feature\n        (binary_feature(), binary_feature(), None),\n        # Categorical feature\n        (category_feature(), category_feature(output_feature=True), None),\n        (category_feature(), category_feature(output_feature=True), {\"loss\": {\"type\": \"softmax_cross_entropy\"}}),\n    ],\n)\ndef test_feature(input_test_feature, output_test_feature, output_loss_parameter, csv_filename):\n    input_features = [input_test_feature]\n\n    of_test_feature = output_test_feature\n    if output_loss_parameter is not None:\n        of_test_feature.update(output_loss_parameter)\n    output_features = [of_test_feature]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename, 100)\n\n    run_experiment(input_features, output_features, dataset=rel_path)\n\n\n@pytest.mark.parametrize(\n    \"input_test_feature, output_test_feature\",\n    [\n        ([category_feature()], [binary_feature(), binary_feature()]),\n        (\n            [category_feature()],\n            [category_feature(decoder={\"vocab_size\": 5}), category_feature(decoder={\"vocab_size\": 7})],\n        ),\n        ([category_feature()], [number_feature(), number_feature()]),\n        (\n            [category_feature()],\n            [sequence_feature(decoder={\"vocab_size\": 5}), sequence_feature(decoder={\"vocab_size\": 7})],\n        ),\n        (\n            [set_feature(encoder={\"vocab_size\": 5})],\n            [set_feature(decoder={\"vocab_size\": 5}), set_feature(decoder={\"vocab_size\": 7})],\n        ),\n        ([category_feature()], [text_feature(decoder={\"vocab_size\": 5}), text_feature(decoder={\"vocab_size\": 7})]),\n        ([category_feature()], [vector_feature(), vector_feature()]),\n        ([vector_feature()], [vector_feature(), vector_feature()]),\n        ([bag_feature()], [vector_feature(), vector_feature()]),\n    ],\n)\ndef test_feature_multiple_outputs(input_test_feature, output_test_feature, csv_filename):\n    # Generate test data\n    rel_path = generate_data(input_test_feature, output_test_feature, csv_filename, 100)\n\n    run_experiment(input_test_feature, output_test_feature, dataset=rel_path)\n\n\ndef test_category_int_dtype(tmpdir):\n    feature = category_feature()\n    input_features = [feature]\n    output_features = [binary_feature()]\n\n    csv_fname = generate_data(input_features, output_features, os.path.join(tmpdir, \"dataset.csv\"))\n    df = pd.read_csv(csv_fname)\n\n    distinct_values = df[feature[NAME]].drop_duplicates().values\n    value_map = {v: idx for idx, v in enumerate(distinct_values)}\n    df[feature[NAME]] = df[feature[NAME]].map(lambda x: value_map[x])\n\n    run_experiment(input_features, output_features, dataset=df)\n"
  },
  {
    "path": "tests/integration_tests/test_timeseries_feature.py",
    "content": "import numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import COLUMN, ENCODER_OUTPUT, INPUT_FEATURES, OUTPUT_FEATURES\nfrom ludwig.features.timeseries_feature import TimeseriesInputFeature\nfrom ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom tests.integration_tests.utils import number_feature, timeseries_feature\n\nBATCH_SIZE = 2\nSEQ_SIZE = 10\nDEFAULT_OUTPUT_SIZE = 4\n\n\n@pytest.mark.parametrize(\"enc_encoder\", [\"stacked_cnn\", \"rnn\", \"passthrough\"])\ndef test_timeseries_feature(enc_encoder):\n    # synthetic time series tensor\n    timeseries_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE], dtype=torch.float32)\n\n    # generate feature config\n    timeseries_feature_config = timeseries_feature(\n        encoder={\n            \"type\": enc_encoder,\n            \"max_len\": SEQ_SIZE,\n            \"fc_layers\": [{\"output_size\": DEFAULT_OUTPUT_SIZE}],\n            # simulated parameters determined by pre-processing\n            \"max_sequence_length\": SEQ_SIZE,\n        }\n    )\n\n    # instantiate input feature object\n    timeseries_feature_config, _ = load_config_with_kwargs(TimeseriesInputFeatureConfig, timeseries_feature_config)\n    timeseries_input_feature = TimeseriesInputFeature(timeseries_feature_config)\n\n    # pass synthetic tensor through input feature\n    encoder_output = timeseries_input_feature(timeseries_tensor)\n\n    # confirm correctness of the encoder output\n    assert isinstance(encoder_output, dict)\n    assert ENCODER_OUTPUT in encoder_output\n    assert isinstance(encoder_output[ENCODER_OUTPUT], torch.Tensor)\n    if enc_encoder == \"passthrough\":\n        assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, SEQ_SIZE, 1)\n    else:\n        assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, DEFAULT_OUTPUT_SIZE)\n\n\ndef test_timeseries_preprocessing_with_nan():\n    config = {\n        \"input_features\": [timeseries_feature(preprocessing={\"padding_value\": 42})],\n        \"output_features\": [number_feature()],\n    }\n\n    # generate synthetic data\n    data = {\n        config[INPUT_FEATURES][0][COLUMN]: [\n            \"1.53 2.3 NaN 6.4 3 \",\n            \"1.53 2.3 2 \",\n            \"1.53 NaN 3 2 \",\n        ],\n        config[OUTPUT_FEATURES][0][COLUMN]: [1.0, 2.0, 3.0],\n    }\n    df = pd.DataFrame(data)\n\n    model = LudwigModel(config)\n    ds = model.preprocess(df)\n    out_df = ds.training_set.to_df()\n\n    assert len(out_df.columns) == len(df.columns)\n\n    expected_df = pd.DataFrame(\n        [\n            [np.array([1.53, 2.3, 42.0, 6.4, 3.0]), 1.0],\n            [np.array([1.53, 2.3, 2.0, 42.0, 42.0]), 2.0],\n            [np.array([1.53, 42.0, 3.0, 2.0, 42.0]), 3.0],\n        ],\n        columns=out_df.columns.to_list(),\n    )\n\n    for row1, row2 in zip(out_df.values, expected_df.values):\n        assert np.allclose(row1[0], row2[0])\n        assert row1[1] == row2[1]\n"
  },
  {
    "path": "tests/integration_tests/test_torchscript.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\nimport shutil\nfrom copy import deepcopy\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\n\ntry:\n    import torchtext\nexcept ImportError:\n    torchtext = None\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.backend import RAY\nfrom ludwig.constants import BATCH_SIZE, COMBINER, EVAL_BATCH_SIZE, LOGITS, NAME, PREDICTIONS, PROBABILITIES, TRAINER\nfrom ludwig.data.preprocessing import preprocess_for_prediction\nfrom ludwig.features.number_feature import numeric_transformation_registry\nfrom ludwig.globals import TRAIN_SET_METADATA_FILE_NAME\nfrom ludwig.models.inference import to_inference_module_input_from_dataframe\nfrom ludwig.utils import output_feature_utils\nfrom ludwig.utils.tokenizers import TORCHSCRIPT_COMPATIBLE_TOKENIZERS\nfrom tests.integration_tests import utils\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    bag_feature,\n    binary_feature,\n    category_feature,\n    date_feature,\n    generate_data,\n    h3_feature,\n    image_feature,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\"should_load_model\", [True, False])\ndef test_torchscript(tmpdir, csv_filename, should_load_model):\n    #######\n    # Setup\n    #######\n    dir_path = tmpdir\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    # Single sequence input, single category output\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n    input_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"type\": \"passthrough\", \"vocab_size\": 3}),\n        category_feature(encoder={\"type\": \"onehot\", \"vocab_size\": 3}),\n        category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 3}),\n        sequence_feature(encoder={\"vocab_size\": 3}),\n        text_feature(encoder={\"vocab_size\": 3}),\n        vector_feature(),\n        image_feature(image_dest_folder),\n        audio_feature(audio_dest_folder),\n        timeseries_feature(),\n        date_feature(),\n        date_feature(),\n        h3_feature(),\n        set_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n    ]\n\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 3}),\n        binary_feature(),\n        number_feature(),\n        set_feature(decoder={\"vocab_size\": 3}),\n        vector_feature(),\n        sequence_feature(decoder={\"vocab_size\": 3}),\n        text_feature(decoder={\"vocab_size\": 3}),\n    ]\n\n    predictions_column_name = \"{}_predictions\".format(output_features[0][\"name\"])\n\n    # Generate test data\n    data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    #############\n    # Train model\n    #############\n    backend = LocalTestBackend()\n    config = {\n        \"model_type\": \"ecd\",\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.train(\n        dataset=data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    ###################\n    # save Ludwig model\n    ###################\n    ludwigmodel_path = os.path.join(dir_path, \"ludwigmodel\")\n    shutil.rmtree(ludwigmodel_path, ignore_errors=True)\n    ludwig_model.save(ludwigmodel_path)\n\n    ###################\n    # load Ludwig model\n    ###################\n    if should_load_model:\n        ludwig_model = LudwigModel.load(ludwigmodel_path, backend=backend)\n\n    ##############################\n    # collect weight tensors names\n    ##############################\n    original_predictions_df, _ = ludwig_model.predict(dataset=data_csv_path)\n    original_weights = deepcopy(list(ludwig_model.model.parameters()))\n    original_weights = [t.cpu() for t in original_weights]\n\n    #################\n    # save torchscript\n    #################\n    torchscript_path = os.path.join(dir_path, \"torchscript\")\n    shutil.rmtree(torchscript_path, ignore_errors=True)\n    ludwig_model.model.save_torchscript(torchscript_path, device=\"cpu\")\n\n    ###################################################\n    # load Ludwig model, obtain predictions and weights\n    ###################################################\n    ludwig_model = LudwigModel.load(ludwigmodel_path, backend=backend)\n    loaded_prediction_df, _ = ludwig_model.predict(dataset=data_csv_path)\n    loaded_weights = deepcopy(list(ludwig_model.model.parameters()))\n    loaded_weights = [t.cpu() for t in loaded_weights]\n\n    #####################################################\n    # restore torchscript, obtain predictions and weights\n    #####################################################\n    training_set_metadata_json_fp = os.path.join(ludwigmodel_path, TRAIN_SET_METADATA_FILE_NAME)\n\n    dataset, training_set_metadata = preprocess_for_prediction(\n        ludwig_model.config_obj.to_dict(),\n        dataset=data_csv_path,\n        training_set_metadata=training_set_metadata_json_fp,\n        include_outputs=False,\n        backend=backend,\n    )\n\n    restored_model = torch.jit.load(torchscript_path)\n\n    # Check the outputs for one of the features for correctness\n    # Here we choose the first output feature (categorical)\n    of_name = list(ludwig_model.model.output_features.keys())[0]\n\n    data_to_predict = {\n        name: torch.from_numpy(dataset.dataset[feature.proc_column])\n        for name, feature in ludwig_model.model.input_features.items()\n    }\n\n    # Get predictions from restored torchscript.\n    logits = restored_model(data_to_predict)\n    restored_predictions = torch.argmax(output_feature_utils.get_output_feature_tensor(logits, of_name, \"logits\"), -1)\n\n    restored_predictions = [training_set_metadata[of_name][\"idx2str\"][idx] for idx in restored_predictions]\n\n    restored_weights = deepcopy(list(restored_model.parameters()))\n    restored_weights = [t.cpu() for t in restored_weights]\n\n    ###############################################\n    # Check if weights and predictions are the same\n    ###############################################\n\n    # Check to weight values match the original model.\n    assert utils.is_all_close(original_weights, loaded_weights)\n    assert utils.is_all_close(original_weights, restored_weights)\n\n    # Check that predictions are identical to the original model.\n    assert np.all(original_predictions_df[predictions_column_name] == loaded_prediction_df[predictions_column_name])\n\n    assert np.all(original_predictions_df[predictions_column_name] == restored_predictions)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_tabular(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    # Configure features to be tested:\n    bin_str_feature_input_feature = binary_feature()\n    bin_str_feature_output_feature = binary_feature(output_feature=True)\n    transformed_number_features = [\n        number_feature(preprocessing={\"normalization\": numeric_transformer})\n        for numeric_transformer in numeric_transformation_registry.keys()\n    ]\n    input_features = [\n        bin_str_feature_input_feature,\n        binary_feature(),\n        *transformed_number_features,\n        number_feature(preprocessing={\"outlier_strategy\": \"fill_with_mean\"}),\n        category_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n        set_feature(encoder={\"vocab_size\": 3}),\n        vector_feature(),\n        # TODO: future support\n        # date_feature(),\n        # h3_feature(),\n    ]\n    output_features = [\n        bin_str_feature_output_feature,\n        binary_feature(output_feature=True),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 3}),\n        set_feature(decoder={\"vocab_size\": 3}),\n        vector_feature(),\n        sequence_feature(decoder={\"vocab_size\": 3}),\n        text_feature(decoder={\"vocab_size\": 3}),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    # Convert bool values to strings, e.g., {'Yes', 'No'}\n    df = pd.read_csv(training_data_csv_path)\n    false_value, true_value = \"No\", \"Yes\"\n    df[bin_str_feature_input_feature[NAME]] = df[bin_str_feature_input_feature[NAME]].map(\n        lambda x: true_value if x else false_value\n    )\n    df[bin_str_feature_output_feature[NAME]] = df[bin_str_feature_output_feature[NAME]].map(\n        lambda x: true_value if x else false_value\n    )\n    df.to_csv(training_data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_binary_only(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    input_features = [\n        binary_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_tabnet_combiner(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    # Configure features to be tested:\n    input_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n        set_feature(encoder={\"vocab_size\": 3}),\n    ]\n    output_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 3}),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        COMBINER: {\n            \"type\": \"tabnet\",\n            \"num_total_blocks\": 2,\n            \"num_shared_blocks\": 2,\n        },\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.xfail(reason=\"torchaudio 2.x: DifferentiableFIR not TorchScript-compatible (upstream)\")\ndef test_torchscript_e2e_audio(csv_filename, tmpdir):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n\n    input_features = [\n        audio_feature(audio_dest_folder),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    # NOTE: audio preprocessing mismatches by very small margins ~O(1e-6) but causes flakiness in e2e test.\n    # Increasing tolerance is a workaround to reduce flakiness for now.\n    # TODO: remove this workaround when audio preprocessing is fixed.\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path, tolerance=1e-6)\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\n    \"kwargs\",\n    [\n        {\"encoder\": {\"type\": \"stacked_cnn\"}},  # Ludwig custom encoder\n        {\"encoder\": {\"type\": \"alexnet\", \"use_pretrained\": False}},  # TorchVision pretrained model encoder\n    ],\n)\ndef test_torchscript_e2e_image(tmpdir, csv_filename, kwargs):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    input_features = [\n        image_feature(image_dest_folder, **kwargs),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_text(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 3}, preprocessing={\"tokenizer\": tokenizer})\n        for tokenizer in TORCHSCRIPT_COMPATIBLE_TOKENIZERS\n    ]\n    output_features = [\n        text_feature(decoder={\"vocab_size\": 3}),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 14, 0),\n    reason=\"requires torchtext 0.14.0 or higher\",\n)\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_text_hf_tokenizer(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [text_feature(encoder={\"vocab_size\": 3, \"type\": \"bert\"})]\n    output_features = [\n        category_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128, EVAL_BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 14, 0),\n    reason=\"requires torchtext 0.14.0 or higher\",\n)\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_text_hf_tokenizer_truncated_sequence(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [text_feature(encoder={\"vocab_size\": 3, \"type\": \"bert\"}, preprocessing={\"max_sequence_length\": 3})]\n    output_features = [\n        category_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_sequence(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        sequence_feature(encoder={\"vocab_size\": 3}, preprocessing={\"tokenizer\": \"space\"}),\n    ]\n    output_features = [\n        sequence_feature(decoder={\"vocab_size\": 3}),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_timeseries(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        timeseries_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_h3(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        h3_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\ndef test_torchscript_e2e_date(tmpdir, csv_filename):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        date_feature(),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path)\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\"vector_type\", [torch.Tensor, list[torch.Tensor]])\ndef test_torchscript_preproc_vector_alternative_type(tmpdir, csv_filename, vector_type):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    feature = vector_feature()\n    input_features = [\n        feature,\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n\n    # Initialize Ludwig model\n    ludwig_model, script_module = initialize_torchscript_module(tmpdir, config, backend, training_data_csv_path)\n\n    # Obtain preprocessed inputs from Python model\n    preproc_inputs_expected, _ = preprocess_for_prediction(\n        ludwig_model.config_obj.to_dict(),\n        training_data_csv_path,\n        ludwig_model.training_set_metadata,\n        backend=backend,\n        include_outputs=False,\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(df, config, load_paths=True)\n\n    def transform_vector_list(vector_list, vector_type):\n        vectors = []\n        for vector_str in vector_list:\n            vectors.append(torch.tensor([float(x) for x in vector_str.split()]))\n\n        if vector_type == torch.Tensor:\n            vectors = torch.stack(vectors)\n        return vectors\n\n    inputs[feature[NAME]] = transform_vector_list(inputs[feature[NAME]], vector_type)\n\n    preproc_inputs = script_module.preprocessor_forward(inputs)\n\n    # Check that preproc_inputs is the same as preproc_inputs_expected.\n    for feature_name_expected, feature_values_expected in preproc_inputs_expected.dataset.items():\n        feature_name = feature_name_expected[: feature_name_expected.rfind(\"_\")]  # remove proc suffix\n        if feature_name not in preproc_inputs.keys():\n            continue\n\n        feature_values = preproc_inputs[feature_name]\n        assert utils.is_all_close(feature_values, feature_values_expected), f\"feature: {feature_name}\"\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\"padding\", [\"left\", \"right\"])\n@pytest.mark.parametrize(\"fill_value\", [\"\", \"1.0\"])\ndef test_torchscript_preproc_timeseries_alternative_type(tmpdir, csv_filename, padding, fill_value):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    feature = timeseries_feature(\n        preprocessing={\n            \"padding\": padding,\n            \"timeseries_length_limit\": 4,\n            \"fill_value\": \"1.0\",\n        },\n        encoder={\"max_len\": 7},\n    )\n    input_features = [\n        feature,\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path, nan_percent=0.2)\n\n    # Initialize Ludwig model\n    ludwig_model, script_module = initialize_torchscript_module(tmpdir, config, backend, training_data_csv_path)\n\n    # Obtain preprocessed inputs from Python model\n    preproc_inputs_expected, _ = preprocess_for_prediction(\n        ludwig_model.config_obj.to_dict(),\n        training_data_csv_path,\n        ludwig_model.training_set_metadata,\n        backend=backend,\n        include_outputs=False,\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(df, config, load_paths=True)\n\n    def transform_timeseries_from_str_list_to_tensor_list(timeseries_list):\n        timeseries = []\n        for timeseries_str in timeseries_list:\n            timeseries.append(torch.tensor([float(x) for x in timeseries_str.split()]))\n        return timeseries\n\n    inputs[feature[NAME]] = transform_timeseries_from_str_list_to_tensor_list(inputs[feature[NAME]])\n\n    preproc_inputs = script_module.preprocessor_forward(inputs)\n\n    # Check that preproc_inputs is the same as preproc_inputs_expected.\n    for feature_name_expected, feature_values_expected in preproc_inputs_expected.dataset.items():\n        feature_name = feature_name_expected[: feature_name_expected.rfind(\"_\")]  # remove proc suffix\n        assert feature_name in preproc_inputs.keys(), f'feature \"{feature_name}\" not found.'\n\n        feature_values = preproc_inputs[feature_name]\n        assert utils.is_all_close(feature_values, feature_values_expected), f'feature \"{feature_name}\" value mismatch.'\n\n\n@pytest.mark.integration_tests_e\n@pytest.mark.parametrize(\n    \"feature\",\n    [\n        number_feature(),\n        binary_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n        set_feature(encoder={\"vocab_size\": 3}),\n        text_feature(encoder={\"vocab_size\": 3}),\n        sequence_feature(encoder={\"vocab_size\": 3}),\n        timeseries_feature(),\n        h3_feature(),\n        # TODO: future support\n        # audio_feature(),  # default BFILL strategy is unintuitive at inference time\n        # image_feature(),  # default BFILL strategy is unintuitive at inference time\n        # vector_feature(), # does not have a missing_value_strategy\n        # date_feature(),   # default fill with datetime.now() strategy is not scriptable\n    ],\n)\ndef test_torchscript_preproc_with_nans(tmpdir, csv_filename, feature):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n    input_features = [\n        feature,\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path, nan_percent=0.2)\n\n    # Initialize Ludwig model\n    ludwig_model, script_module = initialize_torchscript_module(tmpdir, config, backend, training_data_csv_path)\n\n    # Obtain preprocessed inputs from Python model\n    preproc_inputs_expected, _ = preprocess_for_prediction(\n        ludwig_model.config_obj.to_dict(),\n        training_data_csv_path,\n        ludwig_model.training_set_metadata,\n        backend=backend,\n        include_outputs=False,\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(df, config, load_paths=True)\n    preproc_inputs = script_module.preprocessor_forward(inputs)\n\n    # Check that preproc_inputs is the same as preproc_inputs_expected.\n    for feature_name_expected, feature_values_expected in preproc_inputs_expected.dataset.items():\n        feature_name = feature_name_expected[: feature_name_expected.rfind(\"_\")]  # remove proc suffix\n        if feature_name not in preproc_inputs.keys():\n            continue\n\n        feature_values = preproc_inputs[feature_name]\n        assert utils.is_all_close(feature_values, feature_values_expected), f\"feature: {feature_name}\"\n\n\n@pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n@pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\n@pytest.mark.integration_tests_e\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"feature_fn\",\n    [\n        number_feature,\n        image_feature,\n        audio_feature,\n        h3_feature,\n        date_feature,\n        # TODO: future support\n        # binary_feature(),                # Torchscript takes List[str] as input, so currently CPU only\n        # category_feature(encoder={\"vocab_size\": 3}),  # Torchscript takes List[str] as input, so currently CPU only\n        # set_feature(encoder={\"vocab_size\": 3}),       # Torchscript takes List[str] as input, so currently CPU only\n        # sequence_feature(encoder={\"vocab_size\": 3}),  # Torchscript takes List[str] as input, so currently CPU only\n        # text_feature(encoder={\"vocab_size\": 3}),      # Torchscript takes List[str] as input, so currently CPU only\n        # vector_feature(),                # Torchscript takes List[str] as input, so currently CPU only\n        # bag_feature(encoder={\"vocab_size\": 3}),       # Torchscript takes List[str] as input, so currently CPU only\n        # timeseries_feature(),            # Torchscript takes List[str] as input, so currently CPU only\n    ],\n)\ndef test_torchscript_preproc_gpu(tmpdir, csv_filename, feature_fn):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    feature_kwargs = {}\n    if feature_fn in {image_feature, audio_feature}:\n        dest_folder = os.path.join(tmpdir, \"generated_samples\")\n        feature_kwargs[\"folder\"] = dest_folder\n\n    input_features = [\n        feature_fn(**feature_kwargs),\n    ]\n    output_features = [\n        binary_feature(),\n    ]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    backend = RAY\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    _, script_module = initialize_torchscript_module(\n        tmpdir,\n        config,\n        backend,\n        training_data_csv_path,\n        device=torch.device(\"cuda\"),\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(\n        df,\n        config,\n        load_paths=True,\n        device=torch.device(\"cuda\"),\n    )\n    preproc_inputs = script_module.preprocessor_forward(inputs)\n\n    for name, values in preproc_inputs.items():\n        assert values.is_cuda, f'feature \"{name}\" tensors are not on GPU'\n\n\n@pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n@pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\n@pytest.mark.integration_tests_e\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"feature_fn\",\n    [\n        number_feature,\n        category_feature,\n        binary_feature,\n        set_feature,\n        vector_feature,\n        sequence_feature,\n        text_feature,\n    ],\n)\ndef test_torchscript_postproc_gpu(tmpdir, csv_filename, feature_fn):\n    data_csv_path = os.path.join(tmpdir, csv_filename)\n\n    feature_kwargs = {}\n    if feature_fn in {category_feature, set_feature, sequence_feature, text_feature}:\n        feature_kwargs[\"vocab_size\"] = 3\n\n    input_features = [\n        number_feature(),\n    ]\n    output_features = [\n        feature_fn(**feature_kwargs),\n    ]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1, BATCH_SIZE: 128},\n    }\n    backend = RAY\n    training_data_csv_path = generate_data(input_features, output_features, data_csv_path)\n    _, script_module = initialize_torchscript_module(\n        tmpdir,\n        config,\n        backend,\n        training_data_csv_path,\n        device=torch.device(\"cuda\"),\n    )\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(\n        df,\n        config,\n        load_paths=True,\n        device=torch.device(\"cuda\"),\n    )\n    postproc_outputs = script_module(inputs)\n\n    for feature_name, feature_outputs in postproc_outputs.items():\n        for output_name, output_values in feature_outputs.items():\n            assert utils.is_all_tensors_cuda(output_values), f\"{feature_name}.{output_name} tensors are not on GPU\"\n\n\ndef validate_torchscript_outputs(tmpdir, config, backend, training_data_csv_path, tolerance=1e-8):\n    # Train Ludwig (Pythonic) model:\n    ludwig_model, script_module = initialize_torchscript_module(\n        tmpdir,\n        config,\n        backend,\n        training_data_csv_path,\n    )\n\n    # Obtain predictions from Python model\n    preds_dict, _ = ludwig_model.predict(dataset=training_data_csv_path, return_type=dict)\n\n    df = pd.read_csv(training_data_csv_path)\n    inputs = to_inference_module_input_from_dataframe(df, config, load_paths=True)\n    outputs = script_module(inputs)\n\n    # TODO: these are the only outputs we provide from Torchscript for now\n    ts_outputs = {PREDICTIONS, PROBABILITIES, LOGITS}\n\n    # Compare results from Python trained model against Torchscript\n    for feature_name, feature_outputs_expected in preds_dict.items():\n        assert feature_name in outputs\n\n        feature_outputs = outputs[feature_name]\n        for output_name, output_values_expected in feature_outputs_expected.items():\n            if output_name not in ts_outputs:\n                continue\n\n            assert output_name in feature_outputs\n            output_values = feature_outputs[output_name]\n            assert utils.has_no_grad(output_values), f'\"{feature_name}.{output_name}\" tensors have gradients'\n            assert utils.is_all_close(\n                output_values, output_values_expected\n            ), f'\"{feature_name}.{output_name}\" tensors are not close to ludwig model'\n\n\ndef initialize_torchscript_module(tmpdir, config, backend, training_data_csv_path, device=None):\n    # Initialize Ludwig model\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.train(\n        dataset=training_data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    # Always use CPU for torchscript since inference inputs are CPU tensors\n    if device is None:\n        device = torch.device(\"cpu\")\n\n    # Create graph inference model (Torchscript) from trained Ludwig model.\n    script_module = ludwig_model.to_torchscript(device=device)\n    # Ensure torchscript saving/loading does not affect final predictions.\n    script_module_path = os.path.join(tmpdir, \"inference_module.pt\")\n    torch.jit.save(script_module, script_module_path)\n    script_module = torch.jit.load(script_module_path)\n    return ludwig_model, script_module\n"
  },
  {
    "path": "tests/integration_tests/test_trainer.py",
    "content": "import logging\nimport os\nimport shutil\nfrom unittest import mock\n\nimport pytest\nimport torch\nfrom packaging.version import parse as parse_version\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    EFFECTIVE_BATCH_SIZE,\n    EPOCHS,\n    EVAL_BATCH_SIZE,\n    INPUT_FEATURES,\n    MAX_BATCH_SIZE_DATASET_FRACTION,\n    OUTPUT_FEATURES,\n    TRAINER,\n)\nfrom ludwig.globals import MODEL_FILE_NAME\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    text_feature,\n    vector_feature,\n)\n\n\ndef test_tune_learning_rate(tmpdir):\n    config = {\n        INPUT_FEATURES: [text_feature(), binary_feature()],\n        OUTPUT_FEATURES: [binary_feature()],\n        TRAINER: {\n            \"train_steps\": 1,\n            BATCH_SIZE: 128,\n            \"learning_rate\": \"auto\",\n        },\n    }\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(config[INPUT_FEATURES], config[OUTPUT_FEATURES], csv_filename)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n    assert model.config_obj.trainer.learning_rate == 0.0001\n\n\n@pytest.mark.parametrize(\n    \"is_cpu,effective_batch_size,eval_batch_size\",\n    [\n        (True, \"auto\", \"auto\"),\n        (False, 256, 128),\n        (True, \"auto\", None),\n    ],\n    ids=[\"cpu_auto\", \"gpu_fixed\", \"cpu_no_eval_bs\"],\n)\ndef test_ecd_tune_batch_size_and_lr(tmpdir, eval_batch_size, effective_batch_size, is_cpu):\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        number_feature(),\n        binary_feature(),\n        vector_feature(),\n    ]\n\n    num_samples = 30\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename, num_examples=num_samples)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    trainer = {\n        EPOCHS: 2,\n        EFFECTIVE_BATCH_SIZE: effective_batch_size,\n        BATCH_SIZE: \"auto\",\n        \"gradient_accumulation_steps\": \"auto\",\n        \"learning_rate\": \"auto\",\n    }\n\n    if eval_batch_size:\n        trainer[EVAL_BATCH_SIZE] = eval_batch_size\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: trainer,\n    }\n\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n\n    # check preconditions\n    assert model.config_obj.trainer.effective_batch_size == effective_batch_size\n    assert model.config_obj.trainer.batch_size == \"auto\"\n    assert model.config_obj.trainer.gradient_accumulation_steps == \"auto\"\n    assert model.config_obj.trainer.eval_batch_size == eval_batch_size\n    assert model.config_obj.trainer.learning_rate == \"auto\"\n\n    with mock.patch(\"ludwig.trainers.trainer.Trainer.is_cpu_training\") as mock_fn:\n        mock_fn.return_value = is_cpu\n        _, _, output_directory = model.train(\n            training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir\n        )\n\n    def check_postconditions(model):\n        # check batch size\n        assert model.config_obj.trainer.effective_batch_size == effective_batch_size\n        assert model.config_obj.trainer.batch_size != \"auto\"\n        assert model.config_obj.trainer.batch_size > 1\n\n        # check gradient accumulation\n        assert model.config_obj.trainer.gradient_accumulation_steps != \"auto\"\n        if effective_batch_size == \"auto\":\n            assert model.config_obj.trainer.gradient_accumulation_steps == 1\n        else:\n            batch_size = model.config_obj.trainer.batch_size\n            assert model.config_obj.trainer.gradient_accumulation_steps == effective_batch_size // batch_size\n\n        # 4 is the largest possible batch size for this dataset (20% of dataset size)\n        assert model.config_obj.trainer.batch_size <= MAX_BATCH_SIZE_DATASET_FRACTION * num_samples\n\n        assert model.config_obj.trainer.eval_batch_size != \"auto\"\n        assert model.config_obj.trainer.eval_batch_size > 1\n\n        if eval_batch_size in (\"auto\", None):\n            assert model.config_obj.trainer.batch_size == model.config_obj.trainer.eval_batch_size\n        else:\n            assert model.config_obj.trainer.eval_batch_size == eval_batch_size\n\n        # check learning rate\n        assert model.config_obj.trainer.learning_rate == 0.0001  # has sequence feature\n\n    check_postconditions(model)\n\n    model = LudwigModel.load(os.path.join(output_directory, MODEL_FILE_NAME))\n\n    # loaded model should retain the tuned params\n    check_postconditions(model)\n\n\ndef test_changing_parameters_on_plateau(tmpdir):\n    input_features = [sequence_feature(encoder={\"reduce_output\": \"sum\"})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            EPOCHS: 2,\n            BATCH_SIZE: 128,\n            \"learning_rate\": 1.0,\n            \"reduce_learning_rate_on_plateau\": 1,\n            \"increase_batch_size_on_plateau\": 1,\n        },\n    }\n    model = LudwigModel(config, backend=LocalTestBackend())\n\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n\n@pytest.mark.skipif(torch.cuda.device_count() == 0, reason=\"test requires at least 1 gpu\")\n@pytest.mark.skipif(not torch.cuda.is_available(), reason=\"test requires gpu support\")\ndef test_mixed_precision(tmpdir):\n    input_features = [text_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    trainer = {\n        EPOCHS: 2,\n        \"use_mixed_precision\": True,\n    }\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: trainer,\n    }\n\n    # Just test that training completes without error.\n    # TODO(travis): We may want to expand upon this in the future to include some checks on model\n    # convergence like gradient magnitudes, etc. Should also add distributed tests.\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n\n@pytest.mark.skipif(\n    parse_version(torch.__version__) < parse_version(\"2.0\"), reason=\"Model compilation requires PyTorch >= 2.0\"\n)\ndef test_compile(tmpdir):\n    input_features = [text_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    trainer = {\n        EPOCHS: 2,\n        \"compile\": True,\n    }\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: trainer,\n    }\n\n    # Just test that training completes without error.\n    # TODO(travis): We may want to expand upon this in the future to include some checks on model\n    # convergence like gradient magnitudes, etc. Should also add distributed tests.\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n\n@pytest.mark.parametrize(\"gradient_accumulation_steps\", [1, 2])\ndef test_gradient_accumulation(gradient_accumulation_steps: int, tmpdir):\n    input_features = [text_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename, num_examples=64)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    trainer = {\n        EPOCHS: 2,\n        BATCH_SIZE: 8,\n        \"gradient_accumulation_steps\": gradient_accumulation_steps,\n    }\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: trainer,\n    }\n\n    # Just test that training completes without error.\n    # TODO(travis): We may want to expand upon this in the future to include some checks on model\n    # convergence like gradient magnitudes, etc. Should also add distributed tests.\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n\ndef test_enable_gradient_checkpointing(tmpdir, caplog):\n    \"\"\"Test that gradient checkpointing is enabled when specified in the config and that it does not cause an error\n    when the model does not have support for gradient checkpointing.\"\"\"\n    input_features = [text_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    data_csv = generate_data(input_features, output_features, csv_filename)\n    val_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"validation.csv\"))\n    test_csv = shutil.copyfile(data_csv, os.path.join(tmpdir, \"test.csv\"))\n\n    config = {\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\n            \"train_steps\": 2,\n            BATCH_SIZE: 8,\n            \"enable_gradient_checkpointing\": True,\n        },\n    }\n\n    model = LudwigModel(config, backend=LocalTestBackend(), logging_level=logging.INFO)\n    assert model.config_obj.trainer.enable_gradient_checkpointing\n\n    model.train(training_set=data_csv, validation_set=val_csv, test_set=test_csv, output_directory=tmpdir)\n\n    # Check that the warning is emitted when the model does not support gradient checkpointing\n    # but does not prevent training from starting.\n    assert \"Gradient checkpointing is currently only supported for model_type: llm. Skipping...\" in caplog.text\n"
  },
  {
    "path": "tests/integration_tests/test_triton.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport os\n\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, TRAINER\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.utils.data_utils import load_yaml\nfrom ludwig.utils.inference_utils import to_inference_module_input_from_dataframe\nfrom ludwig.utils.triton_utils import export_triton, get_inference_modules, POSTPROCESSOR, PREDICTOR, PREPROCESSOR\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    vector_feature,\n)\n\n\ndef test_triton_torchscript(csv_filename, tmpdir):\n    # Configure features to be tested:\n    input_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 3}),\n        # TODO: future support\n        # sequence_feature(encoder={\"vocab_size\": 3}),\n        # text_feature(encoder={\"vocab_size\": 3}),\n        # vector_feature(),\n        # timeseries_feature(),\n        # date_feature(),\n        # h3_feature(),\n        # set_feature(encoder={\"vocab_size\": 3}),\n        # bag_feature(encoder={\"vocab_size\": 3}),\n        # image_feature(image_dest_folder),\n        # audio_feature(audio_dest_folder),\n    ]\n    output_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 3}),\n        sequence_feature(decoder={\"vocab_size\": 3}),\n        text_feature(decoder={\"vocab_size\": 3}),\n        set_feature(decoder={\"vocab_size\": 3}),\n        vector_feature(),\n    ]\n    backend = LocalTestBackend()\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, csv_filename)\n\n    # Train Ludwig (Pythonic) model:\n    ludwig_model = LudwigModel(config, backend=backend)\n    ludwig_model.train(\n        dataset=training_data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    # Create graph inference model (Torchscript) from trained Ludwig model.\n    triton_path = os.path.join(tmpdir, \"triton\")\n    model_name = \"test_triton\"\n    model_version = \"1\"\n    df = pd.read_csv(training_data_csv_path)\n    triton_artifacts = export_triton(\n        model=ludwig_model, data_example=df, model_name=model_name, output_path=triton_path, model_version=model_version\n    )\n\n    # Validate that artifact paths exist.\n    assert os.path.isdir(triton_path)\n    assert all(os.path.exists(artifact.path) for artifact in triton_artifacts)\n\n    # Load TorchScript models exported for Triton.\n    triton_preprocessor = triton_predictor = triton_postprocessor = None\n    for artifact in triton_artifacts:\n        if artifact.model_name.endswith(PREPROCESSOR) and artifact.content_type == \"application/octet-stream\":\n            triton_preprocessor = torch.jit.load(artifact.path)\n        if artifact.model_name.endswith(PREDICTOR) and artifact.content_type == \"application/octet-stream\":\n            triton_predictor = torch.jit.load(artifact.path)\n        if artifact.model_name.endswith(POSTPROCESSOR) and artifact.content_type == \"application/octet-stream\":\n            triton_postprocessor = torch.jit.load(artifact.path)\n\n    assert triton_preprocessor is not None\n    assert triton_predictor is not None\n    assert triton_postprocessor is not None\n\n    # Forward data through models.\n    data_to_predict = to_inference_module_input_from_dataframe(df, ludwig_model.config, load_paths=True, device=\"cpu\")\n    triton_preprocessor_output = triton_preprocessor(*data_to_predict.values())\n    triton_predictor_output = triton_predictor(*triton_preprocessor_output)\n    triton_postprocessor_output = triton_postprocessor(*triton_predictor_output)\n\n    # Get TorchScript inference modules and forward data.\n    inference_modules = get_inference_modules(ludwig_model, \"cpu\")\n    preprocessor_output = inference_modules[0](data_to_predict)\n    predictor_output = inference_modules[1](preprocessor_output)\n    postprocessor_output = inference_modules[2](predictor_output)\n\n    assert len(postprocessor_output) == len(\n        triton_postprocessor_output\n    ), \"Number of output mismatch after postprocessor step\"\n\n    for i, (_, out_value) in enumerate(postprocessor_output.items()):\n        both_list = isinstance(out_value, list) and isinstance(triton_postprocessor_output[i], list)\n        both_tensor = isinstance(out_value, torch.Tensor) and isinstance(triton_postprocessor_output[i], torch.Tensor)\n        assert both_list or both_tensor, \"Type mismatch in PREDICTIONS, PROBABILITIES, LOGITS output\"\n\n        if isinstance(out_value, list) and len(out_value) > 0 and isinstance(out_value[0], str):\n            assert out_value == triton_postprocessor_output[i], \"Category feature outputs failure.\"\n        elif isinstance(out_value, list) and len(out_value) > 0 and isinstance(out_value[0], torch.Tensor):\n            assert len(out_value) == len(triton_postprocessor_output[i]), \"Set feature outputs failure.\"\n            assert all(\n                torch.allclose(inf, trit) for inf, trit in zip(out_value, triton_postprocessor_output[i])\n            ), \"Set feature outputs failure.\"\n        elif isinstance(out_value, list) and len(out_value) > 0 and isinstance(out_value[0], list):\n            assert len(out_value) == len(\n                triton_postprocessor_output[i]\n            ), \"Sequence (including text, etc.) feature outputs failure.\"\n            assert all(\n                inf == trit for inf, trit in zip(out_value, triton_postprocessor_output[i])\n            ), \"Sequence (including text, etc.) feature outputs failure.\"\n        elif isinstance(out_value, torch.Tensor):\n            assert torch.allclose(out_value, triton_postprocessor_output[i])\n        else:\n            raise ValueError(\"Value should be either List[str] or torch.Tensor.\")\n\n\ndef get_test_config_filenames() -> list[str]:\n    \"\"\"Return list of the config filenames used for Triton export.\"\"\"\n    configs_directory = \"/\".join(__file__.split(\"/\")[:-1] + [\"test_triton_configs\"])\n    return [os.path.join(configs_directory, config_fp) for config_fp in os.listdir(configs_directory)]\n\n\n@pytest.mark.parametrize(\"config_path\", get_test_config_filenames())\ndef test_triton_exportability(config_path, tmpdir):\n    \"\"\"Tests whether Triton export succeeds for a config.\"\"\"\n    config = load_yaml(config_path)\n    dataset = build_synthetic_dataset_df(100, config)\n    ludwig_model = LudwigModel(config)\n    ludwig_model.train(\n        dataset=dataset,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    triton_path = os.path.join(tmpdir, \"triton\")\n    model_name = \"test_triton\"\n    model_version = \"1\"\n    export_triton(\n        model=ludwig_model,\n        data_example=dataset.head(10),\n        model_name=model_name,\n        output_path=triton_path,\n        model_version=model_version,\n    )\n"
  },
  {
    "path": "tests/integration_tests/test_triton_configs/transformer_combiner_with_attention_reduce.yaml",
    "content": "input_features:\n  - name: founded_on_timestamp\n    type: number\n  - name: first_equity_timestamp\n    type: number\n  - name: founded_first_equity_diff\n    type: number\noutput_features:\n  - name: assigned_label\n    type: number\ncombiner:\n  type: transformer\n  hidden_size: 16\n  output_size: 64\n  num_fc_layers: 0\n  reduce_output: attention\n  transformer_output_size: 56\ntrainer:\n  train_steps: 1\n"
  },
  {
    "path": "tests/integration_tests/test_visualization.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# Integration tests for the visualization commands.\n#\n# Author: Ivaylo Stefanov\n# email: ivaylo.stefanov82@gmail.com\n# github: https://github.com/istefano82\n# ==============================================================================\nimport glob\nimport json\nimport os\nimport random\nimport subprocess\nimport sys\n\nimport numpy as np\nimport pytest\n\nfrom ludwig.constants import BATCH_SIZE, ENCODER, TRAINER, TYPE\nfrom ludwig.experiment import experiment_cli\nfrom ludwig.globals import DESCRIPTION_FILE_NAME, PREDICTIONS_PARQUET_FILE_NAME, TEST_STATISTICS_FILE_NAME\nfrom ludwig.utils.data_utils import get_split_path\nfrom ludwig.visualize import _extract_ground_truth_values\nfrom tests.integration_tests.test_visualization_api import obtain_df_splits\nfrom tests.integration_tests.utils import (\n    bag_feature,\n    binary_feature,\n    category_feature,\n    generate_data,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n)\n\npytestmark = pytest.mark.integration_tests_c\n\n\ndef run_experiment_with_visualization(input_features, output_features, dataset):\n    \"\"\"Helper method to run an experiment with visualization enabled.\n\n    Does not garbage collect.\n    \"\"\"\n    output_directory = os.path.dirname(dataset)\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    args = {\n        \"config\": config,\n        \"skip_save_processed_input\": False,\n        \"skip_save_progress\": False,\n        \"skip_save_unprocessed_output\": False,\n        \"skip_save_eval_stats\": False,\n        \"dataset\": dataset,\n        \"output_directory\": output_directory,\n    }\n\n    _, _, _, _, experiment_dir = experiment_cli(**args)\n\n    return experiment_dir\n\n\ndef get_output_feature_name(experiment_dir, output_feature=0):\n    \"\"\"Helper function to extract specified output feature name.\n\n    :param experiment_dir: Path to the experiment directory\n    :param output_feature: position of the output feature the description.json\n    :return output_feature_name: name of the first output feature name from the experiment\n    \"\"\"\n    description_file = os.path.join(experiment_dir, DESCRIPTION_FILE_NAME)\n    with open(description_file, \"rb\") as f:\n        content = json.load(f)\n    output_feature_name = content[\"config\"][\"output_features\"][output_feature][\"name\"]\n    return output_feature_name\n\n\ndef test_visualization_learning_curves_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\"})]\n    output_features = [category_feature(output_feature=True)]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    train_stats = os.path.join(exp_dir_name, \"training_statistics.json\")\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"learning_curves\",\n        \"--training_statistics\",\n        train_stats,\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(\n            command,\n        )\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 4 == len(figure_cnt)\n\n\ndef test_visualization_confusion_matrix_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\"})]\n    output_features = [category_feature(output_feature=True)]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth_metadata = experiment_source_data_name + \".meta.json\"\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confusion_matrix\",\n        \"--test_statistics\",\n        test_stats,\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 2 == len(figure_cnt)\n\n\ndef test_visualization_compare_performance_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Compare performance between two models. To reduce test complexity one model is compared to it self.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [text_feature(encoder={\"type\": \"parallel_cnn\"})]\n    output_features = [category_feature(output_feature=True)]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_performance\",\n        \"--test_statistics\",\n        test_stats,\n        test_stats,\n        \"-m\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command, capture_output=True)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_from_prob_csv_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Probabilities are loaded from csv file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = get_split_path(csv_filename)\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_from_prob\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_from_prob_npy_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Probabilities are loaded from npy file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_from_prob\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_from_pred_npy_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Predictions are loaded from npy file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    ground_truth_metadata = experiment_source_data_name + \".meta.json\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_from_pred\",\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--predictions\",\n        prediction,\n        prediction,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_from_pred_csv_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Predictions are loaded from csv file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    ground_truth_metadata = experiment_source_data_name + \".meta.json\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_from_pred\",\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--predictions\",\n        prediction,\n        prediction,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_subset_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_subset\",\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--top_n_classes\",\n        \"6\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_changing_k_output_pdf(csv_filename):\n    \"\"\"It should be possible to save figures as pdf in the specified directory.\"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    ground_truth_metadata = exp_dir_name + \"/model/training_set_metadata.json\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_performance_changing_k\",\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--top_n_classes\",\n        \"6\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_multiclass_multimetric_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth_metadata = experiment_source_data_name + \".meta.json\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_multiclass_multimetric\",\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--test_statistics\",\n        test_stats,\n        test_stats,\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 4 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_predictions_npy_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Predictions are loaded form npy file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_predictions\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--predictions\",\n        prediction,\n        prediction,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_compare_classifiers_predictions_csv_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    Predictions are loaded form csv file.\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_predictions\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--predictions\",\n        prediction,\n        prediction,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_cmp_classifiers_predictions_distribution_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    prediction = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"compare_classifiers_predictions_distribution\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--predictions\",\n        prediction,\n        prediction,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_cconfidence_thresholding_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_confidence_thresholding_data_vs_acc_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding_data_vs_acc\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_confidence_thresholding_data_vs_acc_subset_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding_data_vs_acc_subset\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"--top_n_classes\",\n        \"3\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_vis_confidence_thresholding_data_vs_acc_subset_per_class_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 5}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding_data_vs_acc_subset_per_class\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"--top_n_classes\",\n        \"3\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        # 3 figures should be saved because experiment setting top_n_classes = 3\n        # hence one figure per class\n        assert 3 == len(figure_cnt)\n\n\ndef test_vis_confidence_thresholding_2thresholds_2d_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    threshold_output_feature_name1 = get_output_feature_name(exp_dir_name)\n    threshold_output_feature_name2 = get_output_feature_name(exp_dir_name, output_feature=1)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding_2thresholds_2d\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        \"--threshold_output_feature_names\",\n        threshold_output_feature_name1,\n        threshold_output_feature_name2,\n        \"--model_names\",\n        \"Model1\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(\n            command,\n        )\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 3 == len(figure_cnt)\n\n\ndef test_vis_confidence_thresholding_2thresholds_3d_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    threshold_output_feature_name1 = get_output_feature_name(exp_dir_name)\n    threshold_output_feature_name2 = get_output_feature_name(exp_dir_name, output_feature=1)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"confidence_thresholding_2thresholds_3d\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        \"--threshold_output_feature_names\",\n        threshold_output_feature_name1,\n        threshold_output_feature_name2,\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(\n            command,\n        )\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\n@pytest.mark.parametrize(\"binary_output_type\", [True, False])\ndef test_visualization_binary_threshold_vs_metric_output_saved(csv_filename, binary_output_type):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    if binary_output_type:\n        output_features = [binary_feature()]\n    else:\n        output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    random.seed(1919)\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    input_features[0][ENCODER][TYPE] = \"parallel_cnn\"\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"binary_threshold_vs_metric\",\n        \"--positive_label\",\n        \"1\",\n        \"--metrics\",\n        \"accuracy\",\n        \"precision\",\n        \"recall\",\n        \"f1\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 4 == len(figure_cnt)\n\n\n@pytest.mark.parametrize(\"binary_output_type\", [True, False])\ndef test_visualization_precision_recall_curves_output_saved(csv_filename, binary_output_type):\n    \"\"\"Ensure pdf and png figures for precision recall curves from the experiments can be saved.\"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    if binary_output_type:\n        output_features = [binary_feature()]\n    else:\n        output_features = [category_feature(decoder={\"vocab_size\": 3}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"precision_recall_curves\",\n        \"--positive_label\",\n        \"1\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_precision_recall_curves_from_test_statistics_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [binary_feature(), bag_feature()]\n    output_features = [binary_feature()]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename, num_examples=20)\n\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"precision_recall_curves_from_test_statistics\",\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--test_statistics\",\n        test_stats,\n        \"--model_names\",\n        \"Model1\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\n@pytest.mark.parametrize(\"binary_output_type\", [True, False])\ndef test_visualization_roc_curves_output_saved(csv_filename, binary_output_type):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    if binary_output_type:\n        output_features = [binary_feature()]\n    else:\n        output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"roc_curves\",\n        \"--positive_label\",\n        \"1\",\n        \"--metrics\",\n        \"accuracy\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_roc_curves_from_test_statistics_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [binary_feature(), bag_feature()]\n    output_features = [binary_feature()]\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"roc_curves_from_test_statistics\",\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--test_statistics\",\n        test_stats,\n        \"--model_names\",\n        \"Model1\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 1 == len(figure_cnt)\n\n\ndef test_visualization_calibration_1_vs_all_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"calibration_1_vs_all\",\n        \"--metrics\",\n        \"accuracy\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"--top_k\",\n        \"6\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 5 == len(figure_cnt)\n\n\ndef test_visualization_calibration_multiclass_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    probability = os.path.join(exp_dir_name, PREDICTIONS_PARQUET_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"calibration_multiclass\",\n        \"--ground_truth\",\n        ground_truth,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--split_file\",\n        split_file,\n        \"--ground_truth_metadata\",\n        exp_dir_name + \"/model/training_set_metadata.json\",\n        \"--probabilities\",\n        probability,\n        probability,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 2 == len(figure_cnt)\n\n\ndef test_visualization_frequency_vs_f1_output_saved(csv_filename):\n    \"\"\"Ensure pdf and png figures from the experiments can be saved.\n\n    :param csv_filename: csv fixture from tests.conftest.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    vis_output_pattern_pdf = os.path.join(exp_dir_name, \"*.pdf\")\n    vis_output_pattern_png = os.path.join(exp_dir_name, \"*.png\")\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    test_stats = os.path.join(exp_dir_name, TEST_STATISTICS_FILE_NAME)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth_metadata = experiment_source_data_name + \".meta.json\"\n    test_cmd_pdf = [\n        sys.executable,\n        \"-m\",\n        \"ludwig.visualize\",\n        \"--visualization\",\n        \"frequency_vs_f1\",\n        \"--ground_truth_metadata\",\n        ground_truth_metadata,\n        \"--output_feature_name\",\n        output_feature_name,\n        \"--test_statistics\",\n        test_stats,\n        test_stats,\n        \"--model_names\",\n        \"Model1\",\n        \"Model2\",\n        \"-od\",\n        exp_dir_name,\n    ]\n    test_cmd_png = test_cmd_pdf.copy() + [\"-ff\", \"png\"]\n\n    commands = [test_cmd_pdf, test_cmd_png]\n    vis_patterns = [vis_output_pattern_pdf, vis_output_pattern_png]\n\n    for command, viz_pattern in zip(commands, vis_patterns):\n        result = subprocess.run(command)\n        figure_cnt = glob.glob(viz_pattern)\n\n        assert 0 == result.returncode\n        assert 2 == len(figure_cnt)\n\n\ndef test_load_ground_truth_split_from_df(csv_filename):\n    import pandas as pd\n\n    ground_truth = pd.DataFrame(\n        {\n            \"PassengerId\": [1],\n            \"Survived\": [0],\n            \"Pclass\": [3],\n            \"Name\": [\"Braund, Mr. Owen Harris\"],\n            \"Sex\": [\"male\"],\n            \"Age\": [22.0],\n            \"SibSp\": [1],\n            \"Parch\": [0],\n            \"Ticket\": [\"A/5 21171\"],\n            \"Fare\": [\"7.25\"],\n            \"Cabin\": [None],\n            \"Embarked\": [\"S\"],\n            \"split\": [0],\n        }\n    )\n    output_feature = \"Survived\"\n    ground_truth_train_split = _extract_ground_truth_values(ground_truth, output_feature, 0)\n    ground_truth_val_split = _extract_ground_truth_values(ground_truth, output_feature, 1)\n    ground_truth_test_split = _extract_ground_truth_values(ground_truth, output_feature, 2)\n\n    assert ground_truth_train_split.equals(pd.Series([0]))\n    assert ground_truth_val_split.empty\n    assert ground_truth_test_split.empty\n\n\ndef test_load_ground_truth_split_from_file(csv_filename):\n    \"\"\"Ensure correct ground truth split is loaded when ground_truth_split is given.\n\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [category_feature(encoder={\"vocab_size\": 10})]\n    output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n\n    # Generate test data\n    rel_path = generate_data(input_features, output_features, csv_filename)\n    exp_dir_name = run_experiment_with_visualization(input_features, output_features, dataset=rel_path)\n    output_feature_name = get_output_feature_name(exp_dir_name)\n    experiment_source_data_name = csv_filename.split(\".\")[0]\n    ground_truth = experiment_source_data_name + \".csv\"\n    split_file = experiment_source_data_name + \".split.parquet\"\n\n    # retrieve ground truth from source data set\n    ground_truth_train_split = _extract_ground_truth_values(ground_truth, output_feature_name, 0, split_file)\n    ground_truth_val_split = _extract_ground_truth_values(ground_truth, output_feature_name, 1, split_file)\n    ground_truth_test_split = _extract_ground_truth_values(ground_truth, output_feature_name, 2, split_file)\n\n    test_df, train_df, val_df = obtain_df_splits(csv_filename)\n    target_predictions_from_train = train_df[output_feature_name]\n    target_predictions_from_val = val_df[output_feature_name]\n    target_predictions_from_test = test_df[output_feature_name]\n\n    assert np.all(ground_truth_train_split.eq(target_predictions_from_train))\n    assert np.all(ground_truth_val_split.eq(target_predictions_from_val))\n    assert np.all(ground_truth_test_split.eq(target_predictions_from_test))\n"
  },
  {
    "path": "tests/integration_tests/test_visualization_api.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport glob\nimport logging\nimport os\nfrom tempfile import TemporaryDirectory\n\nimport numpy as np\nimport pytest\n\nfrom ludwig import visualize\nfrom ludwig.api import LudwigModel, TrainingStats\nfrom ludwig.constants import BATCH_SIZE, ENCODER, NAME, PREDICTIONS, PROBABILITIES, PROBABILITY, TRAINER, TYPE\nfrom ludwig.data.split import get_splitter\nfrom ludwig.globals import HYPEROPT_STATISTICS_FILE_NAME\nfrom ludwig.utils.data_utils import read_csv\nfrom tests.integration_tests.utils import (\n    bag_feature,\n    binary_feature,\n    category_feature,\n    generate_data,\n    LocalTestBackend,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n)\n\npytestmark = pytest.mark.integration_tests_c\n\n\ndef run_api_experiment(input_features, output_features):\n    \"\"\"Helper method to avoid code repetition in running an experiment.\n\n    :param input_features: input schema\n    :param output_features: output schema\n    :return: None\n    \"\"\"\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config)\n    return model\n\n\n@pytest.fixture(scope=\"module\")\ndef experiment_to_use():\n    with TemporaryDirectory() as tmpdir:\n        experiment = Experiment(\"data_for_test.csv\", tmpdir)\n        return experiment\n\n\nclass Experiment:\n    \"\"\"Helper class to create model test data, setup and run experiment.\n\n    Contain the needed model experiment statistics as class attributes.\n    \"\"\"\n\n    def __init__(self, csv_filename, tmpdir):\n        self.tmpdir = tmpdir\n        self.csv_file = os.path.join(tmpdir, csv_filename)\n        self.input_features = [category_feature(encoder={\"vocab_size\": 10})]\n        self.output_features = [category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\")]\n        data_csv = generate_data(self.input_features, self.output_features, self.csv_file)\n        self.model = self._create_model()\n        test_df, train_df, val_df = obtain_df_splits(data_csv)\n        self.train_stats, self.preprocessed_data, self.output_dir = self.model.train(\n            training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpdir, \"results\")\n        )\n        self.test_stats_full, predictions, self.output_dir = self.model.evaluate(\n            dataset=test_df,\n            collect_overall_stats=True,\n            collect_predictions=True,\n            output_directory=self.output_dir,\n            return_type=\"dict\",\n        )\n        self.output_feature_name = self.output_features[0][NAME]\n        self.ground_truth_metadata = self.preprocessed_data[3]\n        self.ground_truth = test_df[self.output_feature_name]\n        # probabilities need to be list of lists containing each row data\n        # from the probability columns\n        # ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate - Return\n        self.probability = predictions[self.output_feature_name][PROBABILITY]\n        self.probabilities = predictions[self.output_feature_name][PROBABILITIES]\n        self.predictions = predictions[self.output_feature_name][PREDICTIONS]\n\n        # numeric encoded values required for some visualizations\n        of_metadata = self.ground_truth_metadata[self.output_feature_name]\n        self.predictions_num = [of_metadata[\"str2idx\"][x] for x in self.predictions]\n\n    def _create_model(self):\n        \"\"\"Configure and setup test model.\"\"\"\n        config = {\n            \"input_features\": self.input_features,\n            \"output_features\": self.output_features,\n            \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n            TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        }\n        return LudwigModel(config, logging_level=logging.WARN)\n\n\ndef obtain_df_splits(data_csv):\n    \"\"\"Split input data csv file in to train, validation and test dataframes.\n\n    :param data_csv: Input data CSV file. :return test_df, train_df, val_df: Train, validation and test dataframe splits\n    \"\"\"\n    data_df = read_csv(data_csv)\n    # Obtain data split array mapping data rows to split type\n    # 0-train, 1-validation, 2-test\n    splitter = get_splitter(\"random\")\n    train_df, val_df, test_df = splitter.split(data_df, LocalTestBackend())\n    return test_df, train_df, val_df\n\n\n@pytest.mark.parametrize(\"training_only\", [True, False])\ndef test_learning_curves_vis_api(experiment_to_use, training_only):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    viz_outputs = (\"pdf\", \"png\")\n    train_stats = experiment.train_stats\n    if training_only:\n        # ensure plot works with only training metrics\n        # Handle situation in Issue #1875\n        train_stats = TrainingStats(train_stats.training, {}, {})\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.learning_curves(\n                [train_stats], output_feature_name=None, output_directory=tmpvizdir, file_format=viz_output\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 3 == len(figure_cnt)\n\n\ndef test_compare_performance_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    # extract test stats only\n    test_stats = experiment.test_stats_full\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_performance(\n                [test_stats, test_stats],\n                output_feature_name=None,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifier_performance_from_prob_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probability = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_performance_from_prob(\n                [probability, probability],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[0],\n                labels_limit=0,\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifier_performance_from_pred_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    prediction = experiment.predictions\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_performance_from_pred(\n                [prediction, prediction],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifiers_performance_subset_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_performance_subset(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[6],\n                labels_limit=0,\n                subset=\"ground_truth\",\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifiers_performance_changing_k_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_performance_changing_k(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_k=3,\n                labels_limit=0,\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifiers_multiclass_multimetric_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    # extract test stats only\n    test_stats = experiment.test_stats_full\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_multiclass_multimetric(\n                [test_stats, test_stats],\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[6],\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 4 == len(figure_cnt)\n\n\ndef test_compare_classifiers_predictions_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    predictions = experiment.predictions\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_predictions(\n                [predictions, predictions],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_compare_classifiers_predictions_distribution_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    predictions = experiment.predictions_num\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.compare_classifiers_predictions_distribution(\n                [predictions, predictions],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.confidence_thresholding(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_data_vs_acc_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.confidence_thresholding_data_vs_acc(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_data_vs_acc_subset_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.confidence_thresholding_data_vs_acc_subset(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[3],\n                labels_limit=0,\n                subset=\"ground_truth\",\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_data_vs_acc_subset_per_class_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.confidence_thresholding_data_vs_acc_subset_per_class(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[2],\n                labels_limit=0,\n                subset=\"ground_truth\",\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            # 3 figures should be saved because experiment setting top_n_classes = 3\n            # hence one figure per class\n            assert 2 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_2thresholds_2d_vis_api(csv_filename):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    encoder = \"parallel_cnn\"\n    with TemporaryDirectory() as tmpvizdir:\n        # Generate test data\n        data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename))\n        input_features[0][ENCODER][TYPE] = encoder\n        model = run_api_experiment(input_features, output_features)\n        test_df, train_df, val_df = obtain_df_splits(data_csv)\n        _, _, output_dir = model.train(\n            training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpvizdir, \"results\")\n        )\n        test_stats, predictions, _ = model.evaluate(dataset=test_df, collect_predictions=True, output_dir=output_dir)\n\n        output_feature_name1 = output_features[0][\"name\"]\n        output_feature_name2 = output_features[1][\"name\"]\n\n        ground_truth_metadata = model.training_set_metadata\n        feature1_cols = [\n            f\"{output_feature_name1}_probabilities_{label}\"\n            for label in ground_truth_metadata[output_feature_name1][\"idx2str\"]\n        ]\n        feature2_cols = [\n            f\"{output_feature_name2}_probabilities_{label}\"\n            for label in ground_truth_metadata[output_feature_name2][\"idx2str\"]\n        ]\n\n        # probabilities need to be list of lists containing each row data from the\n        # probability columns ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate\n        probability1 = predictions.loc[:, feature1_cols].values\n        probability2 = predictions.loc[:, feature2_cols].values\n\n        target_predictions1 = test_df[output_feature_name1]\n        target_predictions2 = test_df[output_feature_name2]\n        ground_truth1 = np.asarray(\n            [ground_truth_metadata[output_feature_name1][\"str2idx\"][prediction] for prediction in target_predictions1]\n        )\n        ground_truth2 = np.asarray(\n            [ground_truth_metadata[output_feature_name2][\"str2idx\"][prediction] for prediction in target_predictions2]\n        )\n        viz_outputs = (\"pdf\", \"png\")\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = os.path.join(output_dir, \"*.{}\").format(viz_output)\n            visualize.confidence_thresholding_2thresholds_2d(\n                [probability1, probability2],\n                [ground_truth1, ground_truth2],\n                model.training_set_metadata,\n                [output_feature_name1, output_feature_name2],\n                labels_limit=0,\n                model_names=[\"Model1\"],\n                output_directory=output_dir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 3 == len(figure_cnt)\n\n\ndef test_confidence_thresholding_2thresholds_3d_vis_api(csv_filename):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [\n        text_feature(encoder={\"vocab_size\": 10, \"min_len\": 1, \"type\": \"stacked_cnn\"}),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10, \"embedding_size\": 5}),\n        set_feature(),\n        sequence_feature(encoder={\"vocab_size\": 10, \"max_len\": 10, \"type\": \"embed\"}),\n    ]\n    output_features = [\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n        category_feature(decoder={\"vocab_size\": 2}, reduce_input=\"sum\"),\n    ]\n    encoder = \"parallel_cnn\"\n    with TemporaryDirectory() as tmpvizdir:\n        # Generate test data\n        data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename))\n        input_features[0][ENCODER][TYPE] = encoder\n        model = run_api_experiment(input_features, output_features)\n        test_df, train_df, val_df = obtain_df_splits(data_csv)\n        _, _, output_dir = model.train(\n            training_set=train_df, validation_set=val_df, output_directory=os.path.join(tmpvizdir, \"results\")\n        )\n        test_stats, predictions, _ = model.evaluate(\n            dataset=test_df, collect_predictions=True, output_directory=output_dir\n        )\n\n        output_feature_name1 = output_features[0][\"name\"]\n        output_feature_name2 = output_features[1][\"name\"]\n\n        ground_truth_metadata = model.training_set_metadata\n        feature1_cols = [\n            f\"{output_feature_name1}_probabilities_{label}\"\n            for label in ground_truth_metadata[output_feature_name1][\"idx2str\"]\n        ]\n        feature2_cols = [\n            f\"{output_feature_name2}_probabilities_{label}\"\n            for label in ground_truth_metadata[output_feature_name2][\"idx2str\"]\n        ]\n\n        # probabilities need to be list of lists containing each row data from the\n        # probability columns ref: https://ludwig-ai.github.io/ludwig-docs/latest/user_guide/api/LudwigModel#evaluate\n        probability1 = predictions.loc[:, feature1_cols].values\n        probability2 = predictions.loc[:, feature2_cols].values\n\n        target_predictions1 = test_df[output_feature_name1]\n        target_predictions2 = test_df[output_feature_name2]\n        ground_truth1 = np.asarray(\n            [ground_truth_metadata[output_feature_name1][\"str2idx\"][prediction] for prediction in target_predictions1]\n        )\n        ground_truth2 = np.asarray(\n            [ground_truth_metadata[output_feature_name2][\"str2idx\"][prediction] for prediction in target_predictions2]\n        )\n        viz_outputs = (\"pdf\", \"png\")\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = os.path.join(output_dir, f\"*.{viz_output}\")\n            visualize.confidence_thresholding_2thresholds_3d(\n                [probability1, probability2],\n                [ground_truth1, ground_truth2],\n                model.training_set_metadata,\n                [output_feature_name1, output_feature_name2],\n                labels_limit=0,\n                output_directory=output_dir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_binary_threshold_vs_metric_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    metrics = [\"accuracy\"]\n    positive_label = 1\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.binary_threshold_vs_metric(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                metrics,\n                positive_label,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_precision_recall_curves_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    positive_label = 1\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.precision_recall_curves(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                positive_label,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_precision_recall_curves_from_test_statistics_vis_api(csv_filename):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [binary_feature(), bag_feature()]\n    output_features = [binary_feature()]\n\n    with TemporaryDirectory() as tmpvizdir:\n        # Generate test data\n        data_csv = generate_data(\n            input_features, output_features, os.path.join(tmpvizdir, csv_filename), num_examples=20\n        )\n        output_feature_name = output_features[0][\"name\"]\n\n        model = run_api_experiment(input_features, output_features)\n        data_df = read_csv(data_csv)\n        _, _, output_dir = model.train(dataset=data_df, output_directory=os.path.join(tmpvizdir, \"results\"))\n        test_stats, _, _ = model.evaluate(dataset=data_df, collect_overall_stats=True, output_directory=output_dir)\n        viz_outputs = (\"pdf\", \"png\")\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = os.path.join(output_dir, f\"*.{viz_output}\")\n            visualize.precision_recall_curves_from_test_statistics(\n                [test_stats, test_stats],\n                output_feature_name,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=output_dir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_roc_curves_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    positive_label = 1\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.roc_curves(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                positive_label,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_roc_curves_from_test_statistics_vis_api(csv_filename):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param csv_filename: csv fixture from tests.fixtures.filenames.csv_filename\n    :return: None\n    \"\"\"\n    input_features = [binary_feature(), bag_feature()]\n    output_features = [binary_feature()]\n\n    with TemporaryDirectory() as tmpvizdir:\n        # Generate test data\n        data_csv = generate_data(input_features, output_features, os.path.join(tmpvizdir, csv_filename))\n        output_feature_name = output_features[0][\"name\"]\n\n        model = run_api_experiment(input_features, output_features)\n        data_df = read_csv(data_csv)\n        _, _, output_dir = model.train(dataset=data_df, output_directory=os.path.join(tmpvizdir, \"results\"))\n        # extract test metrics\n        test_stats, _, _ = model.evaluate(dataset=data_df, collect_overall_stats=True, output_directory=output_dir)\n        test_stats = test_stats\n        viz_outputs = (\"pdf\", \"png\")\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = os.path.join(output_dir, f\"*.{viz_output}\")\n            visualize.roc_curves_from_test_statistics(\n                [test_stats, test_stats],\n                output_feature_name,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=output_dir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 1 == len(figure_cnt)\n\n\ndef test_calibration_1_vs_all_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = os.path.join(tmpvizdir, f\"*.{viz_output}\")\n            visualize.calibration_1_vs_all(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[6],\n                labels_limit=0,\n                model_namess=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 5 == len(figure_cnt)\n\n\ndef test_calibration_multiclass_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    probabilities = experiment.probabilities\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.calibration_multiclass(\n                [probabilities, probabilities],\n                experiment.ground_truth,\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                labels_limit=0,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 2 == len(figure_cnt)\n\n\ndef test_confusion_matrix_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    # extract test stats only\n    test_stats = experiment.test_stats_full\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.confusion_matrix(\n                [test_stats, test_stats],\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[0],\n                normalize=False,\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 4 == len(figure_cnt)\n\n\ndef test_frequency_vs_f1_vis_api(experiment_to_use):\n    \"\"\"Ensure pdf and png figures can be saved via visualization API call.\n\n    :param experiment_to_use: Object containing trained model and results to test visualization\n    :return: None\n    \"\"\"\n    experiment = experiment_to_use\n    # extract test stats\n    test_stats = experiment.test_stats_full\n    viz_outputs = (\"pdf\", \"png\")\n    with TemporaryDirectory() as tmpvizdir:\n        for viz_output in viz_outputs:\n            vis_output_pattern_pdf = tmpvizdir + f\"/*.{viz_output}\"\n            visualize.frequency_vs_f1(\n                [test_stats, test_stats],\n                experiment.ground_truth_metadata,\n                experiment.output_feature_name,\n                top_n_classes=[0],\n                model_names=[\"Model1\", \"Model2\"],\n                output_directory=tmpvizdir,\n                file_format=viz_output,\n            )\n            figure_cnt = glob.glob(vis_output_pattern_pdf)\n            assert 2 == len(figure_cnt)\n\n\n@pytest.mark.distributed\ndef test_hyperopt_report_vis_api(hyperopt_results_multiple_parameters, tmpdir):\n    vis_dir = os.path.join(tmpdir, \"visualizations\")\n\n    # Ensure visualizations directory is empty before creating plots\n    if os.path.exists(vis_dir):\n        for f in os.listdir(vis_dir):\n            os.remove(os.path.join(vis_dir, f))\n\n    visualize.hyperopt_report(\n        os.path.join(hyperopt_results_multiple_parameters, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir\n    )\n\n    # test for creation of output directory\n    assert os.path.isdir(vis_dir)\n\n    figure_cnt = glob.glob(os.path.join(vis_dir, \"*\"))\n    assert 4 == len(figure_cnt)\n\n\n@pytest.mark.distributed\ndef test_hyperopt_hiplot_vis_api(hyperopt_results_multiple_parameters, tmpdir):\n    vis_dir = os.path.join(tmpdir, \"visualizations\")\n\n    # Ensure visualizations directory is empty before creating plots\n    if os.path.exists(vis_dir):\n        for f in os.listdir(vis_dir):\n            os.remove(os.path.join(vis_dir, f))\n\n    visualize.hyperopt_hiplot(\n        os.path.join(hyperopt_results_multiple_parameters, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir\n    )\n\n    # test for creation of output directory\n    assert os.path.isdir(vis_dir)\n\n    # test for generatated html page\n    assert os.path.isfile(os.path.join(vis_dir, \"hyperopt_hiplot.html\"))\n\n\n@pytest.mark.distributed\ndef test_hyperopt_report_vis_api_no_pairplot(hyperopt_results_single_parameter, tmpdir):\n    vis_dir = os.path.join(tmpdir, \"visualizations\")\n\n    # Ensure visualizations directory is empty before creating plots\n    if os.path.exists(vis_dir):\n        for f in os.listdir(vis_dir):\n            os.remove(os.path.join(vis_dir, f))\n\n    visualize.hyperopt_report(\n        os.path.join(hyperopt_results_single_parameter, HYPEROPT_STATISTICS_FILE_NAME), output_directory=vis_dir\n    )\n\n    figure_cnt = glob.glob(os.path.join(vis_dir, \"*\"))\n\n    # Only create plot for single parameter and skip pairplot creation\n    assert len(figure_cnt) == 1\n"
  },
  {
    "path": "tests/integration_tests/utils.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport contextlib\nimport logging\nimport multiprocessing\nimport os\nimport random\nimport shutil\nimport sys\nimport tempfile\nimport traceback\nimport uuid\n\n\ndef strtobool(val):\n    val = str(val).strip().lower()\n    if val in (\"y\", \"yes\", \"t\", \"true\", \"on\", \"1\"):\n        return 1\n    elif val in (\"n\", \"no\", \"f\", \"false\", \"off\", \"0\"):\n        return 0\n    else:\n        raise ValueError(f\"invalid truth value {val!r}\")\n\n\nfrom typing import Any, TYPE_CHECKING  # noqa: E402\n\nimport cloudpickle  # noqa: E402\nimport numpy as np  # noqa: E402\nimport pandas as pd  # noqa: E402\nimport pytest  # noqa: E402\nimport torch  # noqa: E402\nfrom PIL import Image  # noqa: E402\n\nfrom ludwig.api import LudwigModel  # noqa: E402\nfrom ludwig.backend import LocalBackend  # noqa: E402\nfrom ludwig.constants import (  # noqa: E402\n    AUDIO,\n    BAG,\n    BATCH_SIZE,\n    BINARY,\n    CATEGORY,\n    CATEGORY_DISTRIBUTION,\n    COLUMN,\n    DATE,\n    DECODER,\n    ENCODER,\n    H3,\n    IMAGE,\n    MODEL_ECD,\n    NAME,\n    NUMBER,\n    PROC_COLUMN,\n    SEQUENCE,\n    SET,\n    SPLIT,\n    TEXT,\n    TIMESERIES,\n    TRAINER,\n    VECTOR,\n)\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset, DATETIME_FORMATS  # noqa: E402\nfrom ludwig.experiment import experiment_cli  # noqa: E402\nfrom ludwig.features.feature_utils import compute_feature_hash  # noqa: E402\nfrom ludwig.globals import MODEL_FILE_NAME, PREDICTIONS_PARQUET_FILE_NAME  # noqa: E402\nfrom ludwig.schema.encoders.text_encoders import HFEncoderConfig  # noqa: E402\nfrom ludwig.schema.encoders.utils import get_encoder_classes  # noqa: E402\nfrom ludwig.trainers.trainer import Trainer  # noqa: E402\nfrom ludwig.utils import fs_utils  # noqa: E402\nfrom ludwig.utils.data_utils import read_csv, replace_file_extension, use_credentials  # noqa: E402\n\nif TYPE_CHECKING:\n    from ludwig.data.dataset.base import Dataset\n    from ludwig.schema.model_types.base import ModelConfig\n\nlogger = logging.getLogger(__name__)\n\n# Used in sequence-related unit tests (encoders, features) as well as end-to-end integration tests.\n# Missing: passthrough encoder.\nENCODERS = [\"embed\", \"rnn\", \"parallel_cnn\", \"cnnrnn\", \"stacked_parallel_cnn\", \"stacked_cnn\", \"transformer\"]\nTEXT_ENCODERS = ENCODERS + [\"tf_idf\"]\n\nHF_ENCODERS_SHORT = [\"distilbert\"]\n\nHF_ENCODERS = [name for name, cls in get_encoder_classes(MODEL_ECD, TEXT).items() if issubclass(cls, HFEncoderConfig)]\n\nRAY_BACKEND_CONFIG = {\n    \"type\": \"ray\",\n    \"processor\": {\n        \"parallelism\": 2,\n    },\n    \"trainer\": {\n        \"use_gpu\": False,\n        \"num_workers\": 1,\n        \"resources_per_worker\": {\n            \"CPU\": 0.1,\n            \"GPU\": 0,\n        },\n    },\n}\n\n\nclass LocalTestBackend(LocalBackend):\n    @property\n    def supports_multiprocessing(self):\n        return False\n\n\n# Simulates running training on a separate node from the driver process\nclass FakeRemoteBackend(LocalBackend):\n    def create_trainer(self, **kwargs) -> \"BaseTrainer\":  # noqa: F821\n        return FakeRemoteTrainer(**kwargs)\n\n    @property\n    def supports_multiprocessing(self):\n        return False\n\n\nclass FakeRemoteTrainer(Trainer):\n    def train(self, *args, save_path=MODEL_FILE_NAME, **kwargs):\n        with tempfile.TemporaryDirectory() as tmpdir:\n            return super().train(*args, save_path=tmpdir, **kwargs)\n\n\ndef parse_flag_from_env(key, default=False):\n    try:\n        value = os.environ[key]\n    except KeyError:\n        # KEY isn't set, default to `default`.\n        _value = default\n    else:\n        # KEY is set, convert it to True or False.\n        try:\n            if isinstance(value, bool):\n                return 1 if value else 0\n            _value = strtobool(value)\n        except ValueError:\n            # More values are supported, but let's keep the message simple.\n            raise ValueError(f\"If set, {key} must be yes or no.\")\n    return _value\n\n\n_run_private_tests = parse_flag_from_env(\"RUN_PRIVATE\", default=False)\n\n\nprivate_test = pytest.mark.skipif(\n    not _run_private_tests,\n    reason=\"Skipping: this test is marked private, set RUN_PRIVATE=1 in your environment to run\",\n)\n\n\ndef private_param(param):\n    \"\"\"Wrap param to mark it as private, meaning it requires credentials to run.\n\n    Private tests are skipped by default. Set the RUN_PRIVATE environment variable to a truth value to run them.\n    \"\"\"\n    return pytest.param(\n        *param,\n        marks=pytest.mark.skipif(\n            not _run_private_tests,\n            reason=\"Skipping: this test is marked private, set RUN_PRIVATE=1 in your environment to run\",\n        ),\n    )\n\n\ndef generate_data(\n    input_features,\n    output_features,\n    filename=\"test_csv.csv\",\n    num_examples=25,\n    nan_percent=0.0,\n    with_split=False,\n):\n    \"\"\"Helper method to generate synthetic data based on input, output feature specs.\n\n    :param num_examples: number of examples to generate\n    :param input_features: schema\n    :param output_features: schema\n    :param filename: path to the file where data is stored\n    :param nan_percent: percent of values in a feature to be NaN\n    :param with_split: If True, then new column \"split\" is created, containing integer values as follows:\n        0 -- for training set;\n        1 -- for validation set;\n        2 -- for test set.\n\n    :return:\n    \"\"\"\n    df = generate_data_as_dataframe(input_features, output_features, num_examples, nan_percent, with_split=with_split)\n    df.to_csv(filename, index=False)\n    return filename\n\n\ndef generate_data_as_dataframe(\n    input_features,\n    output_features,\n    num_examples=25,\n    nan_percent=0.0,\n    with_split=False,\n) -> pd.DataFrame:\n    \"\"\"Helper method to generate synthetic data based on input, output feature specs.\n\n    Args:\n        input_features: schema\n        output_features: schema\n        num_examples: number of examples to generate\n        nan_percent: percent of values in a feature to be NaN\n        with_split: If True, then new column \"split\" is created, containing integer values as follows:\n            0 -- for training set;\n            1 -- for validation set;\n            2 -- for test set.\n\n    Returns:\n        A pandas DataFrame\n    \"\"\"\n    features = input_features + output_features\n    df = build_synthetic_dataset(num_examples, features)\n    data = [next(df) for _ in range(num_examples + 1)]\n\n    df = pd.DataFrame(data[1:], columns=data[0])\n\n    # Add \"split\" column to DataFrame\n    if with_split:\n        num_val_examples = max(2, int(num_examples * 0.1))\n        num_test_examples = max(2, int(num_examples * 0.1))\n        num_train_examples = num_examples - num_val_examples - num_test_examples\n        df[\"split\"] = [0] * num_train_examples + [1] * num_val_examples + [2] * num_test_examples\n\n    return df\n\n\ndef recursive_update(dictionary, values):\n    for k, v in values.items():\n        if isinstance(v, dict):\n            dictionary[k] = recursive_update(dictionary.get(k, {}), v)\n        else:\n            dictionary[k] = v\n    return dictionary\n\n\ndef random_string(length=5):\n    return uuid.uuid4().hex[:length].upper()\n\n\ndef number_feature(normalization=None, **kwargs):\n    feature = {\n        \"name\": f\"{NUMBER}_{random_string()}\",\n        \"type\": NUMBER,\n        \"preprocessing\": {\"normalization\": normalization},\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef category_feature(output_feature=False, **kwargs):\n    if DECODER in kwargs:\n        output_feature = True\n    feature = {\n        \"name\": f\"{CATEGORY}_{random_string()}\",\n        \"type\": CATEGORY,\n    }\n    if output_feature:\n        feature.update(\n            {\n                DECODER: {\"type\": \"classifier\", \"vocab_size\": 10},\n            }\n        )\n    else:\n        feature.update(\n            {\n                ENCODER: {\"vocab_size\": 10, \"embedding_size\": 5},\n            }\n        )\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef text_feature(output_feature: bool = False, name: str = None, **kwargs):\n    if DECODER in kwargs:\n        output_feature = True\n    if name is not None:\n        feature_name = name\n    else:\n        feature_name = f\"{TEXT}_{random_string()}\"\n    feature = {\n        \"name\": feature_name,\n        \"type\": TEXT,\n    }\n    if output_feature:\n        feature.update(\n            {\n                DECODER: {\"type\": \"generator\", \"vocab_size\": 5, \"max_len\": 7},\n            }\n        )\n    else:\n        feature.update(\n            {\n                ENCODER: {\n                    \"type\": \"parallel_cnn\",\n                    \"vocab_size\": 5,\n                    \"min_len\": 7,\n                    \"max_len\": 7,\n                    \"embedding_size\": 8,\n                    \"state_size\": 8,\n                },\n            }\n        )\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef set_feature(output_feature=False, **kwargs):\n    if DECODER in kwargs:\n        output_feature = True\n    feature = {\n        \"name\": f\"{SET}_{random_string()}\",\n        \"type\": SET,\n    }\n    if output_feature:\n        feature.update(\n            {\n                DECODER: {\"type\": \"classifier\", \"vocab_size\": 10, \"max_len\": 5},\n            }\n        )\n    else:\n        feature.update(\n            {\n                ENCODER: {\"type\": \"embed\", \"vocab_size\": 10, \"max_len\": 5, \"embedding_size\": 5},\n            }\n        )\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef sequence_feature(output_feature=False, **kwargs):\n    if DECODER in kwargs:\n        output_feature = True\n    feature = {\n        \"name\": f\"{SEQUENCE}_{random_string()}\",\n        \"type\": SEQUENCE,\n    }\n    if output_feature:\n        feature.update(\n            {\n                DECODER: {\n                    \"type\": \"generator\",\n                    \"vocab_size\": 10,\n                    \"max_len\": 7,\n                }\n            }\n        )\n    else:\n        feature.update(\n            {\n                ENCODER: {\n                    \"type\": \"embed\",\n                    \"vocab_size\": 10,\n                    \"max_len\": 7,\n                    \"embedding_size\": 8,\n                    \"output_size\": 8,\n                    \"state_size\": 8,\n                    \"num_filters\": 8,\n                    \"hidden_size\": 8,\n                },\n            }\n        )\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef image_feature(folder, **kwargs):\n    feature = {\n        \"name\": f\"{IMAGE}_{random_string()}\",\n        \"type\": IMAGE,\n        \"preprocessing\": {\"in_memory\": True, \"height\": 12, \"width\": 12, \"num_channels\": 3},\n        ENCODER: {\n            \"type\": \"stacked_cnn\",\n        },\n        \"destination_folder\": folder,\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef audio_feature(folder, **kwargs):\n    feature = {\n        \"name\": f\"{AUDIO}_{random_string()}\",\n        \"type\": AUDIO,\n        \"preprocessing\": {\n            \"type\": \"fbank\",\n            \"window_length_in_s\": 0.04,\n            \"window_shift_in_s\": 0.02,\n            \"num_filter_bands\": 80,\n            \"audio_file_length_limit_in_s\": 3.0,\n        },\n        ENCODER: {\n            \"type\": \"stacked_cnn\",\n            \"should_embed\": False,\n            \"conv_layers\": [\n                {\"filter_size\": 400, \"pool_size\": 16, \"num_filters\": 32},\n                {\"filter_size\": 40, \"pool_size\": 10, \"num_filters\": 64},\n            ],\n            \"output_size\": 16,\n        },\n        \"destination_folder\": folder,\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef timeseries_feature(**kwargs):\n    feature = {\n        \"name\": f\"{TIMESERIES}_{random_string()}\",\n        \"type\": TIMESERIES,\n    }\n\n    output_feature = DECODER in kwargs\n    if output_feature:\n        feature.update(\n            {\n                DECODER: {\"type\": \"projector\"},\n            }\n        )\n    else:\n        feature.update(\n            {\n                ENCODER: {\"type\": \"parallel_cnn\", \"max_len\": 7},\n            }\n        )\n\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef binary_feature(**kwargs):\n    feature = {\n        \"name\": f\"{BINARY}_{random_string()}\",\n        \"type\": BINARY,\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef bag_feature(**kwargs):\n    feature = {\n        \"name\": f\"{BAG}_{random_string()}\",\n        \"type\": BAG,\n        ENCODER: {\"type\": \"embed\", \"max_len\": 5, \"vocab_size\": 10, \"embedding_size\": 5},\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef date_feature(**kwargs):\n    feature = {\n        \"name\": f\"{DATE}_{random_string()}\",\n        \"type\": DATE,\n        \"preprocessing\": {\n            \"datetime_format\": random.choice(list(DATETIME_FORMATS.keys())),\n        },\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef h3_feature(**kwargs):\n    feature = {\n        \"name\": f\"{H3}_{random_string()}\",\n        \"type\": H3,\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef vector_feature(**kwargs):\n    feature = {\n        \"name\": f\"{VECTOR}_{random_string()}\",\n        \"type\": VECTOR,\n        \"preprocessing\": {\n            \"vector_size\": 5,\n        },\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef category_distribution_feature(**kwargs):\n    feature = {\n        \"name\": f\"{CATEGORY_DISTRIBUTION}_{random_string()}\",\n        \"type\": CATEGORY_DISTRIBUTION,\n        \"preprocessing\": {\n            \"vocab\": [\"a\", \"b\", \"c\"],\n        },\n        DECODER: {\"type\": \"classifier\"},\n    }\n    recursive_update(feature, kwargs)\n    feature[COLUMN] = feature[NAME]\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef run_experiment(\n    input_features=None, output_features=None, config=None, skip_save_processed_input=True, backend=None, **kwargs\n):\n    \"\"\"Helper method to avoid code repetition in running an experiment. Deletes the data saved to disk related to\n    running an experiment.\n\n    :param input_features: list of input feature dictionaries\n    :param output_features: list of output feature dictionaries\n    :param config: A dictionary containing the Ludwig model configuration\n    :param skip_save_processed_input: (bool, default: `False`) if input\n    dataset is provided it is preprocessed and cached by saving an HDF5\n    and JSON files to avoid running the preprocessing again. If this\n    parameter is `False`, the HDF5 and JSON file are not saved.\n    :param backend: (Union[Backend, str]) `Backend` or string name\n    **kwargs you may also pass extra parameters to the experiment as keyword\n    arguments\n    :return: None\n    \"\"\"\n    if input_features is None and output_features is None and config is None:\n        raise ValueError(\"Cannot run test experiment without features nor config.\")\n\n    if config is None:\n        config = {\n            \"input_features\": input_features,\n            \"output_features\": output_features,\n            \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n            TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n        }\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        args = {\n            \"config\": config,\n            \"backend\": backend or LocalTestBackend(),\n            \"skip_save_training_description\": True,\n            \"skip_save_training_statistics\": True,\n            \"skip_save_processed_input\": skip_save_processed_input,\n            \"skip_save_progress\": True,\n            \"skip_save_unprocessed_output\": True,\n            \"skip_save_model\": True,\n            \"skip_save_predictions\": True,\n            \"skip_save_eval_stats\": True,\n            \"skip_collect_predictions\": True,\n            \"skip_collect_overall_stats\": True,\n            \"skip_save_log\": True,\n            \"output_directory\": tmpdir,\n        }\n        args.update(kwargs)\n\n        return experiment_cli(**args)\n\n\ndef generate_output_features_with_dependencies(main_feature, dependencies):\n    \"\"\"Generates multiple output features specifications with dependencies.\n\n    Example usage:\n        generate_output_features_with_dependencies('sequence_feature', ['category_feature', 'number_feature'])\n\n    Args:\n        main_feature: feature identifier, valid values 'category_feature', 'sequence_feature', 'number_feature'\n        dependencies: list of dependencies for 'main_feature', do not li\n    \"\"\"\n\n    output_features = [\n        category_feature(decoder={\"type\": \"classifier\", \"vocab_size\": 2}, reduce_input=\"sum\", output_feature=True),\n        sequence_feature(decoder={\"type\": \"generator\", \"vocab_size\": 10, \"max_len\": 5}, output_feature=True),\n        number_feature(),\n    ]\n\n    # value portion of dictionary is a tuple: (position, feature_name)\n    #   position: location of output feature in the above output_features list\n    #   feature_name: Ludwig generated feature name\n    feature_names = {\n        \"category_feature\": (0, output_features[0][\"name\"]),\n        \"sequence_feature\": (1, output_features[1][\"name\"]),\n        \"number_feature\": (2, output_features[2][\"name\"]),\n    }\n\n    # generate list of dependencies with real feature names\n    generated_dependencies = [feature_names[feat_name][1] for feat_name in dependencies]\n\n    # specify dependencies for the main_feature\n    output_features[feature_names[main_feature][0]][\"dependencies\"] = generated_dependencies\n\n    return output_features\n\n\ndef generate_output_features_with_dependencies_complex():\n    \"\"\"Generates multiple output features specifications with dependencies.\"\"\"\n\n    tf = text_feature(decoder={\"vocab_size\": 4, \"max_len\": 5, \"type\": \"generator\"})\n    sf = sequence_feature(decoder={\"vocab_size\": 4, \"max_len\": 5, \"type\": \"generator\"}, dependencies=[tf[\"name\"]])\n    nf = number_feature(dependencies=[tf[\"name\"]])\n    vf = vector_feature(dependencies=[sf[\"name\"], nf[\"name\"]])\n    set_f = set_feature(decoder={\"type\": \"classifier\", \"vocab_size\": 4}, dependencies=[tf[\"name\"], vf[\"name\"]])\n    cf = category_feature(\n        decoder={\"type\": \"classifier\", \"vocab_size\": 4}, dependencies=[sf[\"name\"], nf[\"name\"], set_f[\"name\"]]\n    )\n\n    # The correct order ids[tf, sf, nf, vf, set_f, cf]\n    # shuffling it to test the robustness of the topological sort\n    output_features = [nf, tf, set_f, vf, cf, sf]\n\n    return output_features\n\n\ndef _subproc_wrapper(fn, queue, *args, **kwargs):\n    fn = cloudpickle.loads(fn)\n    try:\n        results = fn(*args, **kwargs)\n    except Exception as e:\n        traceback.print_exc(file=sys.stderr)\n        results = e\n    queue.put(results)\n\n\ndef spawn(fn):\n    def wrapped_fn(*args, **kwargs):\n        ctx = multiprocessing.get_context(\"spawn\")\n        queue = ctx.Queue()\n\n        p = ctx.Process(target=_subproc_wrapper, args=(cloudpickle.dumps(fn), queue, *args), kwargs=kwargs)\n\n        p.start()\n        p.join()\n        results = queue.get()\n        if isinstance(results, Exception):\n            raise RuntimeError(\n                f\"Spawned subprocess raised {type(results).__name__}, \" f\"check log output above for stack trace.\"\n            )\n        return results\n\n    return wrapped_fn\n\n\ndef get_weights(model: torch.nn.Module) -> list[torch.Tensor]:\n    return [param.data for param in model.parameters()]\n\n\ndef has_no_grad(\n    val: np.ndarray | torch.Tensor | str | list,\n):\n    \"\"\"Checks if two values are close to each other.\"\"\"\n    if isinstance(val, list):\n        return all(has_no_grad(v) for v in val)\n    if isinstance(val, torch.Tensor):\n        return not val.requires_grad\n    return True\n\n\ndef is_all_close(\n    val1: np.ndarray | torch.Tensor | str | list,\n    val2: np.ndarray | torch.Tensor | str | list,\n    tolerance=1e-4,\n):\n    \"\"\"Checks if two values are close to each other.\"\"\"\n    if isinstance(val1, list):\n        return all(is_all_close(v1, v2, tolerance) for v1, v2 in zip(val1, val2))\n    if isinstance(val1, str):\n        return val1 == val2\n    if isinstance(val1, torch.Tensor):\n        val1 = val1.cpu().detach().numpy()\n    if isinstance(val2, torch.Tensor):\n        val2 = val2.cpu().detach().numpy()\n    return val1.shape == val2.shape and np.allclose(val1, val2, atol=tolerance)\n\n\ndef is_all_tensors_cuda(val: np.ndarray | torch.Tensor | str | list) -> bool:\n    if isinstance(val, list):\n        return all(is_all_tensors_cuda(v) for v in val)\n\n    if isinstance(val, torch.Tensor):\n        return val.is_cuda\n    return True\n\n\ndef run_api_experiment(input_features, output_features, data_csv):\n    \"\"\"Helper method to avoid code repetition in running an experiment.\n\n    :param input_features: input schema\n    :param output_features: output schema\n    :param data_csv: path to data\n    :return: None\n    \"\"\"\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config)\n    output_dir = None\n\n    try:\n        # Training with csv\n        _, _, output_dir = model.train(\n            dataset=data_csv, skip_save_processed_input=True, skip_save_progress=True, skip_save_unprocessed_output=True\n        )\n        model.predict(dataset=data_csv)\n\n        model_dir = os.path.join(output_dir, MODEL_FILE_NAME)\n        loaded_model = LudwigModel.load(model_dir)\n\n        # Necessary before call to get_weights() to materialize the weights\n        loaded_model.predict(dataset=data_csv)\n\n        model_weights = get_weights(model.model)\n        loaded_weights = get_weights(loaded_model.model)\n        for model_weight, loaded_weight in zip(model_weights, loaded_weights):\n            assert torch.allclose(model_weight, loaded_weight)\n    finally:\n        # Remove results/intermediate data saved to disk\n        shutil.rmtree(output_dir, ignore_errors=True)\n\n    try:\n        # Training with dataframe\n        data_df = read_csv(data_csv)\n        _, _, output_dir = model.train(\n            dataset=data_df, skip_save_processed_input=True, skip_save_progress=True, skip_save_unprocessed_output=True\n        )\n        model.predict(dataset=data_df)\n    finally:\n        shutil.rmtree(output_dir, ignore_errors=True)\n\n\ndef add_nans_to_df_in_place(df: pd.DataFrame, nan_percent: float):\n    \"\"\"Adds nans to a pandas dataframe in-place.\"\"\"\n    if nan_percent == 0:\n        # No-op if nan_percent is 0\n        return None\n    if nan_percent < 0 or nan_percent > 1:\n        raise ValueError(\"nan_percent must be between 0 and 1\")\n\n    num_rows = len(df)\n    num_nans_per_col = int(round(nan_percent * num_rows))\n    for col in df.columns:\n        if col == SPLIT:  # do not add NaNs to the split column\n            continue\n        col_idx = df.columns.get_loc(col)\n        for row_idx in random.sample(range(num_rows), num_nans_per_col):\n            df.iloc[row_idx, col_idx] = np.nan\n    return None\n\n\ndef read_csv_with_nan(path, nan_percent=0.0):\n    \"\"\"Converts `nan_percent` of samples in each row of the CSV at `path` to NaNs.\"\"\"\n    df = pd.read_csv(path)\n    add_nans_to_df_in_place(df, nan_percent)\n    return df\n\n\ndef create_data_set_to_use(data_format, raw_data, nan_percent=0.0):\n    # helper function for generating training and test data with specified format\n    # handles all data formats except for hdf5\n    # assumes raw_data is a csv dataset generated by\n    # tests.integration_tests.utils.generate_data() function\n\n    # support for writing to a fwf dataset based on this stackoverflow posting:\n    # https://stackoverflow.com/questions/16490261/python-pandas-write-dataframe-to-fixed-width-file-to-fwf\n    from tabulate import tabulate\n\n    def to_fwf(df: pd.DataFrame, fname: str):\n        content = tabulate(df.values.tolist(), list(df.columns), tablefmt=\"plain\")\n        open(fname, \"w\").write(content)\n\n    pd.DataFrame.to_fwf = to_fwf\n\n    dataset_to_use = None\n\n    if data_format == \"csv\":\n        # Replace the original CSV with a CSV with NaNs\n        dataset_to_use = raw_data\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_csv(dataset_to_use, index=False)\n\n    elif data_format in {\"df\", \"dict\"}:\n        dataset_to_use = read_csv_with_nan(raw_data, nan_percent=nan_percent)\n        if data_format == \"dict\":\n            dataset_to_use = dataset_to_use.to_dict(orient=\"list\")\n\n    elif data_format == \"excel\":\n        dataset_to_use = replace_file_extension(raw_data, \"xlsx\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_excel(dataset_to_use, index=False)\n\n    elif data_format == \"excel_xls\":\n        dataset_to_use = replace_file_extension(raw_data, \"xls\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_excel(dataset_to_use, index=False)\n\n    elif data_format == \"feather\":\n        dataset_to_use = replace_file_extension(raw_data, \"feather\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_feather(dataset_to_use)\n\n    elif data_format == \"fwf\":\n        dataset_to_use = replace_file_extension(raw_data, \"fwf\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_fwf(dataset_to_use)\n\n    elif data_format == \"html\":\n        dataset_to_use = replace_file_extension(raw_data, \"html\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_html(dataset_to_use, index=False)\n\n    elif data_format == \"json\":\n        dataset_to_use = replace_file_extension(raw_data, \"json\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_json(dataset_to_use, orient=\"records\")\n\n    elif data_format == \"jsonl\":\n        dataset_to_use = replace_file_extension(raw_data, \"jsonl\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_json(dataset_to_use, orient=\"records\", lines=True)\n\n    elif data_format == \"parquet\":\n        dataset_to_use = replace_file_extension(raw_data, \"parquet\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_parquet(dataset_to_use, index=False)\n\n    elif data_format == \"pickle\":\n        dataset_to_use = replace_file_extension(raw_data, \"pickle\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_pickle(dataset_to_use)\n\n    elif data_format == \"stata\":\n        dataset_to_use = replace_file_extension(raw_data, \"stata\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_stata(dataset_to_use)\n\n    elif data_format == \"tsv\":\n        dataset_to_use = replace_file_extension(raw_data, \"tsv\")\n        read_csv_with_nan(raw_data, nan_percent=nan_percent).to_csv(dataset_to_use, sep=\"\\t\", index=False)\n\n    elif data_format == \"pandas+numpy_images\":\n        df = read_csv_with_nan(raw_data, nan_percent=nan_percent)\n        processed_df_rows = []\n        for _, row in df.iterrows():\n            processed_df_row = {}\n            for feature_name, raw_feature in row.items():\n                if \"image\" in feature_name and not (isinstance(raw_feature, float) and np.isnan(raw_feature)):\n                    feature = np.array(Image.open(raw_feature))\n                else:\n                    feature = raw_feature\n                processed_df_row[feature_name] = feature\n            processed_df_rows.append(processed_df_row)\n        dataset_to_use = pd.DataFrame(processed_df_rows)\n\n    else:\n        ValueError(f\"'{data_format}' is an unrecognized data format\")\n\n    return dataset_to_use\n\n\ndef augment_dataset_with_none(\n    df: pd.DataFrame, first_row_none: bool = False, last_row_none: bool = False, nan_cols: list | None = None\n) -> pd.DataFrame:\n    \"\"\"Optionally sets the first and last rows of nan_cols of the given dataframe to nan.\n\n    :param df: dataframe containg input features/output features\n    :type df: pd.DataFrame\n    :param first_row_none: indicates whether to set the first rowin the dataframe to np.nan\n    :type first_row_none: bool\n    :param last_row_none: indicates whether to set the last row in the dataframe to np.nan\n    :type last_row_none: bool\n    :param nan_cols: a list of columns in the dataframe to explicitly set the first or last rows to np.nan\n    :type nan_cols: list\n    \"\"\"\n    nan_cols = nan_cols if nan_cols is not None else []\n\n    if first_row_none:\n        for col in nan_cols:\n            df.iloc[0, df.columns.get_loc(col)] = np.nan\n    if last_row_none:\n        for col in nan_cols:\n            df.iloc[-1, df.columns.get_loc(col)] = np.nan\n    return df\n\n\ndef train_with_backend(\n    backend,\n    config,\n    dataset=None,\n    training_set=None,\n    validation_set=None,\n    test_set=None,\n    predict=True,\n    evaluate=True,\n    callbacks=None,\n    skip_save_processed_input=True,\n    skip_save_predictions=True,\n    required_metrics=None,\n):\n    model = LudwigModel(config, backend=backend, callbacks=callbacks)\n    with tempfile.TemporaryDirectory() as output_directory:\n        _, _, _ = model.train(\n            dataset=dataset,\n            training_set=training_set,\n            validation_set=validation_set,\n            test_set=test_set,\n            skip_save_processed_input=skip_save_processed_input,\n            skip_save_progress=True,\n            skip_save_unprocessed_output=True,\n            skip_save_log=True,\n            output_directory=output_directory,\n        )\n\n        if dataset is None:\n            dataset = training_set\n\n        if predict:\n            preds, _ = model.predict(\n                dataset=dataset, skip_save_predictions=skip_save_predictions, output_directory=output_directory\n            )\n            assert preds is not None\n\n            if not skip_save_predictions:\n                read_preds = model.backend.df_engine.read_predictions(\n                    os.path.join(output_directory, PREDICTIONS_PARQUET_FILE_NAME)\n                )\n                # call compute to ensure preds materialize correctly\n                read_preds = read_preds.compute()\n                assert read_preds is not None\n\n        if evaluate:\n            eval_stats, eval_preds, _ = model.evaluate(\n                dataset=dataset, collect_overall_stats=False, collect_predictions=True\n            )\n            assert eval_preds is not None\n            assert_all_required_metrics_exist(eval_stats, required_metrics)\n\n            # Test that eval_stats are approx equal when using local backend\n            with tempfile.TemporaryDirectory() as tmpdir:\n                model.save(tmpdir)\n                local_model = LudwigModel.load(tmpdir, backend=LocalTestBackend())\n                local_eval_stats, _, _ = local_model.evaluate(\n                    dataset=dataset, collect_overall_stats=False, collect_predictions=False\n                )\n\n                # Filter out metrics that are not being aggregated correctly for now\n                # TODO(travis): https://github.com/ludwig-ai/ludwig/issues/1956\n                # Filter out next_token_perplexity since it is only relevant for LLMs\n                def filter(stats):\n                    return {\n                        k: {\n                            metric_name: value\n                            for metric_name, value in v.items()\n                            if metric_name\n                            not in {\n                                \"loss\",\n                                \"root_mean_squared_percentage_error\",\n                                \"jaccard\",\n                                \"token_accuracy\",\n                                \"next_token_perplexity\",\n                            }\n                        }\n                        for k, v in stats.items()\n                    }\n\n                for (feature_name_from_eval, metrics_dict_from_eval), (\n                    feature_name_from_local,\n                    metrics_dict_from_local,\n                ) in zip(filter(eval_stats).items(), filter(local_eval_stats).items()):\n                    for (metric_name_from_eval, metric_value_from_eval), (\n                        metric_name_from_local,\n                        metric_value_from_local,\n                    ) in zip(metrics_dict_from_eval.items(), metrics_dict_from_local.items()):\n                        assert metric_name_from_eval == metric_name_from_local, (\n                            f\"Metric mismatch between eval and local. Metrics from eval: \"\n                            f\"{metrics_dict_from_eval.keys()}. Metrics from local: {metrics_dict_from_local.keys()}\"\n                        )\n                        if (\n                            metric_value_from_eval == metric_value_from_eval\n                            and feature_name_from_eval == feature_name_from_eval\n                        ):\n                            # Check for equality if the values are non-nans.\n                            assert np.isclose(\n                                metric_value_from_eval, metric_value_from_local, rtol=1e-03, atol=1e-04\n                            ), (\n                                f\"Metric {metric_name_from_eval} for feature {feature_name_from_eval}: \"\n                                f\"{metric_value_from_eval} != {metric_value_from_local}\"\n                            )\n\n        return model\n\n\ndef assert_all_required_metrics_exist(\n    feature_to_metrics_dict: dict[str, dict[str, Any]], required_metrics: dict[str, set] | None = None\n):\n    \"\"\"Checks that all `required_metrics` exist in the dictionary returned during Ludwig model evaluation.\n\n    `feature_to_metrics_dict` is a dict where the feature name is a key and the value is a dictionary of metrics:\n\n        {\n            \"binary_1234\": {\n                \"accuracy\": 0.5,\n                \"loss\": 0.5,\n            },\n            \"numerical_1234\": {\n                \"mean_squared_error\": 0.5,\n                \"loss\": 0.5,\n            }\n        }\n\n    `required_metrics` is a dict where the feature name is a key and the value is a set of metric names:\n\n        {\n            \"binary_1234\": {\"accuracy\"},\n            \"numerical_1234\": {\"mean_squared_error\"},\n        }\n\n    Args:\n        feature_to_metrics_dict: dictionary of output feature to a dictionary of metrics\n        required_metrics: optional dictionary of output feature to a set of metrics names. If None, then function\n            returns True immediately.\n    Returns:\n        None. Raises an AssertionError if any required metrics are missing.\n    \"\"\"\n    if required_metrics is None:\n        return\n\n    for feature_name, metrics_dict in feature_to_metrics_dict.items():\n        if feature_name in required_metrics:\n            required_metric_names = set(required_metrics[feature_name])\n            metric_names = set(metrics_dict.keys())\n            assert required_metric_names.issubset(\n                metric_names\n            ), f\"required metrics {required_metric_names} not in metrics {metric_names} for feature {feature_name}\"\n\n\ndef assert_preprocessed_dataset_shape_and_dtype_for_feature(\n    feature_name: str,\n    preprocessed_dataset: \"Dataset\",\n    config_obj: \"ModelConfig\",\n    expected_dtype: np.dtype,\n    expected_shape: tuple,\n):\n    \"\"\"Asserts that the preprocessed dataset has the correct shape and dtype for a given feature type.\n\n    Args:\n        feature_name: the name of the feature to check\n        preprocessed_dataset: the preprocessed dataset\n        config_obj: the model config object\n        expected_dtype: the expected dtype\n        expected_shape: the expected shape\n    Returns:\n        None.\n    Raises:\n        AssertionError if the preprocessed dataset does not have the correct shape and dtype for the given feature type.\n    \"\"\"\n    if_configs = [if_config for if_config in config_obj.input_features if if_config.name == feature_name]\n    # fail fast if given `feature_name`` is not found or is not unique\n    if len(if_configs) != 1:\n        raise ValueError(f\"feature_name {feature_name} found {len(if_configs)} times in config_obj\")\n    if_config = if_configs[0]\n\n    if_config_proc_column = if_config.proc_column\n    for result in [\n        preprocessed_dataset.training_set,\n        preprocessed_dataset.validation_set,\n        preprocessed_dataset.test_set,\n    ]:\n        result_df = result.to_df()\n        result_df_proc_col = result_df[if_config_proc_column]\n\n        # Check that the proc col is of the correct dtype\n        result_df_proc_col_dtypes = set(result_df_proc_col.map(lambda x: x.dtype))\n        assert all(\n            [expected_dtype == dtype for dtype in result_df_proc_col_dtypes]\n        ), f\"proc dtype should be {expected_dtype}, got the following set of values: {result_df_proc_col_dtypes}\"\n\n        # Check that the proc col is of the right dimensions\n        result_df_proc_col_shapes = set(result_df_proc_col.map(lambda x: x.shape))\n        assert all(\n            expected_shape == shape for shape in result_df_proc_col_shapes\n        ), f\"proc shape should be {expected_shape}, got the following set of values: {result_df_proc_col_shapes}\"\n\n\n@contextlib.contextmanager\ndef remote_tmpdir(fs_protocol, bucket):\n    if bucket is None:\n        with tempfile.TemporaryDirectory() as tmpdir:\n            yield f\"{fs_protocol}://{tmpdir}\"\n        return\n\n    prefix = f\"tmp_{uuid.uuid4().hex}\"\n    tmpdir = f\"{fs_protocol}://{bucket}/{prefix}\"\n    try:\n        with use_credentials(minio_test_creds()):\n            fs_utils.makedirs(f\"{fs_protocol}://{bucket}\", exist_ok=True)\n        yield tmpdir\n    finally:\n        try:\n            with use_credentials(minio_test_creds()):\n                fs_utils.delete(tmpdir, recursive=True)\n        except Exception as e:\n            logger.info(f\"failed to delete remote tempdir: {str(e)}\")\n\n\ndef minio_test_creds():\n    return {\n        \"s3\": {\n            \"client_kwargs\": {\n                \"endpoint_url\": os.environ.get(\"LUDWIG_MINIO_ENDPOINT\", \"http://localhost:9000\"),\n                \"aws_access_key_id\": os.environ.get(\"LUDWIG_MINIO_ACCESS_KEY\", \"minio\"),\n                \"aws_secret_access_key\": os.environ.get(\"LUDWIG_MINIO_SECRET_KEY\", \"minio123\"),\n            }\n        }\n    }\n\n\ndef clear_huggingface_cache():\n    cache_path = os.environ.get(\"TRANSFORMERS_CACHE\")\n\n    if cache_path is None:\n        try:\n            from huggingface_hub.constants import HF_HUB_CACHE\n\n            cache_path = HF_HUB_CACHE.rstrip(\"/\")\n        except ImportError:\n            cache_path = os.path.expanduser(\"~/.cache/huggingface\")\n        while not cache_path.endswith(\"huggingface\") and cache_path:\n            cache_path = \"/\".join(cache_path.split(\"/\")[:-1])\n\n    du = shutil.disk_usage(cache_path)\n\n    logger.info(f\"Current disk usage {du} ({100 * du.free / du.total}% usage)\")\n\n    # only clean up cache if less than 25% of disk space is used.\n    if du.free / du.total > 0.25:\n        return\n\n    logger.info(\n        f\"Clearing HuggingFace cache under path: `{cache_path}`. \"\n        f\"Free disk space is {100 * du.free / du.total}% of total disk space.\"\n    )\n    for root, dirs, files in os.walk(cache_path):\n        for f in files:\n            os.unlink(os.path.join(root, f))\n        for d in dirs:\n            shutil.rmtree(os.path.join(root, d))\n\n\ndef run_test_suite(config, dataset, backend):\n    with tempfile.TemporaryDirectory() as tmpdir:\n        model = LudwigModel(config, backend=backend)\n        _, _, output_dir = model.train(dataset=dataset, output_directory=tmpdir)\n\n        model_dir = os.path.join(output_dir, MODEL_FILE_NAME)\n        loaded_model = LudwigModel.load(model_dir, backend=backend)\n        loaded_model.predict(dataset=dataset)\n        return loaded_model\n"
  },
  {
    "path": "tests/ludwig/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ludwig/accounting/test_used_tokens.py",
    "content": "import torch\n\nfrom ludwig.accounting.used_tokens import get_used_tokens_for_ecd, get_used_tokens_for_llm\n\n\ndef test_get_used_tokens_for_ecd():\n    inputs = {\"input1\": torch.tensor([[1, 2], [3, 4]]), \"input2\": torch.tensor([5, 6])}\n    targets = {\"output\": torch.tensor([7, 8, 9])}\n\n    assert get_used_tokens_for_ecd(inputs, targets) == 9\n\n\ndef test_get_used_tokens_for_ecd_no_targets():\n    inputs = {\"input1\": torch.tensor([[1, 2], [3, 4]]), \"input2\": torch.tensor([5, 6])}\n    targets = None\n\n    assert get_used_tokens_for_ecd(inputs, targets) == 6\n\n\ndef test_get_used_tokens_for_llm():\n    class MockTokenizer:\n        pad_token_id = 0\n\n    tokenizer = MockTokenizer()\n    model_inputs = torch.tensor([1, 2, 3, 0, 0])\n\n    assert get_used_tokens_for_llm(model_inputs, tokenizer) == 3\n"
  },
  {
    "path": "tests/ludwig/augmentation/test_augmentation_pipeline.py",
    "content": "import copy\nimport logging\nimport os\nimport tempfile\n\nimport pytest\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import IMAGENET1K\nfrom ludwig.data.dataset_synthesizer import cli_synthesize_dataset\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.features.image_feature import ImageAugmentation\nfrom ludwig.schema.features.image_feature import ImageInputFeatureConfig\n\n\n# define fixture for test  image augmentation\n@pytest.fixture(scope=\"module\")\ndef test_image():\n    # return random normal batch of images of size 2x3x32x32 [batch_size, channels, height, width]\n    return torch.randn(2, 3, 32, 32)\n\n\n# create training data for model training with augmentation pipeline\n@pytest.fixture(scope=\"module\")\ndef train_data_rgb():\n    with tempfile.TemporaryDirectory() as tmp_dir:\n        # setup basic data description for training\n        output_features = [\n            {\n                \"name\": \"binary_output_feature\",\n                \"type\": \"binary\",\n            },\n        ]\n        input_features = [\n            {\n                \"name\": \"my_image\",\n                \"type\": \"image\",\n            },\n        ]\n\n        # add parameters to generate images\n        input_features[0].update(\n            {\n                \"destination_folder\": os.path.join(tmp_dir, \"images\"),\n                \"preprocessing\": {\"height\": 350, \"width\": 350, \"num_channels\": 3},\n            }\n        )\n        feature_list = input_features + output_features\n\n        # create synthetic data\n        data_dir = os.path.join(tmp_dir, \"data\")\n        os.makedirs(data_dir, exist_ok=True)\n        train_fp = os.path.join(tmp_dir, \"train.csv\")\n\n        cli_synthesize_dataset(16, feature_list, train_fp)\n\n        # remove unneeded data generation parameters\n        input_features[0].pop(\"destination_folder\")\n\n        # return training data for testing\n        yield train_fp, input_features, output_features\n\n\n# create training data for model training with augmentation pipeline\n@pytest.fixture(scope=\"module\")\ndef train_data_gray_scale():\n    with tempfile.TemporaryDirectory() as tmp_dir:\n        # setup basic data description for training\n        output_features = [\n            {\n                \"name\": \"binary_output_feature\",\n                \"type\": \"binary\",\n            },\n        ]\n        input_features = [\n            {\n                \"name\": \"my_image\",\n                \"type\": \"image\",\n            },\n        ]\n\n        # add parameters to generate images\n        input_features[0].update(\n            {\n                \"destination_folder\": os.path.join(tmp_dir, \"images\"),\n                \"preprocessing\": {\"height\": 350, \"width\": 350, \"num_channels\": 1},\n            }\n        )\n        feature_list = input_features + output_features\n\n        # create synthetic data\n        data_dir = os.path.join(tmp_dir, \"data\")\n        os.makedirs(data_dir, exist_ok=True)\n        train_fp = os.path.join(tmp_dir, \"train.csv\")\n\n        cli_synthesize_dataset(16, feature_list, train_fp)\n\n        # remove unneeded data generation parameters\n        input_features[0].pop(\"destination_folder\")\n\n        # return training data for testing\n        yield train_fp, input_features, output_features\n\n\n# common function to run model training with augmentation pipeline\ndef run_augmentation_training(\n    train_data: str = \"\",\n    backend: str = \"local\",\n    encoder: dict = None,\n    preprocessing: dict = None,\n    augmentation_pipeline_ops: list[dict] = None,\n):\n    # unpack training data\n    train_fp, input_features, output_features = train_data\n\n    # add encoder and preprocessing specification to input feature\n    input_features[0].update(\n        {\n            \"encoder\": encoder,\n            \"preprocessing\": preprocessing,\n        }\n    )\n\n    # add augmentation pipeline to input feature if specified\n    test_input_features = copy.deepcopy(input_features)\n    test_input_features[0].update({\"augmentation\": augmentation_pipeline_ops})\n\n    config = {\n        \"input_features\": test_input_features,\n        \"output_features\": output_features,\n        \"trainer\": {\n            \"epochs\": 2,\n            \"batch_size\": 8,\n        },\n        \"backend\": {\n            \"type\": backend,\n        },\n    }\n\n    with tempfile.TemporaryDirectory() as tmpdir:\n        model = LudwigModel(config, logging_level=logging.INFO)\n        model.experiment(\n            dataset=train_fp,\n            skip_save_processed_input=True,\n            skip_save_model=True,\n            output_directory=os.path.join(tmpdir, \"output\"),\n        )\n    return model\n\n\n@pytest.mark.parametrize(\n    \"augmentation_pipeline_ops\",\n    [\n        [{\"type\": \"random_horizontal_flip\"}],\n        [\n            {\"type\": \"random_vertical_flip\"},\n            {\"type\": \"random_rotate\", \"degree\": 45},\n        ],\n        [\n            {\"type\": \"random_horizontal_flip\"},\n            {\"type\": \"random_vertical_flip\"},\n            {\"type\": \"random_rotate\", \"degree\": 45},\n            {\"type\": \"random_brightness\"},\n            {\"type\": \"random_blur\", \"kernel_size\": 9},\n            {\"type\": \"random_contrast\"},\n        ],\n    ],\n)\n# test image augmentation pipeline\ndef test_image_augmentation(test_image, augmentation_pipeline_ops):\n    # define augmentation pipeline\n    feature = ImageInputFeatureConfig.from_dict(\n        {\"name\": \"foo\", \"type\": \"image\", \"augmentation\": augmentation_pipeline_ops}\n    )\n    augmentation_pipeline = ImageAugmentation(feature.augmentation)\n    # apply augmentation pipeline to batch of test images\n    augmentation_pipeline(test_image)\n\n\nAUGMENTATION_PIPELINE_OPS = [\n    False,\n    True,\n    [{\"type\": \"random_blur\"}, {\"type\": \"random_rotate\"}],\n]\n\nIMAGE_ENCODER = [\n    {\"type\": \"stacked_cnn\"},\n    {\"type\": \"alexnet\", \"use_pretrained\": False, \"model_cache_dir\": os.path.join(os.getcwd(), \"tv_cache\")},\n]\n\nIMAGE_PREPROCESSING = [\n    {\n        \"standardize_image\": None,\n        \"width\": 300,\n        \"height\": 300,\n    },\n    {\n        \"standardize_image\": IMAGENET1K,\n        \"width\": 300,\n        \"height\": 300,\n    },\n]\n\n\n@pytest.mark.parametrize(\"augmentation_pipeline_ops\", AUGMENTATION_PIPELINE_OPS)\n@pytest.mark.parametrize(\"encoder\", IMAGE_ENCODER)\n@pytest.mark.parametrize(\"preprocessing\", IMAGE_PREPROCESSING)\ndef test_local_model_training_with_augmentation_pipeline(\n    train_data_rgb,\n    encoder,\n    preprocessing,\n    augmentation_pipeline_ops,\n):\n    model = run_augmentation_training(\n        train_data=train_data_rgb,\n        backend=\"local\",\n        encoder=encoder,  # Ludwig encoder\n        preprocessing=preprocessing,  # Ludwig image preprocessing\n        augmentation_pipeline_ops=augmentation_pipeline_ops,  # Ludwig image augmentation\n    )\n\n    if augmentation_pipeline_ops is not False:\n        assert model.config_obj.input_features[0].has_augmentation()\n    else:\n        assert not model.config_obj.input_features[0].has_augmentation()\n\n\n# due to the time it takes to run the tests, run only a subset of the tests\n# and focus on interaction of Ludwig encoder with image preprocessing and augmentation\n@pytest.mark.slow\n@pytest.mark.distributed\n@pytest.mark.parametrize(\"augmentation_pipeline_ops\", AUGMENTATION_PIPELINE_OPS)\n@pytest.mark.parametrize(\"preprocessing\", IMAGE_PREPROCESSING)\ndef test_ray_model_training_with_augmentation_pipeline(\n    train_data_rgb,\n    preprocessing,\n    augmentation_pipeline_ops,\n    ray_cluster_2cpu,\n):\n    model = run_augmentation_training(\n        train_data=train_data_rgb,\n        backend=\"ray\",\n        encoder={\"type\": \"stacked_cnn\"},\n        preprocessing=preprocessing,\n        augmentation_pipeline_ops=augmentation_pipeline_ops,\n    )\n\n    if augmentation_pipeline_ops is not False:\n        assert model.config_obj.input_features[0].has_augmentation()\n    else:\n        assert not model.config_obj.input_features[0].has_augmentation()\n\n\n# this test gray-scale image augmentation pipeline\n@pytest.mark.parametrize(\n    \"augmentation_pipeline_ops\",\n    [\n        False,\n        True,\n        [\n            {\"type\": \"auto_augmentation\"},\n            {\"type\": \"random_horizontal_flip\"},\n            {\"type\": \"random_vertical_flip\"},\n            {\"type\": \"random_rotate\"},\n            {\"type\": \"random_brightness\"},\n            {\"type\": \"random_blur\"},\n            {\"type\": \"random_contrast\"},\n        ],\n    ],\n)\ndef test_ludwig_encoder_gray_scale_image_augmentation_pipeline(\n    train_data_gray_scale,\n    augmentation_pipeline_ops,\n):\n    run_augmentation_training(\n        train_data=train_data_gray_scale,\n        backend=\"local\",\n        encoder={\"type\": \"stacked_cnn\", \"num_filters\": 1},\n        preprocessing={},\n        augmentation_pipeline_ops=augmentation_pipeline_ops,\n    )\n\n\n# this test invalid augmentation pipeline specification\n@pytest.mark.parametrize(\n    \"augmentation_pipeline_ops\",\n    [\n        None,\n        [{\"type\": \"invalid_string\"}],\n        [\"random_horizontal_flip\"],\n        \"random_horizontal_flip\",\n        [\n            {\"type\": \"random_rotate\", \"degree\": \"45\"},\n        ],\n    ],\n)\ndef test_invalid_augmentation_parameters(\n    train_data_rgb,\n    augmentation_pipeline_ops,\n):\n    with pytest.raises(ConfigValidationError):\n        run_augmentation_training(\n            train_data=train_data_rgb,\n            backend=\"local\",\n            encoder={\"type\": \"alexnet\", \"model_cache_dir\": os.path.join(os.getcwd(), \"tv_cache\")},\n            preprocessing={},\n            augmentation_pipeline_ops=augmentation_pipeline_ops,\n        )\n\n\n# tests saving and loading a model with augmentation pipeline\ndef test_load_model_with_augmentation_pipeline(\n    train_data_rgb,\n):\n    augmentation_pipeline_ops = [\n        {\"type\": \"random_blur\"},\n        {\"type\": \"random_rotate\"},\n    ]\n    preprocessing = {\n        \"standardize_image\": None,\n        \"width\": 300,\n        \"height\": 300,\n    }\n    encoder = {\n        \"type\": \"alexnet\",\n        \"use_pretrained\": False,\n        \"model_cache_dir\": os.path.join(os.getcwd(), \"tv_cache\"),\n    }\n\n    model = run_augmentation_training(\n        train_data=train_data_rgb,\n        backend=\"local\",\n        encoder=encoder,  # Ludwig encoder\n        preprocessing=preprocessing,  # Ludwig image preprocessing\n        augmentation_pipeline_ops=augmentation_pipeline_ops,  # Ludwig image augmentation\n    )\n\n    with tempfile.TemporaryDirectory() as tmp_dir:\n        model.save(tmp_dir)\n        LudwigModel.load(tmp_dir)\n"
  },
  {
    "path": "tests/ludwig/augmentation/test_auto_augmentation.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import IMAGE\nfrom ludwig.features.image_feature import get_augmentation_op\nfrom ludwig.schema.features.augmentation.utils import get_augmentation_cls\n\n\n@pytest.fixture(scope=\"module\")\ndef test_image():\n    return torch.randn(5, 3, 256, 256)\n\n\n@pytest.mark.parametrize(\n    \"augmentation_type, augmentation_params\",\n    [\n        (\"auto_augmentation\", {\"method\": \"trivial_augment\"}),\n        (\"auto_augmentation\", {\"method\": \"auto_augment\"}),\n        (\"auto_augmentation\", {\"method\": \"rand_augment\"}),\n    ],\n)\ndef test_auto_augmentation(test_image, augmentation_type, augmentation_params):\n    aug_config = get_augmentation_cls(IMAGE, augmentation_type).from_dict(augmentation_params)\n    augmentation_op_cls = get_augmentation_op(IMAGE, augmentation_type)\n    augmentation_op = augmentation_op_cls(aug_config)\n    augmented_image = augmentation_op(test_image)\n    assert augmented_image.shape == (5, 3, 256, 256)\n"
  },
  {
    "path": "tests/ludwig/augmentation/test_image_augmentation.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import IMAGE\nfrom ludwig.features.image_feature import get_augmentation_op\nfrom ludwig.schema.features.augmentation.utils import get_augmentation_cls\n\n\n@pytest.fixture(scope=\"module\")\ndef test_image():\n    # return random normal image of size 2x3x32x32 [batch_size, channels, height, width]\n    return torch.randn(2, 3, 32, 32)\n\n\n@pytest.mark.parametrize(\n    \"augmentation_type, augmentation_params\",\n    [\n        (\"random_horizontal_flip\", {}),\n        (\"random_vertical_flip\", {}),\n        (\"random_rotate\", {\"degree\": 45}),\n        (\"random_blur\", {\"kernel_size\": 9}),\n        (\"random_blur\", {\"kernel_size\": 15}),\n        (\"random_contrast\", {\"min\": 0.5, \"max\": 1.5}),\n        (\"random_brightness\", {\"min\": 0.5, \"max\": 1.5}),\n    ],\n)\ndef test_image_augmentation(test_image, augmentation_type, augmentation_params):\n    aug_config = get_augmentation_cls(IMAGE, augmentation_type).from_dict(augmentation_params)\n    augmentation_op_cls = get_augmentation_op(IMAGE, augmentation_type)\n    augmentation_op = augmentation_op_cls(aug_config)\n    augmentation_op(test_image)\n"
  },
  {
    "path": "tests/ludwig/automl/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ludwig/automl/test_base_config.py",
    "content": "import os\nfrom decimal import Decimal\n\nimport dask\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport yaml\n\nray = pytest.importorskip(\"ray\")  # noqa\n\n# Prevent Dask from converting object-dtype columns to PyArrow strings.\ndask.config.set({\"dataframe.convert-string\": False})\n\nfrom ludwig.automl.base_config import (  # noqa\n    get_dataset_info,\n    get_dataset_info_from_source,\n    get_field_metadata,\n    get_reference_configs,\n    is_field_boolean,\n)\nfrom ludwig.data.dataframe.dask import DaskEngine  # noqa\nfrom ludwig.data.dataframe.pandas import PandasEngine  # noqa\nfrom ludwig.schema.model_types.base import ModelConfig  # noqa\nfrom ludwig.utils.automl.data_source import DataframeSource, wrap_data_source  # noqa\n\npytestmark = pytest.mark.distributed\n\n\n@pytest.fixture(scope=\"module\")\ndef dummy_df():\n    data = {\n        \"title\": {\n            0: \" Donald Trump Sends ...Disturbing\",\n            1: \" Drunk Bragging Trum...estigation\",\n            2: \" Sheriff David Clark...n The Eye\",\n            3: \" Trump Is So Obsesse...e (IMAGES)\",\n            4: \" Pope Francis Just C...mas Speech\",\n        },\n        \"text\": {\n            0: \"Donald Trump just co...ty Images.\",\n            1: \"House Intelligence C...ty Images.\",\n            2: \"On Friday, it was re...ty Images.\",\n            3: \"On Christmas day, Do...ty Images.\",\n            4: \"Pope Francis used hi...ty Images.\",\n        },\n        \"subject\": {0: \"News\", 1: \"News\", 2: \"News\", 3: \"News\", 4: \"News\"},\n        \"date\": {\n            0: \"December 31, 2017\",\n            1: \"December 31, 2017\",\n            2: \"December 30, 2017\",\n            3: \"December 29, 2017\",\n            4: \"December 25, 2017\",\n        },\n        \"label\": {0: \"Fake\", 1: \"Fake\", 2: \"Fake\", 3: \"Fake\", 4: \"Fake\"},\n    }\n\n    return pd.DataFrame.from_dict(data)\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_is_field_boolean(df_engine, dummy_df):\n    assert np.array_equal(dummy_df.dtypes, [\"object\", \"object\", \"object\", \"object\", \"object\"])\n\n    if isinstance(df_engine, DaskEngine):\n        dummy_df = df_engine.df_lib.from_pandas(dummy_df, npartitions=1)\n\n    source = wrap_data_source(dummy_df)\n\n    for field in dummy_df.columns:\n        assert not is_field_boolean(source, field)\n\n\n@pytest.mark.parametrize(\n    \"df_engine\",\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_dataset_info(df_engine, dummy_df):\n    assert np.array_equal(dummy_df.dtypes, [\"object\", \"object\", \"object\", \"object\", \"object\"])\n\n    if isinstance(df_engine, DaskEngine):\n        dummy_df = df_engine.df_lib.from_pandas(dummy_df, npartitions=1)\n\n    ds_info = get_dataset_info(dummy_df)\n\n    assert [f.dtype for f in ds_info.fields] == [\"object\", \"object\", \"object\", \"object\", \"object\"]\n\n\n@pytest.mark.parametrize(\n    \"col,expected_dtype\",\n    [\n        ([\"a\", \"b\", \"c\", \"d\", \"e\", \"a\", \"b\", \"b\"], \"object\"),\n        ([\"a\", \"b\", \"a\", \"b\", np.nan], \"object\"),\n        ([\"a\", \"b\", \"a\", \"b\", None], \"object\"),\n        ([True, False, True, True, \"\"], \"object\"),\n        ([True, False, True, False, np.nan], \"bool\"),\n    ],\n)\ndef test_object_and_bool_type_inference(col, expected_dtype):\n    df = pd.DataFrame({\"col1\": col})\n    info = get_dataset_info(df)\n    assert info.fields[0].dtype == expected_dtype\n\n\ndef test_reference_configs():\n    ref_configs = get_reference_configs()\n    for dataset in ref_configs[\"datasets\"]:\n        config = dataset[\"config\"]\n\n        # Ensure config is valid with the latest Ludwig schema\n        ModelConfig.from_dict(config)\n\n\ndef repeat(df, n):\n    \"\"\"Repeat a dataframe n times.\"\"\"\n    return pd.concat([df] * n, ignore_index=True)\n\n\ndef test_infer_parquet_types(tmpdir):\n    \"\"\"Test type inference works properly for a parquet file with unconventional types types.\"\"\"\n    # Create a temporary directory to store the parquet file\n    tmpdir = str(tmpdir)\n\n    # Create a dataframe with all the types\n    df = pd.DataFrame(\n        {\n            \"int\": [1, 2, 3],\n            \"float\": [1.1, 2.2, 3.3],\n            \"string\": [\"a\", \"b\", \"c\"],\n            \"datetime\": pd.date_range(\"20130101\", periods=3),\n            \"category\": pd.Series([\"a\", \"b\", \"c\"], dtype=\"category\"),\n            \"bool\": [True, False, True],\n        }\n    )\n    df = repeat(df, 10)\n    df[\"float\"] = df[\"float\"].apply(Decimal)\n    df[\"date\"] = df[\"datetime\"].apply(str)\n\n    # Write the dataframe to parquet and read it back\n    dataset_path = os.path.join(tmpdir, \"dataset.parquet\")\n    df.to_parquet(dataset_path)\n    df = pd.read_parquet(dataset_path)\n\n    # Test type inference\n    ds = DataframeSource(df)\n    ds_info = get_dataset_info_from_source(ds)\n    metas = get_field_metadata(ds_info.fields, ds_info.row_count, targets=[\"bool\"])\n\n    config = yaml.safe_load(\"\"\"\n        input_features:\n            - name: int\n              type: category\n            - name: float\n              type: number\n            - name: string\n              type: category\n            - name: datetime\n              type: date\n            - name: category\n              type: category\n            - name: date\n              type: date\n        output_features:\n            - name: bool\n              type: binary\n        combiner:\n            type: concat\n            output_size: 14\n        trainer:\n            epochs: 2\n            batch_size: 8\n        \"\"\")\n\n    meta_dict = {meta.config.name: meta for meta in metas}\n    for feature in config[\"input_features\"] + config[\"output_features\"]:\n        meta = meta_dict[feature[\"name\"]]\n        assert feature[\"type\"] == meta.config.type, f\"{feature['name']}: {feature['type']} != {meta.config.type}\"\n"
  },
  {
    "path": "tests/ludwig/automl/test_data_source.py",
    "content": "import tempfile\n\nimport pytest\n\nfrom ludwig.constants import TEXT\nfrom ludwig.utils.data_utils import read_csv\n\ntry:\n    import dask.dataframe as dd\n\n    from ludwig.automl import create_auto_config\nexcept ImportError:\n    pass\n\n\nCSV_CONTENT = \"\"\"\nname,gender,lives_in_sf\nJessica,f,\nJim,m,FALSE\n\"\"\"\n\n\ndef get_test_df():\n    temp = tempfile.NamedTemporaryFile(mode=\"w+\")\n    temp.write(CSV_CONTENT)\n    temp.seek(0)\n    ds = read_csv(temp.name, dtype=None)\n    df = dd.from_pandas(ds, npartitions=1)\n    return df\n\n\n@pytest.mark.distributed\ndef test_mixed_csv_data_source(ray_cluster_2cpu):\n    config = create_auto_config(dataset=get_test_df(), target=[], time_limit_s=3600)\n\n    assert len(config[\"input_features\"]) == 2\n    assert config[\"input_features\"][0][\"type\"] == TEXT\n    assert config[\"input_features\"][1][\"type\"] == TEXT\n"
  },
  {
    "path": "tests/ludwig/automl/test_tune_config.py",
    "content": "import pytest\n\ntry:\n    from ludwig.automl.auto_tune_config import reduce_text_feature_max_length\nexcept ImportError:\n    pass\n\n\n@pytest.mark.distributed\ndef test_reduce_text_model_mem_99ptile():\n    config = {\"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}]}\n    training_set_metadata = {\"description\": {\"max_sequence_length_99ptile\": 117.0}}\n    config_upd = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 117}},\n    }\n    reduce_text_feature_max_length(config, training_set_metadata)\n    assert config == config_upd\n\n\n@pytest.mark.distributed\ndef test_reduce_text_model_mem_128():\n    config = {\"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}]}\n    training_set_metadata = {\"description\": {\"max_sequence_length_99ptile\": 512.0}}\n    config_upd = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 128}},\n    }\n    reduce_text_feature_max_length(config, training_set_metadata)\n    assert config == config_upd\n\n\n@pytest.mark.distributed\ndef test_reduce_text_model_mem_override():\n    config = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 256}},\n    }\n    training_set_metadata = {\"description\": {\"max_sequence_length_99ptile\": 117.0}}\n    config_upd = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 117}},\n    }\n    reduce_text_feature_max_length(config, training_set_metadata)\n    assert config == config_upd\n\n\n@pytest.mark.distributed\ndef test_reduce_text_model_mem_respect():\n    config = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 56}},\n    }\n    training_set_metadata = {\"description\": {\"max_sequence_length_99ptile\": 117.0}}\n    config_upd = {\n        \"input_features\": [{\"name\": \"description\", \"column\": \"description\", \"type\": \"text\", \"encoder\": \"bert\"}],\n        \"preprocessing\": {\"text\": {\"max_sequence_length\": 56}},\n    }\n    reduce_text_feature_max_length(config, training_set_metadata)\n    assert config == config_upd\n"
  },
  {
    "path": "tests/ludwig/automl/test_utils.py",
    "content": "import pytest\n\nray = pytest.importorskip(\"ray\")  # noqa\n\nfrom ludwig.utils.automl.utils import get_model_type  # noqa\n\npytestmark = pytest.mark.distributed\n\n\ndef _features(*in_types, out):\n    return {\n        \"input_features\": [{\"name\": f\"in_{i}\", \"type\": dtype} for i, dtype in enumerate(in_types)],\n        \"output_features\": [{\"name\": \"out_0\", \"type\": out}],\n    }\n\n\n@pytest.mark.parametrize(\n    \"config,expected\",\n    [\n        ({**_features(\"text\", out=\"number\")}, \"text\"),\n        ({**_features(\"text\", \"text\", out=\"number\")}, \"concat\"),\n        ({**_features(\"text\", \"text\", out=\"number\"), \"combiner\": {\"type\": \"tabnet\"}}, \"tabnet\"),\n    ],\n)\ndef test_get_model_type(config, expected):\n    actual = get_model_type(config)\n    assert actual == expected\n"
  },
  {
    "path": "tests/ludwig/backend/test_ray.py",
    "content": "import copy\nfrom unittest.mock import patch\n\nimport pytest\n\n# Skip these tests if Ray is not installed\nray = pytest.importorskip(\"ray\")  # noqa\n\nfrom ray.train.torch import TorchConfig  # noqa\n\nfrom ludwig.backend import initialize_backend  # noqa\nfrom ludwig.backend.ray import get_trainer_kwargs  # noqa\nfrom ludwig.constants import AUTO, EXECUTOR, MAX_CONCURRENT_TRIALS, RAY  # noqa\n\n# Mark the entire module as distributed\npytestmark = pytest.mark.distributed\n\n\n@pytest.mark.parametrize(\n    \"trainer_config,cluster_resources,num_nodes,expected_kwargs\",\n    [\n        # Prioritize using the GPU when available over multi-node\n        pytest.param(\n            {},\n            {\"CPU\": 4, \"GPU\": 1},\n            2,\n            dict(\n                backend=TorchConfig(),\n                num_workers=1,\n                use_gpu=True,\n                resources_per_worker={\n                    \"CPU\": 0,\n                    \"GPU\": 1,\n                },\n            ),\n            id=\"ddp\",\n            marks=pytest.mark.distributed,\n        ),\n    ],\n)\ndef test_get_trainer_kwargs(trainer_config, cluster_resources, num_nodes, expected_kwargs):\n    with patch(\"ludwig.backend.ray.ray.cluster_resources\", return_value=cluster_resources):\n        with patch(\"ludwig.backend.ray._num_nodes\", return_value=num_nodes):\n            trainer_config_copy = copy.deepcopy(trainer_config)\n            actual_kwargs = get_trainer_kwargs(**trainer_config_copy)\n\n            # Function should not modify the original input\n            assert trainer_config_copy == trainer_config\n\n            actual_backend = actual_kwargs.pop(\"backend\")\n            expected_backend = expected_kwargs.pop(\"backend\")\n\n            assert type(actual_backend) is type(expected_backend)\n            assert actual_kwargs == expected_kwargs\n\n\n@pytest.mark.distributed\n@pytest.mark.parametrize(\n    \"hyperopt_config_old, hyperopt_config_expected\",\n    [\n        (  # If max_concurrent_trials is none, it should not be set in the updated config\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": None},\n            },\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": None},\n            },\n        ),\n        (  # If max_concurrent_trials is auto, set to cpus // cpus_per_trial\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": \"auto\"},\n            },\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": 4},\n            },\n        ),\n        (  # Even though num_samples is set to 4, this will actually result in 9 trials.\n            # With 4 CPUs and 1 CPU/trial, max_concurrent_trials = 4\n            {\n                \"parameters\": {\n                    \"trainer.learning_rate\": {\"space\": \"grid_search\", \"values\": [0.001, 0.01, 0.1]},\n                    \"combiner.num_fc_layers\": {\"space\": \"grid_search\", \"values\": [1, 2, 3]},\n                },\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": \"auto\"},\n            },\n            {\n                \"parameters\": {\n                    \"trainer.learning_rate\": {\"space\": \"grid_search\", \"values\": [0.001, 0.01, 0.1]},\n                    \"combiner.num_fc_layers\": {\"space\": \"grid_search\", \"values\": [1, 2, 3]},\n                },\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": 4},\n            },\n        ),\n        (  # Ensure user config value (1) is respected if it is passed in\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": 1},\n            },\n            {\n                \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"values\": [0.001, 0.01, 0.1]}},\n                \"executor\": {\"num_samples\": 4, \"cpu_resources_per_trial\": 1, \"max_concurrent_trials\": 1},\n            },\n        ),\n    ],\n    ids=[\"none\", \"auto\", \"auto_with_large_num_trials\", \"1\"],\n)\ndef test_set_max_concurrent_trials(hyperopt_config_old, hyperopt_config_expected, ray_cluster_4cpu):\n    backend = initialize_backend(RAY)\n    if hyperopt_config_old[EXECUTOR].get(MAX_CONCURRENT_TRIALS) == AUTO:\n        hyperopt_config_old[EXECUTOR][MAX_CONCURRENT_TRIALS] = backend.max_concurrent_trials(hyperopt_config_old)\n    assert hyperopt_config_old == hyperopt_config_expected\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_1.yaml",
    "content": "# This benchmarking config is missing because the global experiment name is missing.\nprocess_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py\nhyperopt: false\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nprofiler:\n  enable: false\n  use_torch_profiler: false\n  logging_interval: 0.1\nexperiments:\n  - dataset_name: ames_housing\n    experiment_name: large_learning_rate\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n  - dataset_name: protein\n    config_path: tests/regression_tests/benchmark/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    experiment_name: zscore_normalization\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_2.yaml",
    "content": "# This benchmarking config is invalid beacuse it's missing the export section.\nexperiment_name: github_action\nprocess_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py\nhyperopt: false\nprofiler:\n  enable: false\n  use_torch_profiler: false\n  logging_interval: 0.1\nexperiments:\n  - dataset_name: ames_housing\n    experiment_name: large_learning_rate\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n  - dataset_name: protein\n    config_path: tests/regression_tests/benchmark/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    experiment_name: zscore_normalization\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/invalid/benchmarking_config_3.yaml",
    "content": "# This benchmarking config is invalid because some of the dataset names aren't specified.\nexperiment_name: github_action\nprocess_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py\nhyperopt: false\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nprofiler:\n  enable: false\n  use_torch_profiler: false\n  logging_interval: 0.1\nexperiments:\n  - experiment_name: large_learning_rate\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n  - config_path: tests/regression_tests/benchmark/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    experiment_name: zscore_normalization\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/process_config.py",
    "content": "def process_config(ludwig_config: dict, experiment_dict: dict) -> dict:\n    \"\"\"Modify a Ludwig config.\n\n    :param ludwig_config: a Ludwig config.\n    :param experiment_dict: a benchmarking config experiment dictionary.\n\n    returns: a modified Ludwig config.\n    \"\"\"\n\n    # Only keep input_features and output_features for the ames_housing dataset.\n    if experiment_dict[\"dataset_name\"] == \"ames_housing\":\n        main_config_keys = list(ludwig_config.keys())\n        for key in main_config_keys:\n            if key not in [\"input_features\", \"output_features\"]:\n                del ludwig_config[key]\n\n    # Set the early_stop criteria to stop training after 7 epochs of no score improvement.\n    ludwig_config[\"trainer\"] = {\"early_stop\": 7}\n\n    # use sparse encoder for categorical features to mimic logreg\n    ludwig_config[\"combiner\"] = {\"type\": \"concat\"}\n    for i, feature in enumerate(ludwig_config[\"input_features\"]):\n        if feature[\"type\"] == \"category\":\n            ludwig_config[\"input_features\"][i][\"encoder\"] = \"sparse\"\n    for i, feature in enumerate(ludwig_config[\"output_features\"]):\n        if feature[\"type\"] == \"category\":\n            ludwig_config[\"output_features\"][i][\"encoder\"] = \"sparse\"\n\n    return ludwig_config\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/valid/benchmarking_config_1.yaml",
    "content": "# You can specify any of the global parameters locally to any experiment. This will override the global behavior.\nexperiment_name: github_action\nprocess_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nprofiler:\n  enable: false\n  use_torch_profiler: false\n  logging_interval: 0.1\nexperiments:\n  - dataset_name: ames_housing\n    experiment_name: large_learning_rate\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n    hyperopt: true\n  - dataset_name: protein\n    config_path: tests/regression_tests/benchmark/configs/protein.yaml\n    profiler:\n      enable: true\n      use_torch_profiler: true\n      logging_interval: 0.1\n  - dataset_name: mercedes_benz_greener\n    experiment_name: zscore_normalization\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/valid/benchmarking_config_2.yaml",
    "content": "# This is a minimal example of a valid benchmarking config. the hyperopt section of the benchmarking config\n# will default to false. The profiler section will also default to false.\nexperiment_name: github_action\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nexperiments:\n  - dataset_name: ames_housing\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n  - dataset_name: protein\n    config_path: tests/regression_tests/benchmark/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/example_files/valid/benchmarking_config_3.yaml",
    "content": "# We can skip specifying a global experiment name if it's specified for each experiment.\nprocess_config_file_path: tests/ludwig/benchmarking/example_files/process_config_example.py\nexport:\n  export_artifacts: true\n  export_base_path: s3://benchmarking.us-west-2.ludwig.com/bench/    # include the slash at the end.\nprofiler:\n  enable: true\n  use_torch_profiler: false\n  logging_interval: 0.1\nexperiments:\n  - dataset_name: ames_housing\n    experiment_name: large_learning_rate\n    config_path: tests/regression_tests/benchmark/configs/ames_housing.yaml\n  - dataset_name: protein\n    experiment_name: decay_rate_0.8\n    config_path: tests/regression_tests/benchmark/configs/protein.yaml\n  - dataset_name: mercedes_benz_greener\n    experiment_name: zscore_normalization\n    config_path: tests/regression_tests/benchmark/configs/mercedes_benz_greener.yaml\n"
  },
  {
    "path": "tests/ludwig/benchmarking/test_benchmarking.py",
    "content": "import os\nfrom contextlib import nullcontext as does_not_raise\n\nimport pytest\n\nfrom ludwig.benchmarking.utils import validate_benchmarking_config\nfrom ludwig.utils.data_utils import load_yaml\n\n\ndef get_benchamrking_configs(validity):\n    local_dir = \"/\".join(__file__.split(\"/\")[:-1])\n    return [\n        os.path.join(local_dir, \"example_files\", validity, config_fp)\n        for config_fp in os.listdir(os.path.join(local_dir, \"example_files\", validity))\n    ]\n\n\n@pytest.mark.parametrize(\"benchmarking_config_fp\", get_benchamrking_configs(\"valid\"))\ndef test_valid_benchmarking_configs_valid(benchmarking_config_fp):\n    benchmarking_config = load_yaml(benchmarking_config_fp)\n\n    with does_not_raise():\n        validate_benchmarking_config(benchmarking_config)\n\n\n@pytest.mark.parametrize(\"benchmarking_config_fp\", get_benchamrking_configs(\"invalid\"))\ndef test_invalid_benchmarking_configs_valid(benchmarking_config_fp):\n    benchmarking_config = load_yaml(benchmarking_config_fp)\n\n    with pytest.raises(ValueError):\n        validate_benchmarking_config(benchmarking_config)\n"
  },
  {
    "path": "tests/ludwig/benchmarking/test_profiler.py",
    "content": "import os\nimport time\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nimport torch\nfrom packaging.version import parse as parse_version\n\nif parse_version(pd.__version__) > parse_version(\"1.5.3\"):\n    pytest.skip(allow_module_level=True)\n\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.benchmarking.profiler import LudwigProfiler\nfrom ludwig.constants import BATCH_SIZE, TRAINER\n\n\n@pytest.mark.skipif(\n    parse_version(pd.__version__) > parse_version(\"1.5.3\"),\n    reason=\"experiment_impact_tracker package is incompatible with pandas 2.0\",\n)\ndef test_ludwig_profiler(tmpdir):\n    @LudwigProfiler(tag=\"test_function\", output_dir=tmpdir, use_torch_profiler=False, logging_interval=0.1)\n    def func(duration):\n        time.sleep(duration)\n        x = torch.Tensor(2, 3)\n        y = torch.rand(2, 3)\n        torch.add(x, y)\n\n    train_df = pd.DataFrame(np.random.normal(0, 1, size=(100, 3)), columns=[\"input_1\", \"input_2\", \"output_1\"])\n    eval_df = pd.DataFrame(np.random.normal(0, 1, size=(20, 3)), columns=[\"input_1\", \"input_2\", \"output_1\"])\n\n    config = {\n        \"input_features\": [{\"name\": \"input_1\", \"type\": \"number\"}, {\"name\": \"input_2\", \"type\": \"number\"}],\n        \"output_features\": [{\"name\": \"output_1\", \"type\": \"number\"}],\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        TRAINER: {\"epochs\": 1, BATCH_SIZE: 128},\n    }\n\n    model = LudwigModel(config=config, backend=\"local\")\n\n    with LudwigProfiler(tag=\"profile_1\", output_dir=tmpdir, use_torch_profiler=False, logging_interval=0.1):\n        model.train(\n            dataset=train_df,\n            output_directory=tmpdir,\n            skip_save_training_description=True,\n            skip_save_training_statistics=True,\n            skip_save_model=True,\n            skip_save_progress=True,\n            skip_save_log=True,\n            skip_save_processed_input=True,\n        )\n\n    assert os.path.exists(os.path.join(tmpdir, \"system_resource_usage\", \"profile_1\", \"run_0.json\"))\n\n    with LudwigProfiler(tag=\"profile_2\", output_dir=tmpdir, use_torch_profiler=True, logging_interval=0.1):\n        model.evaluate(dataset=eval_df)\n        func(0.1)\n\n    assert os.path.exists(os.path.join(tmpdir, \"system_resource_usage\", \"profile_2\", \"run_0.json\"))\n    assert os.path.exists(os.path.join(tmpdir, \"torch_ops_resource_usage\", \"profile_2\", \"run_0.json\"))\n\n    func(0.25)\n    func(0.5)\n    assert set(os.listdir(os.path.join(tmpdir, \"system_resource_usage\", \"test_function\"))) == {\n        \"run_0.json\",\n        \"run_1.json\",\n        \"run_2.json\",\n    }\n"
  },
  {
    "path": "tests/ludwig/combiners/test_combiners.py",
    "content": "import logging\nfrom collections import OrderedDict\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.combiners.combiners import (\n    ComparatorCombiner,\n    ConcatCombiner,\n    ProjectAggregateCombiner,\n    SequenceCombiner,\n    SequenceConcatCombiner,\n    TabNetCombiner,\n    TabTransformerCombiner,\n    TransformerCombiner,\n)\nfrom ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, TYPE\nfrom ludwig.schema.combiners.comparator import ComparatorCombinerConfig\nfrom ludwig.schema.combiners.concat import ConcatCombinerConfig\nfrom ludwig.schema.combiners.project_aggregate import ProjectAggregateCombinerConfig\nfrom ludwig.schema.combiners.sequence import SequenceCombinerConfig\nfrom ludwig.schema.combiners.sequence_concat import SequenceConcatCombinerConfig\nfrom ludwig.schema.combiners.tab_transformer import TabTransformerCombinerConfig\nfrom ludwig.schema.combiners.tabnet import TabNetCombinerConfig\nfrom ludwig.schema.combiners.transformer import TransformerCombinerConfig\nfrom ludwig.schema.utils import load_config\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nlogging.getLogger(\"ludwig\").setLevel(logging.INFO)\n\nDEVICE = get_torch_device()\nBATCH_SIZE = 16\nSEQ_SIZE = 12\nHIDDEN_SIZE = 24\nOTHER_HIDDEN_SIZE = 32\nOUTPUT_SIZE = 8\nBASE_OUTPUT_SIZE = 16\nNUM_FILTERS = 20\nRANDOM_SEED = 1919\n\n\n# emulate Input Feature class.  Need to provide output_shape property to\n# mimic what happens during ECD.forward() processing.\nclass PseudoInputFeature:\n    def __init__(self, feature_name, output_shape, feature_type=None):\n        self.name = feature_name\n        self._output_shape = output_shape\n        self.feature_type = feature_type\n\n    def type(self):\n        return self.feature_type\n\n    @property\n    def output_shape(self):\n        return torch.Size(self._output_shape[1:])\n\n\n# helper function to test correctness of combiner output\ndef check_combiner_output(combiner, combiner_output, batch_size):\n    # check for required attributes\n    assert hasattr(combiner, \"input_dtype\")\n    assert hasattr(combiner, \"output_shape\")\n\n    # check for correct data type\n    assert isinstance(combiner_output, dict)\n\n    # required key present\n    assert \"combiner_output\" in combiner_output\n\n    # check for correct output shape\n    assert combiner_output[\"combiner_output\"].shape == (batch_size, *combiner.output_shape)\n\n\n# generates encoder outputs and minimal input feature objects for testing\n@pytest.fixture\ndef features_to_test(feature_list: list[tuple[str, list]]) -> tuple[dict, dict]:\n    # feature_list: list of tuples that define the output_shape and type\n    #    of input features to generate.  tuple[0] is input feature type,\n    #    tuple[1] is expected encoder output shape for the input feature\n\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs = {}\n    input_features = {}\n    for i in range(len(feature_list)):\n        feature_name = f\"feature_{i:02d}\"\n        encoder_outputs[feature_name] = {\n            ENCODER_OUTPUT: torch.randn(feature_list[i][1], dtype=torch.float32, device=DEVICE)\n        }\n        input_features[feature_name] = PseudoInputFeature(feature_name, feature_list[i][1], feature_list[i][0])\n\n    return encoder_outputs, input_features\n\n\n# set up simulated encoder outputs\n@pytest.fixture\ndef encoder_outputs():\n    # generates simulated encoder outputs dictionary:\n    #   feature_1: shape [b, h1] tensor\n    #   feature_2: shape [b, h2] tensor\n    #   feature_3: shape [b, s, h1] tensor\n    #   feature_4: shape [b, sh, h2] tensor\n\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup synthetic encoder output for testing\n    encoder_outputs = {}\n    input_features = OrderedDict()\n    shapes_list = [\n        [BATCH_SIZE, HIDDEN_SIZE],\n        [BATCH_SIZE, OTHER_HIDDEN_SIZE],\n        [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE],\n        [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE],\n    ]\n    feature_names = [\"feature_\" + str(i + 1) for i in range(len(shapes_list))]\n\n    for feature_name, batch_shape in zip(feature_names, shapes_list):\n        encoder_outputs[feature_name] = {ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE)}\n        if len(batch_shape) > 2:\n            encoder_outputs[feature_name][ENCODER_OUTPUT_STATE] = torch.randn(\n                [batch_shape[0], batch_shape[2]], dtype=torch.float32, device=DEVICE\n            )\n\n        # create pseudo input feature object\n        input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape)\n\n    return encoder_outputs, input_features\n\n\n# setup encoder outputs for ComparatorCombiner\n@pytest.fixture\ndef encoder_comparator_outputs():\n    # generates simulated encoder outputs dictionary:\n    #   feature_1: shape [b, h1] tensor\n    #   feature_2: shape [b, h2] tensor\n    #   feature_3: shape [b, s, h1] tensor\n    #   feature_4: shape [b, sh, h2] tensor\n\n    encoder_outputs = {}\n    input_features = {}\n    shapes_list = [\n        [BATCH_SIZE, HIDDEN_SIZE],\n        [BATCH_SIZE, OTHER_HIDDEN_SIZE],\n        [BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE],\n        [BATCH_SIZE, SEQ_SIZE, OTHER_HIDDEN_SIZE],\n    ]\n    text_feature_names = [\"text_feature_\" + str(i + 1) for i in range(len(shapes_list))]\n    image_feature_names = [\"image_feature_\" + str(i + 1) for i in range(len(shapes_list))]\n    for i, (feature_name, batch_shape) in enumerate(zip(text_feature_names, shapes_list)):\n        # is there a better way to do this?\n        if i == 0 or i == 3:\n            dot_product_shape = [batch_shape[0], BASE_OUTPUT_SIZE]\n            encoder_outputs[feature_name] = {\n                ENCODER_OUTPUT: torch.randn(dot_product_shape, dtype=torch.float32, device=DEVICE)\n            }\n            input_features[feature_name] = PseudoInputFeature(feature_name, dot_product_shape)\n        else:\n            encoder_outputs[feature_name] = {\n                ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE)\n            }\n            input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape)\n\n    for i, (feature_name, batch_shape) in enumerate(zip(image_feature_names, shapes_list)):\n        if i == 0 or i == 3:\n            dot_product_shape = [batch_shape[0], BASE_OUTPUT_SIZE]\n            encoder_outputs[feature_name] = {\n                ENCODER_OUTPUT: torch.randn(dot_product_shape, dtype=torch.float32, device=DEVICE)\n            }\n            input_features[feature_name] = PseudoInputFeature(feature_name, dot_product_shape)\n        else:\n            encoder_outputs[feature_name] = {\n                ENCODER_OUTPUT: torch.randn(batch_shape, dtype=torch.float32, device=DEVICE)\n            }\n            input_features[feature_name] = PseudoInputFeature(feature_name, batch_shape)\n\n    return encoder_outputs, input_features\n\n\n# test for simple concatenation combiner\n@pytest.mark.parametrize(\"norm\", [None, \"batch\", \"layer\"])\n@pytest.mark.parametrize(\"number_inputs\", [None, 1])\n@pytest.mark.parametrize(\"flatten_inputs\", [True, False])\n@pytest.mark.parametrize(\"fc_layer\", [None, [{\"output_size\": OUTPUT_SIZE}, {\"output_size\": OUTPUT_SIZE}]])\ndef test_concat_combiner(\n    encoder_outputs: tuple,\n    fc_layer: list[dict] | None,\n    flatten_inputs: bool,\n    number_inputs: int | None,\n    norm: str,\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs_dict, input_features_dict = encoder_outputs\n\n    # setup encoder inputs to combiner based on test case\n    if not flatten_inputs:\n        # clean out rank-3 encoder outputs\n        for feature in [\"feature_3\", \"feature_4\"]:\n            del encoder_outputs_dict[feature]\n            del input_features_dict[feature]\n        if number_inputs == 1:\n            # need only one encoder output for the test\n            del encoder_outputs_dict[\"feature_2\"]\n            del input_features_dict[\"feature_2\"]\n    elif number_inputs == 1:\n        # require only one rank-3 encoder output for testing\n        for feature in [\"feature_1\", \"feature_2\", \"feature_3\"]:\n            del encoder_outputs_dict[feature]\n            del input_features_dict[feature]\n\n    # setup combiner to test with pseudo input features\n    combiner = ConcatCombiner(\n        input_features_dict,\n        config=load_config(ConcatCombinerConfig, fc_layers=fc_layer, flatten_inputs=flatten_inputs, norm=norm),\n    ).to(DEVICE)\n\n    # confirm correctness of input_shape property\n    assert isinstance(combiner.input_shape, dict)\n    for k in encoder_outputs_dict:\n        assert k in combiner.input_shape\n        assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k]\n\n    # combine encoder outputs\n    combiner_output = combiner(encoder_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    if fc_layer is not None:\n        # check for parameter updating if fully connected layer is present\n        target = torch.randn(combiner_output[\"combiner_output\"].shape)\n        fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target)\n        assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n# test for sequence concatenation combiner\n@pytest.mark.parametrize(\"reduce_output\", [None, \"sum\"])\n@pytest.mark.parametrize(\"main_sequence_feature\", [None, \"feature_3\"])\ndef test_sequence_concat_combiner(\n    encoder_outputs: tuple, main_sequence_feature: str | None, reduce_output: str | None\n) -> None:\n    # extract encoder outputs and input feature dictionaries\n    encoder_outputs_dict, input_feature_dict = encoder_outputs\n\n    # setup combiner for testing\n    combiner = SequenceConcatCombiner(\n        input_feature_dict,\n        config=load_config(\n            SequenceConcatCombinerConfig, main_sequence_feature=main_sequence_feature, reduce_output=reduce_output\n        ),\n    ).to(DEVICE)\n\n    # confirm correctness of input_shape property\n    assert isinstance(combiner.input_shape, dict)\n    for k in encoder_outputs_dict:\n        assert k in combiner.input_shape\n        assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k]\n\n    # calculate expected hidden size for concatenated tensors\n    hidden_size = 0\n    for k in encoder_outputs_dict:\n        hidden_size += encoder_outputs_dict[k][ENCODER_OUTPUT].shape[-1]\n\n    # confirm correctness of concatenated_shape\n    assert combiner.concatenated_shape[-1] == hidden_size\n\n    # combine encoder outputs\n    combiner_output = combiner(encoder_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # This combiner does not contain any learnable parameters, bypassing parameter update testing\n\n\n# test for sequence combiner\n@pytest.mark.parametrize(\"reduce_output\", [None, \"sum\"])\n@pytest.mark.parametrize(\"encoder\", [\"rnn\", \"transformer\"])\n@pytest.mark.parametrize(\"main_sequence_feature\", [None, \"feature_3\"])\ndef test_sequence_combiner(\n    encoder_outputs: tuple, main_sequence_feature: str | None, encoder: str, reduce_output: str | None\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs_dict, input_features_dict = encoder_outputs\n\n    combiner = SequenceCombiner(\n        input_features_dict,\n        config=load_config(\n            SequenceCombinerConfig,\n            main_sequence_feature=main_sequence_feature,\n            encoder={TYPE: encoder},\n            reduce_output=reduce_output,\n        ),\n        # following emulates encoder parameters passed in from config file\n        output_size=OUTPUT_SIZE,\n        num_fc_layers=3,\n    ).to(DEVICE)\n\n    # confirm correctness of input_shape property\n    assert isinstance(combiner.input_shape, dict)\n    for k in encoder_outputs_dict:\n        assert k in combiner.input_shape\n        assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k]\n\n    # calculate expected hidden size for concatenated tensors\n    hidden_size = 0\n    for k in encoder_outputs_dict:\n        hidden_size += encoder_outputs_dict[k][ENCODER_OUTPUT].shape[-1]\n\n    # confirm correctness of concatenated_shape\n    assert combiner.concatenated_shape[-1] == hidden_size\n\n    # combine encoder outputs\n    combiner_output = combiner(encoder_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\n    \"feature_list\",  # defines parameter for fixture features_to_test()\n    [\n        [  # only numeric features\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n        ],\n        [  # only numeric features\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n        ],\n        [  # numeric and categorical features\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"number\", [BATCH_SIZE, 12]),  # dense encoder\n            (\"category\", [BATCH_SIZE, 8]),  # dense encoder\n        ],\n    ],\n)\n@pytest.mark.parametrize(\"size\", [4, 8])\n@pytest.mark.parametrize(\"output_size\", [6, 10])\ndef test_tabnet_combiner(features_to_test: dict, size: int, output_size: int) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs, input_features = features_to_test\n\n    # setup combiner to test\n    combiner = TabNetCombiner(\n        input_features,\n        config=load_config(\n            TabNetCombinerConfig,\n            size=size,\n            output_size=output_size,\n            num_steps=3,\n            num_total_blocks=4,\n            num_shared_blocks=2,\n            dropout=0.1,\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs)\n\n    # required key present\n    assert \"combiner_output\" in combiner_output\n    assert \"attention_masks\" in combiner_output\n    assert \"aggregated_attention_masks\" in combiner_output\n\n    assert isinstance(combiner_output[\"combiner_output\"], torch.Tensor)\n    assert combiner_output[\"combiner_output\"].shape == (BATCH_SIZE, output_size)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"fc_layer\", [None, [{\"output_size\": 64}, {\"output_size\": 32}]])\n@pytest.mark.parametrize(\"entity_1\", [[\"text_feature_1\", \"text_feature_4\"]])\n@pytest.mark.parametrize(\"entity_2\", [[\"image_feature_1\", \"image_feature_2\"]])\ndef test_comparator_combiner(\n    encoder_comparator_outputs: tuple, fc_layer: list[dict] | None, entity_1: str, entity_2: str\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_comparator_outputs_dict, input_features_dict = encoder_comparator_outputs\n    # clean out unneeded encoder outputs since we only have 2 layers\n    del encoder_comparator_outputs_dict[\"text_feature_2\"]\n    del encoder_comparator_outputs_dict[\"image_feature_3\"]\n    del encoder_comparator_outputs_dict[\"text_feature_3\"]\n    del encoder_comparator_outputs_dict[\"image_feature_4\"]\n\n    # setup combiner to test set to 256 for case when none as it's the default size\n    output_size = fc_layer[0][\"output_size\"] if fc_layer else 256\n    combiner = ComparatorCombiner(\n        input_features_dict,\n        config=load_config(\n            ComparatorCombinerConfig, entity_1=entity_1, entity_2=entity_2, fc_layers=fc_layer, output_size=output_size\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_comparator_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_comparator_outputs_dict,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"output_size\", [8, 16])\n@pytest.mark.parametrize(\"transformer_output_size\", [4, 12])\ndef test_transformer_combiner(encoder_outputs: tuple, transformer_output_size: int, output_size: int) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs_dict, input_feature_dict = encoder_outputs\n\n    # setup combiner to test\n    combiner = TransformerCombiner(input_features=input_feature_dict, config=load_config(TransformerCombinerConfig)).to(\n        DEVICE\n    )\n\n    # confirm correctness of input_shape property\n    assert isinstance(combiner.input_shape, dict)\n    for k in encoder_outputs_dict:\n        assert k in combiner.input_shape\n        assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k]\n\n    # calculate expected hidden size for concatenated tensors\n    hidden_size = 0\n    for k in encoder_outputs_dict:\n        hidden_size += np.prod(encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:])\n\n    # confirm correctness of effective_input_shape\n    assert combiner.concatenated_shape[-1] == hidden_size\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"projection_size\", [8, 16])\n@pytest.mark.parametrize(\"output_size\", [8, 16])\ndef test_project_aggregate_combiner(encoder_outputs: tuple, projection_size: int, output_size: int) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    encoder_outputs_dict, input_feature_dict = encoder_outputs\n\n    # setup combiner to test\n    combiner = ProjectAggregateCombiner(\n        input_features=input_feature_dict,\n        config=load_config(\n            ProjectAggregateCombinerConfig,\n            projection_size=projection_size,\n            output_size=output_size,\n        ),\n    ).to(DEVICE)\n\n    # confirm correctness of input_shape property\n    assert isinstance(combiner.input_shape, dict)\n    for k in encoder_outputs_dict:\n        assert k in combiner.input_shape\n        assert encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:] == combiner.input_shape[k]\n\n    # calculate expected hidden size for concatenated tensors\n    hidden_size = 0\n    for k in encoder_outputs_dict:\n        hidden_size += np.prod(encoder_outputs_dict[k][ENCODER_OUTPUT].shape[1:])\n\n    # confirm correctness of effective_input_shape\n    assert combiner.concatenated_shape[-1] == hidden_size\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs_dict)\n\n    # check for correctness of combiner output\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(combiner, (encoder_outputs_dict,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n# Magic values for the TabTransformerCombiner test\nPARAMETERS_IN_SELF_ATTENTION = 4\nPARAMETERS_IN_TRANSFORMER_BLOCK = 16\nUNEMBEDDABLE_LAYER_NORM_PARAMETERS = 2\n\n\n@pytest.mark.parametrize(\n    \"feature_list\",  # defines parameter for fixture features_to_test()\n    [\n        [\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n        ],\n        [\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"number\", [BATCH_SIZE, 1]),\n        ],\n        [\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"binary\", [BATCH_SIZE, 1]),\n        ],\n    ],\n)\n@pytest.mark.parametrize(\n    \"num_layers,reduce_output,fc_layers,embed_input_feature_name\",\n    [\n        (1, \"concat\", None, None),\n        (2, \"sum\", [{\"output_size\": 256}], 64),\n        (1, \"sum\", None, \"add\"),\n    ],\n    ids=[\"simple\", \"full\", \"add_embed\"],\n)\ndef test_tabtransformer_combiner_binary_and_number_without_category(\n    features_to_test: tuple,\n    embed_input_feature_name: int | str | None,\n    fc_layers: list | None,\n    reduce_output: str,\n    num_layers: int,\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # retrieve simulated encoder outputs and input features for the test\n    encoder_outputs, input_features = features_to_test\n\n    # setup combiner to test\n    combiner = TabTransformerCombiner(\n        input_features=input_features,\n        config=load_config(\n            TabTransformerCombinerConfig,\n            embed_input_feature_name=embed_input_feature_name,\n            # emulates parameters passed from combiner def\n            num_layers=num_layers,  # number of transformer layers\n            fc_layers=fc_layers,  # fully_connected layer definition\n            reduce_output=reduce_output,  # sequence reducer\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs)\n\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        combiner,\n        (encoder_outputs,),\n        target,\n    )\n\n    # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed\n    # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the\n    # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to\n    # instantiate the TabTransformerCombiner object.\n\n    # The entire transformer stack is by-passed because there is no categorical input features.  Subtract the\n    # number for parameters in the transformer stack to account for this situation.\n\n    assert upc == (\n        tpc - num_layers * PARAMETERS_IN_TRANSFORMER_BLOCK - (1 if embed_input_feature_name is not None else 0)\n    ), f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\n    \"feature_list\",  # defines parameter for fixture features_to_test()\n    [\n        [\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"category\", [BATCH_SIZE, 64]),\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n        ],\n        [\n            (\"binary\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"category\", [BATCH_SIZE, 16]),\n            (\"number\", [BATCH_SIZE, 1]),  # passthrough encoder\n            (\"category\", [BATCH_SIZE, 48]),\n            (\"number\", [BATCH_SIZE, 32]),\n            (\"binary\", [BATCH_SIZE, 1]),\n        ],\n    ],\n)\n@pytest.mark.parametrize(\n    \"num_layers,reduce_output,fc_layers,embed_input_feature_name\",\n    [\n        (1, \"concat\", None, None),\n        (2, \"sum\", [{\"output_size\": 256}], 64),\n        (1, \"sum\", None, \"add\"),\n    ],\n    ids=[\"simple\", \"full\", \"add_embed\"],\n)\ndef test_tabtransformer_combiner_number_and_binary_with_category(\n    features_to_test: tuple,\n    embed_input_feature_name: int | str | None,\n    fc_layers: list | None,\n    reduce_output: str,\n    num_layers: int,\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # retrieve simulated encoder outputs and input features for the test\n    encoder_outputs, input_features = features_to_test\n\n    # setup combiner to test\n    combiner = TabTransformerCombiner(\n        input_features=input_features,\n        config=load_config(\n            TabTransformerCombinerConfig,\n            embed_input_feature_name=embed_input_feature_name,\n            # emulates parameters passed from combiner def\n            num_layers=num_layers,  # number of transformer layers\n            fc_layers=fc_layers,  # fully_connected layer definition\n            reduce_output=reduce_output,  # sequence reducer\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs)\n\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        combiner,\n        (encoder_outputs,),\n        target,\n    )\n\n    # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed\n    # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the\n    # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to\n    # instantiate the TabTransformerCombiner object.\n\n    # With F.scaled_dot_product_attention, all parameters receive gradients even with a single category feature.\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\n    \"feature_list\",  # defines parameter for fixture features_to_test()\n    [\n        [\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"binary\", [BATCH_SIZE, 1]),\n        ],\n        [\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"number\", [BATCH_SIZE, 1]),\n        ],\n        [\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"binary\", [BATCH_SIZE, 1]),\n        ],\n    ],\n)\n@pytest.mark.parametrize(\n    \"num_layers,reduce_output,fc_layers,embed_input_feature_name\",\n    [\n        (1, \"concat\", None, None),\n        (2, \"sum\", [{\"output_size\": 256}], 64),\n        (1, \"sum\", None, \"add\"),\n    ],\n    ids=[\"simple\", \"full\", \"add_embed\"],\n)\ndef test_tabtransformer_combiner_number_or_binary_without_category(\n    features_to_test: tuple,\n    embed_input_feature_name: int | str | None,\n    fc_layers: list | None,\n    reduce_output: str,\n    num_layers: int,\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # retrieve simulated encoder outputs and input features for the test\n    encoder_outputs, input_features = features_to_test\n\n    # setup combiner to test\n    combiner = TabTransformerCombiner(\n        input_features=input_features,\n        config=load_config(\n            TabTransformerCombinerConfig,\n            embed_input_feature_name=embed_input_feature_name,\n            # emulates parameters passed from combiner def\n            num_layers=num_layers,  # number of transformer layers\n            fc_layers=fc_layers,  # fully_connected layer definition\n            reduce_output=reduce_output,  # sequence reducer\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs)\n\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        combiner,\n        (encoder_outputs,),\n        target,\n    )\n\n    # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed\n    # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the\n    # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to\n    # instantiate the TabTransformerCombiner object.\n\n    # The entire transformer stack is by-passed because there is no categorical input features.  Subtract the\n    # number for parameters in the transformer stack to account for this situation.\n\n    assert upc == (\n        tpc - num_layers * PARAMETERS_IN_TRANSFORMER_BLOCK - (1 if embed_input_feature_name is not None else 0)\n    ), f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\n    \"feature_list\",  # defines parameter for fixture features_to_test()\n    [\n        [\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 16]),\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 32]),\n        ],\n        [\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 16]),\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 32]),\n        ],\n        [\n            (\"number\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 16]),\n            (\"binary\", [BATCH_SIZE, 1]),\n            (\"category\", [BATCH_SIZE, 32]),\n        ],\n    ],\n)\n@pytest.mark.parametrize(\n    \"num_layers,reduce_output,fc_layers,embed_input_feature_name\",\n    [\n        (1, \"concat\", None, None),\n        (2, \"sum\", [{\"output_size\": 256}], 64),\n        (1, \"sum\", None, \"add\"),\n    ],\n    ids=[\"simple\", \"full\", \"add_embed\"],\n)\ndef test_tabtransformer_combiner_number_or_binary_with_category(\n    features_to_test: tuple,\n    embed_input_feature_name: int | str | None,\n    fc_layers: list | None,\n    reduce_output: str,\n    num_layers: int,\n) -> None:\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # retrieve simulated encoder outputs and input features for the test\n    encoder_outputs, input_features = features_to_test\n\n    # setup combiner to test\n    combiner = TabTransformerCombiner(\n        input_features=input_features,\n        config=load_config(\n            TabTransformerCombinerConfig,\n            embed_input_feature_name=embed_input_feature_name,\n            # emulates parameters passed from combiner def\n            num_layers=num_layers,  # number of transformer layers\n            fc_layers=fc_layers,  # fully_connected layer definition\n            reduce_output=reduce_output,  # sequence reducer\n        ),\n    ).to(DEVICE)\n\n    # concatenate encoder outputs\n    combiner_output = combiner(encoder_outputs)\n\n    check_combiner_output(combiner, combiner_output, BATCH_SIZE)\n\n    # check for parameter updating\n    target = torch.randn(combiner_output[\"combiner_output\"].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        combiner,\n        (encoder_outputs,),\n        target,\n    )\n\n    # Adjustments to the trainable parameter count (tpc) in the following assertion checks is needed\n    # to account for the different code paths taken in the TabTransformerCombiner forward() method due to the\n    # combination of input feature types (NUMBER, BINARY, CATEGORY) in the dataset and parameters used to\n    # instantiate the TabTransformerCombiner object.\n\n    # This test does not explicity test for a single categorical input feature\n    # in this situation of a one categorical input feature, the query and key parameters are not updated\n\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/config_sampling/test_config_sampling.py",
    "content": "import pytest\n\nfrom ludwig.utils.data_utils import load_json\nfrom tests.training_success.test_training_success import (\n    combiner_config_generator,\n    defaults_config_generator,\n    ecd_trainer_config_generator,\n)\n\n\ndef full_config_generator(generator_fn, *args):\n    return len(list(generator_fn(*args)))\n\n\n@pytest.mark.combinatorial\n@pytest.mark.timeout(600)\ndef test_config_sampling():\n    static_schema = load_json(\"tests/ludwig/config_sampling/static_schema.json\")\n    total_count = 0\n\n    total_count += full_config_generator(defaults_config_generator, \"number\", \"preprocessing\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"number\", \"encoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"number\", \"decoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"number\", \"loss\", static_schema)\n\n    total_count += full_config_generator(defaults_config_generator, \"category\", \"preprocessing\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"category\", \"encoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"category\", \"decoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"category\", \"loss\", static_schema)\n\n    total_count += full_config_generator(defaults_config_generator, \"binary\", \"preprocessing\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"binary\", \"encoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"binary\", \"decoder\", static_schema)\n    total_count += full_config_generator(defaults_config_generator, \"binary\", \"loss\", static_schema)\n\n    total_count += full_config_generator(ecd_trainer_config_generator, static_schema)\n\n    total_count += full_config_generator(combiner_config_generator, \"sequence_concat\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"sequence\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"comparator\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"concat\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"project_aggregate\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"tabnet\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"tabtransformer\", static_schema)\n    total_count += full_config_generator(combiner_config_generator, \"transformer\", static_schema)\n\n    # In place to check for sudden changes in the number of combinatorially generated configs. Update ranges\n    # accordingly if new parameters are added.\n    assert 100 < total_count < 200\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_checks.py",
    "content": "\"\"\"Tests for interdependent parameters.\n\nNote that all testing should be done with the public API, rather than individual checks.\n\n``` ModelConfig.from_dict(config) ```\n\"\"\"\n\nimport contextlib\nfrom typing import Any\n\nimport pytest\nimport yaml\n\nfrom ludwig.constants import COMBINER, TYPE\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom tests.integration_tests.utils import binary_feature, text_feature\n\n\ndef test_passthrough_number_decoder():\n    config = {\n        \"defaults\": {\"number\": {\"decoder\": {\"fc_norm\": None, \"fc_output_size\": 10, \"type\": \"passthrough\"}}},\n        \"input_features\": [\n            {\"name\": \"MSSubClass\", \"type\": \"category\"},\n            {\"name\": \"MSZoning\", \"type\": \"category\"},\n            {\"name\": \"Street\", \"type\": \"category\"},\n            {\"name\": \"Neighborhood\", \"type\": \"category\"},\n        ],\n        \"model_type\": \"ecd\",\n        \"output_features\": [{\"name\": \"SalePrice\", \"type\": \"number\", \"decoder\": {\"type\": \"passthrough\"}}],\n        \"trainer\": {\"train_steps\": 1},\n    }\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_sequence_combiner_with_embed_encoder():\n    config = {\n        \"combiner\": {\n            \"encoder\": {\"dropout\": 0.1641014195584432, \"embedding_size\": 256, \"type\": \"embed\"},\n            \"main_sequence_feature\": None,\n            \"type\": \"sequence\",\n        },\n        \"input_features\": [{\"encoder\": {\"reduce_output\": None, \"type\": \"embed\"}, \"name\": \"Text\", \"type\": \"text\"}],\n        \"model_type\": \"ecd\",\n        \"output_features\": [{\"name\": \"Category\", \"type\": \"category\"}],\n        \"preprocessing\": {\"sample_ratio\": 0.05},\n        \"trainer\": {\"train_steps\": 1},\n    }\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_balance_multiple_class_failure():\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"proc_column\": \"Index\", \"type\": \"number\"},\n            {\"name\": \"random_1\", \"proc_column\": \"random_1\", \"type\": \"number\"},\n            {\"name\": \"random_2\", \"proc_column\": \"random_2\", \"type\": \"number\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"Label\", \"proc_column\": \"Label\", \"type\": \"binary\"},\n            {\"name\": \"Label2\", \"proc_column\": \"Label2\", \"type\": \"binary\"},\n        ],\n        \"preprocessing\": {\"oversample_minority\": 0.2},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_all_features_present_in_comparator_entities():\n    config = {\n        \"combiner\": {\n            \"dropout\": 0.20198506770751617,\n            \"entity_1\": [\"Age\"],\n            \"entity_2\": [\"Sex\", \"Pclass\"],\n            \"norm\": \"batch\",\n            \"num_fc_layers\": 1,\n            \"output_size\": 256,\n            \"type\": \"comparator\",\n        },\n        \"input_features\": [\n            {\"column\": \"Pclass\", \"name\": \"Pclass\", \"type\": \"category\"},\n            {\"column\": \"Sex\", \"name\": \"Sex\", \"type\": \"category\"},\n            {\"column\": \"Age\", \"name\": \"Age\", \"type\": \"number\"},\n            {\"column\": \"SibSp\", \"name\": \"SibSp\", \"type\": \"number\"},\n            {\"column\": \"Parch\", \"name\": \"Parch\", \"type\": \"number\"},\n            {\"column\": \"Fare\", \"name\": \"Fare\", \"type\": \"number\"},\n            {\"column\": \"Embarked\", \"name\": \"Embarked\", \"type\": \"category\"},\n        ],\n        \"model_type\": \"ecd\",\n        \"output_features\": [{\"column\": \"Survived\", \"name\": \"Survived\", \"type\": \"category\"}],\n        \"trainer\": {\"train_steps\": 1},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_balance_non_binary_failure():\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"proc_column\": \"Index\", \"type\": \"number\"},\n            {\"name\": \"random_1\", \"proc_column\": \"random_1\", \"type\": \"number\"},\n            {\"name\": \"random_2\", \"proc_column\": \"random_2\", \"type\": \"number\"},\n        ],\n        \"output_features\": [{\"name\": \"Label\", \"proc_column\": \"Label\", \"type\": \"number\"}],\n        \"preprocessing\": {\"oversample_minority\": 0.2},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_supported_features_config():\n    # ECD supports output text features.\n    ModelConfig.from_dict(\n        {\n            \"input_features\": [binary_feature()],\n            \"output_features\": [text_feature()],\n            \"model_type\": \"ecd\",\n        }\n    )\n\n\n@pytest.mark.parametrize(\n    \"num_fc_layers,fc_layers,expect_success\",\n    [\n        (None, None, True),\n        (1, None, True),\n        (None, [{\"output_size\": 256}], True),\n        (0, [{\"output_size\": 256}], True),\n        (0, None, False),\n    ],\n)\ndef test_comparator_fc_layer_config(num_fc_layers: int | None, fc_layers: dict[str, Any] | None, expect_success: bool):\n    config = {\n        \"input_features\": [\n            {\"name\": \"in1\", \"type\": \"category\"},\n            {\"name\": \"in2\", \"type\": \"category\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"out1\", \"type\": \"binary\"},\n        ],\n        \"combiner\": {\n            \"type\": \"comparator\",\n            \"entity_1\": [\"in1\"],\n            \"entity_2\": [\"in2\"],\n        },\n    }\n\n    if num_fc_layers is not None:\n        config[\"combiner\"][\"num_fc_layers\"] = num_fc_layers\n\n    if fc_layers is not None:\n        config[\"combiner\"][\"fc_layers\"] = fc_layers\n\n    with pytest.raises(ConfigValidationError) if not expect_success else contextlib.nullcontext():\n        ModelConfig.from_dict(config)\n\n\ndef test_dense_binary_encoder_0_layer():\n    config = {\n        \"defaults\": {\"binary\": {\"encoder\": {\"norm\": \"ghost\", \"num_layers\": 0, \"output_size\": 128, \"type\": \"dense\"}}},\n        \"input_features\": [\n            {\"name\": \"X0\", \"type\": \"category\"},\n            {\"name\": \"X1\", \"type\": \"category\"},\n            {\"name\": \"X10\", \"type\": \"binary\"},\n            {\"name\": \"X11\", \"type\": \"binary\"},\n            {\"name\": \"X14\", \"type\": \"binary\", \"encoder\": {\"num_layers\": 0}},\n        ],\n        \"model_type\": \"ecd\",\n        \"output_features\": [{\"name\": \"y\", \"type\": \"number\"}],\n        \"trainer\": {\"train_steps\": 1},\n    }\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"entity_1,entity_2,expected\",\n    [\n        ([\"a1\"], [\"b1\", \"b2\"], True),\n        ([\"a1\", \"a2\"], [\"b1\", \"b2\", \"b3\"], True),\n        ([], [\"b1\", \"b2\"], False),\n        ([], [\"a1\", \"b1\", \"b2\"], False),\n        ([\"a1\", \"b1\", \"b2\"], [], False),\n        ([\"a1\", \"b1\"], [\"b1\", \"b2\"], False),\n        ([\"a1\"], [\"b1\"], False),\n    ],\n)\ndef test_comparator_combiner_entities(entity_1: list[str], entity_2: list[str], expected: bool):\n    config = {\n        \"input_features\": [\n            {\"name\": \"a1\", \"type\": \"category\"},\n            {\"name\": \"b1\", \"type\": \"category\"},\n            {\"name\": \"b2\", \"type\": \"category\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"out1\", \"type\": \"binary\"},\n        ],\n        \"combiner\": {\n            \"type\": \"comparator\",\n            \"entity_1\": entity_1,\n            \"entity_2\": entity_2,\n        },\n    }\n\n    with pytest.raises(ConfigValidationError) if not expected else contextlib.nullcontext():\n        config_obj = ModelConfig.from_dict(config)\n        assert config_obj.combiner.entity_1 == [\"a1\"]\n        assert config_obj.combiner.entity_2 == [\"b1\", \"b2\"]\n\n\ndef test_experiment_binary_fill_with_const():\n    \"\"\"Test that the tagger decoder doesn't work with category input features.\"\"\"\n    config = {\n        \"defaults\": {\"binary\": {\"preprocessing\": {\"missing_value_strategy\": \"fill_with_const\"}}},\n        \"input_features\": [{\"name\": \"binary_1\", \"type\": \"binary\"}],\n        \"model_type\": \"ecd\",\n        \"output_features\": [{\"name\": \"category_output_1\", \"type\": \"category\"}],\n        \"trainer\": {\"train_steps\": 1},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_check_concat_combiner_requirements():\n    config = yaml.safe_load(\"\"\"\ninput_features:\n  - name: description\n    type: text\n    encoder:\n      type: embed\n      reduce_output: null\n    column: description\n  - name: required_experience\n    type: category\n    column: required_experience\noutput_features:\n  - name: title\n    type: category\ncombiner:\n    type: concat\ntrainer:\n  train_steps: 2\nmodel_type: ecd\n\"\"\")\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    # Confirms that the choice of the combiner type is the only reason for the ConfigValidationError.\n    config[COMBINER][TYPE] = \"sequence_concat\"\n    ModelConfig.from_dict(config)\n\n\ndef test_check_llm_input_features():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\ninput_features:\n  - name: sample_1\n    type: text\n  - name: sample_2\n    type: text\noutput_features:\n  - name: label\n    type: text\nbackend:\n  type: ray\n\"\"\")\n\n    # do not allow more than one input feature\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    # do not allow one non-text input feature\n    config[\"input_features\"].pop(-1)\n    config[\"input_features\"][0][\"type\"] = \"category\"\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    # allow exactly one text input feature\n    config[\"input_features\"][0][\"type\"] = \"text\"\n    ModelConfig.from_dict(config)\n\n\ndef test_retrieval_config_none_type():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\nprompt:\n    retrieval:\n        type: null\n        k: 1\n    task: \"Classify the sample input as either negative, neutral, or positive.\"\ninput_features:\n-\n    name: sample\n    type: text\noutput_features:\n-\n    name: label\n    type: text\n\"\"\")\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    # will not fail\n    config[\"prompt\"][\"retrieval\"][\"k\"] = 0\n    ModelConfig.from_dict(config)\n\n\ndef test_retrieval_config_random_type():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\nprompt:\n    retrieval:\n        type: random\n    task: \"Classify the sample input as either negative, neutral, or positive.\"\ninput_features:\n-\n    name: sample\n    type: text\noutput_features:\n-\n    name: label\n    type: text\n\"\"\")\n\n    # should not fail because we auto-set k=1 if k=0 on __post_init__\n    ModelConfig.from_dict(config)\n\n\ndef test_retrieval_config_semantic_type():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\nprompt:\n    retrieval:\n        type: semantic\n    task: \"Classify the sample input as either negative, neutral, or positive.\"\ninput_features:\n-\n    name: sample\n    type: text\noutput_features:\n-\n    name: label\n    type: text\n\"\"\")\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"prompt\"][\"retrieval\"][\"model_name\"] = \"some-huggingface-model\"\n    ModelConfig.from_dict(config)\n\n\n@pytest.mark.skip(\n    reason=\"TODO(geoffrey, arnav): re-enable this when we have reconciled the config with the backend kwarg in api.py\"\n)\ndef test_check_llm_quantization_backend_incompatibility():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\nquantization:\n  bits: 4\ninput_features:\n  - name: sample\n    type: text\noutput_features:\n  - name: label\n    type: text\nbackend:\n  type: ray\n\"\"\")\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"backend\"][\"type\"] = \"local\"\n    ModelConfig.from_dict(config)\n\n    del config[\"backend\"]\n    ModelConfig.from_dict(config)\n\n    del config[\"quantization\"]\n    config[\"backend\"] = {\"type\": \"ray\"}\n    ModelConfig.from_dict(config)\n\n\ndef test_check_qlora():\n    config = yaml.safe_load(\"\"\"\nmodel_type: llm\nbase_model: facebook/opt-350m\nquantization:\n  bits: 4\ninput_features:\n  - name: sample\n    type: text\noutput_features:\n  - name: label\n    type: text\ntrainer:\n  type: finetune\n\"\"\")\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"adapter\"] = {\n        \"type\": \"adaption_prompt\",\n    }\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"adapter\"] = {\n        \"type\": \"lora\",\n    }\n    ModelConfig.from_dict(config)\n\n\ndef test_check_prompt_requirements():\n    config = {\n        \"model_type\": \"llm\",\n        \"input_features\": [\n            text_feature(name=\"input1\", column=\"col1\", encoder={\"type\": \"passthrough\"}),\n        ],\n        \"output_features\": [text_feature(name=\"output1\")],\n        \"base_model\": \"opt-350m\",\n    }\n\n    ModelConfig.from_dict(config)\n\n    config[\"prompt\"] = {\"task\": \"Some task\"}\n    ModelConfig.from_dict(config)\n\n    config[\"prompt\"] = {\"task\": \"Some task\", \"template\": \"Some template not mentioning the task\"}\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"prompt\"] = {\"task\": \"Some task\", \"template\": \"{__invalid__}\"}\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"prompt\"] = {\"task\": \"Some task\", \"template\": \"{__task__}\"}\n    ModelConfig.from_dict(config)\n\n    config[\"prompt\"] = {\"template\": \"{input1}\"}\n    ModelConfig.from_dict(config)\n\n    # Raise an error if template has a placeholder for the output feature.\n    config[\"prompt\"] = {\"template\": \"{input1}: {output1}\"}\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_check_sample_ratio_and_size_compatible():\n    config = {\n        \"input_features\": [binary_feature()],\n        \"output_features\": [binary_feature()],\n        \"model_type\": \"ecd\",\n    }\n    ModelConfig.from_dict(config)\n\n    config[\"preprocessing\"] = {\"sample_size\": 10}\n    ModelConfig.from_dict(config)\n\n    config[\"preprocessing\"][\"sample_ratio\"] = 1\n    ModelConfig.from_dict(config)\n\n    config[\"preprocessing\"][\"sample_ratio\"] = 0.1\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[\"preprocessing\"][\"sample_size\"] = 0\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    del config[\"preprocessing\"][\"sample_size\"]\n    ModelConfig.from_dict(config)\n\n\ndef test_check_llm_text_encoder_is_not_used_with_ecd():\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"Question\",\n                \"type\": \"text\",\n                \"encoder\": {\n                    \"type\": \"auto_transformer\",\n                    \"pretrained_model_name_or_path\": \"meta-llama/Llama-2-7b-hf\",\n                    \"trainable\": False,\n                },\n                \"preprocessing\": {\"cache_encoder_embeddings\": True},\n            }\n        ],\n        \"output_features\": [{\"name\": \"Answer\", \"type\": \"text\"}],\n    }\n\n    with pytest.raises(ConfigValidationError) as excinfo:\n        ModelConfig.from_dict(config)\n\n    assert \"Please use the `model_type: llm` for text-to-text models.\" in str(excinfo.value)\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_combiner.py",
    "content": "import pytest\n\nfrom ludwig.config_validation.validation import check_schema, get_schema\nfrom ludwig.constants import MODEL_ECD, TRAINER\nfrom ludwig.error import ConfigValidationError\nfrom tests.integration_tests.utils import binary_feature, category_feature, number_feature\n\n\ndef test_combiner_schema_is_not_empty_for_ECD():\n    # Essentially verifies that the combiner registry is not empty at import time:\n    assert len(get_schema(MODEL_ECD)[\"properties\"][\"combiner\"][\"allOf\"]) > 0\n\n\n@pytest.mark.parametrize(\"eval_batch_size\", [500000, None])\ndef test_config_tabnet(eval_batch_size):\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n            \"size\": 24,\n            \"output_size\": 26,\n            \"sparsity\": 0.000001,\n            \"bn_virtual_divider\": 32,\n            \"bn_momentum\": 0.4,\n            \"num_steps\": 5,\n            \"relaxation_factor\": 1.5,\n            \"use_keras_batch_norm\": False,\n            \"bn_virtual_bs\": 512,\n        },\n        TRAINER: {\n            \"batch_size\": 16384,\n            \"eval_batch_size\": eval_batch_size,\n            \"epochs\": 1000,\n            \"early_stop\": 20,\n            \"learning_rate\": 0.02,\n            \"optimizer\": {\"type\": \"adam\"},\n            \"learning_rate_scheduler\": {\n                \"decay\": \"linear\",\n                \"decay_steps\": 20000,\n                \"decay_rate\": 0.9,\n                \"staircase\": True,\n            },\n            \"regularization_lambda\": 1,\n            \"regularization_type\": \"l2\",\n        },\n    }\n    check_schema(config)\n\n\ndef test_config_bad_combiner():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n        },\n    }\n\n    # config is valid at this point\n    check_schema(config)\n\n    # combiner without type\n    del config[\"combiner\"][\"type\"]\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # bad combiner type\n    config[\"combiner\"][\"type\"] = \"fake\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # bad combiner format (list instead of dict)\n    config[\"combiner\"] = [{\"type\": \"tabnet\"}]\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # bad combiner parameter types\n    config[\"combiner\"] = {\n        \"type\": \"tabtransformer\",\n        \"num_layers\": 10,\n        \"dropout\": False,\n    }\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # bad combiner parameter range\n    config[\"combiner\"] = {\n        \"type\": \"transformer\",\n        \"dropout\": -1,\n    }\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_config_bad_combiner_types_enums():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\"type\": \"concat\", \"weights_initializer\": \"zeros\"},\n    }\n\n    # config is valid at this point\n    check_schema(config)\n\n    # Test weights initializer:\n    config[\"combiner\"][\"weights_initializer\"] = {\"test\": \"fail\"}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"weights_initializer\"] = \"fail\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"weights_initializer\"] = {}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"weights_initializer\"] = {\"type\": \"fail\"}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"weights_initializer\"] = {\"type\": \"normal\", \"stddev\": 0}\n    check_schema(config)\n\n    # Test bias initializer:\n    del config[\"combiner\"][\"weights_initializer\"]\n    config[\"combiner\"][\"bias_initializer\"] = \"kaiming_uniform\"\n    check_schema(config)\n    config[\"combiner\"][\"bias_initializer\"] = \"fail\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"bias_initializer\"] = {}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"bias_initializer\"] = {\"type\": \"fail\"}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[\"combiner\"][\"bias_initializer\"] = {\"type\": \"zeros\", \"stddev\": 0}\n    check_schema(config)\n\n    # Test norm:\n    del config[\"combiner\"][\"bias_initializer\"]\n    config[\"combiner\"][\"norm\"] = \"batch\"\n    check_schema(config)\n    config[\"combiner\"][\"norm\"] = \"fail\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test activation:\n    del config[\"combiner\"][\"norm\"]\n    config[\"combiner\"][\"activation\"] = \"relu\"\n    check_schema(config)\n    config[\"combiner\"][\"activation\"] = 123\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test reduce_output:\n    del config[\"combiner\"][\"activation\"]\n    config2 = {**config}\n    config2[\"combiner\"][\"type\"] = \"tabtransformer\"\n    config2[\"combiner\"][\"reduce_output\"] = \"sum\"\n    check_schema(config)\n    config2[\"combiner\"][\"reduce_output\"] = \"fail\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config2)\n\n    # Test reduce_output = None:\n    config2[\"combiner\"][\"reduce_output\"] = None\n    check_schema(config2)\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_encoder.py",
    "content": "import pytest\n\nfrom ludwig.constants import DEFAULTS, ENCODER, INPUT_FEATURES, NAME, OUTPUT_FEATURES, SEQUENCE, TEXT, TIMESERIES, TYPE\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.model_config import ModelConfig\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    number_feature,\n    sequence_feature,\n    text_feature,\n    timeseries_feature,\n)\n\n\n@pytest.mark.parametrize(\"feature_type\", [SEQUENCE, TEXT, TIMESERIES])\ndef test_default_transformer_encoder(feature_type):\n    \"\"\"Tests that a transformer hyperparameter divisibility error is correctly recognized in feature defaults.\n\n    Transformers require that `hidden_size % num_heads == 0`. 9 and 18 were selected as test values because they were\n    the values from the original error.\n    \"\"\"\n    config = {\n        INPUT_FEATURES: [number_feature(), {TYPE: feature_type, NAME: f\"test_{feature_type}\"}],\n        OUTPUT_FEATURES: [binary_feature()],\n        DEFAULTS: {feature_type: {ENCODER: {TYPE: \"transformer\", \"hidden_size\": 9, \"num_heads\": 18}}},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        m = ModelConfig.from_dict(config)\n        print(m)\n\n    config[DEFAULTS][feature_type][ENCODER][\"hidden_size\"] = 18\n    config[DEFAULTS][feature_type][ENCODER][\"num_heads\"] = 9\n\n    ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\"feature_gen\", [sequence_feature, text_feature, timeseries_feature])\ndef test_input_feature_transformer_encoder(feature_gen):\n    \"\"\"Tests that a transformer hyperparameter divisibility error is correctly recognized for a specific feature.\n\n    Transformers require that `hidden_size % num_heads == 0`. 9 and 18 were selected as test values because they were\n    the values from the original error.\n    \"\"\"\n    config = {\n        INPUT_FEATURES: [\n            number_feature(),\n            feature_gen(**{ENCODER: {TYPE: \"transformer\", \"hidden_size\": 9, \"num_heads\": 18}}),\n        ],\n        OUTPUT_FEATURES: [binary_feature()],\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[INPUT_FEATURES][1][ENCODER][\"hidden_size\"] = 18\n    config[INPUT_FEATURES][1][ENCODER][\"num_heads\"] = 9\n\n    ModelConfig.from_dict(config)\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_features.py",
    "content": "import pytest\n\nfrom ludwig.config_validation.validation import check_schema\nfrom ludwig.error import ConfigValidationError\nfrom tests.integration_tests.utils import binary_feature, category_feature, number_feature, text_feature\n\n\ndef test_config_input_output_features():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\"}),\n            number_feature(encoder={\"type\": \"passthrough\"}),\n        ],\n        \"output_features\": [binary_feature(decoder={\"type\": \"regressor\"})],\n    }\n\n    check_schema(config)\n\n\ndef test_incorrect_input_features_config():\n    config = {\n        \"input_features\": [\n            category_feature(preprocessing={\"normalization\": \"zscore\"}),\n        ],\n        \"output_features\": [binary_feature()],\n    }\n\n    # TODO(ksbrar): Circle back after discussing whether additional properties should be allowed long-term.\n    # # Not a preprocessing param for category feature\n    # with pytest.raises(ValidationError):\n    #     check_schema(config)\n\n    config = {\n        \"input_features\": [\n            text_feature(preprocessing={\"padding_symbol\": 0}),\n        ],\n        \"output_features\": [binary_feature()],\n    }\n\n    # Incorrect type for padding_symbol preprocessing param\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    config = {\n        \"input_features\": [\n            binary_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n    }\n    del config[\"input_features\"][0][\"type\"]\n\n    # No type\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_incorrect_output_features_config():\n    config = {\n        \"input_features\": [\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature(decoder=\"classifier\")],\n    }\n\n    # Invalid decoder for binary output feature\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_too_few_features_config():\n    ifeatures = [number_feature()]\n    ofeatures = [binary_feature()]\n\n    check_schema(\n        {\n            \"input_features\": ifeatures,\n            \"output_features\": ofeatures,\n        }\n    )\n\n    # Must have at least one input feature\n    with pytest.raises(ConfigValidationError):\n        check_schema(\n            {\n                \"input_features\": [],\n                \"output_features\": ofeatures,\n            }\n        )\n\n    # Must have at least one output feature\n    with pytest.raises(ConfigValidationError):\n        check_schema(\n            {\n                \"input_features\": ifeatures,\n                \"output_features\": [],\n            }\n        )\n\n\ndef test_multi_output_features_config():\n    # Multi-output is fine for ECD\n    check_schema(\n        {\n            \"input_features\": [number_feature()],\n            \"output_features\": [binary_feature(), number_feature()],\n            \"model_type\": \"ecd\",\n        }\n    )\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_hyperopt.py",
    "content": "from itertools import repeat\nfrom unittest.mock import patch\n\nimport pytest\n\n# Imported to populate the registry\nimport ludwig.schema.hyperopt.parameter  # noqa: F401\nimport ludwig.schema.hyperopt.scheduler  # noqa: F401\nimport ludwig.schema.hyperopt.search_algorithm  # noqa: F401\nfrom ludwig.constants import (\n    EXECUTOR,\n    HYPEROPT,\n    INPUT_FEATURES,\n    OUTPUT_FEATURES,\n    PARAMETERS,\n    SCHEDULER,\n    SEARCH_ALG,\n    TYPE,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.hyperopt import utils\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom tests.integration_tests.utils import binary_feature, text_feature\n\n\n@pytest.mark.parametrize(\n    \"dependencies,raises_exception\",\n    [\n        ([], False),\n        ([(\"ludwig\", \"ludwig\")], False),\n        ([(\"ludwig\", \"ludwig\"), (\"marshmallow\", \"marshmallow\")], False),\n        ([(\"fake_dependency\", \"fake_dependency\")], True),\n        ([(\"ludwig\", \"ludwig\"), (\"fake_dependency\", \"fake_dependency\")], True),\n    ],\n)\ndef test_check_scheduler_dependencies_installed(dependencies, raises_exception):\n    config = {\n        INPUT_FEATURES: [text_feature()],\n        OUTPUT_FEATURES: [binary_feature()],\n        HYPEROPT: {\n            PARAMETERS: {\"trainer.learning_rate\": {\"space\": \"choice\", \"categories\": [0.0001, 0.001, 0.01, 0.1]}},\n            EXECUTOR: {SCHEDULER: {TYPE: \"fifo\"}},\n        },\n    }\n\n    with patch(\"ludwig.schema.hyperopt.utils.get_scheduler_dependencies\", return_value=dependencies):\n        if raises_exception:\n            with pytest.raises(ConfigValidationError):\n                ModelConfig.from_dict(config)\n        else:\n            ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"dependencies,raises_exception\",\n    [\n        ([], False),\n        ([(\"ludwig\", \"ludwig\")], False),\n        ([(\"ludwig\", \"ludwig\"), (\"marshmallow\", \"marshmallow\")], False),\n        ([(\"fake_dependency\", \"fake_dependency\")], True),\n        ([(\"ludwig\", \"ludwig\"), (\"fake_dependency\", \"fake_dependency\")], True),\n    ],\n)\ndef test_check_search_algorithm_dependencies_installed(dependencies, raises_exception):\n    config = {\n        INPUT_FEATURES: [text_feature()],\n        OUTPUT_FEATURES: [binary_feature()],\n        HYPEROPT: {\n            PARAMETERS: {\"trainer.learning_rate\": {\"space\": \"choice\", \"categories\": [0.0001, 0.001, 0.01, 0.1]}},\n            SEARCH_ALG: {TYPE: \"random\"},\n        },\n    }\n\n    with patch(\"ludwig.schema.hyperopt.utils.get_search_algorithm_dependencies\", return_value=dependencies):\n        if raises_exception:\n            with pytest.raises(ConfigValidationError):\n                ModelConfig.from_dict(config)\n        else:\n            ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"space,raises_exception\",\n    list(zip(utils.parameter_config_registry.keys(), repeat(False, len(utils.parameter_config_registry))))\n    + [(\"fake_space\", True)],\n)\ndef test_parameter_type_check(space, raises_exception):\n    \"\"\"Test that the parameter type is a valid hyperparameter search space.\n\n    This should only be valid until the search space schema is updated to validate spaces as config objects rather than\n    dicts. That update is non-trivial, so to hold over until it is ready we cast the dicts to the corresponding\n    parameter objects and validate as an aux check. The test covers every valid space and one invalid space.\n    \"\"\"\n    config = {\n        INPUT_FEATURES: [text_feature()],\n        OUTPUT_FEATURES: [binary_feature()],\n        HYPEROPT: {\n            SEARCH_ALG: {TYPE: \"random\"},\n            PARAMETERS: {\n                \"trainer.learning_rate\": {\n                    \"space\": space,\n                }\n            },\n        },\n    }\n\n    if not raises_exception:\n        ModelConfig.from_dict(config)\n    else:\n        with pytest.raises(ConfigValidationError):\n            ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"referenced_parameter,raises_exception\",\n    [\n        # Passing cases\n        (\"trainer.learning_rate\", False),\n        (\"in_feature.encoder.num_fc_layers\", False),\n        (\"out_feature.decoder.num_fc_layers\", False),\n        # Invalid cases with various nesting of invalid names\n        (\"\", True),\n        (\" \", True),\n        (\"foo.bar\", True),\n        (\"trainer.bar\", True),\n        (\"foo.learning_rate\", True),\n        (\"in_feature.encoder.bar\", True),\n        (\"in_feature.foo.num_fc_layers\", True),\n        (\"out_feature.encoder.bar\", True),\n        (\"out_feature.foo.num_fc_layers\", True),\n    ],\n)\ndef test_parameter_key_check(referenced_parameter, raises_exception):\n    \"\"\"Test that references to config parameters are validated correctly.\n\n    Hyperopt parameters reference the config parameters they search with `.` notation to access different subsections,\n    e.g. `trainer.learning_rate`. These are added to the config as arbitrary strings, and an invalid reference should be\n    considered a validation error since we will otherwise search over an unused space or defer the error to train time.\n    \"\"\"\n    config = {\n        INPUT_FEATURES: [text_feature(name=\"in_feature\")],\n        OUTPUT_FEATURES: [binary_feature(name=\"out_feature\")],\n        HYPEROPT: {\n            SEARCH_ALG: {TYPE: \"random\"},\n            PARAMETERS: {referenced_parameter: {\"space\": \"choice\", \"categories\": [1, 2, 3, 4]}},\n        },\n    }\n\n    if raises_exception:\n        with pytest.raises(ConfigValidationError):\n            ModelConfig.from_dict(config)\n    else:\n        ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"categories,raises_exception\",\n    [\n        # Passing case\n        (\n            [\n                {\n                    \"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            False,\n        ),\n        # Errors in top level parameter names (4 cases)\n        (\n            [\n                {\n                    \"foo\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\n                    \"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"foo\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\n                    \"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"foo\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\n                    \"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"combiner\": {\"type\": \"concat\"}, \"foo\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        # Errors in nested parameters (6 cases)\n        (\n            [\n                {\"combiner\": {\"bar\": \"tabnet\", \"bn_virtual_bs\": 256}, \"trainer\": {\"bar\": 0.001, \"batch_size\": 64}},\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\"combiner\": {\"type\": \"tabnet\", \"bar\": 256}, \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64}},\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            False,\n        ),\n        (\n            [\n                {\"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256}, \"trainer\": {\"bar\": 0.001, \"batch_size\": 64}},\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256}, \"trainer\": {\"bar\": 0.001, \"batch_size\": 64}},\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256}, \"trainer\": {\"learning_rate\": 0.001, \"bar\": 64}},\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"batch_size\": 256}},\n            ],\n            True,\n        ),\n        (\n            [\n                {\n                    \"combiner\": {\"type\": \"tabnet\", \"bn_virtual_bs\": 256},\n                    \"trainer\": {\"learning_rate\": 0.001, \"batch_size\": 64},\n                },\n                {\"combiner\": {\"type\": \"concat\"}, \"trainer\": {\"bar\": 256}},\n            ],\n            True,\n        ),\n    ],\n)\ndef test_nested_parameter_key_check(categories, raises_exception):\n    \"\"\"Test that nested parameters are validated correctly.\"\"\"\n    config = {\n        INPUT_FEATURES: [text_feature(name=\"in_feature\")],\n        OUTPUT_FEATURES: [binary_feature(name=\"out_feature\")],\n        HYPEROPT: {SEARCH_ALG: {TYPE: \"random\"}, PARAMETERS: {\".\": {\"space\": \"choice\", \"categories\": categories}}},\n    }\n\n    if raises_exception:\n        with pytest.raises(ConfigValidationError):\n            ModelConfig.from_dict(config)\n    else:\n        ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"config\",\n    [\n        {\n            \"out_feature.decoder.fc_layers\": {\n                \"space\": \"choice\",\n                \"categories\": [\n                    [{\"output_size\": 64}, {\"output_size\": 32}],\n                    [{\"output_size\": 64}],\n                    [{\"output_size\": 32}],\n                ],\n            }\n        }\n    ],\n)\ndef test_flat_parameter_edge_cases(config):\n    config = {\n        INPUT_FEATURES: [text_feature(name=\"in_feature\")],\n        OUTPUT_FEATURES: [binary_feature(name=\"out_feature\")],\n        HYPEROPT: {SEARCH_ALG: {TYPE: \"random\"}, PARAMETERS: config},\n    }\n\n    ModelConfig.from_dict(config)\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_misc.py",
    "content": "import pytest\n\nfrom ludwig.config_validation.validation import check_schema, get_schema\nfrom ludwig.constants import (\n    ACTIVE,\n    BACKEND,\n    CATEGORY,\n    COLUMN,\n    DECODER,\n    DEFAULTS,\n    ENCODER,\n    LOSS,\n    MODEL_ECD,\n    MODEL_LLM,\n    NAME,\n    PREPROCESSING,\n    PROC_COLUMN,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.features.feature_registries import get_output_type_registry\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.combiners.utils import get_combiner_jsonschema\nfrom ludwig.schema.defaults.ecd import ECDDefaultsConfig\nfrom ludwig.schema.features.preprocessing.audio import AudioPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.bag import BagPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.binary import BinaryPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.category import CategoryPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.date import DatePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.h3 import H3PreprocessingConfig\nfrom ludwig.schema.features.preprocessing.image import ImagePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.number import NumberPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.sequence import SequencePreprocessingConfig\nfrom ludwig.schema.features.preprocessing.set import SetPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.text import TextPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.timeseries import TimeseriesPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.vector import VectorPreprocessingConfig\nfrom ludwig.schema.features.utils import get_input_feature_jsonschema, get_output_feature_jsonschema\nfrom ludwig.schema.llms.peft import LoraConfig\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom ludwig.schema.utils import ludwig_dataclass, unload_jsonschema_from_marshmallow_class\nfrom tests.integration_tests.utils import (\n    audio_feature,\n    bag_feature,\n    binary_feature,\n    category_feature,\n    date_feature,\n    ENCODERS,\n    h3_feature,\n    image_feature,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\n\ndef test_config_features():\n    all_input_features = [\n        audio_feature(\"/tmp/destination_folder\", encoder={\"type\": \"parallel_cnn\"}),\n        bag_feature(encoder={\"type\": \"embed\"}),\n        binary_feature(encoder={\"type\": \"passthrough\"}),\n        category_feature(encoder={\"type\": \"dense\"}),\n        date_feature(encoder={\"type\": \"embed\"}),\n        h3_feature(encoder={\"type\": \"embed\"}),\n        image_feature(\"/tmp/destination_folder\", encoder={\"type\": \"stacked_cnn\"}),\n        number_feature(encoder={\"type\": \"passthrough\"}),\n        sequence_feature(encoder={\"type\": \"parallel_cnn\"}),\n        set_feature(encoder={\"type\": \"embed\"}),\n        text_feature(encoder={\"type\": \"parallel_cnn\"}),\n        timeseries_feature(encoder={\"type\": \"parallel_cnn\"}),\n        vector_feature(encoder={\"type\": \"dense\"}),\n    ]\n    all_output_features = [\n        binary_feature(decoder={\"type\": \"regressor\"}),\n        category_feature(decoder={\"type\": \"classifier\"}),\n        number_feature(decoder={\"type\": \"regressor\"}),\n        sequence_feature(decoder={\"type\": \"generator\"}),\n        set_feature(decoder={\"type\": \"classifier\"}),\n        text_feature(decoder={\"type\": \"generator\"}),\n        vector_feature(decoder={\"type\": \"projector\"}),\n    ]\n\n    # validate config with all features\n    config = {\n        \"input_features\": all_input_features,\n        \"output_features\": all_output_features,\n    }\n    check_schema(config)\n\n    # test various invalid output features\n    input_only_features = [\n        feature for feature in all_input_features if feature[\"type\"] not in get_output_type_registry().keys()\n    ]\n    for input_feature in input_only_features:\n        config = {\n            \"input_features\": all_input_features,\n            \"output_features\": all_output_features + [input_feature],\n        }\n\n        with pytest.raises(ConfigValidationError):\n            check_schema(config)\n\n\ndef test_config_encoders():\n    for encoder in ENCODERS:\n        config = {\n            \"input_features\": [\n                sequence_feature(encoder={\"type\": encoder, \"reduce_output\": \"sum\"}),\n                image_feature(\"/tmp/destination_folder\"),\n            ],\n            \"output_features\": [category_feature(decoder={\"type\": \"classifier\", \"vocab_size\": 2}, reduce_input=\"sum\")],\n            \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n        }\n        check_schema(config)\n\n\ndef test_config_with_backend():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n            \"size\": 24,\n            \"output_size\": 26,\n            \"sparsity\": 0.000001,\n            \"bn_virtual_divider\": 32,\n            \"bn_momentum\": 0.4,\n            \"num_steps\": 5,\n            \"relaxation_factor\": 1.5,\n            \"bn_virtual_bs\": 512,\n        },\n        TRAINER: {\n            \"batch_size\": 16384,\n            \"eval_batch_size\": 500000,\n            \"epochs\": 1000,\n            \"early_stop\": 20,\n            \"learning_rate\": 0.02,\n            \"optimizer\": {\"type\": \"adam\"},\n            \"learning_rate_scheduler\": {\n                \"decay\": \"linear\",\n                \"decay_steps\": 20000,\n                \"decay_rate\": 0.9,\n                \"staircase\": True,\n            },\n            \"regularization_lambda\": 1,\n            \"regularization_type\": \"l2\",\n        },\n        BACKEND: {\"type\": \"ray\", \"trainer\": {\"num_workers\": 2}},\n    }\n    check_schema(config)\n\n\ndef test_config_bad_feature_type():\n    config = {\n        \"input_features\": [{\"name\": \"foo\", \"type\": \"fake\"}],\n        \"output_features\": [category_feature(encoder={\"vocab_size\": 2}, reduce_input=\"sum\")],\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_config_bad_encoder_name():\n    config = {\n        \"input_features\": [sequence_feature(encoder={\"type\": \"fake\", \"reduce_output\": \"sum\"})],\n        \"output_features\": [category_feature(decoder={\"type\": \"classifier\", \"vocab_size\": 2}, reduce_input=\"sum\")],\n        \"combiner\": {\"type\": \"concat\", \"output_size\": 14},\n    }\n\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_config_fill_values():\n    vector_fill_values = [\"1.0 0.0 1.04 10.49\", \"1 2 3 4 5\" \"0\" \"1.0\" \"\"]\n    binary_fill_values = [\"yes\", \"No\", \"1\", \"TRUE\", 1]\n    for vector_fill_value, binary_fill_value in zip(vector_fill_values, binary_fill_values):\n        config = {\n            \"input_features\": [\n                vector_feature(preprocessing={\"fill_value\": vector_fill_value}),\n            ],\n            \"output_features\": [binary_feature(preprocessing={\"fill_value\": binary_fill_value})],\n        }\n        check_schema(config)\n\n    bad_vector_fill_values = [\"one two three\", \"1,2,3\", 0]\n    bad_binary_fill_values = [\"one\", 2, \"maybe\"]\n    for vector_fill_value, binary_fill_value in zip(bad_vector_fill_values, bad_binary_fill_values):\n        config = {\n            \"input_features\": [\n                vector_feature(preprocessing={\"fill_value\": vector_fill_value}),\n            ],\n            \"output_features\": [binary_feature(preprocessing={\"fill_value\": binary_fill_value})],\n        }\n        with pytest.raises(ConfigValidationError):\n            check_schema(config)\n\n\ndef test_validate_with_preprocessing_defaults():\n    config = {\n        \"input_features\": [\n            audio_feature(\n                \"/tmp/destination_folder\",\n                preprocessing=AudioPreprocessingConfig().to_dict(),\n                encoder={\"type\": \"parallel_cnn\"},\n            ),\n            bag_feature(preprocessing=BagPreprocessingConfig().to_dict(), encoder={\"type\": \"embed\"}),\n            binary_feature(preprocessing=BinaryPreprocessingConfig().to_dict(), encoder={\"type\": \"passthrough\"}),\n            category_feature(preprocessing=CategoryPreprocessingConfig().to_dict(), encoder={\"type\": \"dense\"}),\n            date_feature(preprocessing=DatePreprocessingConfig().to_dict(), encoder={\"type\": \"embed\"}),\n            h3_feature(preprocessing=H3PreprocessingConfig().to_dict(), encoder={\"type\": \"embed\"}),\n            image_feature(\n                \"/tmp/destination_folder\",\n                preprocessing=ImagePreprocessingConfig().to_dict(),\n                encoder={\"type\": \"stacked_cnn\"},\n            ),\n            number_feature(preprocessing=NumberPreprocessingConfig().to_dict(), encoder={\"type\": \"passthrough\"}),\n            sequence_feature(preprocessing=SequencePreprocessingConfig().to_dict(), encoder={\"type\": \"parallel_cnn\"}),\n            set_feature(preprocessing=SetPreprocessingConfig().to_dict(), encoder={\"type\": \"embed\"}),\n            text_feature(preprocessing=TextPreprocessingConfig().to_dict(), encoder={\"type\": \"parallel_cnn\"}),\n            timeseries_feature(\n                preprocessing=TimeseriesPreprocessingConfig().to_dict(), encoder={\"type\": \"parallel_cnn\"}\n            ),\n            vector_feature(preprocessing=VectorPreprocessingConfig().to_dict(), encoder={\"type\": \"dense\"}),\n        ],\n        \"output_features\": [{\"name\": \"target\", \"type\": \"category\"}],\n        TRAINER: {\n            \"learning_rate_scheduler\": {\n                \"decay\": \"linear\",\n            },\n            \"learning_rate\": 0.001,\n            \"validation_field\": \"target\",\n            \"validation_metric\": \"accuracy\",\n        },\n    }\n\n    check_schema(config)\n\n\ndef test_ecd_defaults_schema():\n    schema = ECDDefaultsConfig()\n    assert schema.binary.decoder.type == \"regressor\"\n    assert schema.binary.encoder.type == \"passthrough\"\n    assert schema.category.encoder.dropout == 0.0\n    assert ENCODER in schema.category.to_dict()\n    assert PREPROCESSING in schema.category.to_dict()\n    assert DECODER in schema.category.to_dict()\n    assert LOSS in schema.category.to_dict()\n\n\ndef test_validate_defaults_schema():\n    config = {\n        \"input_features\": [\n            category_feature(),\n            number_feature(),\n        ],\n        \"output_features\": [category_feature(output_feature=True)],\n        \"defaults\": {\n            \"category\": {\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"drop_row\",\n                },\n                \"encoder\": {\n                    \"type\": \"sparse\",\n                },\n                \"decoder\": {\n                    \"type\": \"classifier\",\n                    \"norm_params\": None,\n                    \"dropout\": 0.0,\n                    \"use_bias\": True,\n                },\n                \"loss\": {\n                    \"type\": \"softmax_cross_entropy\",\n                    \"confidence_penalty\": 0,\n                },\n            },\n            \"number\": {\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"fill_with_const\",\n                    \"fill_value\": 0,\n                },\n                \"loss\": {\"type\": \"mean_absolute_error\"},\n            },\n        },\n    }\n\n    check_schema(config)\n\n    config[DEFAULTS][CATEGORY][NAME] = \"TEST\"\n\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_validate_no_trainer_type():\n    config = {\n        \"model_type\": \"ecd\",\n        \"input_features\": [\n            category_feature(),\n            number_feature(),\n        ],\n        \"output_features\": [category_feature(output_feature=True)],\n        \"trainer\": {\"learning_rate\": \"auto\", \"batch_size\": \"auto\"},\n    }\n\n    # Ensure validation succeeds with ECD trainer params and ECD model type\n    check_schema(config)\n\n\ndef test_schema_no_duplicates():\n    schema = get_schema()\n\n    popped_fields = [NAME, TYPE, COLUMN, PROC_COLUMN, ACTIVE]\n\n    for field in popped_fields:\n        assert field not in schema[\"properties\"][\"input_features\"][\"items\"][\"allOf\"][0][\"then\"][\"properties\"]\n        assert field not in schema[\"properties\"][\"output_features\"][\"items\"][\"allOf\"][0][\"then\"][\"properties\"]\n        assert field not in schema[\"properties\"][\"combiner\"][\"allOf\"][0][\"then\"][\"properties\"]\n        assert field not in schema[\"properties\"][\"trainer\"][\"properties\"][\"optimizer\"][\"allOf\"][0][\"then\"][\"properties\"]\n        assert (\n            field\n            not in schema[\"properties\"][\"input_features\"][\"items\"][\"allOf\"][0][\"then\"][\"properties\"][\"encoder\"][\n                \"allOf\"\n            ][0][\"then\"][\"properties\"]\n        )\n        assert (\n            field\n            not in schema[\"properties\"][\"output_features\"][\"items\"][\"allOf\"][0][\"then\"][\"properties\"][\"decoder\"][\n                \"allOf\"\n            ][0][\"then\"][\"properties\"]\n        )\n\n\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD, MODEL_LLM])\ndef test_ludwig_schema_serialization(model_type):\n    import json\n\n    schema = get_schema(model_type)\n\n    try:\n        json.dumps(schema)\n    except TypeError as e:\n        raise TypeError(\n            f\"Ludwig schema of type `{model_type}` cannot be represented by valid JSON. See further details: {e}\"\n        )\n\n\ndef test_encoder_descriptions():\n    \"\"\"This test tests that each encoder in the enum for each feature type has a description.\"\"\"\n    schema = get_input_feature_jsonschema(MODEL_ECD)\n\n    for feature_schema in schema[\"allOf\"]:\n        type_data = feature_schema[\"then\"][\"properties\"][\"encoder\"][\"properties\"][\"type\"]\n        assert len(set(type_data[\"enumDescriptions\"].keys())) > 0\n        assert set(type_data[\"enumDescriptions\"].keys()).issubset(set(type_data[\"enum\"]))\n\n\ndef test_combiner_descriptions():\n    \"\"\"This test tests that each combiner in the enum for available combiners has a description.\"\"\"\n    combiner_json_schema = get_combiner_jsonschema()\n    type_data = combiner_json_schema[\"properties\"][\"type\"]\n    assert len(set(type_data[\"enumDescriptions\"].keys())) > 0\n    assert set(type_data[\"enumDescriptions\"].keys()).issubset(set(type_data[\"enum\"]))\n\n\ndef test_decoder_descriptions():\n    \"\"\"This test tests that each decoder in the enum for each feature type has a description.\"\"\"\n    schema = get_output_feature_jsonschema(MODEL_ECD)\n\n    for feature_schema in schema[\"allOf\"]:\n        type_data = feature_schema[\"then\"][\"properties\"][\"decoder\"][\"properties\"][\"type\"]\n        assert len(type_data[\"enumDescriptions\"].keys()) > 0\n        assert set(type_data[\"enumDescriptions\"].keys()).issubset(set(type_data[\"enum\"]))\n\n\ndef test_deprecation_warning_raised_for_unknown_parameters():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n            \"unknown_parameter_combiner\": False,\n        },\n        TRAINER: {\n            \"epochs\": 1000,\n        },\n    }\n    with pytest.warns(DeprecationWarning, match=\"not a valid parameter\"):\n        ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"encoder_config,expected_adapter\",\n    [\n        ({\"type\": \"bert\", \"trainable\": True}, None),\n        ({\"type\": \"bert\", \"trainable\": True, \"adapter\": None}, None),\n        ({\"type\": \"bert\", \"trainable\": True, \"adapter\": {\"type\": \"lora\"}}, LoraConfig()),\n        (\n            {\n                \"type\": \"bert\",\n                \"trainable\": True,\n                \"adapter\": {\"type\": \"lora\", \"r\": 16, \"alpha\": 32, \"dropout\": 0.1, \"bias_type\": \"all\"},\n            },\n            LoraConfig(r=16, alpha=32, dropout=0.1, bias_type=\"all\"),\n        ),\n    ],\n)\ndef test_text_encoder_adapter(encoder_config, expected_adapter):\n    config = {\n        \"input_features\": [text_feature(encoder=encoder_config)],\n        \"output_features\": [category_feature(decoder={\"type\": \"classifier\", \"vocab_size\": 2}, reduce_input=\"sum\")],\n    }\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.input_features[0].encoder.adapter == expected_adapter\n\n\ndef test_default_param_metadata():\n    @ludwig_dataclass\n    class TestClass(schema_utils.BaseMarshmallowConfig):\n        test_schema_entry: str = schema_utils.StringOptions(\n            options=[\"test\"],\n            default=\"test\",\n            description=\"\",\n        )\n\n    test_class = unload_jsonschema_from_marshmallow_class(TestClass)\n\n    assert test_class[\"properties\"][\"test_schema_entry\"][\"parameter_metadata\"] is not None\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_preprocessing.py",
    "content": "import pytest\n\nfrom ludwig.config_validation.preprocessing import check_global_max_sequence_length_fits_prompt_template\nfrom ludwig.config_validation.validation import check_schema\nfrom tests.integration_tests.utils import binary_feature, category_feature\n\n\ndef test_config_preprocessing():\n    input_features = [category_feature(), category_feature()]\n    output_features = [binary_feature()]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"preprocessing\": {\n            \"split\": {\n                \"type\": \"random\",\n                \"probabilities\": [0.6, 0.2, 0.2],\n            },\n            \"oversample_minority\": 0.4,\n        },\n    }\n\n    check_schema(config)\n\n    # TODO(ksbrar): Circle back after discussing whether additional properties should be allowed long-term.\n    # config[\"preprocessing\"][\"fake_parameter\"] = True\n\n    # with pytest.raises(Exception):\n    #     ModelConfig(config)\n\n\ndef test_check_global_max_sequence_length_fits_prompt_template():\n    check_global_max_sequence_length_fits_prompt_template(\n        {\"input_feature\": {\"prompt_template_num_tokens\": 10}}, {\"global_max_sequence_length\": 10}\n    )\n    check_global_max_sequence_length_fits_prompt_template(\n        {\"input_feature\": {\"prompt_template_num_tokens\": 100}}, {\"global_max_sequence_length\": 1000}\n    )\n    check_global_max_sequence_length_fits_prompt_template(\n        {\"input_feature\": {\"prompt_template_num_tokens\": 100}}, {\"global_max_sequence_length\": None}\n    )\n\n    with pytest.raises(ValueError):\n        # Prompt template token length cannot be larger than the global max sequence length.\n        check_global_max_sequence_length_fits_prompt_template(\n            {\"input_feature\": {\"prompt_template_num_tokens\": 10}}, {\"global_max_sequence_length\": 5}\n        )\n\n    with pytest.raises(ValueError):\n        # Any input feature's prompt template token length can trigger the global max sequence length.\n        check_global_max_sequence_length_fits_prompt_template(\n            {\"input_feature\": {\"prompt_template_num_tokens\": 5}, \"input_feature_2\": {\"prompt_template_num_tokens\": 20}},\n            {\"global_max_sequence_length\": 10},\n        )\n"
  },
  {
    "path": "tests/ludwig/config_validation/test_validate_config_trainer.py",
    "content": "import pytest\n\nfrom ludwig.config_validation.validation import check_schema\nfrom ludwig.constants import TRAINER\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.optimizers import optimizer_registry\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom tests.integration_tests.utils import binary_feature, category_feature, number_feature\n\n# Note: simple tests for now, but once we add dependent fields we can add tests for more complex relationships in this\n# file. Currently verifies that the nested fields work, as the others are covered by basic marshmallow validation:\n\n\ndef test_config_trainer_empty_null_and_default():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n        },\n        TRAINER: {},\n    }\n    check_schema(config)\n\n    config[TRAINER] = None\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    config[TRAINER] = ECDTrainerConfig.Schema().dump({})\n    check_schema(config)\n\n\ndef test_config_trainer_bad_optimizer():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n        },\n        TRAINER: {},\n    }\n    check_schema(config)\n\n    # Test manually set-to-null optimizer vs unspecified:\n    config[TRAINER][\"optimizer\"] = None\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    assert ECDTrainerConfig.Schema().load({}).optimizer is not None\n\n    # Test all types in optimizer_registry supported:\n    for key in optimizer_registry.keys():\n        config[TRAINER][\"optimizer\"] = {\"type\": key}\n        check_schema(config)\n\n    # Test invalid optimizer type:\n    config[TRAINER][\"optimizer\"] = {\"type\": 0}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[TRAINER][\"optimizer\"] = {\"type\": \"invalid\"}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n\ndef test_optimizer_property_validation():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n        },\n        TRAINER: {},\n    }\n    check_schema(config)\n\n    # Test that an optimizer's property types are enforced:\n    config[TRAINER][\"optimizer\"] = {\"type\": \"rmsprop\"}\n    check_schema(config)\n\n    config[TRAINER][\"optimizer\"][\"momentum\"] = \"invalid\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test extra keys are excluded and defaults are loaded appropriately:\n    config[TRAINER][\"optimizer\"][\"momentum\"] = 10\n    config[TRAINER][\"optimizer\"][\"extra_key\"] = \"invalid\"\n    check_schema(config)\n    assert not hasattr(ECDTrainerConfig.Schema().load(config[TRAINER]).optimizer, \"extra_key\")\n\n    # Test bad parameter range:\n    config[TRAINER][\"optimizer\"] = {\"type\": \"rmsprop\", \"eps\": -1}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test config validation for tuple types:\n    config[TRAINER][\"optimizer\"] = {\"type\": \"adam\", \"betas\": (0.1, 0.1)}\n    check_schema(config)\n\n\ndef test_clipper_property_validation():\n    config = {\n        \"input_features\": [\n            category_feature(encoder={\"type\": \"dense\", \"vocab_size\": 2}, reduce_input=\"sum\"),\n            number_feature(),\n        ],\n        \"output_features\": [binary_feature()],\n        \"combiner\": {\n            \"type\": \"tabnet\",\n        },\n        TRAINER: {},\n    }\n    check_schema(config)\n\n    # Test null/empty clipper:\n    config[TRAINER][\"gradient_clipping\"] = None\n    check_schema(config)\n    config[TRAINER][\"gradient_clipping\"] = {}\n    check_schema(config)\n    assert (\n        ECDTrainerConfig.Schema().load(config[TRAINER]).gradient_clipping\n        == ECDTrainerConfig.Schema().load({}).gradient_clipping\n    )\n\n    # Test invalid clipper type:\n    config[TRAINER][\"gradient_clipping\"] = 0\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n    config[TRAINER][\"gradient_clipping\"] = \"invalid\"\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test that an optimizer's property types are enforced:\n    config[TRAINER][\"gradient_clipping\"] = {\"clipglobalnorm\": None}\n    check_schema(config)\n    config[TRAINER][\"gradient_clipping\"] = {\"clipglobalnorm\": 1}\n    check_schema(config)\n    config[TRAINER][\"gradient_clipping\"] = {\"clipglobalnorm\": \"invalid\"}\n    with pytest.raises(ConfigValidationError):\n        check_schema(config)\n\n    # Test extra keys are excluded and defaults are loaded appropriately:\n    config[TRAINER][\"gradient_clipping\"] = {\"clipnorm\": 1}\n    config[TRAINER][\"gradient_clipping\"][\"extra_key\"] = \"invalid\"\n    assert not hasattr(ECDTrainerConfig.Schema().load(config[TRAINER]).gradient_clipping, \"extra_key\")\n"
  },
  {
    "path": "tests/ludwig/contrib/test_contrib.py",
    "content": "import argparse\nfrom collections.abc import Sequence\n\nimport pytest\n\nfrom ludwig.contrib import add_contrib_callback_args\nfrom ludwig.contribs.aim import AimCallback\nfrom ludwig.contribs.comet import CometCallback\nfrom ludwig.contribs.mlflow import MlflowCallback\nfrom ludwig.contribs.wandb import WandbCallback\n\n\n@pytest.mark.parametrize(\n    \"sys_argv,expected\",\n    [\n        ([], []),\n        ([\"--mlflow\"], [MlflowCallback]),\n        ([\"--aim\"], [AimCallback]),\n        ([\"--comet\"], [CometCallback]),\n        ([\"--wandb\"], [WandbCallback]),\n    ],\n)\ndef test_add_contrib_callback_args(sys_argv: Sequence[str], expected: list[type]):\n    parser = argparse.ArgumentParser()\n    add_contrib_callback_args(parser)\n    args = parser.parse_args(sys_argv)\n    callbacks = args.callbacks or []\n\n    assert len(callbacks) == len(expected)\n    for callback, expected_cls in zip(callbacks, expected):\n        assert isinstance(callback, expected_cls)\n"
  },
  {
    "path": "tests/ludwig/data/dataframe/test_dask.py",
    "content": "import pandas as pd\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom tests.integration_tests.utils import generate_data_as_dataframe\n\n\n@pytest.mark.distributed\ndef test_from_ray_dataset_empty(tmpdir, ray_cluster_2cpu):\n    import dask.dataframe as dd\n\n    # Verifies that when the dataset is an empty MapBatches(BatchInferModel), we mitigate Ray's native to_dask()\n    # IndexError.\n    config = {\n        \"input_features\": [\n            {\"name\": \"cat1\", \"type\": \"category\", \"vocab_size\": 2},\n            {\"name\": \"num1\", \"type\": \"number\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"bin1\", \"type\": \"binary\"},\n        ],\n        \"trainer\": {\"epochs\": 1},\n    }\n    train_input_df = generate_data_as_dataframe(config[\"input_features\"], config[\"output_features\"])\n    model = LudwigModel(config, backend=\"ray\")\n    model.train(\n        train_input_df,\n        output_directory=tmpdir,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_processed_output=True,\n        skip_save_processed_input=True,\n    )\n\n    predict_input_df = dd.from_pandas(pd.DataFrame([], columns=[\"cat1\", \"num1\", \"bin1\"]), npartitions=1)\n    model.predict(predict_input_df)\n"
  },
  {
    "path": "tests/ludwig/data/test_cache_util.py",
    "content": "import copy\nimport uuid\nfrom unittest import mock\n\nimport pytest\n\nfrom ludwig.constants import INPUT_FEATURES, OUTPUT_FEATURES\nfrom ludwig.data.cache.util import calculate_checksum\nfrom ludwig.schema.model_types.base import ModelConfig\nfrom ludwig.types import FeatureConfigDict, ModelConfigDict\nfrom ludwig.utils.misc_utils import merge_dict\n\n\ndef _gen_config(input_features: list[FeatureConfigDict]) -> ModelConfigDict:\n    return {INPUT_FEATURES: input_features, OUTPUT_FEATURES: [{\"name\": \"out1\", \"type\": \"binary\"}]}\n\n\n@pytest.mark.parametrize(\n    \"input_features,diff,expected\",\n    [\n        (\n            [\n                {\n                    \"name\": \"in1\",\n                    \"type\": \"text\",\n                    \"encoder\": {\"type\": \"parallel_cnn\"},\n                }\n            ],\n            [\n                {\n                    \"encoder\": {\"type\": \"stacked_cnn\"},\n                }\n            ],\n            True,\n        ),\n        (\n            [\n                {\n                    \"name\": \"in1\",\n                    \"type\": \"text\",\n                    \"preprocessing\": {\"cache_encoder_embeddings\": True},\n                    \"encoder\": {\"type\": \"bert\"},\n                }\n            ],\n            [\n                {\n                    \"encoder\": {\"type\": \"distilbert\"},\n                }\n            ],\n            False,\n        ),\n    ],\n)\ndef test_calculate_checksum(input_features: list[FeatureConfigDict], diff: list[FeatureConfigDict], expected: bool):\n    config = _gen_config(input_features)\n\n    diff_features = [merge_dict(f, df) for f, df in zip(input_features, diff)]\n    diff_config = _gen_config(diff_features)\n\n    mock_dataset = mock.Mock()\n    mock_dataset.checksum = uuid.uuid4().hex\n\n    assert (\n        calculate_checksum(mock_dataset, ModelConfig.from_dict(config).to_dict())\n        == calculate_checksum(mock_dataset, ModelConfig.from_dict(diff_config).to_dict())\n    ) == expected\n\n\ndef test_proc_col_checksum_consistency():\n    \"\"\"Tests that proc_col is equal if checksum are equal.\"\"\"\n    config_dict1 = {\n        \"input_features\": [{\"name\": \"txt1\", \"type\": \"text\", \"encoder\": {\"type\": \"bert\"}}],\n        \"output_features\": [{\"name\": \"bin1\", \"type\": \"binary\"}],\n    }\n    config1 = ModelConfig.from_dict(config_dict1)\n\n    config_dict2 = copy.deepcopy(config_dict1)\n    config_dict2[\"input_features\"][0][\"preprocessing\"] = {\n        \"tokenizer\": \"bert\",\n    }\n    config2 = ModelConfig.from_dict(config_dict2)\n\n    mock_dataset = mock.Mock()\n    mock_dataset.checksum = uuid.uuid4().hex\n    assert calculate_checksum(mock_dataset, config1.to_dict()) == calculate_checksum(mock_dataset, config2.to_dict())\n\n    for if1, if2 in zip(config1.input_features, config2.input_features):\n        assert if1.name == if2.name\n        assert if1.proc_column == if2.proc_column\n\n    for of1, of2 in zip(config1.output_features, config2.output_features):\n        assert of1.name == of2.name\n        assert of1.proc_column == of2.proc_column\n\n\ndef test_proc_col_checksum_consistency_same_preprocessing_different_types():\n    \"\"\"Tests that proc_col is different if preprocessing and names are the same but types are different.\"\"\"\n    config = {\n        \"input_features\": [\n            # Same name, different types, same preprocessing\n            {\"name\": \"num1\", \"type\": \"number\", \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mode\"}},\n            {\"name\": \"num2\", \"type\": \"category\", \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mode\"}},\n        ],\n        \"output_features\": [\n            {\"name\": \"num3\", \"type\": \"number\", \"preprocessing\": {\"missing_value_strategy\": \"fill_with_mode\"}}\n        ],\n    }\n    config = ModelConfig.from_dict(config)\n\n    assert config.input_features[0].proc_column != config.input_features[1].proc_column\n\n\n@pytest.mark.distributed\ndef test_checksum_determinism(ray_cluster_2cpu):\n    \"\"\"Tests that checksums are deterministic across different processes (no unordered hash maps).\"\"\"\n    import ray\n\n    # Generate a lot of features so the probability of a reordering of feature sets is very high.\n    config = {\n        INPUT_FEATURES: [{\"name\": f\"in{i}\", \"type\": \"number\"} for i in range(100)],\n        OUTPUT_FEATURES: [{\"name\": \"out1\", \"type\": \"binary\"}],\n    }\n    config = ModelConfig.from_dict(config)\n\n    mock_dataset = mock.Mock()\n    mock_dataset.checksum = uuid.uuid4().hex\n\n    @ray.remote(max_calls=1)\n    def calculate_checksum_remote(dataset, config):\n        return calculate_checksum(dataset, config)\n\n    # Run each checksum calculation as a remote function so it gets its own Python interpreter, as\n    # the hash function in Python is deterministic within a process, but not between different processes.\n    # See: https://docs.python.org/3/reference/datamodel.html#object.__hash__\n    checksum1 = ray.get(calculate_checksum_remote.remote(mock_dataset, config.to_dict()))\n    checksum2 = ray.get(calculate_checksum_remote.remote(mock_dataset, config.to_dict()))\n    assert checksum1 == checksum2\n"
  },
  {
    "path": "tests/ludwig/data/test_dataset_synthesizer.py",
    "content": "from ludwig.data import dataset_synthesizer\n\n\ndef test_build_synthetic_dataset(tmpdir):\n    features = [\n        {\"name\": \"text\", \"type\": \"text\"},\n        {\"name\": \"category\", \"type\": \"category\"},\n        {\"name\": \"number\", \"type\": \"number\"},\n        {\"name\": \"binary\", \"type\": \"binary\"},\n        {\"name\": \"set\", \"type\": \"set\"},\n        {\"name\": \"bag\", \"type\": \"bag\"},\n        {\"name\": \"sequence\", \"type\": \"sequence\"},\n        {\"name\": \"timeseries\", \"type\": \"timeseries\"},\n        {\"name\": \"date\", \"type\": \"date\"},\n        {\"name\": \"h3\", \"type\": \"h3\"},\n        {\"name\": \"vector\", \"type\": \"vector\"},\n        {\"name\": \"audio\", \"type\": \"audio\"},\n        {\"name\": \"image\", \"type\": \"image\"},\n    ]\n    assert len(list(dataset_synthesizer.build_synthetic_dataset(100, features, tmpdir))) == 101  # Extra for the header.\n"
  },
  {
    "path": "tests/ludwig/data/test_negative_sampling.py",
    "content": "import pandas as pd\n\nfrom ludwig.data.negative_sampling import negative_sample\n\n\ndef test_negative_sample():\n    df = pd.DataFrame(\n        {\n            \"user_id\": [1, 1, 2, 2, 3],\n            \"item_id\": [\"a\", \"b\", \"b\", \"c\", \"a\"],\n            \"label\": [1, 1, 1, 1, 1],\n        }\n    )\n\n    df_with_samples = negative_sample(df, \"user_id\", \"item_id\", \"label\")\n\n    assert 9 <= len(df_with_samples) <= 10\n    assert df_with_samples[\"label\"].sum() == 5\n\n    # Check data types\n    assert df_with_samples[\"user_id\"].dtype == \"int64\"\n    assert df_with_samples[\"item_id\"].dtype == \"object\"\n\n    # Check that the negative samples are unique user-item pairs\n    assert len(df_with_samples) == len(df_with_samples.drop_duplicates([\"user_id\", \"item_id\"]))\n"
  },
  {
    "path": "tests/ludwig/data/test_postprocessing.py",
    "content": "import torch\n\nfrom ludwig.data.postprocessing import convert_dict_to_df\n\n\ndef test_convert_dict_to_df():\n    d = {\n        \"binary_C82EB\": {\n            \"predictions\": torch.tensor([True, True, True, False]),\n            \"probabilities\": torch.tensor([[0.4777, 0.5223], [0.4482, 0.5518], [0.4380, 0.5620], [0.5059, 0.4941]]),\n        },\n        \"category_1491D\": {\n            \"predictions\": [\"NkNUG\", \"NkNUG\", \"NkNUG\", \"NkNUG\"],\n            \"probabilities\": torch.tensor(\n                [\n                    [0.1058, 0.4366, 0.1939, 0.2637],\n                    [0.0816, 0.4807, 0.1978, 0.2399],\n                    [0.0907, 0.4957, 0.1829, 0.2308],\n                    [0.0728, 0.5015, 0.1900, 0.2357],\n                ]\n            ),\n        },\n        \"num_7B25F\": {\"predictions\": torch.tensor([2.0436, 2.1158, 2.1222, 2.1964])},\n    }\n\n    df = convert_dict_to_df(d)\n\n    assert df.shape == (4, 5)\n    # Check that all elements in nested lists are stored in each row\n    assert all(len(row) == 2 for row in df[\"binary_C82EB_probabilities\"])\n    assert all(len(row) == 4 for row in df[\"category_1491D_probabilities\"])\n"
  },
  {
    "path": "tests/ludwig/data/test_preprocessing.py",
    "content": "from ludwig.data.preprocessing import is_input_feature\nfrom tests.integration_tests.utils import text_feature\n\n\ndef test_is_input_feature():\n    # Adds encoder when output_feature=False\n    assert is_input_feature(text_feature(output_feature=False)) is True\n    # Adds decoder when output_feature=True\n    assert is_input_feature(text_feature(output_feature=True)) is False\n"
  },
  {
    "path": "tests/ludwig/data/test_ray_data.py",
    "content": "import os\nimport shutil\nfrom unittest import mock\n\nimport pandas as pd\nimport pytest\n\n# Skip these tests if Ray is not installed\nray = pytest.importorskip(\"ray\")  # noqa\ndask = pytest.importorskip(\"dask\")  # noqa\n\nfrom ludwig.data.dataset.ray import RayDatasetBatcher, read_remote_parquet  # noqa\n\n# Mark the entire module as distributed\npytestmark = pytest.mark.distributed\n\n\ndef test_async_reader_error():\n    \"\"\"Test that RayDatasetBatcher handles a dataset that produces no batches.\n\n    When the dataset's iter_batches raises an error in the producer thread, the batcher should end up with\n    last_batch=True (no data to consume).\n    \"\"\"\n    mock_dataset = mock.Mock()\n    # map_batches returns a mock whose iter_batches yields nothing (empty iteration)\n    mock_mapped = mock.Mock()\n    mock_mapped.iter_batches.return_value = iter([])\n    mock_dataset.map_batches.return_value = mock_mapped\n\n    features = {\n        \"num1\": {\"name\": \"num1\", \"type\": \"number\"},\n        \"bin1\": {\"name\": \"bin1\", \"type\": \"binary\"},\n    }\n    training_set_metadata = {\n        \"num1\": {},\n        \"bin1\": {},\n    }\n\n    batcher = RayDatasetBatcher(\n        dataset=mock_dataset,\n        features=features,\n        training_set_metadata=training_set_metadata,\n        batch_size=64,\n        samples_per_epoch=100,\n    )\n    # With no data to read, the batcher should immediately signal last batch\n    assert batcher.last_batch()\n\n\n@pytest.fixture(scope=\"module\")\ndef parquet_file(ray_cluster_2cpu) -> str:\n    \"\"\"Write a multi-file parquet dataset to the cwd.\n\n    Returns:\n        The path to the parquet dataset.\n    \"\"\"\n    # The data needs to be written to a multi-file parquet format, otherwise the issue doesn't repro. To do this, we\n    # partitition a test dataframe with dask and then write to file.\n    df = pd.DataFrame({\"col1\": list(range(1000)), \"col2\": list(range(1000))})\n    df = dask.dataframe.from_pandas(df, chunksize=100)\n\n    # Typically we would write test data to a temporary directory, but the issue this was set up to test only happens\n    # when using relative filepaths.\n    cwd = os.getcwd()\n    filepath = os.path.join(cwd, \"data.training.parquet\")\n    df.to_parquet(filepath, engine=\"pyarrow\")\n\n    yield filepath\n\n    # Clean up the data\n    shutil.rmtree(filepath)\n\n\n@pytest.fixture(scope=\"module\", params=[\"absolute\", \"relative\"])\ndef parquet_filepath(parquet_file: str, request: \"pytest.FixtureRequest\") -> str:\n    \"\"\"Convert a filepath in the CWD to either an absolute or relative path.\n\n    Args:\n        parquet_file: Absolute path to a parquet file in the CWD\n        request: pytest request fixture with the fixture parameters\n\n    Returns:\n        Either the absolute or relative path of the parquet file.\n    \"\"\"\n    filepath_type = request.param\n    return parquet_file if filepath_type == \"absolute\" else os.path.basename(parquet_file)\n\n\ndef test_read_remote_parquet(parquet_filepath: str):\n    \"\"\"Test for the fix to https://github.com/ludwig-ai/ludwig/issues/3440.\n\n    Parquet file reads will fail with `pyarrow.lib.ArrowInvalid` under the following conditions:\n        1) The Parquet data is in multi-file format\n        2) A relative filepath is passed to the read function\n        3) A filesystem object is passed to the read function\n\n    The issue can be resolved by either:\n        1) Passing an absolute filepath\n        2) Not passing a filesystem object\n    \"\"\"\n    read_remote_parquet(parquet_filepath)\n"
  },
  {
    "path": "tests/ludwig/data/test_split.py",
    "content": "from datetime import datetime, timedelta\nfrom itertools import combinations\nfrom random import randrange\nfrom unittest.mock import Mock\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.data.dataframe.pandas import PandasEngine\nfrom ludwig.data.split import get_splitter\n\ntry:\n    from ludwig.data.dataframe.dask import DaskEngine\nexcept ImportError:\n    DaskEngine = Mock\n\n\ndef test_make_divisions_ensure_minimum_rows():\n    from ludwig.data.split import _make_divisions_ensure_minimum_rows\n\n    # Constraints are satisfied, the function should make no change to divisions.\n    divisions = _make_divisions_ensure_minimum_rows((70, 80), 100, min_val_rows=3, min_test_rows=3)\n    assert divisions[0] == 70\n    assert divisions[1] == 80\n    # Constraints are satisfied, the function should make no change to divisions.\n    divisions = _make_divisions_ensure_minimum_rows((20, 22), 25, min_val_rows=0, min_test_rows=0)\n    assert divisions[0] == 20\n    assert divisions[1] == 22\n    # The number of rows in validation set is too small.\n    divisions = _make_divisions_ensure_minimum_rows((17, 19), 25, min_val_rows=3, min_test_rows=3)\n    assert divisions[0] == 16\n    assert divisions[1] == 19\n    # The number of rows in validation and test sets are both too small.\n    divisions = _make_divisions_ensure_minimum_rows((20, 22), 25, min_val_rows=3, min_test_rows=3)\n    assert divisions[0] == 19\n    assert divisions[1] == 22\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_random_split(df_engine, ray_cluster_2cpu):\n    nrows = 100\n    npartitions = 10\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    probs = (0.7, 0.1, 0.2)\n    split_params = {\n        \"type\": \"random\",\n        \"probabilities\": probs,\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend, random_seed=42)\n\n    assert len(splits) == 3\n    for split, p in zip(splits, probs):\n        if isinstance(df_engine, DaskEngine):\n            # Dask splitting is not exact, so apply soft constraint here\n            assert np.isclose(len(split), int(nrows * p), atol=5)\n        else:\n            assert len(split) == int(nrows * p)\n\n    # Test determinism\n    def compute(dfs):\n        return [df.compute() if isinstance(backend.df_engine, DaskEngine) else df for df in dfs]\n\n    splits = compute(splits)\n    splits2 = compute(splitter.split(df, backend, random_seed=7))\n    for s1, s2 in zip(splits, splits2):\n        assert not s1.equals(s2)\n\n    splits3 = compute(splitter.split(df, backend, random_seed=42))\n    for s1, s3 in zip(splits, splits3):\n        assert s1.equals(s3)\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_random_split_zero_probability_for_test_produces_no_zombie(df_engine, ray_cluster_2cpu):\n    nrows = 102\n    npartitions = 10\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    probs = (0.7, 0.3, 0.0)\n    split_params = {\n        \"type\": \"random\",\n        \"probabilities\": probs,\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend, random_seed=42)\n\n    assert len(splits[-1]) == 0\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_fixed_split(df_engine, ray_cluster_2cpu):\n    nrows = 100\n    npartitions = 10\n    thresholds = [60, 80, 100]\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n\n    def get_split(v):\n        if v < thresholds[0]:\n            return 0\n        if thresholds[0] <= v < thresholds[1]:\n            return 1\n        return 2\n\n    df[\"split_col\"] = df[\"C\"].map(get_split).astype(np.int8)\n\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    split_params = {\n        \"type\": \"fixed\",\n        \"column\": \"split_col\",\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend)\n\n    assert len(splits) == 3\n\n    last_t = 0\n    for split, t in zip(splits, thresholds):\n        if isinstance(df_engine, DaskEngine):\n            split = split.compute()\n\n        assert np.all(split[\"C\"] < t)\n        assert np.all(split[\"C\"] >= last_t)\n        last_t = t\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\", \"nrows\", \"atol\"),\n    [\n        pytest.param(PandasEngine(), 100, 1, id=\"pandas\"),\n        # Splitting with a distributed engine becomes more accurate with more rows.\n        pytest.param(DaskEngine(_use_ray=False), 10000, 10, id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\n@pytest.mark.parametrize(\n    \"class_probs\",\n    [\n        pytest.param(np.array([0.33, 0.33, 0.34]), id=\"balanced\"),\n        pytest.param(np.array([0.6, 0.2, 0.2]), id=\"imbalanced\"),\n    ],\n)\ndef test_stratify_split(df_engine, nrows, atol, class_probs, ray_cluster_2cpu):\n    npartitions = 10\n    thresholds = np.cumsum((class_probs * nrows).astype(int))\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n\n    def get_category(v):\n        if v < thresholds[0]:\n            return 0\n        if thresholds[0] <= v < thresholds[1]:\n            return 1\n        return 2\n\n    df[\"category\"] = df.index.map(get_category).astype(np.int8)\n\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    probs = (0.7, 0.1, 0.2)\n    split_params = {\n        \"type\": \"stratify\",\n        \"column\": \"category\",\n        \"probabilities\": probs,\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend, random_seed=42)\n    assert len(splits) == 3\n\n    ratios = class_probs * nrows\n    for split, p in zip(splits, probs):\n        if isinstance(df_engine, DaskEngine):\n            split = split.compute()\n        for idx, r in enumerate(ratios):\n            actual = np.sum(split[\"category\"] == idx)\n            expected = int(r * p)\n            assert np.isclose(actual, expected, atol=atol)\n\n    # Test determinism\n    splits2 = splitter.split(df, backend, random_seed=7)\n    for s1, s2 in zip(splits, splits2):\n        if isinstance(df_engine, DaskEngine):\n            s1 = s1.compute()\n            s2 = s2.compute()\n        assert not s1.equals(s2)\n\n    splits3 = splitter.split(df, backend, random_seed=42)\n    for s1, s3 in zip(splits, splits3):\n        if isinstance(df_engine, DaskEngine):\n            s1 = s1.compute()\n            s3 = s3.compute()\n        assert s1.equals(s3)\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\", \"atol\"),\n    [\n        pytest.param(PandasEngine(), 1, id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), 10, id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_single_occurrence_stratified_split(df_engine, atol, ray_cluster_2cpu):\n    nrows = 1000\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 2)), columns=[\"A\", \"B\"])\n    # create 4 classes, where two of them each occurs once in the dataframe.\n    df[\"category\"] = (nrows // 2 - 1) * [0, 1] + [2, 3]\n\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=10)\n\n    probs = (0.7, 0.1, 0.2)\n    split_params = {\n        \"type\": \"stratify\",\n        \"column\": \"category\",\n        \"probabilities\": probs,\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend, random_seed=42)\n    assert len(splits) == 3\n\n    ratios = np.array([0.499, 0.499, 0.001, 0.001]) * nrows\n    for split, p in zip(splits, probs):\n        if isinstance(df_engine, DaskEngine):\n            split = split.compute()\n        for idx, r in enumerate(ratios):\n            actual = np.sum(split[\"category\"] == idx)\n            expected = int(r * p)\n            assert np.isclose(actual, expected, atol=atol)\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\n@pytest.mark.parametrize(\"format\", [\"str\", \"datetime\"])\ndef test_datetime_split(format, df_engine, ray_cluster_2cpu):\n    nrows = 100\n    npartitions = 10\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n\n    def random_date(*args, **kwargs):\n        start = datetime.strptime(\"1/1/1990 1:30 PM\", \"%m/%d/%Y %I:%M %p\")\n        end = datetime.strptime(\"1/1/2022 4:50 AM\", \"%m/%d/%Y %I:%M %p\")\n        delta = end - start\n        int_delta = (delta.days * 24 * 60 * 60) + delta.seconds\n        random_second = randrange(int_delta)\n        t = start + timedelta(seconds=random_second)\n        return str(t) if format == \"str\" else t\n\n    df[\"date_col\"] = df[\"C\"].map(random_date)\n\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    probs = (0.7, 0.1, 0.2)\n    split_params = {\n        \"type\": \"datetime\",\n        \"column\": \"date_col\",\n        \"probabilities\": probs,\n    }\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend)\n\n    assert len(splits) == 3\n\n    min_datestr = \"1990-01-01 00:00:00\"\n    for split, p in zip(splits, probs):\n        if isinstance(df_engine, DaskEngine):\n            # Dask splitting is not exact, so apply soft constraint here\n            split = split.compute()\n            assert len(split) >= 1\n            # Dask splitting is not exact, so we can potentially apply soft constraint. However, this can also be flaky:\n            # https://github.com/ludwig-ai/ludwig/actions/runs/4590907163/jobs/8106746310?pr=3315.\n            # assert np.isclose(len(split), int(nrows * p), atol=15)\n        else:\n            assert len(split) == int(nrows * p)\n\n        assert np.all(split[\"date_col\"] > min_datestr)\n        min_datestr = split[\"date_col\"].max()\n\n\n@pytest.mark.parametrize(\n    (\"df_engine\",),\n    [\n        pytest.param(PandasEngine(), id=\"pandas\"),\n        pytest.param(DaskEngine(_use_ray=False), id=\"dask\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_hash_split(df_engine, ray_cluster_2cpu):\n    nrows = 100\n    npartitions = 10\n\n    df = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n    df[\"id\"] = np.arange(0, 100)\n\n    if isinstance(df_engine, DaskEngine):\n        df = df_engine.df_lib.from_pandas(df, npartitions=npartitions)\n\n    probabilities = [0.8, 0.1, 0.1]\n    split_params = {\"type\": \"hash\", \"column\": \"id\", \"probabilities\": probabilities}\n    splitter = get_splitter(**split_params)\n\n    backend = Mock()\n    backend.df_engine = df_engine\n    splits = splitter.split(df, backend)\n    assert len(splits) == 3\n    if isinstance(df_engine, DaskEngine):\n        splits = [split.compute() for split in splits]\n\n    # IDs should not overlap between splits\n    assert all([set(split1[\"id\"]).isdisjoint(set(split2[\"id\"])) for split1, split2 in combinations(splits, 2)])\n\n    for split, p in zip(splits, probabilities):\n        # Should be approximately the same size as the desired proportion\n        assert nrows * p - 5 <= len(split[\"id\"]) <= nrows * p + 5\n\n    # Need to ensure deterministic splitting even as we append data\n    df2 = pd.DataFrame(np.random.randint(0, 100, size=(nrows, 3)), columns=[\"A\", \"B\", \"C\"])\n    df2[\"id\"] = np.arange(100, 200)\n\n    nrows *= 2\n\n    df = df_engine.df_lib.concat([df, df2])\n\n    splits2 = splitter.split(df, backend)\n    assert len(splits2) == 3\n    if isinstance(df_engine, DaskEngine):\n        splits2 = [split.compute() for split in splits2]\n\n    # IDs should not overlap between splits\n    assert all([set(split1[\"id\"]).isdisjoint(set(split2[\"id\"])) for split1, split2 in combinations(splits2, 2)])\n\n    for split1, split2, p in zip(splits, splits2, probabilities):\n        ids1 = set(split1[\"id\"].values.tolist())\n        ids2 = set(split2[\"id\"].values.tolist())\n\n        assert nrows * p - 10 <= len(ids2) <= nrows * p + 10\n\n        # All elements from the first round of splitting are in the same split, even after appending\n        # more rows\n        assert ids1.issubset(ids2)\n"
  },
  {
    "path": "tests/ludwig/datasets/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2020 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "tests/ludwig/datasets/download_all_datasets.py",
    "content": "#! /usr/bin/env python\n#\n# Lists and downloads all datasets, including Kaggle datasets, into ./download_datasets\n\n# You must have valid kaggle credentials in your environment, a few GB of disk space, and good internet bandwidth.\n# Also, for each dataset associated with a Kaggle competition you'll need to sign in to Kaggle and accept the terms of\n# the competition.\n#\nfrom ludwig import datasets\n\n\ndef download_all_datasets():\n    \"\"\"Downloads all datasets to ./downloaded_datasets.\"\"\"\n    dataset_names = datasets.list_datasets()\n\n    print(\"Datasets: \")\n    for name in dataset_names:\n        print(f\"  {name}\")\n    print(\"Downloading all datasets\")\n\n    # Download All Datasets\n    for dataset_name in dataset_names:\n        print(f\"Downloading {dataset_name}\")\n        datasets.download_dataset(dataset_name, \"./downloaded_datasets\")\n\n\nif __name__ == \"__main__\":\n    download_all_datasets()\n"
  },
  {
    "path": "tests/ludwig/datasets/mnist/test_mnist_workflow.py",
    "content": "import gzip\nimport os\nimport shutil\nfrom unittest import mock\n\nimport ludwig.datasets\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetState\n\n\ndef test_download_mnist_dataset(tmpdir):\n    train_image_archive_filename = os.path.join(tmpdir, \"train-images-idx3-ubyte\")\n    train_image_handle = open(train_image_archive_filename, \"w+b\")\n    train_image_handle.write(b\"This binary string will be written as training mage data\")\n    train_image_handle.close()\n    with open(train_image_archive_filename, \"rb\") as f_in:\n        with gzip.open(train_image_archive_filename + \".gz\", \"wb\") as f_out:\n            shutil.copyfileobj(f_in, f_out)\n\n    train_labels_archive_filename = os.path.join(tmpdir, \"train-labels-idx1-ubyte\")\n    train_labels_handle = open(train_labels_archive_filename, \"w\")\n    train_labels_handle.write(\"0\")\n    train_labels_handle.close()\n    with open(train_labels_archive_filename, \"rb\") as f_in:\n        with gzip.open(train_labels_archive_filename + \".gz\", \"wb\") as f_out:\n            shutil.copyfileobj(f_in, f_out)\n\n    test_image_archive_filename = os.path.join(tmpdir, \"t10k-images-idx3-ubyte\")\n    test_image_handle = open(test_image_archive_filename, \"w+b\")\n    test_image_handle.write(b\"This binary string will be written as test mage data\")\n    test_image_handle.close()\n    with open(test_image_archive_filename, \"rb\") as f_in:\n        with gzip.open(test_image_archive_filename + \".gz\", \"wb\") as f_out:\n            shutil.copyfileobj(f_in, f_out)\n\n    test_labels_archive_filename = os.path.join(tmpdir, \"t10k-labels-idx1-ubyte\")\n    test_labels_handle = open(test_labels_archive_filename, \"w\")\n    test_labels_handle.write(\"0\")\n    test_labels_handle.close()\n    with open(test_labels_archive_filename, \"rb\") as f_in:\n        with gzip.open(test_labels_archive_filename + \".gz\", \"wb\") as f_out:\n            shutil.copyfileobj(f_in, f_out)\n\n    download_urls = [\n        \"file://\" + train_image_archive_filename + \".gz\",\n        \"file://\" + train_labels_archive_filename + \".gz\",\n        \"file://\" + test_image_archive_filename + \".gz\",\n        \"file://\" + test_labels_archive_filename + \".gz\",\n    ]\n\n    config = DatasetConfig(\n        version=1.0,\n        name=\"mnist\",\n        download_urls=download_urls,\n    )\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n    with mock.patch(\"ludwig.datasets._load_dataset_config\", return_value=config):\n        dataset = ludwig.datasets.get_dataset(\"mnist\", cache_dir=tmpdir)\n        assert not dataset.state == DatasetState.DOWNLOADED\n        assert not dataset.state == DatasetState.TRANSFORMED\n        dataset.download()\n\n        assert dataset.state == DatasetState.DOWNLOADED\n    ludwig.datasets._get_dataset_configs.cache_clear()\n"
  },
  {
    "path": "tests/ludwig/datasets/model_configs/train_all_model_configs.py",
    "content": "#! /usr/bin/env python\n#\n# Trains a ludwig model for every dataset which has a default_model_config.\n# You must have valid kaggle credentials in your environment, a few GB of disk space, and good internet bandwidth.\n# Also, for each dataset associated with a Kaggle competition you'll need to sign in to Kaggle and accept the terms of\n# the competition.\n#\nimport multiprocessing\nimport time\nfrom dataclasses import dataclass\n\nimport pandas as pd\n\nfrom ludwig import datasets, visualize\nfrom ludwig.api import LudwigModel\nfrom ludwig.globals import LUDWIG_VERSION\nfrom ludwig.utils.misc_utils import get_commit_hash\n\n\n@dataclass\nclass TrainingResults:\n    \"\"\"Results of a training run for a dataset.\"\"\"\n\n    ludwig_version: str\n    ludwig_commit: str | None\n    dataset_version: str\n    dataset_name: str\n    has_config: bool\n    output_directory: str | None = None\n    splits: str | None = None\n    metric: str | None = None\n    performance: float | None = None\n    load_time: float | None = None\n    train_time: float | None = None\n    eval_time: float | None = None\n\n\ndef _train_dataset_process(dataset_name, results_queue):\n    \"\"\"Runs each train job in a new process.\"\"\"\n    load_start_time = time.time()\n    dataset = datasets.get_dataset(dataset_name)\n    config = dataset.default_model_config\n    df = dataset.load()\n    load_end_time = time.time()\n    if \"split\" not in df:\n        df[\"split\"] = 0\n    available_splits = sorted(df.split.unique())\n    results = TrainingResults(\n        LUDWIG_VERSION,\n        get_commit_hash(),\n        dataset.version,\n        dataset.name,\n        config is not None,\n        splits=\" \".join([str(s) for s in available_splits]),\n        load_time=load_end_time - load_start_time,\n    )\n    if config:\n        dataset.export(\".\")\n        print(f\"Training {dataset_name}\")\n\n        # Train model on config\n        train_start_time = time.time()\n        model = LudwigModel(config)\n        train_stats, _, output_directory = model.train(dataset=df, model_name=dataset_name)\n\n        # If dataset has a test split with labels, evaluate on test set.  If not, evaluate on training set.\n        evaluate_start_time = time.time()\n        eval_stats, _, _ = model.evaluate(\n            df,\n            split=2 if 2 in available_splits else 0,\n            collect_predictions=False,\n            collect_overall_stats=True,\n        )\n        evaluate_end_time = time.time()\n\n        # Visualize learning curve\n        visualize.learning_curves([train_stats], model_names=[dataset_name], output_directory=output_directory)\n\n        results.output_directory = output_directory\n\n        # Get metric for first output feature\n        first_of_name = config[\"output_features\"][0][\"name\"]\n        stats = eval_stats[first_of_name]\n        if \"accuracy\" in stats:\n            results.metric = \"accuracy\"\n            results.performance = stats[\"accuracy\"]\n        elif \"root_mean_squared_error\" in stats:\n            results.metric = \"root_mean_squared_error\"\n            results.performance = stats[\"root_mean_squared_error\"]\n        elif \"mean_squared_error\" in stats:\n            results.metric = \"mean_squared_error\"\n            results.performance = stats[\"mean_squared_error\"]\n        elif \"mean_absolute_error\" in stats:\n            results.metric = \"mean_absolute_error\"\n            results.performance = stats[\"mean_absolute_error\"]\n        elif \"loss\" in stats:\n            results.metric = \"loss\"\n            results.performance = stats[\"loss\"]\n        results.train_time = evaluate_start_time - train_start_time\n        results.eval_time = evaluate_end_time - evaluate_start_time\n        print(f\"Trained {dataset_name} in {evaluate_end_time - load_start_time:.2f} seconds\")\n    results_queue.put(results)\n\n\ndef train_all_datasets():\n    # Maps dataset name to current running process.\n    max_processes = 4\n    running_processes = {}\n    accumulated_results = []\n    # As each process completes it pushes its results onto the results_queue.\n    results_queue = multiprocessing.Queue()\n    for dataset_name in datasets.list_datasets():\n        if len(running_processes) >= max_processes:\n            # Block until a subprocess completes\n            next_results = results_queue.get()\n            accumulated_results.append(next_results)\n            process = running_processes[next_results.dataset_name]\n            process.join()\n            del running_processes[next_results.dataset_name]\n        process = multiprocessing.Process(target=_train_dataset_process, args=[dataset_name, results_queue])\n        running_processes[dataset_name] = process\n        process.start()\n    while len(running_processes) > 0:\n        if len(running_processes) < 4:\n            remaining_datasets = \", \".join(sorted(running_processes.keys()))\n            print(f\"Finishing up, waiting for {len(running_processes)} to complete ({remaining_datasets})\")\n        else:\n            print(f\"Finishing up, waiting for {len(running_processes)} to complete\")\n        # Block until a subprocess completes, clear it out,\n        next_results = results_queue.get()\n        accumulated_results.append(next_results)\n        process = running_processes[next_results.dataset_name]\n        process.join()\n        del running_processes[next_results.dataset_name]\n    results_df = pd.DataFrame(accumulated_results)\n    with pd.option_context(\n        \"display.max_rows\", None, \"display.max_columns\", None, \"display.precision\", 3, \"display.width\", 120\n    ):\n        results_to_display = results_df[results_df[\"has_config\"]].copy()\n        results_to_display = results_to_display.drop(\n            columns=[\"dataset_version\", \"output_directory\", \"ludwig_version\", \"ludwig_commit\", \"has_config\"]\n        )\n        print(results_to_display)\n    results_df.to_csv(\"train_all_model_configs_results.csv\", index=False)\n\n\nif __name__ == \"__main__\":\n    train_all_datasets()\n"
  },
  {
    "path": "tests/ludwig/datasets/test_dataset_configs.py",
    "content": "import ludwig.datasets\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\nfrom tests.integration_tests.utils import private_test\n\n\n@private_test\ndef test_get_config_and_load(tmpdir):\n    yosemite_config = ludwig.datasets._get_dataset_config(\"yosemite\")\n    assert isinstance(yosemite_config, DatasetConfig)\n\n    yosemite_dataset = ludwig.datasets.get_dataset(\"yosemite\", cache_dir=tmpdir)\n    assert isinstance(yosemite_dataset, DatasetLoader)\n    df = yosemite_dataset.load()\n    assert df is not None\n    assert len(df) == 18721  # Expected number of rows in Yosemite temperatures dataset.\n\n    # DISABLED: Flaky for tests, probably due to the dataset size.\n    # # Test loading dataset without 'split' and 'Unnamed: 0' columns in config.\n    # twitter_bots_config = ludwig.datasets._get_dataset_config(\"twitter_bots\")\n    # assert isinstance(twitter_bots_config, DatasetConfig)\n\n    # twitter_bots_dataset = ludwig.datasets.get_dataset(\"twitter_bots\", cache_dir=tmpdir)\n    # assert isinstance(twitter_bots_dataset, DatasetLoader)\n    # df = twitter_bots_dataset.load()\n    # assert df is not None\n    # assert len(df.columns) == 22  # Expected number of columns in Twitter bots dataset including split column.\n\n\ndef test_get_config_kaggle(tmpdir):\n    twitter_bots_config = ludwig.datasets._get_dataset_config(\"twitter_bots\")\n    assert isinstance(twitter_bots_config, DatasetConfig)\n\n    twitter_bots_dataset = ludwig.datasets.get_dataset(\"twitter_bots\", cache_dir=tmpdir)\n    # Twitter bots dataset is large, so we won't load it in this unit test.\n    assert isinstance(twitter_bots_dataset, DatasetLoader)\n    assert twitter_bots_dataset.is_kaggle_dataset\n"
  },
  {
    "path": "tests/ludwig/datasets/test_dataset_links.py",
    "content": "#! /usr/bin/env python\n#\n# Checks all dataset download links (just those with URLs, not including kaggle datasets).\"\"\"\n#\nimport logging\nfrom concurrent.futures import as_completed, ThreadPoolExecutor\n\nimport pytest\nimport requests\n\nimport ludwig\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(__name__)\n\n\n@pytest.mark.slow\ndef test_links():\n    # Iterate through all datasets, ensure links are valid and reachable.\n    all_datasets = ludwig.datasets.list_datasets()\n\n    tasks = {}\n    with ThreadPoolExecutor(max_workers=10) as executor:\n        for dataset_name in all_datasets:\n            config = ludwig.datasets._get_dataset_config(dataset_name)\n            download_urls = [config.download_urls] if isinstance(config.download_urls, str) else config.download_urls\n            for url in download_urls:\n                future = executor.submit(_check_url, dataset_name, url)\n                tasks[future] = (dataset_name, url)\n\n        failures = []\n        for future in as_completed(tasks):\n            dataset_name, url = tasks[future]\n            error = future.result()\n            if error:\n                failures.append(error)\n\n    assert not failures, \"Failed URLs:\\n\" + \"\\n\".join(failures)\n\n\ndef _check_url(dataset_name, url):\n    logger.info(f\"Checking {dataset_name}: {url}\")\n    try:\n        response = requests.head(url, timeout=30)\n        if not response.ok:\n            return f\"Failed to download {dataset_name} from {url} (status {response.status_code})\"\n    except requests.RequestException as e:\n        return f\"Failed to download {dataset_name} from {url} ({e})\"\n    return None\n"
  },
  {
    "path": "tests/ludwig/datasets/test_datasets.py",
    "content": "import importlib\nimport importlib.util\nimport io\nimport os\nimport uuid\nfrom unittest import mock\n\nimport pandas as pd\nimport pytest\n\nimport ludwig.datasets\nfrom ludwig.api import LudwigModel\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetState\nfrom tests.integration_tests.utils import private_test\n\nSUPPORTED_UNCOMPRESSED_FILETYPES = [\"json\", \"jsonl\", \"tsv\", \"csv\"]\n\n\ndef test_load_csv_dataset(tmpdir):\n    input_df = pd.DataFrame(\n        {\"name\": [\"Raphael\", \"Donatello\"], \"mask\": [\"red\", \"purple\"], \"weapon\": [\"sai\", \"bo staff\"], \"split\": [0, 1]}\n    )\n\n    extracted_filename = \"input.csv\"\n    compression_opts = dict(method=\"zip\", archive_name=extracted_filename)\n\n    archive_filename = os.path.join(tmpdir, \"archive.zip\")\n    input_df.to_csv(archive_filename, index=False, compression=compression_opts)\n\n    config = DatasetConfig(\n        version=1.0,\n        name=\"fake_csv_dataset\",\n        download_urls=[\"file://\" + archive_filename],\n    )\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n    with mock.patch(\"ludwig.datasets._load_dataset_config\", return_value=config):\n        dataset = ludwig.datasets.get_dataset(\"fake_csv_dataset\", cache_dir=tmpdir)\n\n        assert not dataset.state == DatasetState.DOWNLOADED\n        assert not dataset.state == DatasetState.TRANSFORMED\n\n        output_df = dataset.load()\n        pd.testing.assert_frame_equal(input_df, output_df)\n\n        assert dataset.state == DatasetState.TRANSFORMED\n    ludwig.datasets._get_dataset_configs.cache_clear()\n\n\n@pytest.mark.parametrize(\"f_type\", SUPPORTED_UNCOMPRESSED_FILETYPES)\ndef test_multifile_join_dataset(tmpdir, f_type):\n    if f_type != \"jsonl\":\n        train_df = pd.DataFrame(\n            {\"name\": [\"Raphael\", \"Donatello\"], \"mask\": [\"red\", \"purple\"], \"weapon\": [\"sai\", \"bo staff\"]}\n        )\n\n        test_df = pd.DataFrame({\"name\": [\"Jack\", \"Bob\"], \"mask\": [\"green\", \"yellow\"], \"weapon\": [\"knife\", \"gun\"]})\n\n        val_df = pd.DataFrame({\"name\": [\"Tom\"], \"mask\": [\"pink\"], \"weapon\": [\"stick\"]})\n    else:\n        train_df = pd.DataFrame([{\"name\": \"joe\"}, {\"mask\": \"green\"}, {\"weapon\": \"stick\"}])\n        test_df = pd.DataFrame([{\"name\": \"janice\"}, {\"mask\": \"black\"}, {\"weapon\": \"gun\"}])\n        val_df = pd.DataFrame([{\"name\": \"sara\"}, {\"mask\": \"pink\"}, {\"weapon\": \"gun\"}])\n\n    # filetypes = ['json', 'tsv', 'jsonl']\n    train_filename = \"train.\" + f_type\n    test_filename = \"test.\" + f_type\n    val_filename = \"val.\" + f_type\n    train_filepath = os.path.join(tmpdir, train_filename)\n    test_filepath = os.path.join(tmpdir, test_filename)\n    val_filepath = os.path.join(tmpdir, val_filename)\n\n    if f_type == \"json\":\n        train_df.to_json(train_filepath)\n        test_df.to_json(test_filepath)\n        val_df.to_json(val_filepath)\n    elif f_type == \"jsonl\":\n        train_df.to_json(train_filepath, orient=\"records\", lines=True)\n        test_df.to_json(test_filepath, orient=\"records\", lines=True)\n        val_df.to_json(val_filepath, orient=\"records\", lines=True)\n    elif f_type == \"tsv\":\n        train_df.to_csv(train_filepath, sep=\"\\t\")\n        test_df.to_csv(test_filepath, sep=\"\\t\")\n        val_df.to_csv(val_filepath, sep=\"\\t\")\n    else:\n        train_df.to_csv(train_filepath)\n        test_df.to_csv(test_filepath)\n        val_df.to_csv(val_filepath)\n\n    config = DatasetConfig(\n        version=1.0,\n        name=\"fake_multifile_dataset\",\n        download_urls=[\"file://\" + train_filepath, \"file://\" + test_filepath, \"file://\" + val_filepath],\n        train_filenames=train_filename,\n        validation_filenames=val_filename,\n        test_filenames=test_filename,\n    )\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n    with mock.patch(\"ludwig.datasets._load_dataset_config\", return_value=config):\n        dataset = ludwig.datasets.get_dataset(\"fake_multifile_dataset\", cache_dir=tmpdir)\n\n        assert not dataset.state == DatasetState.DOWNLOADED\n        assert not dataset.state == DatasetState.TRANSFORMED\n\n        output_df = dataset.load()\n        assert output_df.shape[0] == train_df.shape[0] + test_df.shape[0] + val_df.shape[0]\n\n        assert dataset.state == DatasetState.TRANSFORMED\n    ludwig.datasets._get_dataset_configs.cache_clear()\n\n\n@pytest.mark.parametrize(\n    \"include_competitions,include_data_modalities\", [(True, True), (True, False), (False, True), (False, False)]\n)\ndef test_get_datasets_info(include_competitions, include_data_modalities):\n    dataset_output_features = ludwig.datasets.get_datasets_output_features(\n        include_competitions=include_competitions, include_data_modalities=include_data_modalities\n    )\n\n    assert len(dataset_output_features) > 1\n    assert isinstance(dataset_output_features, dict)\n    assert dataset_output_features[\"twitter_bots\"].get(\"name\", None)\n    assert dataset_output_features[\"twitter_bots\"].get(\"output_features\", None)\n    assert isinstance(dataset_output_features[\"twitter_bots\"][\"output_features\"], list)\n    assert dataset_output_features[\"twitter_bots\"][\"output_features\"][0].get(\"name\", None)\n    assert dataset_output_features[\"twitter_bots\"][\"output_features\"][0].get(\"type\", None)\n\n    if include_competitions:\n        assert dataset_output_features[\"titanic\"].get(\"name\", None)\n    else:\n        assert dataset_output_features.get(\"titanic\", None) is None\n\n    if include_data_modalities:\n        data_modalities = dataset_output_features[\"twitter_bots\"].get(\"data_modalities\", None)\n        assert data_modalities\n        assert len(data_modalities) >= 1\n    else:\n        assert dataset_output_features[\"twitter_bots\"].get(\"data_modalities\", None) is None\n\n    dataset_output_features = ludwig.datasets.get_datasets_output_features(dataset=\"twitter_bots\")\n    assert len(dataset_output_features[\"output_features\"]) == 1\n    assert dataset_output_features[\"name\"] == \"twitter_bots\"\n\n\ndef test_get_dataset_buffer():\n    buffer = ludwig.datasets.get_buffer(\"iris\")\n\n    assert isinstance(buffer, io.BytesIO)\n\n\ndef test_train_dataset_uri(tmpdir):\n    input_df = pd.DataFrame(\n        {\n            \"input\": [\"a\", \"b\", \"a\", \"b\", \"a\", \"b\", \"c\", \"c\", \"a\", \"b\"],\n            \"output\": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],\n            \"split\": [0, 0, 0, 0, 0, 0, 0, 1, 2, 2],\n        }\n    )\n\n    extracted_filename = \"input.csv\"\n    compression_opts = dict(method=\"zip\", archive_name=extracted_filename)\n\n    archive_filename = os.path.join(tmpdir, \"archive.zip\")\n    input_df.to_csv(archive_filename, index=False, compression=compression_opts)\n\n    dataset_name = f\"fake_csv_dataset_{uuid.uuid4().hex}\"\n    config = DatasetConfig(\n        version=1.0,\n        name=dataset_name,\n        download_urls=[\"file://\" + archive_filename],\n    )\n\n    model_config = {\n        \"input_features\": [{\"name\": \"input\", \"type\": \"category\"}],\n        \"output_features\": [{\"name\": \"output\", \"type\": \"number\"}],\n        \"preprocessing\": {\"split\": {\"type\": \"fixed\"}},\n        \"combiner\": {\"type\": \"concat\", \"fc_size\": 14},\n        \"trainer\": {\"batch_size\": 8, \"epochs\": 1},\n    }\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n    with mock.patch(\"ludwig.datasets._load_dataset_config\", return_value=config):\n        with mock.patch(\"ludwig.datasets.loaders.dataset_loader.get_default_cache_location\", return_value=str(tmpdir)):\n            model = LudwigModel(model_config, backend=\"local\")\n\n            results = model.train(dataset=f\"ludwig://{dataset_name}\")\n            proc_result = results.preprocessed_data\n            train_df1 = proc_result.training_set.to_df()\n            val_df1 = proc_result.validation_set.to_df()\n            test_df1 = proc_result.test_set.to_df()\n\n            assert len(train_df1) == 7\n            assert len(val_df1) == 1\n            assert len(test_df1) == 2\n\n            results = model.train(\n                training_set=f\"ludwig://{dataset_name}\",\n                validation_set=f\"ludwig://{dataset_name}\",\n                test_set=f\"ludwig://{dataset_name}\",\n            )\n            proc_result_split = results.preprocessed_data\n            train_df2 = proc_result_split.training_set.to_df()\n            val_df2 = proc_result_split.validation_set.to_df()\n            test_df2 = proc_result_split.test_set.to_df()\n\n            assert len(train_df2) == 7\n            assert len(val_df2) == 1\n            assert len(test_df2) == 2\n\n            sort_col = train_df1.columns[-1]\n\n            def sort_df(df):\n                return df.sort_values(by=[sort_col]).reset_index(drop=True)\n\n            assert sort_df(train_df1).equals(sort_df(train_df2))\n            assert sort_df(val_df1).equals(sort_df(val_df2))\n            assert sort_df(test_df1).equals(sort_df(test_df2))\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n\n\n@private_test\n@pytest.mark.parametrize(\"dataset_name, size\", [(\"code_alpaca\", 20000), (\"consumer_complaints\", 38000)])\ndef test_ad_hoc_dataset_download(tmpdir, dataset_name, size):\n    dataset_config = ludwig.datasets._get_dataset_config(dataset_name)\n    assert isinstance(dataset_config, DatasetConfig)\n\n    ludwig_dataset = ludwig.datasets.get_dataset(dataset_name, cache_dir=tmpdir)\n    df = ludwig_dataset.load()\n    assert df is not None\n    assert len(df) >= size\n\n\n@pytest.mark.skipif(not importlib.util.find_spec(\"datasets\"), reason=\"huggingface datasets not installed\")\n@pytest.mark.xfail(reason=\"HuggingFace datasets library no longer supports loading datasets via scripts\")\ndef test_hf_dataset_loading():\n    import datasets\n\n    loader = ludwig.datasets.get_dataset(\"hugging_face\")\n    data = loader.load(\"JeremyAlain/123_test\", \"data_0\")\n    hf_data = datasets.load_dataset(path=\"JeremyAlain/123_test\", name=\"data_0\")\n\n    assert len(data) == hf_data[\"train\"].num_rows\n\n    train, val, test = loader.load(\"neil-code/dialogsum-test\", None, split=True)\n    hf_data = datasets.load_dataset(path=\"neil-code/dialogsum-test\")\n\n    assert len(train) == hf_data[\"train\"].num_rows\n    assert len(val) == hf_data[\"validation\"].num_rows\n    assert len(test) == hf_data[\"test\"].num_rows\n"
  },
  {
    "path": "tests/ludwig/datasets/test_model_configs.py",
    "content": "import ludwig.datasets\n\n\ndef test_default_model_config(tmpdir):\n    titanic_configs = ludwig.datasets.model_configs_for_dataset(\"titanic\")\n    assert len(titanic_configs) > 0\n\n    titanic = ludwig.datasets.get_dataset(\"titanic\", cache_dir=tmpdir)\n    assert titanic.default_model_config is not None\n\n    assert titanic.default_model_config == titanic_configs[\"default\"]\n\n\ndef test_best_model_config(tmpdir):\n    higgs_configs = ludwig.datasets.model_configs_for_dataset(\"higgs\")\n    assert len(higgs_configs) > 0\n\n    higgs = ludwig.datasets.get_dataset(\"higgs\", cache_dir=tmpdir)\n    assert higgs.default_model_config is not None\n    assert higgs.best_model_config is not None\n\n    assert higgs.default_model_config == higgs_configs[\"default\"]\n    assert higgs.best_model_config == higgs_configs[\"best\"]\n\n\ndef test_dataset_has_no_model_configs(tmpdir):\n    bbc_news_configs = ludwig.datasets.model_configs_for_dataset(\"bbcnews\")\n    assert len(bbc_news_configs) == 0\n\n    bbcnews = ludwig.datasets.get_dataset(\"bbcnews\", cache_dir=tmpdir)\n    assert bbcnews.default_model_config is None\n"
  },
  {
    "path": "tests/ludwig/datasets/titanic/test_titanic_workflow.py",
    "content": "import os\nimport zipfile\nfrom shutil import copy\nfrom unittest import mock\n\nimport pandas as pd\n\nimport ludwig.datasets\nfrom ludwig.datasets.dataset_config import DatasetConfig\nfrom ludwig.datasets.loaders.dataset_loader import DatasetState\n\n\ndef test_download_titanic_dataset(tmpdir):\n    titanic_train_df = pd.DataFrame(\n        {\n            \"passenger_id\": [1216, 699, 234],\n            \"pclass\": [3, 3, 4],\n            \"name\": [\"sai bo\", \"bo staff\", \"tae kwan nic\"],\n            \"sex\": [\"female\", \"male\", \"male\"],\n            \"age\": [38, 28, 18],\n            \"sibsp\": [0, 1, 0],\n            \"parch\": [1, 1, 2],\n            \"ticket\": [335432, 315089, 322472],\n            \"fare\": [7.7333, 8.6625, 9.8765],\n            \"cabin\": [1, 2, 4],\n            \"embarked\": [\"C\", \"Q\", \"S\"],\n            \"boat\": [0, 0, 0],\n            \"body\": [0, 1, 0],\n            \"home.dest\": [\"Croatia\", \"Italy\", \"Sweden\"],\n            \"survived\": [0, 1, 0],\n        }\n    )\n\n    titanic_test_df = pd.DataFrame(\n        {\n            \"passenger_id\": [1216, 699, 234],\n            \"pclass\": [3, 3, 4],\n            \"name\": [\"mo bo\", \"bo bo bo\", \"Rafael Nadal\"],\n            \"sex\": [\"female\", \"male\", \"male\"],\n            \"age\": [28, 18, 30],\n            \"sibsp\": [0, 1, 0],\n            \"parch\": [1, 1, 2],\n            \"ticket\": [335412, 215089, 922472],\n            \"fare\": [17.7333, 18.6625, 19.8765],\n            \"cabin\": [2, 2, 1],\n            \"embarked\": [\"Q\", \"Q\", \"C\"],\n            \"boat\": [0, 0, 0],\n            \"body\": [0, 1, 0],\n            \"home.dest\": [\"Sweden\", \"Slovenia\", \"Italy\"],\n            \"survived\": [0, 1, 0],\n        }\n    )\n\n    train_fname = os.path.join(tmpdir, \"train.csv\")\n    titanic_train_df.to_csv(train_fname, index=False)\n\n    test_fname = os.path.join(tmpdir, \"test.csv\")\n    titanic_test_df.to_csv(test_fname, index=False)\n\n    archive_filename = os.path.join(tmpdir, \"titanic.zip\")\n    with zipfile.ZipFile(archive_filename, \"w\") as z:\n        z.write(train_fname, \"train.csv\")\n        z.write(test_fname, \"test.csv\")\n\n    config = DatasetConfig(\n        version=1.0,\n        name=\"titanic\",\n        kaggle_competition=\"titanic\",\n        archive_filenames=\"titanic.zip\",\n        # Normally we would verify the zip file, but in this test the zip file is created every time and contains the\n        # creation dates of the csv files so its digest will be different every time the test is run.\n        sha256={\n            \"test.csv\": \"348c49a95fe099fcc3b9142c82fb6becb87edc0f4d2c69c485e0dce4af8625e0\",\n            \"train.csv\": \"483556c465414fd78deb02b25f39a0de844b0728c1ef0505df0e5b3e40fec995\",\n        },\n        train_filenames=\"train.csv\",\n        test_filenames=\"test.csv\",\n    )\n\n    def download_files(competition_name, path):\n        assert competition_name == \"titanic\"\n        copy(archive_filename, path)\n\n    ludwig.datasets._get_dataset_configs.cache_clear()\n    with mock.patch(\"ludwig.datasets._load_dataset_config\", return_value=config):\n        with mock.patch(\"ludwig.datasets.kaggle.create_kaggle_client\") as mock_kaggle_cls:\n            mock_kaggle_api = mock.MagicMock()\n            mock_kaggle_api.competition_download_files = download_files\n            mock_kaggle_cls.return_value = mock_kaggle_api\n\n            dataset = ludwig.datasets.get_dataset(\"titanic\", cache_dir=tmpdir)\n            assert not dataset.state == DatasetState.DOWNLOADED\n\n            dataset.download()\n            assert dataset.state == DatasetState.DOWNLOADED\n            mock_kaggle_api.authenticate.assert_called_once()\n\n            assert not dataset.state == DatasetState.TRANSFORMED\n            dataset.extract()\n            # Normally we would verify before extracting, but in this test the zip file is created on each run and\n            # changes between test runs. Instead we verify the extracted .csv files.\n            dataset.verify()\n            dataset.transform()\n            assert dataset.state == DatasetState.TRANSFORMED\n\n            output_train_df, output_test_df, output_val_df = dataset.load(split=True)\n            assert len(output_train_df) == len(titanic_train_df)\n            assert len(output_test_df) == len(titanic_test_df)\n            assert len(output_val_df) == 0\n    ludwig.datasets._get_dataset_configs.cache_clear()\n"
  },
  {
    "path": "tests/ludwig/decoders/test_image_decoder.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT, ENCODER_OUTPUT_STATE, HIDDEN, LOGITS\nfrom ludwig.decoders.image_decoders import UNetDecoder\nfrom ludwig.encoders.image.base import UNetEncoder\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"height,width,num_channels,num_classes\", [(224, 224, 1, 2), (224, 224, 3, 8)])\n@pytest.mark.parametrize(\"batch_size\", [4, 1])\ndef test_unet_decoder(height, width, num_channels, num_classes, batch_size):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    unet_encoder = UNetEncoder(height=height, width=width, num_channels=num_channels)\n    inputs = torch.rand(batch_size, num_channels, height, width)\n    encoder_outputs = unet_encoder(inputs)\n    assert encoder_outputs[ENCODER_OUTPUT].shape[1:] == unet_encoder.output_shape\n    assert len(encoder_outputs[ENCODER_OUTPUT_STATE]) == 4\n\n    hidden = torch.reshape(encoder_outputs[ENCODER_OUTPUT], [batch_size, -1])\n\n    unet_decoder = UNetDecoder(hidden.size(dim=1), height, width, 1, num_classes)\n    combiner_outputs = {\n        HIDDEN: hidden,\n        ENCODER_OUTPUT_STATE: encoder_outputs[ENCODER_OUTPUT_STATE].copy(),  # create a copy\n    }\n\n    output = unet_decoder(combiner_outputs, target=None)\n\n    assert list(output[LOGITS].size()) == [batch_size, num_classes, height, width]\n\n    # check for parameter updating\n    target = torch.randn(output[LOGITS].shape)\n    combiner_outputs[ENCODER_OUTPUT_STATE] = encoder_outputs[ENCODER_OUTPUT_STATE]  # restore state\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(unet_decoder, (combiner_outputs, None), target)\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/decoders/test_llm_decoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import BACKEND, BASE_MODEL, GENERATION, INPUT_FEATURES, MODEL_TYPE, OUTPUT_FEATURES\nfrom ludwig.decoders.llm_decoders import TextExtractorDecoder\nfrom ludwig.schema.model_config import ModelConfig\nfrom tests.integration_tests.utils import text_feature\n\nTEST_MODEL_NAME = \"hf-internal-testing/tiny-random-GPTJForCausalLM\"\n\n\ndef test_text_extractor_decoder():\n    max_new_tokens = 4\n\n    input_features = [\n        {\n            \"name\": \"Question\",\n            \"type\": \"text\",\n            \"encoder\": {\"type\": \"passthrough\"},\n        }\n    ]\n    output_features = [text_feature(output_feature=True, name=\"Answer\", decoder={\"type\": \"text_extractor\"})]\n\n    config = {\n        MODEL_TYPE: \"llm\",\n        BASE_MODEL: TEST_MODEL_NAME,\n        GENERATION: {\n            \"temperature\": 0.1,\n            \"top_p\": 0.75,\n            \"top_k\": 40,\n            \"num_beams\": 4,\n            \"max_new_tokens\": max_new_tokens,\n        },\n        INPUT_FEATURES: input_features,\n        OUTPUT_FEATURES: output_features,\n        BACKEND: \"local\",\n    }\n\n    config = ModelConfig.from_dict(config)\n    decoder_config = config.output_features[0].decoder\n\n    decoder = TextExtractorDecoder(32, decoder_config)\n\n    inputs = [\n        torch.tensor([1, 1, 1, 2, 2, 2, 2]),  # baseline\n        torch.tensor([1, 1, 1, 2]),  # too short; test padding\n        torch.tensor([1, 1, 1, 1, 2, 2, 2]),  # test different input length\n    ]\n    input_lengths = [3, 3, 4]\n\n    # tests happy path\n    outputs = decoder.forward(inputs, input_lengths, max_new_tokens)\n    assert outputs[\"predictions\"].shape == (3, max_new_tokens)\n    # Create a Boolean mask for elements equal to 0 or 2 (padding or output)\n    mask = (outputs[\"predictions\"] == 0) | (outputs[\"predictions\"] == 2)\n    assert mask.all()\n\n    # test overly long generation fails without updated max_new_tokens\n    inputs.append(torch.tensor([1, 1, 1, 2, 2, 2, 2, 2]))  # too long; test downstream failure)\n    input_lengths.append(3)\n    with pytest.raises(ValueError):\n        outputs = decoder.forward(inputs, input_lengths, max_new_tokens)\n\n    # test overly long generation succeeds with new max_new_tokens\n    new_max_new_tokens = 5\n    outputs = decoder.forward(inputs, input_lengths, new_max_new_tokens)\n    assert outputs[\"predictions\"].shape == (4, new_max_new_tokens)\n    mask = (outputs[\"predictions\"] == 0) | (outputs[\"predictions\"] == 2)\n    assert mask.all()\n"
  },
  {
    "path": "tests/ludwig/decoders/test_sequence_decoder.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import HIDDEN, LOGITS\nfrom ludwig.decoders.sequence_decoders import (\n    LSTMDecoder,\n    RNNDecoder,\n    SequenceGeneratorDecoder,\n    SequenceLSTMDecoder,\n    SequenceRNNDecoder,\n)\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"cell_type\", [\"rnn\", \"gru\"])\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\n@pytest.mark.parametrize(\"batch_size\", [20, 1])\ndef test_rnn_decoder(cell_type, num_layers, batch_size):\n    hidden_size = 256\n    vocab_size = 50\n\n    input = torch.randint(vocab_size, size=(batch_size,))\n    initial_hidden = torch.zeros(num_layers, batch_size, hidden_size)\n    rnn_decoder = RNNDecoder(hidden_size, vocab_size, cell_type, num_layers=num_layers)\n\n    output = rnn_decoder(input, initial_hidden)\n\n    assert len(output) == 2\n    assert list(output[0].size()) == [batch_size, 1, vocab_size]\n    assert list(output[1].size()) == [num_layers, batch_size, hidden_size]\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\n@pytest.mark.parametrize(\"batch_size\", [20, 1])\ndef test_lstm_decoder(num_layers, batch_size):\n    hidden_size = 256\n    vocab_size = 50\n\n    input = torch.randint(vocab_size, size=(batch_size,))\n    initial_hidden = torch.zeros(num_layers, batch_size, hidden_size)\n    initial_cell_state = torch.zeros(num_layers, batch_size, hidden_size)\n    lstm_decoder = LSTMDecoder(hidden_size, vocab_size, num_layers=num_layers)\n\n    output = lstm_decoder(input, initial_hidden, initial_cell_state)\n\n    assert len(output) == 3\n    assert list(output[0].size()) == [batch_size, 1, vocab_size]\n    assert list(output[1].size()) == [num_layers, batch_size, hidden_size]\n    assert list(output[2].size()) == [num_layers, batch_size, hidden_size]\n\n\n@pytest.mark.parametrize(\"cell_type\", [\"rnn\", \"gru\"])\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\n@pytest.mark.parametrize(\"batch_size\", [20, 1])\ndef test_sequence_rnn_decoder(cell_type, num_layers, batch_size):\n    hidden_size = 256\n    vocab_size = 50\n    max_sequence_length = 10\n\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])}\n    sequence_rnn_decoder = SequenceRNNDecoder(\n        hidden_size, vocab_size, max_sequence_length, cell_type, num_layers=num_layers\n    )\n\n    output = sequence_rnn_decoder(combiner_outputs, target=None)\n\n    assert list(output.size()) == [batch_size, max_sequence_length, vocab_size]\n\n    # check for parameter updating\n    target = torch.randn(output.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_rnn_decoder, (combiner_outputs, None), target)\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\n@pytest.mark.parametrize(\"batch_size\", [20, 1])\ndef test_sequence_lstm_decoder(num_layers, batch_size):\n    hidden_size = 256\n    vocab_size = 50\n    max_sequence_length = 10\n\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])}\n    sequence_lstm_decoder = SequenceLSTMDecoder(hidden_size, vocab_size, max_sequence_length, num_layers=num_layers)\n\n    output = sequence_lstm_decoder(combiner_outputs, target=None)\n\n    assert list(output.size()) == [batch_size, max_sequence_length, vocab_size]\n\n    # check for parameter updating\n    target = torch.randn(output.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        sequence_lstm_decoder, (combiner_outputs, None), target\n    )\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"cell_type\", [\"rnn\", \"gru\", \"lstm\"])\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\n@pytest.mark.parametrize(\"batch_size\", [20, 1])\ndef test_sequence_generator_decoder(cell_type, num_layers, batch_size):\n    hidden_size = 256\n    vocab_size = 50\n    max_sequence_length = 10\n\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    combiner_outputs = {HIDDEN: torch.rand([batch_size, hidden_size])}\n    sequence_rnn_decoder = SequenceGeneratorDecoder(\n        input_size=hidden_size,\n        vocab_size=vocab_size,\n        max_sequence_length=max_sequence_length,\n        cell_type=cell_type,\n        num_layers=num_layers,\n    )\n\n    output = sequence_rnn_decoder(combiner_outputs, target=None)\n\n    assert list(output[LOGITS].size()) == [batch_size, max_sequence_length, vocab_size]\n\n    # check for parameter updating\n    target = torch.randn(output[LOGITS].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_rnn_decoder, (combiner_outputs, None), target)\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/decoders/test_sequence_decoder_utils.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT_STATE, HIDDEN\nfrom ludwig.decoders import sequence_decoder_utils\nfrom ludwig.modules.reduction_modules import SequenceReducer\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\ndef test_get_rnn_init_state_uses_hidden(num_layers):\n    batch_size = 16\n    sequence_length = 32\n    state_size = 64\n    combiner_outputs = {}\n    combiner_outputs[HIDDEN] = torch.rand([batch_size, sequence_length, state_size])\n\n    # With sequence reduction.\n    result = sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode=\"sum\"), num_layers)\n    assert list(result.size()) == [num_layers, batch_size, state_size]\n\n    # Without sequence reduction.\n    with pytest.raises(ValueError):\n        sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode=\"none\"), num_layers)\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\ndef test_get_rnn_init_state_prefers_encoder_output_state(num_layers):\n    batch_size = 16\n    state_size = 64\n    combiner_outputs = {}\n    combiner_outputs[HIDDEN] = torch.rand([batch_size, state_size])\n    combiner_outputs[ENCODER_OUTPUT_STATE] = torch.rand([batch_size, state_size * 2])\n\n    result = sequence_decoder_utils.get_rnn_init_state(combiner_outputs, SequenceReducer(reduce_mode=\"sum\"), num_layers)\n\n    assert list(result.size()) == [num_layers, batch_size, state_size * 2]\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\ndef test_get_lstm_init_state_uses_hidden(num_layers):\n    batch_size = 16\n    sequence_length = 32\n    state_size = 64\n    combiner_outputs = {}\n    combiner_outputs[HIDDEN] = torch.rand([batch_size, sequence_length, state_size])\n\n    # With sequence reduction.\n    decoder_hidden_state, decoder_cell_state = sequence_decoder_utils.get_lstm_init_state(\n        combiner_outputs, SequenceReducer(reduce_mode=\"sum\"), num_layers\n    )\n    assert list(decoder_hidden_state.size()) == [num_layers, batch_size, state_size]\n    assert list(decoder_cell_state.size()) == [num_layers, batch_size, state_size]\n\n    # Without sequence reduction.\n    with pytest.raises(ValueError):\n        sequence_decoder_utils.get_lstm_init_state(combiner_outputs, SequenceReducer(reduce_mode=\"none\"), num_layers)\n\n\n@pytest.mark.parametrize(\"num_layers\", [1, 2])\ndef test_get_lstm_init_state_prefers_encoder_output_state(num_layers):\n    batch_size = 16\n    state_size = 64\n    combiner_outputs = {}\n    combiner_outputs[HIDDEN] = torch.rand([batch_size, state_size])\n    combiner_outputs[ENCODER_OUTPUT_STATE] = torch.rand([batch_size, state_size * 2])\n\n    decoder_hidden_state, decoder_cell_state = sequence_decoder_utils.get_lstm_init_state(\n        combiner_outputs, SequenceReducer(reduce_mode=\"sum\"), num_layers\n    )\n\n    assert list(decoder_hidden_state.size()) == [num_layers, batch_size, state_size * 2]\n    assert list(decoder_cell_state.size()) == [num_layers, batch_size, state_size * 2]\n"
  },
  {
    "path": "tests/ludwig/decoders/test_sequence_tagger.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import HIDDEN, LOGITS\nfrom ludwig.decoders.sequence_tagger import SequenceTaggerDecoder\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"use_attention\", [True, False])\n@pytest.mark.parametrize(\"use_bias\", [True, False])\ndef test_sequence_tagger(use_attention, use_bias):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    batch_size = 20\n    combiner_output_state_size = 100\n    vocab_size = 150\n    max_sequence_length = 30\n    decoder_inputs = {HIDDEN: torch.rand(batch_size, max_sequence_length, combiner_output_state_size)}\n    tagger_decoder = SequenceTaggerDecoder(\n        combiner_output_state_size, vocab_size, max_sequence_length, use_attention=use_attention, use_bias=use_bias\n    )\n\n    outputs = tagger_decoder(decoder_inputs)\n\n    assert outputs[LOGITS].size()[1:] == tagger_decoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[LOGITS].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(tagger_decoder, (decoder_inputs,), target)\n    assert upc == tpc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ludwig/encoders/test_bag_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.bag_encoders import BagEmbedWeightedEncoder\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"dropout\", [0, 0.9])\n@pytest.mark.parametrize(\"num_fc_layers\", [0, 2])\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"]])\n@pytest.mark.parametrize(\"embedding_size\", [10])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_set_encoder(vocab: list[str], embedding_size: int, representation: str, num_fc_layers: int, dropout: float):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    bag_encoder = BagEmbedWeightedEncoder(\n        vocab=vocab,\n        representation=representation,\n        embedding_size=embedding_size,\n        num_fc_layers=num_fc_layers,\n        dropout=dropout,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 9, size=(2, len(vocab))).to(DEVICE)\n    outputs = bag_encoder(inputs)[ENCODER_OUTPUT]\n    assert outputs.shape[1:] == bag_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(bag_encoder, (inputs,), target)\n\n    if dropout == 0:\n        assert upc == tpc, f\"Not all parameters updated.  Parameters not updated: {not_updated}.\\nModule: {bag_encoder}\"\n    else:\n        # given random seed and configuration, non-zero dropout can take various values\n        assert (upc == tpc) or (\n            upc == 0\n        ), f\"Not all parameterss updated.  Parameters not updated: {not_updated}.\\nModule: {bag_encoder}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_category_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.category_encoders import CategoricalEmbedEncoder, CategoricalSparseEncoder\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"trainable\", [True, False])\n@pytest.mark.parametrize(\"vocab\", [[\"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"violet\"], [\"a\", \"b\", \"c\"]])\n@pytest.mark.parametrize(\"embedding_size\", [4, 6, 10])\ndef test_categorical_dense_encoder(vocab: list[str], embedding_size: int, trainable: bool):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    dense_encoder = CategoricalEmbedEncoder(\n        vocab=vocab,\n        embedding_size=embedding_size,\n        embeddings_trainable=trainable,\n    ).to(DEVICE)\n    inputs = torch.randint(len(vocab), (10,)).to(DEVICE)  # Chooses 10 items from vocab with replacement.\n    inputs = torch.unsqueeze(inputs, 1)\n    outputs = dense_encoder(inputs)[ENCODER_OUTPUT]\n    # In dense mode, the embedding size should be less than or equal to vocab size.\n    assert outputs.shape[-1] == min(embedding_size, len(vocab))\n    # Ensures output shape matches encoder expected output shape.\n    assert outputs.shape[1:] == dense_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(dense_encoder, (inputs,), target)\n\n    if trainable:\n        assert fpc == 0, \"Embedding layer should be trainable, but found to be frozen.\"\n    else:\n        assert fpc == 1, \"Embedding layer should be frozen, but found to be trainable.\"\n\n    assert upc == tpc, f\"Not all parameters updated.  Parameters not updated: {not_updated}.\\nModule: {dense_encoder}\"\n\n\n@pytest.mark.parametrize(\"trainable\", [True, False])\n@pytest.mark.parametrize(\"vocab\", [[\"red\", \"orange\", \"yellow\", \"green\", \"blue\", \"violet\"], [\"a\", \"b\", \"c\"]])\ndef test_categorical_sparse_encoder(vocab: list[str], trainable: bool):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    sparse_encoder = CategoricalSparseEncoder(vocab=vocab, embeddings_trainable=trainable).to(DEVICE)\n    inputs = torch.randint(len(vocab), (10,)).to(DEVICE)  # Chooses 10 items from vocab with replacement.\n    inputs = torch.unsqueeze(inputs, 1)\n    outputs = sparse_encoder(inputs)[ENCODER_OUTPUT]\n    # In sparse mode, embedding_size will always be equal to vocab size.\n    assert outputs.shape[-1] == len(vocab)\n    # Ensures output shape matches encoder expected output shape.\n    assert outputs.shape[1:] == sparse_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(sparse_encoder, (inputs,), target)\n\n    if trainable:\n        assert fpc == 0, \"Embedding layer should be trainable, but found to be frozen.\"\n    else:\n        assert fpc == 1, \"Embedding layer should be frozen, but found to be trainable.\"\n\n    assert upc == tpc, f\"Not all parameters updated.  Parameters not updated: {not_updated}.\\nModule: {sparse_encoder}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_date_encoders.py",
    "content": "import logging\n\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.date_encoders import DateEmbed, DateWave\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\nDEVICE = get_torch_device()\n\nlogger = logging.getLogger(__name__)\n\n\ndef test_date_embed():\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    date_embed = DateEmbed().to(DEVICE)\n    inputs = torch.tensor(\n        [[2022, 6, 25, 5, 176, 9, 30, 59, 34259], [2022, 6, 25, 5, 176, 9, 30, 59, 34259]], dtype=torch.int32\n    ).to(DEVICE)\n    outputs = date_embed(inputs)\n    assert outputs[ENCODER_OUTPUT].size()[1:] == date_embed.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(date_embed, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\ndef test_date_wave():\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    date_embed = DateWave().to(DEVICE)\n    inputs = torch.tensor(\n        [[2022, 6, 25, 5, 176, 9, 30, 59, 34259], [2022, 6, 25, 5, 176, 9, 30, 59, 34259]], dtype=torch.int32\n    ).to(DEVICE)\n    outputs = date_embed(inputs)\n    assert outputs[ENCODER_OUTPUT].size()[1:] == date_embed.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(date_embed, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_generic_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.generic_encoders import DenseEncoder, PassthroughEncoder\n\n\n@pytest.mark.parametrize(\"input_size\", [1, 2, 10])\n@pytest.mark.parametrize(\"categorical\", [True, False])\ndef test_generic_passthrough_encoder(input_size: int, categorical: bool):\n    passthrough_encoder = PassthroughEncoder(input_size)\n    # Passthrough encoder allows categorical input feature (int), dense encoder's input must be float.\n    if categorical:\n        inputs = torch.randint(10, (10, input_size))\n    else:\n        inputs = torch.rand((10, input_size))\n    outputs = passthrough_encoder(inputs)\n    # Ensures output shape matches encoder expected output shape.\n    assert outputs[ENCODER_OUTPUT].shape[1:] == passthrough_encoder.output_shape\n\n\n@pytest.mark.parametrize(\"input_size\", [1, 2, 10])\n@pytest.mark.parametrize(\"num_layers\", [1, 3, 6])\n@pytest.mark.parametrize(\"output_size\", [1, 2, 10, 256])\ndef test_generic_dense_encoder(input_size: int, num_layers: int, output_size: int):\n    dense_encoder = DenseEncoder(input_size, num_layers=num_layers, output_size=output_size)\n    inputs = torch.rand((10, input_size))\n    outputs = dense_encoder(inputs)\n    # Ensures output shape matches encoder expected output shape.\n    assert outputs[ENCODER_OUTPUT].shape[1:] == dense_encoder.output_shape\n"
  },
  {
    "path": "tests/ludwig/encoders/test_h3_encoders.py",
    "content": "import logging\n\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders import h3_encoders\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\nDEVICE = get_torch_device()\n\nlogger = logging.getLogger(__name__)\n\n\ndef test_h3_embed():\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    embed = h3_encoders.H3Embed().to(DEVICE)\n    inputs = torch.tensor(\n        [\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n        ],\n        dtype=torch.int32,\n    ).to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\ndef test_h3_weighted_sum():\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    embed = h3_encoders.H3WeightedSum().to(DEVICE)\n    inputs = torch.tensor(\n        [\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n        ],\n        dtype=torch.int32,\n    ).to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n\n\ndef test_h3_rnn_embed():\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    embed = h3_encoders.H3RNN().to(DEVICE)\n    inputs = torch.tensor(\n        [\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n            [2, 0, 14, 102, 7, 0, 3, 5, 0, 5, 5, 0, 5, 7, 7, 7, 7, 7, 7],\n        ],\n        dtype=torch.int32,\n    ).to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs[ENCODER_OUTPUT].size()[1:] == embed.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(embed, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_image_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.image.base import MLPMixerEncoder, ResNetEncoder, Stacked2DCNN, UNetEncoder, ViTEncoder\nfrom ludwig.encoders.image.torchvision import (\n    TVAlexNetEncoder,\n    TVConvNeXtEncoder,\n    TVDenseNetEncoder,\n    TVEfficientNetEncoder,\n    TVGoogLeNetEncoder,\n    TVInceptionV3Encoder,\n    TVMaxVitEncoder,\n    TVMNASNetEncoder,\n    TVMobileNetV2Encoder,\n    TVMobileNetV3Encoder,\n    TVRegNetEncoder,\n    TVResNetEncoder,\n    TVResNeXtEncoder,\n    TVShuffleNetV2Encoder,\n    TVSqueezeNetEncoder,\n    TVSwinTransformerEncoder,\n    TVVGGEncoder,\n    TVViTEncoder,\n    TVWideResNetEncoder,\n)\nfrom ludwig.utils.image_utils import torchvision_model_registry\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"height,width,num_conv_layers,num_channels\", [(224, 224, 5, 3)])\ndef test_stacked2d_cnn(height: int, width: int, num_conv_layers: int, num_channels: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    stacked_2d_cnn = Stacked2DCNN(\n        height=height, width=width, num_conv_layers=num_conv_layers, num_channels=num_channels\n    )\n    inputs = torch.rand(2, num_channels, height, width)\n    outputs = stacked_2d_cnn(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == stacked_2d_cnn.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(stacked_2d_cnn, (inputs,), target)\n\n    assert tpc == upc, f\"Not all expected parameters updated.  Parameters not updated {not_updated}.\"\n\n\n@pytest.mark.parametrize(\"height,width,num_channels\", [(224, 224, 1), (224, 224, 3)])\ndef test_resnet_encoder(height: int, width: int, num_channels: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    resnet = ResNetEncoder(height=height, width=width, num_channels=num_channels)\n    inputs = torch.rand(2, num_channels, height, width)\n    outputs = resnet(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == resnet.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(resnet, (inputs,), target)\n\n    assert tpc == upc, f\"Not all expected parameters updated.  Parameters not updated {not_updated}.\"\n\n\n@pytest.mark.parametrize(\"height,width,num_channels\", [(224, 224, 3)])\ndef test_mlp_mixer_encoder(height: int, width: int, num_channels: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    mlp_mixer = MLPMixerEncoder(height=height, width=width, num_channels=num_channels)\n    inputs = torch.rand(2, num_channels, height, width)\n    outputs = mlp_mixer(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == mlp_mixer.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(mlp_mixer, (inputs,), target)\n\n    assert tpc == upc, f\"Not all expected parameters updated.  Parameters not updated {not_updated}.\"\n\n\n@pytest.mark.parametrize(\"image_size,num_channels\", [(224, 3)])\n@pytest.mark.parametrize(\"use_pretrained\", [True, False])\ndef test_vit_encoder(image_size: int, num_channels: int, use_pretrained: bool):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    vit = ViTEncoder(\n        height=image_size,\n        width=image_size,\n        num_channels=num_channels,\n        use_pretrained=use_pretrained,\n    )\n    inputs = torch.rand(2, num_channels, image_size, image_size)\n    outputs = vit(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == vit.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(vit, (inputs,), target)\n\n    assert tpc == upc, f\"Not all expected parameters updated.  Parameters not updated {not_updated}.\"\n\n\n@pytest.mark.parametrize(\"height,width,num_channels\", [(224, 224, 1), (224, 224, 3)])\ndef test_unet_encoder(height: int, width: int, num_channels: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    unet_encoder = UNetEncoder(height=height, width=width, num_channels=num_channels)\n    inputs = torch.rand(2, num_channels, height, width)\n    outputs = unet_encoder(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == unet_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(unet_encoder, (inputs,), target)\n\n    assert tpc == upc, f\"Not all expected parameters updated.  Parameters not updated {not_updated}.\"\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [v.variant_id for v in torchvision_model_registry[\"alexnet\"].values()])\ndef test_tv_alexnet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVAlexNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"convnext\"].values())).variant_id])\ndef test_tv_convnext_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVConvNeXtEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"densenet\"].values())).variant_id])\ndef test_tv_densenet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVDenseNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n# test only model variants that do not require large amount of memory\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"efficientnet\"].values())).variant_id])\ndef test_tv_efficientnet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVEfficientNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [v.variant_id for v in torchvision_model_registry[\"googlenet\"].values()])\ndef test_tv_googlenet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVGoogLeNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [v.variant_id for v in torchvision_model_registry[\"inceptionv3\"].values()])\ndef test_tv_inceptionv3_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVInceptionV3Encoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"maxvit\"].values())).variant_id])\ndef test_tv_maxvit_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVMaxVitEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"mnasnet\"].values())).variant_id])\ndef test_tv_mnasnet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVMNASNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [v.variant_id for v in torchvision_model_registry[\"mobilenetv2\"].values()])\ndef test_tv_mobilenetv2_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVMobileNetV2Encoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"mobilenetv3\"].values())).variant_id])\ndef test_tv_mobilenetv3_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVMobileNetV3Encoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n# test only model variants that do not require large amount of memory\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"regnet\"].values())).variant_id])\ndef test_tv_regnet_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVRegNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"resnet\"].values())).variant_id])\ndef test_tv_resnet_torch_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVResNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"resnext\"].values())).variant_id])\ndef test_tv_resnext_encoder(\n    model_variant: int,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVResNeXtEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"shufflenet_v2\"].values())).variant_id])\ndef test_tv_shufflenet_v2_encoder(\n    model_variant: str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVShuffleNetV2Encoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [v.variant_id for v in torchvision_model_registry[\"squeezenet\"].values()])\ndef test_tv_squeezenet_encoder(\n    model_variant: str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVSqueezeNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\n    \"model_variant\", [next(iter(torchvision_model_registry[\"swin_transformer\"].values())).variant_id]\n)\ndef test_tv_swin_transformer_encoder(\n    model_variant: str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVSwinTransformerEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"vgg\"].values())).variant_id])\ndef test_tv_vgg_encoder(\n    model_variant: int | str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVVGGEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n# test only VIT model variants that do not require large amount of memory\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"vit\"].values())).variant_id])\ndef test_tv_vit_encoder(\n    model_variant: str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVViTEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n\n\n@pytest.mark.parametrize(\n    \"trainable,saved_weights_in_checkpoint,use_pretrained\",\n    [(True, True, False), (False, False, False)],\n    ids=[\"trainable\", \"frozen\"],\n)\n@pytest.mark.parametrize(\"model_variant\", [next(iter(torchvision_model_registry[\"wide_resnet\"].values())).variant_id])\ndef test_tv_wide_resnet_encoder(\n    model_variant: str,\n    use_pretrained: bool,\n    saved_weights_in_checkpoint: bool,\n    trainable: bool,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVWideResNetEncoder(\n        model_variant=model_variant,\n        use_pretrained=use_pretrained,\n        saved_weights_in_checkpoint=saved_weights_in_checkpoint,\n        trainable=trainable,\n    )\n    inputs = torch.rand(2, *pretrained_model.input_shape)\n    outputs = pretrained_model(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == pretrained_model.output_shape\n"
  },
  {
    "path": "tests/ludwig/encoders/test_llm_encoders.py",
    "content": "import copy\n\nimport pytest\nimport torch\nimport torch.nn as nn\nfrom transformers import AutoConfig, PreTrainedModel\n\nfrom ludwig.encoders.text_encoders import LLMEncoder\nfrom ludwig.schema.encoders.text_encoders import LLMEncoderConfig\nfrom ludwig.schema.llms.peft import AdaloraConfig, BaseAdapterConfig, IA3Config, LoraConfig\nfrom ludwig.utils.llm_utils import get_context_len\n\n# Mapping of adapter types to test against and their respective config objects.\nADAPTER_CONFIG_MAP = {\n    \"adalora\": AdaloraConfig,\n    \"ia3\": IA3Config,\n    \"lora\": LoraConfig,\n}\n\n\n@pytest.fixture()\ndef encoder_config() -> LLMEncoderConfig:\n    \"\"\"Create a baseline LLMEncoderConfig.\n\n    Returns:\n        A baseline LLMEncoderConfig with a small model, no adapter, and no quantization\n    \"\"\"\n    return LLMEncoderConfig(\n        type=\"llm\",\n        max_sequence_length=256,\n        base_model=\"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        adapter=None,\n        quantization=None,\n    )\n\n\n@pytest.fixture()\ndef model_config(encoder_config):\n    return AutoConfig.from_pretrained(encoder_config.base_model)\n\n\nclass WrapperModule(nn.Module):\n    def __init__(self, encoder: LLMEncoder):\n        super().__init__()\n        self.encoder = encoder\n\n\nclass TestLLMEncoder:\n    def create_encoder_config_with_adapter(\n        self, encoder_config: LLMEncoderConfig, adapter: str, **kwargs\n    ) -> BaseAdapterConfig:\n        \"\"\"Create a config for the requested adapter.\n\n        Args:\n            adapter: name of the adapter\n\n        Returns:\n            A config object for the requested adapter. If any keyword args are passed, they will be used to initialize\n            the config.\n        \"\"\"\n        new_config = copy.deepcopy(encoder_config)\n        new_config.adapter = ADAPTER_CONFIG_MAP[adapter](**kwargs)\n        return new_config\n\n    def adapter_param_name_prefix(self, adapter: str) -> str:\n        \"\"\"Get the PEFT paramter name prefix for a given adapter type.\n\n        Args:\n            adapter: A valid config value for `adapter.type`\n\n        Returns:\n            The PEFT-applied prefix for the adapter's parameter names.\n\n        Raises:\n            KeyError: raised when the provided adapter name is not valid for LLMEncoder.\n        \"\"\"\n        return LLMEncoder.ADAPTER_PARAM_NAME_PREFIX[adapter]\n\n    def test_init(self, encoder_config: LLMEncoderConfig, model_config):\n        # Test initializing without an adapter\n        encoder = LLMEncoder(encoder_config=encoder_config)\n\n        assert encoder.model_name == encoder_config.base_model\n        assert isinstance(encoder.model, PreTrainedModel)\n        # Check adapter was not initialized\n        for k in ADAPTER_CONFIG_MAP.keys():\n            prefix = self.adapter_param_name_prefix(k)\n            assert all(map(lambda k: prefix not in k, encoder.state_dict().keys()))\n        assert encoder.input_shape == torch.Size([encoder_config.max_sequence_length])\n        assert encoder.output_shape == torch.Size([encoder_config.max_sequence_length, model_config.hidden_size])\n\n        # The final layer must not be trainable because it is not used\n        last_module = list(encoder.model.modules())[-1]\n        assert all(not p.requires_grad for p in last_module.parameters())\n\n        # Test that max sequence length falls back to the context length when too large\n        context_len = get_context_len(model_config)\n        cl_config = copy.deepcopy(encoder_config)\n        cl_config.max_sequence_length = context_len + 1\n\n        encoder = LLMEncoder(encoder_config=cl_config)\n\n        assert encoder.model_name == encoder_config.base_model\n        assert isinstance(encoder.model, PreTrainedModel)\n        # Check adapter was not initialized\n        for k in ADAPTER_CONFIG_MAP.keys():\n            prefix = self.adapter_param_name_prefix(k)\n            assert all(map(lambda k: prefix not in k, encoder.state_dict().keys()))\n        assert encoder.input_shape == torch.Size([context_len])\n        assert encoder.output_shape == torch.Size([context_len, model_config.hidden_size])\n\n        # The final layer must not be trainable because it is not used\n        last_module = list(encoder.model.modules())[-1]\n        assert all(not p.requires_grad for p in last_module.parameters())\n\n    @pytest.mark.parametrize(\"adapter\", list(ADAPTER_CONFIG_MAP.keys()))\n    def test_init_with_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, model_config):\n        from peft import PeftModel\n\n        encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter)\n        encoder = LLMEncoder(encoder_config=encoder_config_with_adapter)\n        prefix = self.adapter_param_name_prefix(adapter)\n\n        # The adapter should not be initialized until `prepare_for_training` is called\n        assert not isinstance(encoder.model, PeftModel)\n        assert not any(map(lambda k: prefix in k, encoder.state_dict().keys()))\n\n        assert encoder.model_name == encoder_config.base_model\n        assert encoder.input_shape == torch.Size([encoder_config.max_sequence_length])\n        assert encoder.output_shape == torch.Size([encoder_config.max_sequence_length, model_config.hidden_size])\n\n        # The final layer must not be trainable because it is not used\n        last_module = list(encoder.model.modules())[-1]\n        assert all(not p.requires_grad for p in last_module.parameters())\n\n    @pytest.mark.parametrize(\"adapter\", list(ADAPTER_CONFIG_MAP.keys()))\n    def test_prepare_for_training(self, encoder_config: LLMEncoderConfig, adapter: str):\n        from peft import PeftModel\n\n        encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter)\n        encoder = LLMEncoder(encoder_config=encoder_config_with_adapter)\n        prefix = self.adapter_param_name_prefix(adapter)\n\n        # The adapter should not be initialized until `prepare_for_training` is called\n        assert not isinstance(encoder.model, PeftModel)\n        assert not any(map(lambda k: prefix in k, encoder.state_dict().keys()))\n\n        # Initialize the adapter\n        encoder.prepare_for_training()\n\n        # At this point, the adapter should be initialized and the state dict should contain adapter parameters\n        assert isinstance(encoder.model, PeftModel)\n        assert any(map(lambda k: prefix in k, encoder.state_dict().keys()))\n\n    def test_save_to_state_dict(self, encoder_config: LLMEncoderConfig, tmpdir):\n        # With no adapter, the state dict should only contain the model parameters\n        encoder = LLMEncoder(encoder_config=encoder_config)\n        # Check adapter was not initialized\n        for k in ADAPTER_CONFIG_MAP.keys():\n            prefix = self.adapter_param_name_prefix(k)\n            assert all(map(lambda k: prefix not in k, encoder.state_dict().keys()))\n\n    @pytest.mark.parametrize(\"adapter\", list(ADAPTER_CONFIG_MAP.keys()))\n    def test_save_to_state_dict_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, tmpdir):\n        # With an adapter, the state dict should only contain adapter parameters\n        encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter)\n        encoder = LLMEncoder(encoder_config=encoder_config_with_adapter)\n        prefix = self.adapter_param_name_prefix(adapter)\n        # Initialize the adapters\n        encoder.prepare_for_training()\n        assert all(map(lambda k: prefix in k, encoder.state_dict().keys()))\n\n    @pytest.mark.parametrize(\"wrap\", [False, True], ids=[\"no_wrapper\", \"with_wrapper\"])\n    def test_load_from_state_dict(self, encoder_config: LLMEncoderConfig, wrap: bool):\n        def weights_init(m):\n            \"\"\"Reinitialize the weights of a torch module.\"\"\"\n            if hasattr(m, \"weight\") and m.weight.ndim > 1:\n                torch.nn.init.xavier_uniform_(m.weight.data)\n\n        # Create two encoders from the same config\n        encoder1 = LLMEncoder(encoder_config=encoder_config)\n        encoder2 = LLMEncoder(encoder_config=encoder_config)\n\n        if wrap:\n            encoder1 = WrapperModule(encoder1)\n            encoder2 = WrapperModule(encoder2)\n\n        # Reinitialize the weights of one encoder so the two are not identical\n        encoder2.apply(weights_init)\n\n        # Ensure that the weights are different\n        encoder1_sd = encoder1.state_dict()\n        encoder2_sd = encoder2.state_dict()\n        assert any(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), encoder1_sd.keys()))\n\n        # Load the weights of encoder1 back into encoder2 and ensure the weights are equal\n        encoder2.load_state_dict(encoder1_sd)\n        encoder2_sd = encoder2.state_dict()\n        assert all(map(lambda k: torch.equal(encoder1_sd[k], encoder2_sd[k]), encoder1_sd.keys()))\n\n    @pytest.mark.parametrize(\"wrap\", [False, True], ids=[\"no_wrapper\", \"with_wrapper\"])\n    @pytest.mark.parametrize(\"adapter\", list(ADAPTER_CONFIG_MAP.keys()))\n    def test_load_from_state_dict_adapter(self, encoder_config: LLMEncoderConfig, adapter: str, wrap: bool):\n        def weights_init(m):\n            \"\"\"Reinitialize the weights of a torch module.\"\"\"\n            if hasattr(m, \"weight\") and m.weight.ndim > 1:\n                torch.nn.init.xavier_uniform_(m.weight.data)\n\n        prefix = self.adapter_param_name_prefix(adapter)\n\n        # Update the config with an adapter\n        encoder_config_with_adapter = self.create_encoder_config_with_adapter(encoder_config, adapter)\n\n        # Create two encoders from the same config\n        encoder1 = LLMEncoder(encoder_config=encoder_config_with_adapter)\n        encoder2 = LLMEncoder(encoder_config=encoder_config_with_adapter)\n\n        # Initialize the adapters\n        encoder1.prepare_for_training()\n        encoder2.prepare_for_training()\n\n        if wrap:\n            encoder1 = WrapperModule(encoder1)\n            encoder2 = WrapperModule(encoder2)\n\n        encoder2.apply(weights_init)\n\n        encoder1_sd = encoder1.state_dict()\n        encoder2_sd = encoder2.state_dict()\n        adapter_keys = [k for k in encoder1_sd.keys() if prefix in k and \"weight\" in k]\n        model_keys = [k for k in encoder1_sd.keys() if prefix not in k]\n\n        # The LoRA weights should no longer be equal\n        assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), adapter_keys))\n\n        # The remaining weights should also no longer be equal\n        assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), model_keys))\n\n        # Load the weights of encoder1 back into encoder2\n        encoder2.load_state_dict(encoder1_sd)\n        encoder2_sd = encoder2.state_dict()\n\n        # The LoRA weights should now be equal again\n        assert all(map(lambda k: torch.equal(encoder1_sd[k], encoder2_sd[k]), adapter_keys))\n\n        # The remaining weights should still be unequal\n        assert all(map(lambda k: not torch.equal(encoder1_sd[k], encoder2_sd[k]), model_keys))\n"
  },
  {
    "path": "tests/ludwig/encoders/test_sequence_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.sequence_encoders import (\n    SequenceEmbedEncoder,\n    SequencePassthroughEncoder,\n    StackedRNN,\n    StackedTransformer,\n)\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nDEVICE = get_torch_device()\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"reduce_output\", [\"mean\", \"last\", \"concat\", None])\ndef test_sequence_passthrough_encoder(reduce_output: str):\n    batch_size = 10\n    sequence_length = 32\n    sequence_passthrough_encoder = SequencePassthroughEncoder(\n        reduce_output=reduce_output, max_sequence_length=sequence_length, encoding_size=8\n    ).to(DEVICE)\n    inputs = torch.rand(batch_size, sequence_length, 8).to(DEVICE)\n    outputs = sequence_passthrough_encoder(inputs)\n    # SequencePassthroughEncoder does not implement output_shape, expect output to match input shape after reduce.\n    assert outputs[ENCODER_OUTPUT].shape[1:] == sequence_passthrough_encoder.reduce_sequence.output_shape\n\n\n@pytest.mark.parametrize(\n    \"encoder_type\",\n    [SequenceEmbedEncoder, StackedRNN, StackedTransformer],\n)\n@pytest.mark.parametrize(\"reduce_output\", [\"mean\", \"last\", \"concat\", None])\n@pytest.mark.parametrize(\"vocab_size\", [2, 1024])  # Uses vocabularies smaller than (and larger than) embedding size.\ndef test_sequence_encoders(encoder_type: type, reduce_output: str, vocab_size: int):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    batch_size = 10\n    sequence_length = 32\n    sequence_encoder = encoder_type(\n        vocab=list(range(1, vocab_size + 1)), max_sequence_length=sequence_length, reduce_output=reduce_output\n    ).to(DEVICE)\n    inputs = torch.randint(2, (batch_size, sequence_length)).to(DEVICE)\n    outputs = sequence_encoder(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == sequence_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(sequence_encoder, (inputs,), target)\n\n    assert (\n        upc == tpc\n    ), f\"Not all parameters updated.  Parameters not updated: {not_updated}.\\nModule: {sequence_encoder}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_set_encoders.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.encoders.set_encoders import SetSparseEncoder\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"num_fc_layers\", [0, 2])\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"]])\n@pytest.mark.parametrize(\"embedding_size\", [10])\n@pytest.mark.parametrize(\"representation\", [\"sparse\"])\ndef test_set_encoder(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n    num_fc_layers: int,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # setup encoder to test\n    set_encoder = SetSparseEncoder(\n        vocab=vocab,\n        representation=representation,\n        embedding_size=embedding_size,\n        num_fc_layers=num_fc_layers,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE)\n    outputs = set_encoder(inputs)[ENCODER_OUTPUT]\n    assert outputs.shape[1:] == set_encoder.output_shape\n\n    # check for parameter updating\n    target = torch.randn(outputs.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(set_encoder, (inputs,), target)\n    assert tpc == upc, f\"Failed to update parameters. Parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/encoders/test_text_encoders.py",
    "content": "import json\nimport os\nfrom unittest import mock\n\nimport pytest\nimport torch\n\nimport ludwig.schema.encoders.utils as schema_encoders_utils\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT, MODEL_ECD, NAME, TEXT, TRAINER\nfrom ludwig.encoders import text_encoders\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.data_utils import load_json\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\nfrom tests.integration_tests.utils import (\n    category_feature,\n    clear_huggingface_cache,\n    generate_data,\n    HF_ENCODERS,\n    HF_ENCODERS_SHORT,\n    LocalTestBackend,\n    text_feature,\n)\n\nDEVICE = get_torch_device()\nRANDOM_SEED = 1919\n\n\ndef _load_pretrained_hf_model_no_weights(\n    modelClass: type,\n    pretrained_model_name_or_path: str | os.PathLike | None,\n    **pretrained_kwargs,\n):\n    \"\"\"Loads a HF model architecture without loading the weights.\"\"\"\n    from transformers import AutoConfig, AutoModel\n\n    config = AutoConfig.from_pretrained(pretrained_model_name_or_path)\n    return AutoModel.from_config(config), False\n\n\ndef get_mismatched_config_params(ludwig_results_dir, ludwig_model):\n    saved_config_dict = load_json(os.path.join(ludwig_results_dir, MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME))\n    saved_config_obj = ModelConfig.from_dict(saved_config_dict)\n\n    mismatches = []\n    for input_feature_config in saved_config_obj.input_features.to_list():\n        feature_name = input_feature_config[NAME]\n        encoder_config_from_file = input_feature_config[ENCODER]\n        encoder_config_from_model = ludwig_model.model.input_features.get(feature_name).encoder_obj.config.to_dict()\n        for k, v in encoder_config_from_model.items():\n            # Skip saved_weights_in_checkpoint because this value is not yet set when the global config\n            # is modified with the final encoder config.\n            if k == \"saved_weights_in_checkpoint\":\n                continue\n\n            if encoder_config_from_file[k] != v:\n                mismatch = {\n                    \"feature_name\": feature_name,\n                    \"param_name\": k,\n                    \"val_from_file\": encoder_config_from_file[k],\n                    \"val_from_model\": v,\n                }\n                mismatches.append(mismatch)\n    return mismatches\n\n\n# Use a curated subset of HF encoders that are compatible with transformers 5.x.\n# Full encoder-schema coverage is tested by test_encoder_names_constant_synced_with_schema.\n_HF_ENCODERS_E2E = [\"albert\", \"bert\", \"distilbert\", \"electra\", \"roberta\", \"auto_transformer\"]\n\n\n@pytest.mark.parametrize(\"encoder_name\", _HF_ENCODERS_E2E)\ndef test_hf_ludwig_model_e2e(tmpdir, csv_filename, encoder_name):\n    \"\"\"Tests HuggingFace encoders end-to-end.\n\n    This test validates the following:\n        1. Encoder config defaults are compatible with Ludwig experiments.\n        2. Ludwig correctly updates the encoder config with the parameters introduced by the HF encoder.\n        3. Ludwig correctly loads checkpoints containing HF encoder weights.\n    \"\"\"\n    input_features = [\n        text_feature(\n            encoder={\n                \"vocab_size\": 30,\n                \"min_len\": 1,\n                \"type\": encoder_name,\n            }\n        )\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    if encoder_name == \"auto_transformer\":\n        # need to explciitly set the pretrained model name for auto_transformer\n        input_features[0][ENCODER][\n            \"pretrained_model_name_or_path\"\n        ] = \"hf-internal-testing/tiny-bert-for-token-classification\"\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n    model = LudwigModel(config=config, backend=LocalTestBackend())\n\n    with mock.patch(\n        \"ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback\",\n        side_effect=_load_pretrained_hf_model_no_weights,\n    ):\n        # Validates that the defaults associated with the encoder are compatible with Ludwig training.\n        _, _, _, results_dir = model.experiment(dataset=rel_path, output_directory=tmpdir)\n\n        # Validate that the saved config reflects the parameters introduced by the HF encoder.\n        # This ensures that the config updates after initializing the encoder.\n        mismatched_config_params = get_mismatched_config_params(results_dir, model)\n        if len(mismatched_config_params) > 0:\n            raise AssertionError(\n                f\"Config parameters mismatched with encoder parameters: \"\n                f\"{json.dumps(mismatched_config_params, indent=4)}\"\n            )\n\n        # Validate the model can be loaded.\n        # This ensures that the config reflects the internal architecture of the encoder.\n        LudwigModel.load(os.path.join(results_dir, MODEL_FILE_NAME))\n    clear_huggingface_cache()\n\n\n@pytest.mark.parametrize(\"reduce_output\", [None, \"last\", \"sum\", \"mean\", \"max\", \"concat\"])\n@pytest.mark.parametrize(\"encoder_name\", HF_ENCODERS_SHORT)\ndef test_hf_ludwig_model_reduce_options(tmpdir, csv_filename, encoder_name, reduce_output):\n    input_features = [\n        text_feature(\n            preprocessing={\n                \"max_sequence_length\": 10,\n            },\n            encoder={\n                \"vocab_size\": 30,\n                \"min_len\": 1,\n                \"type\": encoder_name,\n                \"reduce_output\": reduce_output,\n            },\n        )\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    if encoder_name == \"auto_transformer\":\n        # need to explciitly set the pretrained model name for auto_transformer\n        input_features[0][ENCODER][\n            \"pretrained_model_name_or_path\"\n        ] = \"hf-internal-testing/tiny-bert-for-token-classification\"\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n\n    try:\n        ModelConfig.from_dict(config)\n    except ConfigValidationError as e:\n        pytest.skip(e.message)\n\n    model = LudwigModel(\n        config=config,\n        backend=LocalTestBackend(),\n    )\n\n    # Validates that the defaults associated with the encoder are compatible with Ludwig training.\n    with mock.patch(\n        \"ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback\",\n        side_effect=_load_pretrained_hf_model_no_weights,\n    ):\n        model.train(\n            dataset=rel_path,\n            output_directory=tmpdir,\n            skip_save_training_description=True,\n            skip_save_training_statistics=True,\n            skip_save_model=True,\n            skip_save_progress=True,\n            skip_save_log=True,\n            skip_save_processed_input=True,\n        )\n\n    clear_huggingface_cache()\n\n\n@pytest.mark.parametrize(\n    \"pretrained_model_name_or_path\",\n    [\n        \"hf-internal-testing/tiny-random-OPTModel\",\n        \"hf-internal-testing/tiny-random-BertModel\",\n        \"hf-internal-testing/tiny-random-DistilBertModel\",\n    ],\n)\ndef test_hf_ludwig_model_auto_transformers(tmpdir, csv_filename, pretrained_model_name_or_path):\n    \"\"\"Tests different AutoModel types to ensure our wrapper handles them correctly.\n\n    This is needed because different PretrainedModel implemetnations have different input / output signatures.\n    \"\"\"\n    input_features = [\n        text_feature(\n            preprocessing={\n                \"max_sequence_length\": 10,\n            },\n            encoder={\n                \"vocab_size\": 30,\n                \"min_len\": 1,\n                \"type\": \"auto_transformer\",\n                \"pretrained_model_name_or_path\": pretrained_model_name_or_path,\n            },\n        )\n    ]\n    output_features = [category_feature(decoder={\"vocab_size\": 2})]\n    rel_path = generate_data(input_features, output_features, csv_filename)\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1},\n    }\n    model = LudwigModel(config=config, backend=LocalTestBackend())\n\n    # Validates that the defaults associated with the encoder are compatible with Ludwig training.\n    with mock.patch(\n        \"ludwig.encoders.text_encoders.load_pretrained_hf_model_with_hub_fallback\",\n        side_effect=_load_pretrained_hf_model_no_weights,\n    ):\n        model.train(dataset=rel_path, output_directory=tmpdir)\n\n\n@pytest.mark.parametrize(\"trainable\", [True, False])\ndef test_distilbert_param_updates(trainable: bool):\n    max_sequence_length = 20\n    distil_bert_encoder = text_encoders.DistilBERTEncoder(\n        use_pretrained=False,\n        max_sequence_length=max_sequence_length,\n        trainable=trainable,\n    ).to(DEVICE)\n\n    # send a random input through the model with its initial weights\n    inputs = torch.rand((2, max_sequence_length)).type(distil_bert_encoder.input_dtype).to(DEVICE)\n    outputs = distil_bert_encoder(inputs)\n\n    # perform a backward pass to update the model params\n    target = torch.randn(outputs[ENCODER_OUTPUT].shape).to(DEVICE)\n    check_module_parameters_updated(distil_bert_encoder, (inputs,), target)\n\n    # send the same input through the model again. should be different if trainable, else the same\n    outputs2 = distil_bert_encoder(inputs)\n\n    encoder_output1 = outputs[ENCODER_OUTPUT]\n    encoder_output2 = outputs2[ENCODER_OUTPUT]\n\n    if trainable:\n        # Outputs should be different if the model was updated\n        assert not torch.equal(encoder_output1, encoder_output2)\n    else:\n        # Outputs should be the same if the model wasn't updated\n        assert torch.equal(encoder_output1, encoder_output2)\n\n\n@pytest.mark.parametrize(\"encoder_name\", HF_ENCODERS)\ndef test_encoder_names_constant_synced_with_schema(encoder_name):\n    \"\"\"Ensures that each value in the HF_ENCODERS constant is represented by an equivalent schema object.\"\"\"\n    schema_encoders_utils.get_encoder_cls(MODEL_ECD, TEXT, encoder_name)\n\n\n@pytest.mark.parametrize(\"vocab_size\", [20])\ndef test_tfidf_encoder(vocab_size: int):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    batch_size = 10\n    sequence_length = 32\n    vocab = [str(i) for i in range(1, vocab_size + 1)]\n    str2idf = {s: 1 for s in vocab}\n    text_encoder = text_encoders.TfIdfEncoder(\n        max_sequence_length=sequence_length,\n        str2idf=str2idf,\n        vocab=vocab,\n        vocab_size=vocab_size,\n    ).to(DEVICE)\n\n    assert len(text_encoder.output_shape) == 1\n    assert text_encoder.output_shape[0] == vocab_size\n    assert len(list(text_encoder.parameters())) == 0\n\n    inputs = torch.randint(2, (batch_size, sequence_length)).to(DEVICE)\n    outputs = text_encoder(inputs)\n    assert outputs[ENCODER_OUTPUT].shape[1:] == text_encoder.output_shape\n\n\ndef test_hf_auto_transformer_use_pretrained():\n    \"\"\"This test ensures that use_pretrained is always True when using the auto_transformer text encoder even if a\n    user explicitly sets it to False.\"\"\"\n    config = {\n        \"input_features\": [\n            text_feature(\n                encoder={\n                    \"type\": \"auto_transformer\",\n                    \"use_pretrained\": False,\n                    \"pretrained_model_name_or_path\": \"hf-internal-testing/tiny-random-bloom\",\n                },\n            )\n        ],\n        \"output_features\": [category_feature(decoder={\"vocab_size\": 2})],\n    }\n\n    model = LudwigModel(config=config, backend=LocalTestBackend())\n    assert model.config_obj.input_features[0].encoder.use_pretrained\n"
  },
  {
    "path": "tests/ludwig/encoders/test_timm_encoder.py",
    "content": "import pytest\nimport torch\n\ntimm = pytest.importorskip(\"timm\", reason=\"timm not installed\")\n\nfrom ludwig.encoders.image.timm import (  # noqa: E402\n    TimmCAFormerEncoder,\n    TimmConvFormerEncoder,\n    TimmEncoder,\n    TimmPoolFormerEncoder,\n)\n\n\n@pytest.mark.parametrize(\n    \"encoder_cls,model_name\",\n    [\n        (TimmEncoder, \"resnetv2_50\"),\n        (TimmCAFormerEncoder, \"caformer_s18\"),\n        (TimmConvFormerEncoder, \"convformer_s18\"),\n        (TimmPoolFormerEncoder, \"poolformerv2_s12\"),\n    ],\n    ids=[\"timm_resnet\", \"caformer\", \"convformer\", \"poolformer\"],\n)\ndef test_timm_encoder_forward(encoder_cls, model_name):\n    encoder = encoder_cls(model_name=model_name, use_pretrained=False, trainable=True)\n\n    # Get the expected input shape from the encoder\n    input_shape = encoder.input_shape  # (C, H, W)\n    batch = torch.randn(2, *input_shape)\n\n    output = encoder(batch)\n    assert \"encoder_output\" in output\n\n    out_tensor = output[\"encoder_output\"]\n    assert out_tensor.shape[0] == 2\n    assert out_tensor.shape[1:] == encoder.output_shape\n\n\n@pytest.mark.parametrize(\"trainable\", [True, False])\ndef test_timm_encoder_trainable(trainable):\n    encoder = TimmCAFormerEncoder(model_name=\"caformer_s18\", use_pretrained=False, trainable=trainable)\n\n    for p in encoder.model.parameters():\n        assert p.requires_grad == trainable\n\n\ndef test_timm_encoder_output_shape_property():\n    encoder = TimmEncoder(model_name=\"caformer_s18\", use_pretrained=False)\n    assert len(encoder.output_shape) == 1\n    assert encoder.output_shape[0] > 0\n"
  },
  {
    "path": "tests/ludwig/evaluation/test_evaluation.py",
    "content": "import os\n\nimport pandas as pd\nimport yaml\n\nfrom ludwig.api import LudwigModel\n\n\ndef test_eval_steps_determinism():\n    # Force CPU to avoid CUBLAS errors with tiny random LLM models on GPU.\n    old_val = os.environ.get(\"CUDA_VISIBLE_DEVICES\")\n    os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"\"\n    try:\n        _run_eval_steps_determinism()\n    finally:\n        if old_val is None:\n            os.environ.pop(\"CUDA_VISIBLE_DEVICES\", None)\n        else:\n            os.environ[\"CUDA_VISIBLE_DEVICES\"] = old_val\n\n\ndef _run_eval_steps_determinism():\n    df = pd.DataFrame(\n        {\n            \"in\": \"a b c d e f g h i j k l m n o p q r s t\".split(\" \"),\n            \"out\": [i for i in range(20)],\n            \"split\": ([0] * 10) + ([2] * 10),\n        }\n    )\n    config = yaml.safe_load(\"\"\"\n    model_type: llm\n    base_model: hf-internal-testing/tiny-random-GPT2LMHeadModel\n\n    input_features:\n      - name: in\n        type: text\n\n    output_features:\n      - name: out\n        type: text\n\n    prompt:\n        template: >-\n            {in}\n\n    generation:\n        temperature: null\n        do_sample: False\n        max_new_tokens: 64\n\n    preprocessing:\n        split:\n            type: fixed\n            column: split\n\n    trainer:\n        type: finetune\n        epochs: 1\n        batch_size: 1\n        eval_batch_size: 2\n        learning_rate: 0.00001\n        gradient_clipping:\n            clipglobalnorm: 1.0\n\n    backend:\n        type: local\n    \"\"\")\n    model = LudwigModel(config=config)\n    model.train(df)\n    results1 = model.evaluate(df)\n\n    model.config_obj.trainer.eval_steps = 4\n    results2 = model.evaluate(df)\n    results3 = model.evaluate(df)\n\n    for k in results1[0][\"out\"]:\n        # The core assertion: repeated evaluations with the same eval_steps\n        # setting must produce identical results (determinism).\n        assert (\n            results2[0][\"out\"][k] == results3[0][\"out\"][k]\n        ), f\"Metric '{k}' differs between repeated evaluations: {results2[0]['out'][k]} vs {results3[0]['out'][k]}\"\n"
  },
  {
    "path": "tests/ludwig/explain/test_captum.py",
    "content": "import torch\n\nfrom ludwig.explain.captum import get_token_attributions\n\n\ndef test_get_token_attributions():\n    feature_name = \"text_8D824\"\n    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)\n    model = type(\"Model\", (), {})()\n    model.training_set_metadata = {\n        feature_name: {\n            \"idx2str\": [\n                \"<EOS>\",\n                \"<SOS>\",\n                \"<PAD>\",\n                \"<UNK>\",\n                \"oypszb\",\n                \"yscnrkzw\",\n                \"llcgslcvzr\",\n            ]\n        }\n    }\n    token_attributions = torch.tensor(\n        [\n            [-0.1289, -0.3222, -0.4931, -0.2914, -0.2891, -0.2871, -0.4118, -0.4647, 0.0000],\n            # zero norm should not lead to division by zero\n            [0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000],\n        ],\n        dtype=torch.float64,\n    )\n\n    toks_and_attrs = get_token_attributions(model, feature_name, input_ids, token_attributions)\n\n    # assert equality up to 4 decimal places\n    assert [[(ta[0], round(ta[1], 4)) for ta in tas] for tas in toks_and_attrs] == [\n        [\n            # normalized attributions\n            (\"<SOS>\", -0.1289),\n            (\"yscnrkzw\", -0.3222),\n            (\"llcgslcvzr\", -0.4931),\n            (\"oypszb\", -0.2914),\n            (\"oypszb\", -0.2891),\n            (\"oypszb\", -0.2871),\n            (\"llcgslcvzr\", -0.4118),\n            (\"<EOS>\", -0.4647),\n            (\"<PAD>\", 0.0),\n        ],\n        [\n            # zero norm should retain original zero attributions\n            (\"<SOS>\", 0.0),\n            (\"oypszb\", 0.0),\n            (\"yscnrkzw\", 0.0),\n            (\"llcgslcvzr\", 0.0),\n            (\"oypszb\", 0.0),\n            (\"oypszb\", 0.0),\n            (\"llcgslcvzr\", 0.0),\n            (\"yscnrkzw\", 0.0),\n            (\"<EOS>\", 0.0),\n        ],\n    ]\n"
  },
  {
    "path": "tests/ludwig/explain/test_util.py",
    "content": "import logging\nimport os\n\nimport pandas as pd\nimport torch\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import NAME\nfrom ludwig.explain.util import get_absolute_module_key_from_submodule, replace_layer_with_copy\nfrom tests.integration_tests.utils import binary_feature, generate_data, LocalTestBackend, text_feature\n\n\ndef test_get_absolute_module_key_from_submodule():\n    class ParentModule(torch.nn.Module):\n        def __init__(self):\n            super().__init__()\n            self.child_module_1 = ChildModule()\n            self.child_module_2 = ChildModule()\n\n    class ChildModule(torch.nn.Module):\n        def __init__(self):\n            super().__init__()\n            self.linear = torch.nn.Linear(10, 10)\n\n    # the expected module names are those that are relative to the parent module, i.e. \"child_module_1.linear.weight\"\n    parent_module = ParentModule()\n    expected_module_names = set()\n    for parent_param_name, _ in parent_module.named_parameters():\n        expected_module_names.add(parent_param_name)\n\n    # incorrect module names are those that are relative to the child module, not the parent module,\n    # i.e. \"linear.weight\" and \"linear.bias\"\n    incorrect_param_names = set()\n    for child_param_name, _ in parent_module.child_module_1.named_parameters():\n        incorrect_param_names.add(child_param_name)\n\n    module_names_child_1 = set(get_absolute_module_key_from_submodule(parent_module, parent_module.child_module_1))\n    module_names_child_2 = set(get_absolute_module_key_from_submodule(parent_module, parent_module.child_module_2))\n\n    # check that the module names are not equivalent to the incorrect module names\n    assert set.isdisjoint(module_names_child_1, incorrect_param_names)\n    assert set.isdisjoint(module_names_child_2, incorrect_param_names)\n\n    # check that the module names are disjoint from one another because they are relative to the parent module\n    assert set.isdisjoint(module_names_child_1, module_names_child_2)\n\n    # check that the union of the two sets is equal to the expected module names\n    assert set.union(module_names_child_1, module_names_child_2) == expected_module_names\n\n\ndef test_replace_layer_with_copy(tmpdir):\n    text_feature_1 = text_feature()\n    text_feature_2 = text_feature(tied=text_feature_1[\"name\"])\n    input_features = [text_feature_1, text_feature_2]\n    output_features = [binary_feature()]\n\n    csv_filename = os.path.join(tmpdir, \"training.csv\")\n    generate_data(input_features, output_features, csv_filename, num_examples=200)\n    df = pd.read_csv(csv_filename)\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        \"trainer\": {\n            \"epochs\": 1,\n        },\n    }\n    model = LudwigModel(config, logging_level=logging.WARNING, backend=LocalTestBackend())\n    model.train(df)\n\n    input_feature_module = model.model.input_features.get(text_feature_2[NAME])\n    target_layer = input_feature_module.encoder_obj.get_embedding_layer()\n\n    data_ptrs_before = {}\n    for param_name, param in input_feature_module.named_parameters():\n        data_ptrs_before[param_name] = param.data_ptr()\n\n    keys_to_copy = get_absolute_module_key_from_submodule(input_feature_module, target_layer)\n    replace_layer_with_copy(input_feature_module, target_layer)\n\n    data_ptrs_after = {}\n    for param_name, param in input_feature_module.named_parameters():\n        data_ptrs_after[param_name] = param.data_ptr()\n\n    # Check that the data pointers are different for the copied keys and that they are the same for the rest.\n    for param_name, _ in input_feature_module.named_parameters():\n        if param_name in keys_to_copy:\n            assert (\n                data_ptrs_before[param_name] != data_ptrs_after[param_name]\n            ), f\"Data pointers should be different for copied key {param_name}\"\n        else:\n            assert (\n                data_ptrs_before[param_name] == data_ptrs_after[param_name]\n            ), f\"Data pointers should be the same for non-copied key {param_name}\"\n"
  },
  {
    "path": "tests/ludwig/features/__init__.py",
    "content": ""
  },
  {
    "path": "tests/ludwig/features/test_audio_feature.py",
    "content": "import os\nfrom random import choice\nfrom string import ascii_lowercase, ascii_uppercase, digits\n\nimport pandas as pd\nimport pytest\nimport torch\n\nfrom ludwig.backend import LOCAL_BACKEND\nfrom ludwig.constants import BFILL, ENCODER_OUTPUT, PROC_COLUMN\nfrom ludwig.features.audio_feature import AudioFeatureMixin, AudioInputFeature\nfrom ludwig.schema.features.audio_feature import AudioInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.utils import audio_feature, category_feature, generate_data\n\nBATCH_SIZE = 2\nSEQ_SIZE = 20\nAUDIO_W_SIZE = 16\n\nCHARS = ascii_uppercase + ascii_lowercase + digits\nVOCAB = [\"\".join(choice(CHARS) for _ in range(2)) for _ in range(256)]\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"encoder\", [\"rnn\", \"stacked_cnn\", \"parallel_cnn\", \"stacked_parallel_cnn\", \"rnn\", \"cnnrnn\"])\ndef test_audio_input_feature(encoder: str) -> None:\n    audio_config = {\n        \"name\": \"audio_feature\",\n        \"type\": \"audio\",\n        \"preprocessing\": {\n            \"type\": \"fbank\",\n            \"window_length_in_s\": 0.04,\n            \"window_shift_in_s\": 0.02,\n            \"num_filter_bands\": 80,\n            \"audio_file_length_limit_in_s\": 3.0,\n        },\n        \"encoder\": {\n            \"type\": encoder,\n            \"should_embed\": False,\n            \"vocab\": VOCAB,\n            \"max_sequence_length\": SEQ_SIZE,\n            \"embedding_size\": AUDIO_W_SIZE,\n        },\n    }\n\n    audio_config, _ = load_config_with_kwargs(AudioInputFeatureConfig, audio_config)\n    audio_input_feature = AudioInputFeature(audio_config).to(DEVICE)\n\n    audio_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, AUDIO_W_SIZE], dtype=torch.float32).to(DEVICE)\n    encoder_output = audio_input_feature(audio_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape[1:] == audio_input_feature.output_shape\n\n\n@pytest.mark.parametrize(\"feature_type\", [\"raw\", \"stft\", \"stft_phase\", \"group_delay\", \"fbank\"])\ndef test_add_feature_data(feature_type, tmpdir):\n    preprocessing_params = {\n        \"audio_file_length_limit_in_s\": 3.0,\n        \"missing_value_strategy\": BFILL,\n        \"in_memory\": True,\n        \"padding_value\": 0,\n        \"norm\": \"per_file\",\n        \"type\": feature_type,\n        \"window_length_in_s\": 0.04,\n        \"window_shift_in_s\": 0.02,\n        \"num_fft_points\": None,\n        \"window_type\": \"hamming\",\n        \"num_filter_bands\": 80,\n    }\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n    audio_feature_config = audio_feature(audio_dest_folder, preprocessing=preprocessing_params)\n    data_df_path = generate_data(\n        [audio_feature_config],\n        [category_feature(vocab_size=5, reduce_input=\"sum\")],\n        os.path.join(tmpdir, \"data.csv\"),\n        num_examples=10,\n    )\n    data_df = pd.read_csv(data_df_path)\n    metadata = {\n        audio_feature_config[\"name\"]: AudioFeatureMixin.get_feature_meta(\n            {}, data_df[audio_feature_config[\"name\"]], preprocessing_params, LOCAL_BACKEND, True\n        )\n    }\n\n    proc_df = {}\n    AudioFeatureMixin.add_feature_data(\n        feature_config=audio_feature_config,\n        input_df=data_df,\n        proc_df=proc_df,\n        metadata=metadata,\n        preprocessing_parameters=preprocessing_params,\n        backend=LOCAL_BACKEND,\n        skip_save_processed_input=False,\n    )\n\n    assert len(proc_df[audio_feature_config[PROC_COLUMN]]) == 10\n"
  },
  {
    "path": "tests/ludwig/features/test_bag_feature.py",
    "content": "from random import choice\nfrom string import ascii_lowercase, ascii_uppercase, digits\n\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT\nfrom ludwig.features.bag_feature import BagInputFeature\nfrom ludwig.schema.features.bag_feature import BagInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nSEQ_SIZE = 20\nBAG_W_SIZE = 256\nEMBEDDING_SIZE = 5\n\nCHARS = ascii_uppercase + ascii_lowercase + digits\nVOCAB = [\"\".join(choice(CHARS) for _ in range(2)) for _ in range(256)]\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef bag_config():\n    return {\n        \"name\": \"bag_feature\",\n        \"type\": \"bag\",\n        \"encoder\": {\n            \"max_len\": 5,\n            \"vocab_size\": 10,\n            \"embedding_size\": EMBEDDING_SIZE,\n            \"vocab\": VOCAB,\n        },\n    }\n\n\n@pytest.mark.parametrize(\"encoder\", [\"embed\"])\ndef test_bag_input_feature(bag_config: dict, encoder: str) -> None:\n    bag_config[ENCODER].update({\"type\": encoder})\n    bag_config, _ = load_config_with_kwargs(BagInputFeatureConfig, bag_config)\n    bag_input_feature = BagInputFeature(bag_config).to(DEVICE)\n    bag_tensor = torch.randn([BATCH_SIZE, SEQ_SIZE, BAG_W_SIZE], dtype=torch.float32).to(DEVICE)\n    encoder_output = bag_input_feature(bag_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape[1:][1:] == bag_input_feature.output_shape\n"
  },
  {
    "path": "tests/ludwig/features/test_binary_feature.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT\nfrom ludwig.features.binary_feature import BinaryInputFeature, BinaryOutputFeature\nfrom ludwig.schema.features.binary_feature import BinaryInputFeatureConfig, BinaryOutputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nBINARY_W_SIZE = 1\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef binary_config():\n    return {\n        \"name\": \"binary_feature\",\n        \"type\": \"binary\",\n    }\n\n\n@pytest.mark.parametrize(\"encoder\", [\"passthrough\", \"dense\"])\ndef test_binary_input_feature(binary_config: dict, encoder: str):\n    binary_config.update({ENCODER: {\"type\": encoder}})\n    binary_config, _ = load_config_with_kwargs(BinaryInputFeatureConfig, binary_config)\n    binary_input_feature = BinaryInputFeature(binary_config).to(DEVICE)\n\n    binary_tensor = binary_input_feature.create_sample_input(batch_size=BATCH_SIZE).to(DEVICE)\n    assert binary_tensor.shape == torch.Size([BATCH_SIZE])\n    assert binary_tensor.dtype == torch.bool\n\n    encoder_output = binary_input_feature(binary_tensor)\n\n    assert encoder_output[ENCODER_OUTPUT].shape[1:] == binary_input_feature.output_shape\n\n\ndef test_binary_output_feature():\n    binary_output_config = {\n        \"name\": \"binary_feature\",\n        \"type\": \"binary\",\n        \"input_size\": BINARY_W_SIZE,\n        \"decoder\": {\n            \"type\": \"regressor\",\n            \"input_size\": 1,\n        },\n        \"loss\": {\n            \"type\": \"binary_weighted_cross_entropy\",\n            \"positive_class_weight\": 1,\n            \"robust_lambda\": 0,\n            \"confidence_penalty\": 0,\n        },\n    }\n    binary_output_config, _ = load_config_with_kwargs(BinaryOutputFeatureConfig, binary_output_config)\n    binary_output_feature = BinaryOutputFeature(binary_output_config, {}).to(DEVICE)\n    combiner_outputs = dict()\n    combiner_outputs[\"combiner_output\"] = torch.randn([BATCH_SIZE, BINARY_W_SIZE], dtype=torch.float32).to(DEVICE)\n\n    binary_output = binary_output_feature(combiner_outputs, {})\n\n    assert \"last_hidden\" in binary_output\n    assert \"logits\" in binary_output\n    assert binary_output[\"logits\"].size() == torch.Size([BATCH_SIZE])\n\n\ndef test_binary_output_feature_without_positive_class_weight():\n    binary_output_config = {\n        \"name\": \"binary_feature\",\n        \"type\": \"binary\",\n        \"input_size\": BINARY_W_SIZE,\n        \"decoder\": {\n            \"type\": \"regressor\",\n            \"input_size\": 1,\n        },\n        \"loss\": {\n            \"type\": \"binary_weighted_cross_entropy\",\n            \"positive_class_weight\": None,\n            \"robust_lambda\": 0,\n            \"confidence_penalty\": 0,\n        },\n    }\n    binary_output_config, _ = load_config_with_kwargs(BinaryOutputFeatureConfig, binary_output_config)\n    binary_output_feature = BinaryOutputFeature(binary_output_config, {}).to(DEVICE)\n    combiner_outputs = {}\n    combiner_outputs[\"combiner_output\"] = torch.randn([BATCH_SIZE, BINARY_W_SIZE], dtype=torch.float32).to(DEVICE)\n\n    binary_output = binary_output_feature(combiner_outputs, {})\n\n    assert \"last_hidden\" in binary_output\n    assert \"logits\" in binary_output\n    assert binary_output[\"logits\"].size() == torch.Size([BATCH_SIZE])\n"
  },
  {
    "path": "tests/ludwig/features/test_category_feature.py",
    "content": "from copy import deepcopy\n\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT, TYPE\nfrom ludwig.features.category_feature import CategoryInputFeature\nfrom ludwig.schema.features.category_feature import ECDCategoryInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef category_config():\n    return {\n        \"name\": \"category_column_name\",\n        \"type\": \"category\",\n        \"tied\": None,\n        \"encoder\": {\n            \"embedding_size\": 256,\n            \"embeddings_on_cpu\": False,\n            \"pretrained_embeddings\": None,\n            \"embeddings_trainable\": True,\n            \"dropout\": 0.0,\n            \"vocab\": [\"a\", \"b\", \"c\"],\n            \"embedding_initializer\": None,\n        },\n    }\n\n\n@pytest.mark.parametrize(\"encoder\", [\"dense\", \"sparse\"])\ndef test_category_input_feature(\n    category_config: dict,\n    encoder: str,\n) -> None:\n    # setup image input feature definition\n    category_def = deepcopy(category_config)\n    category_def[ENCODER][TYPE] = encoder\n\n    # pickup any other missing parameters\n    defaults = ECDCategoryInputFeatureConfig(name=\"foo\").to_dict()\n    category_def = merge_dict(defaults, category_def)\n\n    # ensure no exceptions raised during build\n    category_config, _ = load_config_with_kwargs(ECDCategoryInputFeatureConfig, category_def)\n    input_feature_obj = CategoryInputFeature(category_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = torch.randint(0, 3, size=(BATCH_SIZE,), dtype=torch.int32).to(DEVICE)\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n"
  },
  {
    "path": "tests/ludwig/features/test_date_feature.py",
    "content": "from copy import deepcopy\nfrom datetime import date, datetime, timezone\nfrom typing import Any\n\nimport pytest\nimport torch\nfrom dateutil.parser import parse\n\nfrom ludwig.constants import ENCODER_OUTPUT, FILL_WITH_CONST, MISSING_VALUE_STRATEGY\nfrom ludwig.features import date_feature\nfrom ludwig.features.date_feature import DateInputFeature\nfrom ludwig.schema.features.date_feature import DateInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.types import FeatureConfigDict\nfrom ludwig.utils.date_utils import create_vector_from_datetime_obj\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nDATE_W_SIZE = 9\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef date_config():\n    return {\"name\": \"date_column_name\", \"type\": \"date\"}\n\n\ndef test_date_input_feature(date_config: FeatureConfigDict):\n    # setup image input feature definition\n    feature_def = deepcopy(date_config)\n\n    # pickup any other missing parameters\n    defaults = DateInputFeatureConfig(name=\"foo\").to_dict()\n    set_def = merge_dict(defaults, feature_def)\n\n    # ensure no exceptions raised during build\n    feature_config, _ = load_config_with_kwargs(DateInputFeatureConfig, set_def)\n    input_feature_obj = DateInputFeature(feature_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = input_feature_obj.create_sample_input(batch_size=BATCH_SIZE).to(DEVICE)\n    assert input_tensor.shape == torch.Size((BATCH_SIZE, DATE_W_SIZE))\n    assert input_tensor.dtype == torch.int32\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n\n\n@pytest.mark.parametrize(\n    \"date_str,datetime_format,expected_list\",\n    [\n        (\"2012-02-26T13:51:50.417-07:00\", None, [2012, 2, 26, 6, 57, 13, 51, 50, 49910]),\n        (\"2022-06-25 09:30:59\", None, [2022, 6, 25, 5, 176, 9, 30, 59, 34259]),\n        (\"2022-06-25\", None, [2022, 6, 25, 5, 176, 0, 0, 0, 0]),\n    ],\n)\ndef test_date_to_list(date_str, datetime_format, expected_list):\n    preprocessing_parameters = None\n    assert (\n        date_feature.DateInputFeature.date_to_list(date_str, datetime_format, preprocessing_parameters) == expected_list\n    )\n\n\n@pytest.fixture(scope=\"module\")\ndef reference_date_list() -> list[int]:\n    return create_vector_from_datetime_obj(\n        datetime.fromtimestamp(1691600953.443032, tz=timezone.utc).replace(tzinfo=None)\n    )\n\n\n@pytest.fixture(scope=\"module\")\ndef fill_value() -> str:\n    return \"1970-01-01 00:00:00\"\n\n\n@pytest.fixture(scope=\"module\")\ndef fill_value_list(fill_value: str) -> list[int]:\n    return create_vector_from_datetime_obj(parse(fill_value))\n\n\n@pytest.mark.parametrize(\n    \"timestamp,datetime_format,expected_list\",\n    [\n        pytest.param(1691600953.443032, None, \"reference_date_list\", id=\"float-s\"),\n        pytest.param(1691600953443.032, None, \"reference_date_list\", id=\"float-ms\"),\n        pytest.param(1691600953, None, \"reference_date_list\", id=\"int-s\"),\n        pytest.param(1691600953443, None, \"reference_date_list\", id=\"int-ms\"),\n        pytest.param(1691600953.443032, \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"float-s-fmt\"),\n        pytest.param(1691600953443.032, \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"float-ms-fmt\"),\n        pytest.param(1691600953, \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"int-s-fmt\"),\n        pytest.param(1691600953443, \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"int-ms-fmt\"),\n        pytest.param(\"1691600953.443032\", None, \"reference_date_list\", id=\"string[float]-s\"),\n        pytest.param(\"1691600953443.0032\", None, \"reference_date_list\", id=\"string[float]-ms\"),\n        pytest.param(\"1691600953\", None, \"reference_date_list\", id=\"string[int]-s\"),\n        pytest.param(\"1691600953443\", None, \"reference_date_list\", id=\"string[int]-ms\"),\n        pytest.param(\"1691600953.443032\", \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"string[float]-s-fmt\"),\n        pytest.param(\"1691600953443.0032\", \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"string[float]-ms-fmt\"),\n        pytest.param(\"1691600953\", \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"string[int]-s-fmt\"),\n        pytest.param(\"1691600953443\", \"%d/%m/%y %H:%M:%S.%f\", \"reference_date_list\", id=\"string[int]-ms-fmt\"),\n        pytest.param(\"foo\", None, \"fill_value_list\", id=\"string error\"),\n        pytest.param([1691600953.443032], None, \"fill_value_list\", id=\"list error\"),\n        pytest.param(None, None, \"fill_value_list\", id=\"NoneType error\"),\n    ],\n)\ndef test_date_to_list_numeric(timestamp: Any, datetime_format: str, expected_list: list[int], fill_value: str, request):\n    \"\"\"Test that numeric datetime formats are converted correctly.\n\n    Currently, we support int, float, and string representations of POSIX timestamps in seconds and milliseconds. Valid\n    timestamps should be converted to datetime lists by `luwdig.utils.date_utils.create_vector_from_datetime_object`.\n    If a string format is provided, it should be ignored.\n\n    Args:\n        timestamp: Input to be converted to a date vector\n        datetime_format: Optional format string, should be ignored under the hood with these timestamps.\n        expected_list: The expected output of `DateFeatureMixin.date_to_list`\n        fill_value: Date to be used as fallback\n        request: pytest request fixture\n    \"\"\"\n    expected_result = request.getfixturevalue(expected_list)\n\n    # The default fill value is `datetime.now`, for testing we override this to be a constant.\n    preprocessing_parameters = {MISSING_VALUE_STRATEGY: FILL_WITH_CONST, \"fill_value\": fill_value}\n\n    # No exception should ever be raised from `date_to_list` due to a parsing error. The expected behavior is to fall\n    # back to the fill value.\n    dt = date_feature.DateInputFeature.date_to_list(timestamp, datetime_format, preprocessing_parameters)\n    assert dt == expected_result\n\n\ndef test_date_to_list__DatetimeObjectFromParsedJSON():\n    preprocessing_parameters = None\n    datetime_obj = datetime.fromisoformat(\"2022-06-25\")\n    assert date_feature.DateInputFeature.date_to_list(datetime_obj, None, preprocessing_parameters) == [\n        2022,\n        6,\n        25,\n        5,\n        176,\n        0,\n        0,\n        0,\n        0,\n    ]\n\n\ndef test_date_to_list__UsesFillValueOnInvalidDate():\n    preprocessing_parameters = {\"fill_value\": \"2013-02-26\"}\n    invalid_date_str = \"2012abc-02\"\n    datetime_format = None\n    assert date_feature.DateInputFeature.date_to_list(invalid_date_str, datetime_format, preprocessing_parameters) == [\n        2013,\n        2,\n        26,\n        1,\n        57,\n        0,\n        0,\n        0,\n        0,\n    ]\n\n\n@pytest.fixture(scope=\"module\")\ndef date_obj():\n    return date.fromisoformat(\"2022-06-25\")\n\n\n@pytest.fixture(scope=\"module\")\ndef date_obj_vec():\n    return create_vector_from_datetime_obj(datetime.fromisoformat(\"2022-06-25\"))\n\n\ndef test_date_object_to_list(date_obj, date_obj_vec, fill_value):\n    \"\"\"Test support for datetime.date object conversion.\n\n    Args:\n        date_obj: Date object to convert into a vector\n        date_obj_vector: Expected vector version of `date_obj`\n    \"\"\"\n    computed_date_vec = date_feature.DateInputFeature.date_to_list(\n        date_obj, None, preprocessing_parameters={MISSING_VALUE_STRATEGY: FILL_WITH_CONST, \"fill_value\": fill_value}\n    )\n    assert computed_date_vec == date_obj_vec\n"
  },
  {
    "path": "tests/ludwig/features/test_feature_utils.py",
    "content": "import numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.features import feature_utils\n\n\ndef test_ludwig_feature_dict():\n    feature_dict = feature_utils.LudwigFeatureDict()\n\n    to_module = torch.nn.Module()\n    type_module = torch.nn.Module()\n\n    feature_dict.set(\"to\", to_module)\n    feature_dict.set(\"type\", type_module)\n\n    assert iter(feature_dict) is not None\n    assert next(feature_dict) is not None\n    assert len(feature_dict) == 2\n    assert feature_dict.keys() == [\"to\", \"type\"]\n    assert feature_dict.items() == [(\"to\", to_module), (\"type\", type_module)]\n    assert feature_dict.get(\"to\"), to_module\n\n    feature_dict.update({\"to_empty\": torch.nn.Module()})\n\n    assert len(feature_dict) == 3\n    assert [key for key in feature_dict] == [\"to\", \"type\", \"to_empty\"]\n\n\ndef test_ludwig_feature_dict_with_periods():\n    feature_dict = feature_utils.LudwigFeatureDict()\n\n    to_module = torch.nn.Module()\n\n    feature_dict.set(\"to.\", to_module)\n\n    assert feature_dict.keys() == [\"to.\"]\n    assert feature_dict.items() == [(\"to.\", to_module)]\n    assert feature_dict.get(\"to.\") == to_module\n\n\n@pytest.mark.parametrize(\"sequence_type\", [list, tuple, np.array])\ndef test_compute_token_probabilities(sequence_type):\n    inputs = sequence_type(\n        [\n            [0.1, 0.2, 0.7],\n            [0.3, 0.4, 0.3],\n            [0.6, 0.3, 0.2],\n        ]\n    )\n\n    token_probabilities = feature_utils.compute_token_probabilities(inputs)\n    assert np.allclose(token_probabilities, [0.7, 0.4, 0.6])\n\n\ndef test_compute_sequence_probability():\n    inputs = np.array([0.7, 0.4, 0.6])\n\n    sequence_probability = feature_utils.compute_sequence_probability(\n        inputs, max_sequence_length=2, return_log_prob=False\n    )\n\n    assert np.allclose(sequence_probability, [0.28])  # 0.7 * 0.4\n"
  },
  {
    "path": "tests/ludwig/features/test_h3_feature.py",
    "content": "from ludwig.features import h3_feature\n\n\ndef test_h3_to_list():\n    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]\n    assert h3_feature.H3FeatureMixin.h3_to_list(576495936675512319) == [\n        1,\n        0,\n        0,\n        0,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n    ]\n    assert h3_feature.H3FeatureMixin.h3_to_list(102576495936675512319) == [\n        1,\n        7,\n        8,\n        71,\n        2,\n        7,\n        1,\n        2,\n        2,\n        6,\n        1,\n        6,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n    ]\n    assert h3_feature.H3FeatureMixin.h3_to_list(50102576495936675512319) == [\n        2,\n        0,\n        14,\n        102,\n        7,\n        0,\n        3,\n        5,\n        0,\n        5,\n        5,\n        0,\n        5,\n        7,\n        7,\n        7,\n        7,\n        7,\n        7,\n    ]\n"
  },
  {
    "path": "tests/ludwig/features/test_image_feature.py",
    "content": "from copy import deepcopy\n\nimport pytest\nimport torch\n\nfrom ludwig.constants import (\n    BFILL,\n    CROP_OR_PAD,\n    ENCODER,\n    ENCODER_OUTPUT,\n    ENCODER_OUTPUT_STATE,\n    INTERPOLATE,\n    LOGITS,\n    TYPE,\n)\nfrom ludwig.features.image_feature import _ImagePreprocessing, ImageInputFeature, ImageOutputFeature\nfrom ludwig.schema.features.image_feature import ImageInputFeatureConfig, ImageOutputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.utils import image_feature\n\nBATCH_SIZE = 2\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef image_config():\n    return {\n        \"name\": \"image_column_name\",\n        \"type\": \"image\",\n        \"tied\": None,\n        \"encoder\": {\n            \"type\": \"stacked_cnn\",\n            \"conv_layers\": None,\n            \"num_conv_layers\": None,\n            \"filter_size\": 3,\n            \"num_filters\": 256,\n            \"strides\": (1, 1),\n            \"padding\": \"valid\",\n            \"dilation_rate\": (1, 1),\n            \"conv_use_bias\": True,\n            \"conv_weights_initializer\": \"xavier_uniform\",\n            \"conv_bias_initializer\": \"zeros\",\n            \"conv_norm\": None,\n            \"conv_norm_params\": None,\n            \"conv_activation\": \"relu\",\n            \"conv_dropout\": 0,\n            \"pool_function\": \"max\",\n            \"pool_size\": (2, 2),\n            \"pool_strides\": None,\n            \"fc_layers\": None,\n            \"num_fc_layers\": 1,\n            \"output_size\": 16,\n            \"fc_use_bias\": True,\n            \"fc_weights_initializer\": \"xavier_uniform\",\n            \"fc_bias_initializer\": \"zeros\",\n            \"fc_norm\": None,\n            \"fc_norm_params\": None,\n            \"fc_activation\": \"relu\",\n            \"fc_dropout\": 0,\n        },\n        \"preprocessing\": {\n            \"height\": 28,\n            \"width\": 28,\n            \"num_channels\": 1,\n            \"scaling\": \"pixel_normalization\",\n        },\n    }\n\n\n@pytest.mark.parametrize(\n    \"encoder, height, width, num_channels\",\n    [\n        (\"stacked_cnn\", 28, 28, 3),\n        (\"stacked_cnn\", 28, 28, 1),\n        (\"mlp_mixer\", 32, 32, 3),\n    ],\n)\ndef test_image_input_feature(image_config: dict, encoder: str, height: int, width: int, num_channels: int) -> None:\n    # setup image input feature definition\n    image_def = deepcopy(image_config)\n    image_def[ENCODER][TYPE] = encoder\n    image_def[ENCODER][\"height\"] = height\n    image_def[ENCODER][\"width\"] = width\n    image_def[ENCODER][\"num_channels\"] = num_channels\n\n    # pickup any other missing parameters\n    defaults = ImageInputFeatureConfig(name=\"foo\").to_dict()\n    set_def = merge_dict(defaults, image_def)\n\n    # ensure no exceptions raised during build\n    image_config, _ = load_config_with_kwargs(ImageInputFeatureConfig, set_def)\n    input_feature_obj = ImageInputFeature(image_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = torch.rand(size=(BATCH_SIZE, num_channels, height, width), dtype=torch.float32).to(DEVICE)\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n\n    # todo: remove code\n    # # test for parameter updates\n    # before = [(x[0], x[1].clone()) for x in input_feature_obj.named_parameters()]\n    # loss_function = torch.nn.MSELoss()\n    # optimizer = torch.optim.SGD(input_feature_obj.parameters(), lr=0.1)\n    # target_tensor = torch.ones(encoder_output['encoder_output'].shape, dtype=torch.float32)\n    #\n    # # do parameter update\n    # loss = loss_function(encoder_output['encoder_output'], target_tensor)\n    # loss.backward()\n    # optimizer.step()\n    #\n    # after = [(x[0], x[1].clone()) for x in input_feature_obj.named_parameters()]\n    #\n    # # check for parameter update\n    # for b, a in zip(before, after):\n    #     if not (b[1] != a[1]).any():\n    #         raise RuntimeError(\n    #             f'no parameter update for {a[0]}'\n    #         )\n\n\n@pytest.mark.parametrize(\n    \"encoder, decoder, height, width, num_channels, num_classes\",\n    [\n        (\"unet\", \"unet\", 128, 128, 3, 2),\n        (\"unet\", \"unet\", 32, 32, 3, 7),\n    ],\n)\ndef test_image_output_feature(\n    encoder: str,\n    decoder: str,\n    height: int,\n    width: int,\n    num_channels: int,\n    num_classes: int,\n) -> None:\n    # setup image input feature definition\n    input_feature_def = image_feature(\n        folder=\".\",\n        encoder={\n            \"type\": encoder,\n            \"height\": height,\n            \"width\": width,\n            \"num_channels\": num_channels,\n        },\n    )\n    # create image input feature object\n    feature_cls = ImageInputFeature\n    schema_cls = ImageInputFeatureConfig\n    input_config = schema_cls.from_dict(input_feature_def)\n    input_feature_obj = feature_cls(input_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = torch.rand(size=(BATCH_SIZE, num_channels, height, width), dtype=torch.float32).to(DEVICE)\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n    if encoder == \"unet\":\n        assert len(encoder_output[ENCODER_OUTPUT_STATE]) == 4\n\n    hidden = torch.reshape(encoder_output[ENCODER_OUTPUT], [BATCH_SIZE, -1])\n\n    # setup image output feature definition\n    output_feature_def = image_feature(\n        folder=\".\",\n        decoder={\n            \"type\": decoder,\n            \"height\": height,\n            \"width\": width,\n            \"num_channels\": num_channels,\n            \"num_classes\": num_classes,\n        },\n        input_size=hidden.size(dim=1),\n    )\n    # create image output feature object\n    feature_cls = ImageOutputFeature\n    schema_cls = ImageOutputFeatureConfig\n    output_config = schema_cls.from_dict(output_feature_def)\n    output_feature_obj = feature_cls(output_config, {}).to(DEVICE)\n\n    combiner_outputs = {\n        \"combiner_output\": hidden,\n        ENCODER_OUTPUT_STATE: encoder_output[ENCODER_OUTPUT_STATE],\n    }\n\n    image_output = output_feature_obj(combiner_outputs, {})\n\n    assert LOGITS in image_output\n    assert image_output[LOGITS].size() == torch.Size([BATCH_SIZE, num_classes, height, width])\n\n\ndef test_image_preproc_module_bad_num_channels():\n    metadata = {\n        \"preprocessing\": {\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"resize_method\": \"interpolate\",\n            \"scaling\": \"pixel_normalization\",\n            \"num_processes\": 1,\n            \"infer_image_num_channels\": True,\n            \"infer_image_dimensions\": True,\n            \"infer_image_max_height\": 256,\n            \"infer_image_max_width\": 256,\n            \"infer_image_sample_size\": 100,\n            \"height\": 12,\n            \"width\": 12,\n            \"num_channels\": 2,\n            \"num_classes\": 0,\n            \"channel_class_map\": [],\n        },\n        \"reshape\": (2, 12, 12),\n    }\n    module = _ImagePreprocessing(metadata)\n\n    with pytest.raises(ValueError):\n        module(torch.rand(2, 3, 10, 10))\n\n\n@pytest.mark.parametrize(\"resize_method\", [INTERPOLATE, CROP_OR_PAD])\n@pytest.mark.parametrize([\"num_channels\", \"num_channels_expected\"], [(1, 3), (3, 1)])\ndef test_image_preproc_module_list_of_tensors(resize_method, num_channels, num_channels_expected):\n    metadata = {\n        \"preprocessing\": {\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"resize_method\": resize_method,\n            \"scaling\": \"pixel_normalization\",\n            \"num_processes\": 1,\n            \"infer_image_num_channels\": True,\n            \"infer_image_dimensions\": True,\n            \"infer_image_max_height\": 256,\n            \"infer_image_max_width\": 256,\n            \"infer_image_sample_size\": 100,\n            \"height\": 12,\n            \"width\": 12,\n            \"num_channels\": num_channels_expected,\n            \"num_classes\": 0,\n            \"channel_class_map\": [],\n        },\n        \"reshape\": (num_channels_expected, 12, 12),\n    }\n    module = _ImagePreprocessing(metadata)\n\n    res = module([torch.rand(num_channels, 25, 25), torch.rand(num_channels, 10, 10)])\n\n    assert res.shape == torch.Size((2, num_channels_expected, 12, 12))\n\n\n@pytest.mark.parametrize(\"resize_method\", [INTERPOLATE, CROP_OR_PAD])\n@pytest.mark.parametrize([\"num_channels\", \"num_channels_expected\"], [(1, 3), (3, 1)])\ndef test_image_preproc_module_tensor(resize_method, num_channels, num_channels_expected):\n    metadata = {\n        \"preprocessing\": {\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"resize_method\": resize_method,\n            \"scaling\": \"pixel_normalization\",\n            \"num_processes\": 1,\n            \"infer_image_num_channels\": True,\n            \"infer_image_dimensions\": True,\n            \"infer_image_max_height\": 256,\n            \"infer_image_max_width\": 256,\n            \"infer_image_sample_size\": 100,\n            \"height\": 12,\n            \"width\": 12,\n            \"num_channels\": num_channels_expected,\n            \"num_classes\": 0,\n            \"channel_class_map\": [],\n        },\n        \"reshape\": (num_channels_expected, 12, 12),\n    }\n    module = _ImagePreprocessing(metadata)\n\n    res = module(torch.rand(2, num_channels, 10, 10))\n\n    assert res.shape == torch.Size((2, num_channels_expected, 12, 12))\n\n\n@pytest.mark.parametrize([\"height\", \"width\"], [(224, 224), (32, 32)])\ndef test_image_preproc_module_class_map(height, width):\n    metadata = {\n        \"preprocessing\": {\n            \"num_processes\": 1,\n            \"resize_method\": CROP_OR_PAD,\n            \"infer_image_num_channels\": True,\n            \"infer_image_dimensions\": True,\n            \"infer_image_max_height\": height,\n            \"infer_image_max_width\": width,\n            \"infer_image_sample_size\": 100,\n            \"infer_image_num_classes\": True,\n            \"height\": height,\n            \"width\": width,\n            \"num_channels\": 3,\n            \"num_classes\": 8,\n            \"channel_class_map\": [\n                [40, 40, 40],\n                [40, 40, 41],\n                [40, 41, 40],\n                [40, 41, 41],\n                [41, 40, 40],\n                [41, 40, 41],\n                [41, 41, 40],\n                [41, 41, 41],\n            ],\n        },\n    }\n    module = _ImagePreprocessing(metadata)\n\n    res = module(torch.randint(40, 42, (2, 3, height, width)))\n\n    assert res.shape == torch.Size((2, height, width))\n    assert torch.all(res.ge(0)) and torch.all(res.le(7))\n"
  },
  {
    "path": "tests/ludwig/features/test_number_feature.py",
    "content": "from copy import deepcopy\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.features.number_feature import _OutlierReplacer, NumberInputFeature\nfrom ludwig.schema.features.number_feature import ECDNumberInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef number_config():\n    return {\"name\": \"number_column_name\", \"type\": \"number\"}\n\n\ndef test_number_input_feature(\n    number_config: dict,\n) -> None:\n    # setup image input feature definition\n    number_def = deepcopy(number_config)\n\n    # pickup any other missing parameters\n    defaults = ECDNumberInputFeatureConfig(name=\"foo\").to_dict()\n    set_def = merge_dict(defaults, number_def)\n\n    # ensure no exceptions raised during build\n    number_config, _ = load_config_with_kwargs(ECDNumberInputFeatureConfig, set_def)\n    input_feature_obj = NumberInputFeature(number_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = input_feature_obj.create_sample_input(batch_size=BATCH_SIZE)\n    assert input_tensor.shape == torch.Size([BATCH_SIZE])\n    assert input_tensor.dtype == torch.float32\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n\n\ndef test_outlier_replacer():\n    replacer = _OutlierReplacer(\n        {\"mean\": 50, \"std\": 30, \"preprocessing\": {\"outlier_threshold\": 2.0, \"computed_outlier_fill_value\": 42}}\n    )\n\n    t = torch.from_numpy(np.array([10, 20, 1000, -500, 80], dtype=np.float32))\n    t_out_expected = torch.from_numpy(np.array([10, 20, 42, 42, 80], dtype=np.float32))\n\n    t_out = replacer(t)\n    assert torch.equal(t_out, t_out_expected)\n"
  },
  {
    "path": "tests/ludwig/features/test_sequence_features.py",
    "content": "import numpy as np\nimport pytest\nimport torch\n\ntry:\n    import torchtext\nexcept ImportError:\n    torchtext = None\n\nfrom ludwig.constants import ENCODER_OUTPUT, LAST_HIDDEN, LOGITS, SEQUENCE, TEXT, TYPE\nfrom ludwig.features.sequence_feature import _SequencePreprocessing, SequenceInputFeature, SequenceOutputFeature\nfrom ludwig.features.text_feature import TextInputFeature, TextOutputFeature\nfrom ludwig.schema.features.sequence_feature import SequenceInputFeatureConfig, SequenceOutputFeatureConfig\nfrom ludwig.schema.features.text_feature import ECDTextInputFeatureConfig, ECDTextOutputFeatureConfig\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.utils import ENCODERS, sequence_feature\n\nDEVICE = get_torch_device()\nBATCH_SIZE = 8\nSEQ_SIZE = 6\nVOCAB_SIZE = 64\n\n\n@pytest.fixture(scope=\"module\")\ndef input_sequence() -> tuple[torch.Tensor, list]:\n    # generates a realistic looking synthetic sequence tensor, i.e.\n    # each sequence will have non-zero tokens at the beginning with\n    # trailing zero tokens, including a max length token with a single\n    # zero token at the end.  Example:\n    # [\n    #   [3, 5, 6, 0, 0, 0],\n    #   [10, 11, 12, 13, 14, 0],   # max length sequence\n    #   [32, 0, 0, 0, 0, 0]        # minimum length sequence\n    # ]\n    input_tensor = torch.zeros([BATCH_SIZE, SEQ_SIZE], dtype=torch.int32).to(DEVICE)\n    sequence_lengths = np.random.randint(1, SEQ_SIZE, size=BATCH_SIZE)\n    for i in range(input_tensor.shape[0]):\n        input_tensor[i, : sequence_lengths[i]] = torch.tensor(\n            np.random.randint(2, VOCAB_SIZE, size=sequence_lengths[i])\n        )\n\n    # emulate idx2str structure\n    idx2str = [\"<PAD>\", \"<UNK>\"] + [str(i) for i in range(2, VOCAB_SIZE)]\n\n    return input_tensor, idx2str\n\n\n@pytest.mark.parametrize(\"encoder\", ENCODERS)\n@pytest.mark.parametrize(\"sequence_type\", [SEQUENCE, TEXT])\ndef test_sequence_input_feature(input_sequence: tuple, encoder: str, sequence_type: str):\n    # test assumes \"sequence data\" has been tokenized and converted to\n    # numeric representation.  Focus of this test is primarily on\n    # integration with encoder with correctly sized encoder tensor and\n    # required properties are present\n\n    input_sequence, idx2str = input_sequence\n\n    # setup input sequence feature definition\n    # use sequence_feature() to generate baseline\n    # sequence definition and then augment with\n    # pre-processing metadata parameters\n    input_feature_def = sequence_feature(\n        encoder={\n            \"type\": encoder,\n            \"max_len\": SEQ_SIZE,\n            # augment with emulated pre-processing metadata\n            \"max_sequence_length\": SEQ_SIZE,\n            \"vocab\": idx2str,\n        }\n    )\n    input_feature_def[TYPE] = sequence_type\n\n    # create sequence input feature object\n    feature_cls = SequenceInputFeature if sequence_type == SEQUENCE else TextInputFeature\n    schema_cls = SequenceInputFeatureConfig if sequence_type == SEQUENCE else ECDTextInputFeatureConfig\n    sequence_config = schema_cls.from_dict(input_feature_def)\n    input_feature_obj = feature_cls(sequence_config).to(DEVICE)\n\n    # confirm dtype property\n    assert input_feature_obj.input_dtype == torch.int32\n\n    # confirm input_shape property\n    assert input_feature_obj.input_shape == (SEQ_SIZE,)\n\n    # confirm output_shape property default output shape\n    # from sequence_feature() function\n    encoder_output = input_feature_obj(input_sequence)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n\n\n@pytest.mark.parametrize(\"sequence_type\", [SEQUENCE, TEXT])\ndef test_sequence_output_feature(sequence_type: str):\n    output_feature_def = sequence_feature(\n        decoder={\n            \"type\": \"generator\",\n            \"max_len\": SEQ_SIZE,\n            \"max_sequence_length\": SEQ_SIZE,\n            \"vocab_size\": VOCAB_SIZE,\n        },\n        input_size=VOCAB_SIZE,\n    )\n    output_feature_def[TYPE] = sequence_type\n\n    feature_cls = SequenceOutputFeature if sequence_type == SEQUENCE else TextOutputFeature\n    schema_cls = SequenceOutputFeatureConfig if sequence_type == SEQUENCE else ECDTextOutputFeatureConfig\n    sequence_config = schema_cls.from_dict(output_feature_def)\n    output_feature_obj = feature_cls(sequence_config, {}).to(DEVICE)\n    combiner_outputs = {\n        \"combiner_output\": torch.randn([BATCH_SIZE, SEQ_SIZE, VOCAB_SIZE], dtype=torch.float32).to(DEVICE)\n    }\n\n    text_output = output_feature_obj(combiner_outputs, {})\n\n    assert LAST_HIDDEN in text_output\n    assert LOGITS in text_output\n    assert text_output[LOGITS].size() == torch.Size([BATCH_SIZE, SEQ_SIZE, VOCAB_SIZE])\n\n\ndef test_sequence_preproc_module_bad_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"dutch_lemmatize\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\"<EOS>\": 0, \"<SOS>\": 1, \"<PAD>\": 2, \"<UNK>\": 3, \"▁hell\": 4, \"o\": 5, \"▁world\": 6},\n    }\n\n    with pytest.raises(ValueError):\n        _SequencePreprocessing(metadata)\n\n\ndef test_sequence_preproc_module_space_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"space\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"hello\": 4,\n            \"world\": 5,\n            \"paleontology\": 6,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"    paleontology\", \"unknown\", \"hello    world hello\", \"hello world hello     world    \"])\n\n    assert torch.allclose(\n        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]])\n    )\n\n\ndef test_text_preproc_module_space_punct_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"space_punct\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"this\": 4,\n            \"sentence\": 5,\n            \"has\": 6,\n            \"punctuation\": 7,\n            \",\": 8,\n            \".\": 9,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"punctuation\", \",,,,\", \"this... this... punctuation\", \"unknown\"])\n\n    assert torch.allclose(\n        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]])\n    )\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0),\n    reason=\"requires torchtext 0.12.0 or higher\",\n)\ndef test_sequence_preproc_module_sentencepiece_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"sentencepiece\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"▁hell\": 4,\n            \"o\": 5,\n            \"▁world\": 6,\n            \"▁pale\": 7,\n            \"ont\": 8,\n            \"ology\": 9,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"paleontology\", \"unknown\", \"hello world hello\", \"hello world hello world\"])\n\n    assert torch.allclose(\n        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]])\n    )\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0),\n    reason=\"requires torchtext 0.12.0 or higher\",\n)\ndef test_sequence_preproc_module_clip_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"clip\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"hello</w>\": 4,\n            \"world</w>\": 5,\n            \"pale\": 7,\n            \"ontology</w>\": 8,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"paleontology\", \"unknown\", \"hello world hello\", \"hello world hello world\"])\n\n    assert torch.allclose(\n        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]])\n    )\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 12, 0),\n    reason=\"requires torchtext 0.12.0 or higher\",\n)\ndef test_sequence_preproc_module_gpt2bpe_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"gpt2bpe\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"hello\": 4,\n            \"Ġworld\": 5,\n            \"Ġhello\": 7,\n            \"p\": 8,\n            \"ale\": 9,\n            \"ont\": 10,\n            \"ology\": 11,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"paleontology\", \"unknown\", \"hello world hello\", \"hello world hello world\"])\n\n    assert torch.allclose(\n        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]])\n    )\n\n\n@pytest.mark.skipif(\n    torchtext is None or torch.torch_version.TorchVersion(torchtext.__version__) < (0, 13, 0),\n    reason=\"requires torchtext 0.13.0 or higher\",\n)\ndef test_sequence_preproc_module_bert_tokenizer():\n    metadata = {\n        \"preprocessing\": {\n            \"lowercase\": True,\n            \"tokenizer\": \"bert\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding_symbol\": \"<PAD>\",\n            \"computed_fill_value\": \"<UNK>\",\n        },\n        \"max_sequence_length\": SEQ_SIZE,\n        \"str2idx\": {\n            \"<EOS>\": 0,\n            \"<SOS>\": 1,\n            \"<PAD>\": 2,\n            \"<UNK>\": 3,\n            \"hello\": 4,\n            \"world\": 5,\n            \"pale\": 7,\n            \"##ont\": 8,\n            \"##ology\": 9,\n        },\n    }\n    module = _SequencePreprocessing(metadata)\n\n    res = module([\"paleontology\", \"unknown\", \"hello world hello\", \"hello world hello world\"])\n\n    assert torch.allclose(\n        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]])\n    )\n"
  },
  {
    "path": "tests/ludwig/features/test_set_feature.py",
    "content": "from copy import deepcopy\n\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT\nfrom ludwig.features.set_feature import SetInputFeature\nfrom ludwig.schema.features.set_feature import SetInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.misc_utils import merge_dict\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef set_config():\n    return {\n        \"name\": \"set_column_name\",\n        \"type\": \"set\",\n        \"tied\": None,\n        \"encoder\": {\n            \"type\": \"embed\",\n            \"vocab\": [\"a\", \"b\", \"c\"],\n            \"representation\": \"dense\",\n            \"embedding_size\": 50,\n            \"embeddings_trainable\": True,\n            \"pretrained_embeddings\": None,\n            \"embeddings_on_cpu\": False,\n            \"fc_layers\": None,\n            \"num_fc_layers\": 0,\n            \"use_bias\": True,\n            \"weights_initializer\": \"uniform\",\n            \"bias_initializer\": \"zeros\",\n            \"norm\": None,\n            \"norm_params\": None,\n            \"activation\": \"relu\",\n            \"dropout\": 0.0,\n            \"reduce_output\": \"sum\",\n        },\n    }\n\n\ndef test_set_input_feature(set_config: dict) -> None:\n    # setup image input feature definition\n    set_def = deepcopy(set_config)\n\n    # pickup any other missing parameters\n    defaults = SetInputFeatureConfig(name=\"foo\").to_dict()\n    set_def = merge_dict(defaults, set_def)\n\n    # ensure no exceptions raised during build\n    set_config, _ = load_config_with_kwargs(SetInputFeatureConfig, set_def)\n    input_feature_obj = SetInputFeature(set_config).to(DEVICE)\n\n    # check one forward pass through input feature\n    input_tensor = torch.randint(0, 2, size=(BATCH_SIZE, len(set_def[ENCODER][\"vocab\"])), dtype=torch.int64).to(DEVICE)\n\n    encoder_output = input_feature_obj(input_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape == (BATCH_SIZE, *input_feature_obj.output_shape)\n"
  },
  {
    "path": "tests/ludwig/features/test_text_feature.py",
    "content": "import pandas as pd\nimport pytest\nimport torch\nfrom transformers import AutoTokenizer\n\nfrom ludwig.backend import LocalBackend\nfrom ludwig.constants import IGNORE_INDEX_TOKEN_ID, LOGITS, PREDICTIONS, PROBABILITIES\nfrom ludwig.features import text_feature\n\nTEST_MODEL_NAME = \"hf-internal-testing/tiny-random-OPTForCausalLM\"\n\n\ndef test_backwards_compatibility():\n    # Tests that legacy level-based metadata keys are supported.\n    metadata = {\n        \"SibSp\": {\n            \"char_idx2str\": [\"<EOS>\", \"<SOS>\", \"<PAD>\", \"<UNK>\", \"0\", \"1\", \"2\", \"4\", \"3\", \"8\", \"5\"],\n            \"char_max_sequence_length\": 3,\n            \"char_pad_idx\": 2,\n            \"char_pad_symbol\": \"<PAD>\",\n            \"char_str2freq\": {\n                \"0\": 608,\n                \"1\": 209,\n                \"2\": 28,\n                \"3\": 16,\n                \"4\": 18,\n                \"5\": 5,\n                \"8\": 7,\n                \"<EOS>\": 0,\n                \"<PAD>\": 0,\n                \"<SOS>\": 0,\n                \"<UNK>\": 0,\n            },\n            \"char_str2idx\": {\n                \"0\": 4,\n                \"1\": 5,\n                \"2\": 6,\n                \"3\": 8,\n                \"4\": 7,\n                \"5\": 10,\n                \"8\": 9,\n                \"<EOS>\": 0,\n                \"<PAD>\": 2,\n                \"<SOS>\": 1,\n                \"<UNK>\": 3,\n            },\n            \"char_unk_symbol\": \"<UNK>\",\n            \"char_vocab_size\": 11,\n            \"preprocessing\": {\n                \"char_most_common\": 70,\n                \"char_sequence_length_limit\": 1024,\n                \"char_tokenizer\": \"characters\",\n                \"char_vocab_file\": None,\n                \"computed_fill_value\": \"<UNK>\",\n                \"fill_value\": \"<UNK>\",\n                \"lowercase\": True,\n                \"missing_value_strategy\": \"fill_with_const\",\n                \"padding\": \"right\",\n                \"padding_symbol\": \"<PAD>\",\n                \"pretrained_model_name_or_path\": None,\n                \"unknown_symbol\": \"<UNK>\",\n                \"word_most_common\": 20000,\n                \"word_sequence_length_limit\": 256,\n                \"word_tokenizer\": \"space_punct\",\n                \"word_vocab_file\": None,\n            },\n            \"word_idx2str\": [\"<EOS>\", \"<SOS>\", \"<PAD>\", \"<UNK>\", \"0\", \"1\", \"2\", \"4\", \"3\", \"8\", \"5\"],\n            \"word_max_sequence_length\": 3,\n            \"word_pad_idx\": 2,\n            \"word_pad_symbol\": \"<PAD>\",\n            \"word_str2freq\": {\n                \"0\": 608,\n                \"1\": 209,\n                \"2\": 28,\n                \"3\": 16,\n                \"4\": 18,\n                \"5\": 5,\n                \"8\": 7,\n                \"<EOS>\": 0,\n                \"<PAD>\": 0,\n                \"<SOS>\": 0,\n                \"<UNK>\": 0,\n            },\n            \"word_str2idx\": {\n                \"0\": 4,\n                \"1\": 5,\n                \"2\": 6,\n                \"3\": 8,\n                \"4\": 7,\n                \"5\": 10,\n                \"8\": 9,\n                \"<EOS>\": 0,\n                \"<PAD>\": 2,\n                \"<SOS>\": 1,\n                \"<UNK>\": 3,\n            },\n            \"word_unk_symbol\": \"<UNK>\",\n            \"word_vocab_size\": 11,\n        }\n    }\n\n    column = pd.core.series.Series([\"hello world\", \"hello world\"])\n\n    feature_data = text_feature.TextInputFeature.feature_data(\n        column, metadata[\"SibSp\"], metadata[\"SibSp\"][\"preprocessing\"], LocalBackend()\n    )\n\n    assert list(feature_data[0]) == [1, 3, 3]\n    assert list(feature_data[1]) == [1, 3, 3]\n\n\n@pytest.mark.parametrize(\"vocab_size\", [8])\n@pytest.mark.parametrize(\n    \"targets\",\n    [\n        ([78, 79, 504, 76, 397, 84, 0], [\" first she 18 yearman our<s>\"]),\n        ([IGNORE_INDEX_TOKEN_ID, IGNORE_INDEX_TOKEN_ID, IGNORE_INDEX_TOKEN_ID, 76, 397, 84, 0], [\" yearman our<s>\"]),\n    ],\n)\n@pytest.mark.parametrize(\"predictions\", [[78, 79, 504, 76, 397, 84, 0]])\ndef test_get_decoded_targets_and_predictions(vocab_size, targets, predictions):\n    tokenizer = AutoTokenizer.from_pretrained(TEST_MODEL_NAME)\n\n    # Scenario 1: Prediction and target tensors have the same length, so nothing should change\n    targets, decoded_texts_gt = targets\n    targets = torch.tensor([targets])\n    predictions = {\n        PREDICTIONS: torch.tensor([predictions], dtype=torch.int64),\n        PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32),\n        LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32),\n    }\n    (\n        decoded_targets,\n        decoded_predictions,\n    ) = text_feature.get_decoded_targets_and_predictions(targets, predictions, tokenizer)\n\n    assert isinstance(decoded_targets, list)\n    assert isinstance(decoded_predictions, list)\n    assert all(isinstance(x, str) for x in decoded_targets)\n    assert all(isinstance(x, str) for x in decoded_predictions)\n    assert decoded_targets == decoded_predictions == decoded_texts_gt\n"
  },
  {
    "path": "tests/ludwig/features/test_timeseries_feature.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.constants import ENCODER, ENCODER_OUTPUT, TYPE\nfrom ludwig.features.timeseries_feature import TimeseriesInputFeature\nfrom ludwig.schema.features.timeseries_feature import TimeseriesInputFeatureConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils.torch_utils import get_torch_device\n\nSEQ_SIZE = 2\nTIMESERIES_W_SIZE = 1\nMAX_LEN = 7\nEMBEDDING_SIZE = 5\nDEVICE = get_torch_device()\n\n\n@pytest.fixture(scope=\"module\")\ndef timeseries_config():\n    return {\n        \"name\": \"timeseries_12\",\n        \"type\": \"timeseries\",\n        \"encoder\": {\n            \"max_len\": MAX_LEN,\n            \"embedding_size\": EMBEDDING_SIZE,\n            \"max_sequence_length\": SEQ_SIZE,\n            \"output_size\": 8,\n            \"state_size\": 8,\n            \"num_filters\": 8,\n            \"hidden_size\": 8,\n        },\n    }\n\n\n@pytest.mark.parametrize(\"encoder\", [\"rnn\", \"stacked_cnn\", \"parallel_cnn\"])\ndef test_timeseries_input_feature(timeseries_config: dict, encoder: str) -> None:\n    timeseries_config[ENCODER][TYPE] = encoder\n\n    timeseries_config, _ = load_config_with_kwargs(TimeseriesInputFeatureConfig, timeseries_config)\n    timeseries_input_feature = TimeseriesInputFeature(timeseries_config).to(DEVICE)\n    timeseries_tensor = torch.randn([SEQ_SIZE, TIMESERIES_W_SIZE], dtype=torch.float32).to(DEVICE)\n    encoder_output = timeseries_input_feature(timeseries_tensor)\n    assert encoder_output[ENCODER_OUTPUT].shape[1:] == timeseries_input_feature.output_shape\n"
  },
  {
    "path": "tests/ludwig/hyperopt/test_hyperopt.py",
    "content": "import pytest\n\nfrom ludwig.constants import INPUT_FEATURES, NAME, OUTPUT_FEATURES, TYPE\nfrom ludwig.hyperopt.utils import log_warning_if_all_grid_type_parameters, substitute_parameters\nfrom ludwig.schema.model_config import ModelConfig\n\nBASE_CONFIG = {\n    INPUT_FEATURES: [{NAME: \"title\", TYPE: \"text\"}],\n    OUTPUT_FEATURES: [{NAME: \"summary\", TYPE: \"text\"}],\n}\n\n\ndef _get_config():\n    return {\n        \"input_features\": [{\"name\": \"Date received\", \"type\": \"category\"}],\n        \"output_features\": [{\"name\": \"Product\", \"type\": \"category\"}],\n        \"hyperopt\": {\n            \"goal\": \"minimize\",\n            \"metric\": \"loss\",\n            \"executor\": {\n                \"type\": \"ray\",\n                \"scheduler\": {\n                    \"type\": \"async_hyperband\",\n                    \"max_t\": 3600,\n                    \"time_attr\": \"time_total_s\",\n                    \"grace_period\": 72,\n                    \"reduction_factor\": 5,\n                },\n                \"num_samples\": 10,\n                \"time_budget_s\": 3600,\n                \"cpu_resources_per_trial\": 1,\n            },\n            \"parameters\": {\"trainer.learning_rate\": {\"space\": \"choice\", \"categories\": [0.005, 0.01, 0.02, 0.025]}},\n            \"search_alg\": {\"type\": \"variant_generator\"},\n            \"output_feature\": \"Product\",\n        },\n    }\n\n\n@pytest.mark.parametrize(\n    \"parameters, expected\",\n    [\n        (\n            {\n                \"combiner.type\": \"tabnet\",\n                \"combiner.fc_layers\": [{\"output_size\": 64}, {\"output_size\": 32}],\n                \"trainer.learning_rate\": 0.1,\n                \"trainer.batch_size\": 256,\n            },\n            {\n                **BASE_CONFIG,\n                \"combiner\": {\"type\": \"tabnet\", \"fc_layers\": [{\"output_size\": 64}, {\"output_size\": 32}]},\n                \"trainer\": {\"learning_rate\": 0.1, \"batch_size\": 256},\n            },\n        ),\n        (\n            {\n                \"title.encoder.type\": \"bert\",\n                \"summary.decoder.reduce_input\": \"sum\",\n                \"trainer.learning_rate\": 0.1,\n                \"trainer.batch_size\": 256,\n            },\n            {\n                INPUT_FEATURES: [{NAME: \"title\", TYPE: \"text\", \"encoder\": {\"type\": \"bert\"}}],\n                OUTPUT_FEATURES: [{NAME: \"summary\", TYPE: \"text\", \"decoder\": {\"reduce_input\": \"sum\"}}],\n                \"trainer\": {\"learning_rate\": 0.1, \"batch_size\": 256},\n            },\n        ),\n        (\n            {\n                \".\": {\n                    \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 2},\n                    \"trainer\": {\"learning_rate_scaling\": \"linear\"},\n                },\n                \"trainer.learning_rate\": 0.1,\n            },\n            {\n                **BASE_CONFIG,\n                \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 2},\n                \"trainer\": {\"learning_rate_scaling\": \"linear\", \"learning_rate\": 0.1},\n            },\n        ),\n        (\n            {\n                \".\": {\n                    \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 2},\n                    \"trainer\": {\"learning_rate_scaling\": \"linear\"},\n                },\n                \"trainer\": {\n                    \"learning_rate\": 0.1,\n                    \"batch_size\": 256,\n                },\n            },\n            {\n                **BASE_CONFIG,\n                \"combiner\": {\"type\": \"concat\", \"num_fc_layers\": 2},\n                \"trainer\": {\"learning_rate_scaling\": \"linear\", \"learning_rate\": 0.1, \"batch_size\": 256},\n            },\n        ),\n    ],\n    ids=[\"flat\", \"features\", \"nested\", \"multi-nested\"],\n)\ndef test_substitute_parameters(parameters, expected):\n    actual_config = substitute_parameters(BASE_CONFIG, parameters)\n    assert actual_config == expected\n\n\ndef test_grid_search_more_than_one_sample():\n    \"\"\"Test logs a user warning indicating that duplicate trials will be created because all of the parameters in\n    the search space are of type grid_search and the number of samples is greater than 1.\"\"\"\n    with pytest.warns(RuntimeWarning):\n        log_warning_if_all_grid_type_parameters(\n            {\n                \"parameters\": {\n                    \"trainer.learning_rate\": {\"space\": \"grid_search\", \"values\": [0.001, 0.005, 0.1]},\n                    \"defaults.text.encoder.type\": {\"space\": \"grid_search\", \"values\": [\"parallel_cnn\", \"stacked_cnn\"]},\n                },\n                \"executor\": {\"num_samples\": 2},\n            }\n        )\n\n\n@pytest.mark.parametrize(\n    \"parameters, expected_num_samples\",\n    [\n        (\n            {\n                \"trainer.learning_rate\": {\"space\": \"grid_search\", \"values\": [0.001, 0.005, 0.1]},\n                \"defaults.category.encoder.type\": {\"space\": \"grid_search\", \"values\": [\"dense\", \"sparse\"]},\n            },\n            1,\n        ),\n        (\n            {\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.0001,\n                    \"upper\": 0.01,\n                },\n                \"defaults.category.encoder.type\": {\"space\": \"grid_search\", \"values\": [\"dense\", \"sparse\"]},\n            },\n            1,\n        ),\n        (\n            {\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.0001,\n                    \"upper\": 0.01,\n                },\n            },\n            10,\n        ),\n    ],\n    ids=[\"all_grid_search\", \"mixed\", \"no_grid_search\"],\n)\ndef test_default_num_samples(parameters, expected_num_samples):\n    \"\"\"This test ensures that the default number of samples is set correctly when the user does not specify the\n    number of samples in the hyperopt config.\"\"\"\n    config = _get_config()\n\n    # Override to set num_samples to None so we can test inference logic\n    config[\"hyperopt\"][\"executor\"][\"num_samples\"] = None\n    config[\"hyperopt\"][\"parameters\"] = parameters\n\n    processed_config = ModelConfig.from_dict(config).to_dict()\n\n    assert processed_config[\"hyperopt\"][\"executor\"][\"num_samples\"] == expected_num_samples\n"
  },
  {
    "path": "tests/ludwig/model_export/test_onnx_exporter.py",
    "content": "import unittest\nfrom unittest.mock import MagicMock, patch\n\nimport pytest\n\nonnx = pytest.importorskip(\"onnx\")\n\nfrom ludwig.api import LudwigModel  # noqa: E402\nfrom ludwig.model_export.base_model_exporter import LudwigTorchWrapper  # noqa: E402\nfrom ludwig.model_export.onnx_exporter import OnnxExporter  # noqa: E402\n\n\nclass TestOnnxExporter(unittest.TestCase):\n    @patch.object(LudwigModel, \"load\")\n    @patch.object(LudwigTorchWrapper, \"eval\")\n    @patch(\"torch.onnx\")\n    def test_onnx_export(\n        self,\n        mock_onnx,\n        mock_ludwig_torch_wrapper_eval,\n        mock_ludwig_model_load,\n    ):\n        sample_model_path = MagicMock()\n        sample_export_path = MagicMock()\n        sample_output_model_name = MagicMock()\n        mock_ludwig_model_load.return_value = MagicMock()\n        mock_onnx.export.return_value = MagicMock()\n        onnx_exporter = OnnxExporter()\n\n        onnx_exporter.export(sample_model_path, sample_export_path, sample_output_model_name)\n\n        mock_ludwig_torch_wrapper_eval.assert_called_once()\n        mock_ludwig_model_load.assert_called_once()\n"
  },
  {
    "path": "tests/ludwig/models/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "tests/ludwig/models/test_trainable_image_layers.py",
    "content": "import logging\nimport os\n\nimport pytest\nimport torch\nfrom torchvision.models import resnet18, ResNet18_Weights\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.dataset_synthesizer import cli_synthesize_dataset\n\n\n# pytest fixture to do one time setup of required data\n@pytest.fixture(scope=\"module\")\ndef setup_data(tmp_path_factory):\n    # setup location for training data\n    data_dir = tmp_path_factory.mktemp(\"data\", numbered=False)\n    train_fp = os.path.join(data_dir, \"train.csv\")\n\n    # setup local cache to torchvision model to avoid multiple downloads\n    tv_cache = tmp_path_factory.mktemp(\"tv_cache\", numbered=False)\n\n    # describe synthetic data to create\n    feature_list = [\n        {\"name\": \"binary_output_feature\", \"type\": \"binary\"},\n        {\n            \"name\": \"image\",\n            \"type\": \"image\",\n            \"destination_folder\": os.path.join(data_dir, \"images\"),\n            \"preprocessing\": {\"height\": 600, \"width\": 600, \"num_channels\": 3},\n        },\n    ]\n\n    # create synthetic data\n    cli_synthesize_dataset(10, feature_list, train_fp)\n\n    return train_fp, str(tv_cache)\n\n\n@pytest.mark.parametrize(\"trainable\", [True, False])\ndef test_trainable_torchvision_layers(setup_data, trainable):\n    # retrieve data setup from fixture\n    train_fp, tv_cache = setup_data\n\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"image\",\n                \"type\": \"image\",\n                \"encoder\": {\n                    \"type\": \"resnet\",\n                    \"model_variant\": 18,\n                    \"model_cache_dir\": tv_cache,\n                    \"trainable\": trainable,\n                },\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"binary_output_feature\",\n                \"type\": \"binary\",\n            }\n        ],\n        \"trainer\": {\n            \"epochs\": 2,\n        },\n    }\n\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    _, _, output_dir = model.train(dataset=train_fp, skip_save_processed_input=True)\n\n    # instantiate native torchvision to get original parameter values\n    os.environ[\"TORCH_HOME\"] = tv_cache\n    tv_model = resnet18(weights=ResNet18_Weights.DEFAULT)\n\n    # replace last layer to match image encoder setup\n    tv_model.fc = torch.nn.Identity()\n\n    # compare Ludwig image encoder parameter against original native torchvision weights\n    # if trainable is True, parameters should differ, otherwise all parameters should be unchanged\n    if trainable:\n        for p1, p2 in zip(\n            model.model.input_features.get(\"image\").encoder_obj.model.parameters(), tv_model.parameters()\n        ):\n            assert not torch.all(p1.cpu() == p2.cpu())\n    else:\n        for p1, p2 in zip(\n            model.model.input_features.get(\"image\").encoder_obj.model.parameters(), tv_model.parameters()\n        ):\n            assert torch.all(p1.cpu() == p2.cpu())\n"
  },
  {
    "path": "tests/ludwig/models/test_training_determinism.py",
    "content": "import logging\nimport os\n\nimport numpy as np\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BATCH_SIZE, EVAL_BATCH_SIZE, TRAINER\nfrom ludwig.utils.numerical_test_utils import assert_all_finite\n\n\ndef _assert_stats_close(stats1, stats2, rtol=1e-2, atol=1e-2):\n    \"\"\"Assert that two nested stats structures are approximately equal.\n\n    CUDA floating-point operations may introduce non-deterministic differences across runs, so we use approximate\n    comparison instead of exact equality.\n    \"\"\"\n    if isinstance(stats1, dict):\n        assert set(stats1.keys()) == set(stats2.keys())\n        for k in stats1:\n            _assert_stats_close(stats1[k], stats2[k], rtol=rtol, atol=atol)\n    elif isinstance(stats1, (list, tuple)):\n        assert len(stats1) == len(stats2)\n        for v1, v2 in zip(stats1, stats2):\n            _assert_stats_close(v1, v2, rtol=rtol, atol=atol)\n    elif isinstance(stats1, (int, float, np.integer, np.floating)):\n        np.testing.assert_allclose(float(stats1), float(stats2), rtol=rtol, atol=atol)\n    elif hasattr(stats1, \"__dict__\"):\n        _assert_stats_close(vars(stats1), vars(stats2), rtol=rtol, atol=atol)\n    else:\n        assert stats1 == stats2\n\n\nfrom tests.integration_tests.utils import (  # noqa: E402\n    audio_feature,\n    bag_feature,\n    binary_feature,\n    category_feature,\n    date_feature,\n    generate_data,\n    h3_feature,\n    image_feature,\n    number_feature,\n    sequence_feature,\n    set_feature,\n    text_feature,\n    timeseries_feature,\n    vector_feature,\n)\n\n\n@pytest.mark.distributed\n@pytest.mark.skip(reason=\"https://github.com/ludwig-ai/ludwig/issues/2686\")\ndef test_training_determinism_ray_backend(csv_filename, tmpdir, ray_cluster_4cpu):\n    experiment_output_1, experiment_output_2 = train_twice(\"ray\", csv_filename, tmpdir)\n\n    eval_stats_1, train_stats_1, _, _ = experiment_output_1\n    eval_stats_2, train_stats_2, _, _ = experiment_output_2\n\n    assert_all_finite(eval_stats_1)\n    assert_all_finite(eval_stats_2)\n    assert_all_finite(train_stats_1)\n    assert_all_finite(train_stats_2)\n\n    np.testing.assert_equal(eval_stats_1, eval_stats_2)\n    np.testing.assert_equal(train_stats_1, train_stats_2)\n\n\ndef test_training_determinism_local_backend(csv_filename, tmpdir):\n    experiment_output_1, experiment_output_2 = train_twice(\"local\", csv_filename, tmpdir)\n\n    eval_stats_1, train_stats_1, _, _ = experiment_output_1\n    eval_stats_2, train_stats_2, _, _ = experiment_output_2\n\n    assert_all_finite(eval_stats_1)\n    assert_all_finite(eval_stats_2)\n    assert_all_finite(train_stats_1)\n    assert_all_finite(train_stats_2)\n\n    _assert_stats_close(eval_stats_1, eval_stats_2)\n    _assert_stats_close(train_stats_1, train_stats_2)\n\n\ndef train_twice(backend, csv_filename, tmpdir):\n    image_dest_folder = os.path.join(tmpdir, \"generated_images\")\n    audio_dest_folder = os.path.join(tmpdir, \"generated_audio\")\n\n    # Configure features to be tested:\n    input_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(encoder={\"vocab_size\": 10}),\n        sequence_feature(encoder={\"vocab_size\": 3}),\n        text_feature(encoder={\"vocab_size\": 3}),\n        vector_feature(),\n        timeseries_feature(),\n        date_feature(),\n        h3_feature(),\n        set_feature(encoder={\"vocab_size\": 3}),\n        bag_feature(encoder={\"vocab_size\": 3}),\n        image_feature(image_dest_folder),\n        audio_feature(audio_dest_folder),\n    ]\n    output_features = [\n        binary_feature(),\n        number_feature(),\n        category_feature(decoder={\"vocab_size\": 10}),\n    ]\n    # NOTE: It's important that we set batch size and eval batch size explicitly to bypass all batch size tuning, which\n    # is non-deterministic, even with fixed random seeds.\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"epochs\": 2, BATCH_SIZE: 128, EVAL_BATCH_SIZE: 2},\n    }\n\n    # Generate training data\n    training_data_csv_path = generate_data(input_features, output_features, csv_filename, num_examples=100)\n\n    ludwig_model_1 = LudwigModel(config, logging_level=logging.ERROR, backend=backend)\n    ludwig_model_2 = LudwigModel(config, logging_level=logging.ERROR, backend=backend)\n    experiment_output_1 = ludwig_model_1.experiment(\n        dataset=training_data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n    experiment_output_2 = ludwig_model_2.experiment(\n        dataset=training_data_csv_path,\n        skip_save_training_description=True,\n        skip_save_training_statistics=True,\n        skip_save_model=True,\n        skip_save_progress=True,\n        skip_save_log=True,\n        skip_save_processed_input=True,\n    )\n\n    return experiment_output_1, experiment_output_2\n"
  },
  {
    "path": "tests/ludwig/models/test_training_success.py",
    "content": "from contextlib import nullcontext as no_error_raised\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import BINARY, TRAINER\nfrom tests.integration_tests.utils import binary_feature, category_feature, generate_data\n\n\ndef generate_data_and_train(config, csv_filename):\n    # Generate training data\n    training_data_csv_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n\n    # Train Ludwig (Pythonic) model:\n    ludwig_model = LudwigModel(config)\n\n    with no_error_raised():\n        ludwig_model.experiment(\n            dataset=training_data_csv_path,\n            skip_save_training_description=True,\n            skip_save_training_statistics=True,\n            skip_save_model=True,\n            skip_save_progress=True,\n            skip_save_log=True,\n            skip_save_processed_input=True,\n        )\n\n\ndef test_category_passthrough_encoder(csv_filename):\n    input_features = [category_feature(), category_feature()]\n    output_features = [category_feature(output_feature=True)]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\"train_steps\": 1},\n        \"defaults\": {\"category\": {\"encoder\": {\"type\": \"passthrough\"}}},\n    }\n    generate_data_and_train(config, csv_filename)\n\n\ndef test_binary_encoders(csv_filename):\n    config = {\n        \"input_features\": [\n            {\"name\": \"binary1\", \"type\": BINARY, \"encoder\": {\"type\": \"passthrough\"}},\n            {\"name\": \"binary2\", \"type\": BINARY, \"encoder\": {\"type\": \"dense\"}},\n        ],\n        \"output_features\": [binary_feature(output_feature=True)],\n        TRAINER: {\"train_steps\": 1},\n    }\n    generate_data_and_train(config, csv_filename)\n"
  },
  {
    "path": "tests/ludwig/modules/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "tests/ludwig/modules/test_attention.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules.attention_modules import (\n    FeedForwardAttentionReducer,\n    MultiHeadSelfAttention,\n    TransformerBlock,\n    TransformerStack,\n)\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"input_hidden_size\", [128, 256])\n@pytest.mark.parametrize(\"input_seq_size\", [10])\n@pytest.mark.parametrize(\"input_batch_size\", [16])\ndef test_feed_forward_attention_reducer(input_batch_size: int, input_seq_size: int, input_hidden_size: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # Generate synthetic data\n    current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32)\n\n    # instantiate feed forward attention reducer\n    feed_forward_attention_reducer = FeedForwardAttentionReducer(input_hidden_size)\n\n    result = feed_forward_attention_reducer(current_inputs)\n\n    # ensure returned tensor is the correct shape\n    assert list(result.shape) == [input_batch_size, input_hidden_size]\n\n    # check for parameter updating if fully connected layer is present\n    target = torch.randn(result.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        feed_forward_attention_reducer,\n        (current_inputs,),\n        target,\n    )\n    assert upc == tpc, f\"Some parameters not updated.  These parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\"input_hidden_size\", [128, 256])\n@pytest.mark.parametrize(\"input_seq_size\", [1, 10])\n@pytest.mark.parametrize(\"input_batch_size\", [16])\ndef test_multihead_self_attention(input_batch_size: int, input_seq_size: int, input_hidden_size: int):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # Generate synthetic data\n    current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32)\n\n    # instantiate feed forward attention reducer\n    multihead_self_attention = MultiHeadSelfAttention(input_hidden_size, input_hidden_size)\n\n    result = multihead_self_attention(current_inputs)\n\n    # ensure returned tensor is the correct shape\n    assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size]\n\n    # check for parameter updating if fully connected layer is present\n    target = torch.randn(result.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        multihead_self_attention,\n        (current_inputs,),\n        target,\n    )\n\n    # With F.scaled_dot_product_attention, all parameters receive gradients even with a single-token sequence.\n    assert upc == tpc, f\"Some parameters not updated.  These parameters not updated: {not_updated}\"\n\n\n# heads must be a divisor of input_hidden_size\n@pytest.mark.parametrize(\n    \"input_batch_size,input_seq_size,input_hidden_size,output_size,heads\",\n    [\n        (16, 10, 128, 64, 8),\n        (16, 20, 256, 128, 16),\n        (32, 10, 256, 256, 8),\n    ],\n    ids=[\"small\", \"medium\", \"large\"],\n)\ndef test_transformer_block(\n    input_batch_size: int,\n    input_seq_size: int,\n    input_hidden_size: int,\n    output_size: int,\n    heads: int,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # Generate synthetic data\n    current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32)\n\n    # instantiate feed forward attention reducer\n    transformer_block = TransformerBlock(input_hidden_size, input_seq_size, input_hidden_size, heads, output_size)\n\n    result = transformer_block(current_inputs)\n\n    # ensure returned tensor is the correct shape\n    assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size]\n\n    # check for parameter updating if fully connected layer is present\n    target = torch.randn(result.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        transformer_block,\n        (current_inputs,),\n        target,\n    )\n    assert upc == tpc, f\"Some parameters not updated.  These parameters not updated: {not_updated}\"\n\n\n@pytest.mark.parametrize(\n    \"input_batch_size,input_seq_size,input_hidden_size,output_size,heads,num_layers\",\n    [\n        (16, 10, 128, 64, 8, 1),\n        (16, 20, 256, 128, 16, 1),\n        (32, 10, 256, 256, 8, 4),\n    ],\n    ids=[\"single_layer_small\", \"single_layer_medium\", \"multi_layer\"],\n)\ndef test_transformer_stack(\n    input_batch_size: int,\n    input_seq_size: int,\n    input_hidden_size: int,\n    output_size: int,\n    heads: int,\n    num_layers: int,\n):\n    # make repeatable\n    set_random_seed(RANDOM_SEED)\n\n    # Generate synthetic data\n    current_inputs = torch.normal(0, 1, size=[input_batch_size, input_seq_size, input_hidden_size], dtype=torch.float32)\n\n    # instantiate feed forward attention reducer\n    transformer_stack = TransformerStack(\n        input_hidden_size,\n        input_seq_size,\n        input_hidden_size,\n        heads,\n        output_size,\n        num_layers,\n    )\n\n    result = transformer_stack(current_inputs)\n\n    # ensure returned tensor is the correct shape\n    assert list(result.shape) == [input_batch_size, input_seq_size, input_hidden_size]\n\n    # check for parameter updating if fully connected layer is present\n    target = torch.randn(result.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        transformer_stack,\n        (current_inputs,),\n        target,\n    )\n    assert upc == tpc, f\"Some parameters not updated.  These parameters not updated: {not_updated}\"\n"
  },
  {
    "path": "tests/ludwig/modules/test_convolutional_modules.py",
    "content": "from collections.abc import Callable\n\nimport pytest\nimport torch\n\nfrom ludwig.modules.convolutional_modules import (\n    Conv1DLayer,\n    Conv1DStack,\n    Conv2DLayer,\n    Conv2DLayerFixedPadding,\n    Conv2DStack,\n    ParallelConv1D,\n    ParallelConv1DStack,\n    ResNet,\n    ResNetBlock,\n    ResNetBlockLayer,\n    ResNetBottleneckBlock,\n)\nfrom ludwig.utils.image_utils import get_img_output_shape\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nBATCH_SIZE = 2\nSEQ_SIZE = 17\nHIDDEN_SIZE = 8\nNUM_FILTERS = 4\n\nRANDOM_SEED = 1919\n\n\n###\n# Helper function to compute expected output shape\n# for Conv1D related layers\n###\ndef expected_seq_size(\n    seq_size: int,  # input max sequence length\n    padding: str,  # conv1d padding: 'same' or 'valid'\n    kernel_size: int,  # conv1d kernel size\n    stride: int,  # conv1d stride\n    dilation: int,  # conv1d dilation rate\n    pool_size: None | int,  # pooling layer kernel size\n    pool_padding: str,  # pooling layer padding: 'same' or 'valid'\n    pool_stride: int,  # pooling layer stride\n) -> int:\n    # output shape for the convolutional layer\n    output_seq_size = get_img_output_shape(\n        img_height=0,  # img_height set to zero for 1D structure\n        img_width=seq_size,  # img_width equates to max sequence length\n        kernel_size=kernel_size,\n        stride=stride,\n        padding=padding,\n        dilation=dilation,\n    )\n    if pool_size is not None:\n        # pooling layer present, adjust expected output shape for pooling layer\n        output_seq_size = get_img_output_shape(\n            img_height=0,  # img_height set to zero for 1D structure\n            img_width=output_seq_size[1],  # img_width equates to max sequence length\n            kernel_size=pool_size,\n            stride=pool_stride,\n            padding=pool_padding,\n            dilation=1,  # pooling layer only support unit dilation\n        )\n    return output_seq_size[1]\n\n\n###\n# 1D Convolutional Tests\n###\n@pytest.mark.parametrize(\"pool_function\", [\"max\", \"mean\"])\n@pytest.mark.parametrize(\n    \"pool_size, pool_padding, pool_stride\",\n    [(None, None, None), (3, \"same\", 1), (5, \"same\", 1), (3, \"valid\", 2), (5, \"valid\", 2)],\n)\n@pytest.mark.parametrize(\"dilation\", [1, 2])\n@pytest.mark.parametrize(\"strides, padding\", [(1, \"same\"), (1, \"valid\"), (2, \"valid\")])\n@pytest.mark.parametrize(\"kernel_size\", [3, 5])\ndef test_conv1d_layer(\n    kernel_size: int,\n    strides: int,\n    padding: str,\n    dilation: int,\n    pool_size: None | int,\n    pool_padding: str,\n    pool_stride: int,\n    pool_function: str,\n) -> None:\n    # make test repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    # setup synthetic tensor for test\n    input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32)\n\n    conv1_layer = Conv1DLayer(\n        in_channels=HIDDEN_SIZE,\n        out_channels=NUM_FILTERS,\n        max_sequence_length=SEQ_SIZE,\n        kernel_size=kernel_size,\n        strides=strides,\n        padding=padding,\n        dilation=dilation,\n        pool_function=pool_function,\n        pool_size=pool_size,\n        pool_strides=pool_stride,\n        pool_padding=pool_padding,\n    )\n\n    out_tensor = conv1_layer(input)\n\n    # check for correct output class\n    assert isinstance(out_tensor, torch.Tensor)\n\n    # check for correct output shape\n    output_seq_size = expected_seq_size(\n        seq_size=SEQ_SIZE,\n        padding=padding,\n        kernel_size=kernel_size,\n        stride=strides,\n        dilation=dilation,\n        pool_size=pool_size,\n        pool_padding=pool_padding,\n        pool_stride=pool_stride,\n    )\n    assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS)\n\n\n@pytest.mark.parametrize(\"dropout\", [0, 0.5])\n@pytest.mark.parametrize(\n    \"layers, num_layers\",\n    [\n        (None, None),  # setup up default number of layers with default values\n        (None, 4),  # setup of 4 layers with default values\n        ([{\"num_filters\": NUM_FILTERS - 2}, {\"num_filters\": NUM_FILTERS + 2}], None),  # 2 custom layers\n    ],\n)\ndef test_conv1d_stack(layers: None | list, num_layers: None | int, dropout: float) -> None:\n    # make test repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    # setup synthetic input tensor for test\n    input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32)\n\n    conv1_stack = Conv1DStack(\n        in_channels=HIDDEN_SIZE,\n        out_channels=NUM_FILTERS,\n        max_sequence_length=SEQ_SIZE,\n        layers=layers,\n        num_layers=num_layers,\n        default_num_filters=NUM_FILTERS,\n        default_dropout=dropout,\n    )\n\n    # check for correct stack formation\n    if layers is None:\n        assert len(conv1_stack.stack) == 6 if num_layers is None else num_layers\n    else:\n        # custom layer specification\n        assert len(conv1_stack.stack) == len(layers)\n        assert conv1_stack.stack[0].out_channels == NUM_FILTERS - 2\n        assert conv1_stack.stack[1].out_channels == NUM_FILTERS + 2\n\n    # generate output tensor\n    out_tensor = conv1_stack(input)\n\n    # check for correct output class\n    assert isinstance(out_tensor, torch.Tensor)\n\n    assert out_tensor.size()[1:] == conv1_stack.output_shape[:]\n\n    # check for correct output shape\n    last_module = conv1_stack.stack[-1]\n    output_seq_size = expected_seq_size(\n        seq_size=last_module.input_shape[0],\n        padding=last_module.padding,\n        kernel_size=last_module.kernel_size,\n        stride=last_module.stride,\n        dilation=last_module.dilation,\n        pool_size=last_module.pool_size,\n        pool_padding=last_module.pool_padding,\n        pool_stride=last_module.pool_strides,\n    )\n    if layers is None:\n        # default stack setup\n        assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS)\n    else:\n        # custom stack setup\n        assert out_tensor.size() == (BATCH_SIZE, output_seq_size, NUM_FILTERS + 2)\n\n    # check for parameter updates\n    target = torch.randn(conv1_stack.output_shape)\n    _, tpc, upc, not_updated = check_module_parameters_updated(conv1_stack, (input,), target)\n    if dropout == 0:\n        # all trainable parameters should be updated\n        assert tpc == upc, (\n            f\"All parameter not updated. Parameters not updated: {not_updated}\" f\"\\nModule structure:\\n{conv1_stack}\"\n        )\n    else:\n        # with specified config and random seed, non-zero dropout update parameter count could take different values\n        assert (tpc == upc) or (upc == 1), (\n            f\"All parameter not updated. Parameters not updated: {not_updated}\" f\"\\nModule structure:\\n{conv1_stack}\"\n        )\n\n\n@pytest.mark.parametrize(\n    \"layers\",\n    [\n        None,  # setup up default number of layers with default values\n        [{\"filter_size\": 3}, {\"filter_size\": 4}],  # custom parallel layers\n    ],\n)\ndef test_parallel_conv1d(layers: None | list) -> None:\n    input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32)\n\n    parallel_conv1d = ParallelConv1D(\n        in_channels=HIDDEN_SIZE,\n        out_channels=NUM_FILTERS,\n        max_sequence_length=SEQ_SIZE,\n        layers=layers,\n        default_num_filters=NUM_FILTERS,\n    )\n\n    # check for correct stack formation\n    if layers is None:\n        assert len(parallel_conv1d.parallel_layers) == 4\n    else:\n        # custom layer specification\n        assert len(parallel_conv1d.parallel_layers) == len(layers)\n        assert parallel_conv1d.parallel_layers[0].kernel_size == 3\n        assert parallel_conv1d.parallel_layers[1].kernel_size == 4\n\n    # generate output tensor\n    out_tensor = parallel_conv1d(input)\n\n    # check for correct output class\n    assert isinstance(out_tensor, torch.Tensor)\n\n    # check for correct output shape\n    parallel_module = parallel_conv1d.parallel_layers[0]\n    output_seq_size = expected_seq_size(\n        seq_size=parallel_module.input_shape[0],\n        padding=parallel_module.padding,\n        kernel_size=parallel_module.kernel_size,\n        stride=parallel_module.stride,\n        dilation=parallel_module.dilation,\n        pool_size=parallel_module.pool_size,\n        pool_padding=parallel_module.pool_padding,\n        pool_stride=parallel_module.pool_strides,\n    )\n\n    assert out_tensor.size() == (BATCH_SIZE, output_seq_size, len(parallel_conv1d.parallel_layers) * NUM_FILTERS)\n\n\nTEST_FILTER_SIZE0 = 7\nTEST_FILTER_SIZE1 = 5\n\n\n@pytest.mark.parametrize(\"dropout\", [0, 0.99])\n@pytest.mark.parametrize(\n    \"stacked_layers\",\n    [\n        None,  # setup up default number of layers with default values\n        # custom stacked parallel layers\n        [\n            [  # parallel_conv1d_stack.stack[0]\n                {\"filter_size\": 3},\n                {\"filter_size\": 5},\n                {\"filter_size\": TEST_FILTER_SIZE0},\n            ],\n            [  # parallel_conv1d_stack.stack[1]\n                {\"filter_size\": 2},\n                {\"filter_size\": 3},\n                {\"filter_size\": 4},\n                {\"filter_size\": TEST_FILTER_SIZE1},\n            ],\n        ],\n    ],\n)\ndef test_parallel_conv1d_stack(stacked_layers: None | list, dropout: float) -> None:\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    # setup synthetic input tensor for test\n    input = torch.randn([BATCH_SIZE, SEQ_SIZE, HIDDEN_SIZE], dtype=torch.float32)\n\n    parallel_conv1d_stack = ParallelConv1DStack(\n        in_channels=HIDDEN_SIZE,\n        out_channels=NUM_FILTERS,\n        max_sequence_length=SEQ_SIZE,\n        stacked_layers=stacked_layers,\n        default_num_filters=NUM_FILTERS,\n        default_dropout=dropout,\n    )\n\n    # check for correct stack formation\n    if stacked_layers is None:\n        assert len(parallel_conv1d_stack.stack) == 3\n        for i in range(len(parallel_conv1d_stack.stack)):\n            assert len(parallel_conv1d_stack.stack[i].parallel_layers) == 4\n    else:\n        # spot check custom layer specification\n        assert len(parallel_conv1d_stack.stack) == len(stacked_layers)\n        assert len(parallel_conv1d_stack.stack[0].parallel_layers) == 3\n        assert parallel_conv1d_stack.stack[0].parallel_layers[2].kernel_size == TEST_FILTER_SIZE0\n        assert len(parallel_conv1d_stack.stack[1].parallel_layers) == 4\n        assert parallel_conv1d_stack.stack[1].parallel_layers[3].kernel_size == TEST_FILTER_SIZE1\n\n    # generate output tensor\n    out_tensor = parallel_conv1d_stack(input)\n\n    # check for correct output class\n    assert isinstance(out_tensor, torch.Tensor)\n\n    # check output shape\n    assert out_tensor.size() == (BATCH_SIZE, *parallel_conv1d_stack.output_shape)\n\n    # check for parameter updates\n    target = torch.randn(parallel_conv1d_stack.output_shape)\n    _, tpc, upc, not_updated = check_module_parameters_updated(parallel_conv1d_stack, (input,), target)\n    if dropout == 0:\n        # all trainable parameters should be updated\n        assert tpc == upc, (\n            f\"All parameter not updated. Parameters not updated: {not_updated}\"\n            f\"\\nModule structure:\\n{parallel_conv1d_stack}\"\n        )\n    else:\n        # With high dropout (0.99), most gradients are zeroed out. The exact number of updated\n        # parameters depends on the random seed and PyTorch version.\n        assert upc > 0, (\n            f\"No parameters updated with dropout={dropout}. Parameters not updated: {not_updated}\"\n            f\"\\nModule structure:\\n{parallel_conv1d_stack}\"\n        )\n\n\n###\n#  2D Convolutional Tests\n###\n@pytest.mark.parametrize(\n    (\"img_height,img_width,in_channels,out_channels,pool_kernel_size,\" \"pool_stride,pool_padding,pool_dilation\"),\n    [(224, 224, 3, 16, 2, 2, 0, 1)],\n)\n@pytest.mark.parametrize(\"stride,padding\", [(1, \"valid\"), (1, \"same\"), (2, \"valid\")])\n@pytest.mark.parametrize(\"kernel_size\", [1, 3, 5])\n@pytest.mark.parametrize(\"dilation\", [1, 2])\n@pytest.mark.parametrize(\"norm\", [\"batch\", \"layer\"])\ndef test_conv2d_layer(\n    img_height: int,\n    img_width: int,\n    in_channels: int,\n    out_channels: int,\n    kernel_size: int,\n    stride: int,\n    padding: int | tuple[int] | str,\n    dilation: int | tuple[int],\n    norm: str,\n    pool_kernel_size: int | tuple[int],\n    pool_stride: int,\n    pool_padding: int | tuple[int] | str,\n    pool_dilation: int | tuple[int],\n) -> None:\n    conv2d_layer = Conv2DLayer(\n        img_height=img_height,\n        img_width=img_width,\n        in_channels=in_channels,\n        out_channels=out_channels,\n        kernel_size=kernel_size,\n        stride=stride,\n        padding=padding,\n        dilation=dilation,\n        norm=norm,\n        pool_kernel_size=pool_kernel_size,\n        pool_stride=pool_stride,\n        pool_padding=pool_padding,\n        pool_dilation=pool_dilation,\n    )\n    input_tensor = torch.rand(2, in_channels, img_height, img_width)\n    output_tensor = conv2d_layer(input_tensor)\n    assert output_tensor.shape[1:] == conv2d_layer.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width\", [(224, 224)])\n@pytest.mark.parametrize(\n    \"layers,num_layers,first_in_channels\",\n    [\n        (None, None, 3),\n        (None, 5, 3),\n        ([{\"out_channels\": 8}], None, 3),\n        ([{\"out_channels\": 8, \"in_channels\": 3}], None, None),\n    ],\n)\ndef test_conv2d_stack(\n    img_height: int,\n    img_width: int,\n    layers: list[dict] | None,\n    num_layers: int | None,\n    first_in_channels: int | None,\n) -> None:\n    conv2d_stack = Conv2DStack(\n        img_height=img_height,\n        img_width=img_width,\n        layers=layers,\n        num_layers=num_layers,\n        first_in_channels=first_in_channels,\n    )\n    input_tensor = torch.rand(2, 3, img_height, img_width)\n    output_tensor = conv2d_stack(input_tensor)\n    assert output_tensor.shape[1:] == conv2d_stack.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width,in_channels\", [(224, 224, 8)])\n@pytest.mark.parametrize(\"stride\", [1, 3])\n@pytest.mark.parametrize(\"groups\", [1, 8])\ndef test_conv2d_layer_fixed_padding(\n    img_height: int, img_width: int, in_channels: int, stride: int, groups: int\n) -> None:\n    conv2d_fixed_padding = Conv2DLayerFixedPadding(\n        img_height=img_height, img_width=img_width, in_channels=in_channels, stride=stride, groups=groups\n    )\n    input_tensor = torch.rand(2, in_channels, img_height, img_width)\n    output_tensor = conv2d_fixed_padding(input_tensor)\n    assert output_tensor.shape[1:] == conv2d_fixed_padding.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width,first_in_channels,out_channels\", [(224, 224, 64, 64)])\n@pytest.mark.parametrize(\n    \"projection_shortcut\",\n    [None, Conv2DLayerFixedPadding(img_height=224, img_width=224, in_channels=64, out_channels=64)],\n)\ndef test_resnet_block(\n    img_height: int, img_width: int, first_in_channels: int, out_channels: int, projection_shortcut: Callable\n) -> None:\n    resnet_block = ResNetBlock(\n        img_height=img_height,\n        img_width=img_width,\n        first_in_channels=first_in_channels,\n        out_channels=out_channels,\n        projection_shortcut=projection_shortcut,\n    )\n    input_tensor = torch.rand(2, first_in_channels, img_height, img_width)\n    output_tensor = resnet_block(input_tensor)\n    assert output_tensor.shape[1:] == resnet_block.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width,first_in_channels,out_channels\", [(224, 224, 64, 64)])\n@pytest.mark.parametrize(\n    \"projection_shortcut\",\n    [None, Conv2DLayerFixedPadding(img_height=224, img_width=224, in_channels=64, out_channels=256)],\n)\ndef test_resnet_bottleneck_block(\n    img_height: int, img_width: int, first_in_channels: int, out_channels: int, projection_shortcut: Callable\n) -> None:\n    resnet_block = ResNetBottleneckBlock(\n        img_height=img_height,\n        img_width=img_width,\n        first_in_channels=first_in_channels,\n        out_channels=out_channels,\n        projection_shortcut=projection_shortcut,\n    )\n    input_tensor = torch.rand(2, first_in_channels, img_height, img_width)\n    output_tensor = resnet_block(input_tensor)\n    assert output_tensor.shape[1:] == resnet_block.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width,first_in_channels,out_channels,num_blocks\", [(224, 224, 3, 32, 3)])\n@pytest.mark.parametrize(\"is_bottleneck, block_fn\", [(True, ResNetBottleneckBlock), (False, ResNetBlock)])\ndef test_resnet_block_layer(\n    img_height: int,\n    img_width: int,\n    first_in_channels: int,\n    out_channels: int,\n    is_bottleneck: bool,\n    block_fn: ResNetBlock | ResNetBottleneckBlock,\n    num_blocks: int,\n):\n    resnet_block_layer = ResNetBlockLayer(\n        img_height=img_height,\n        img_width=img_width,\n        first_in_channels=first_in_channels,\n        out_channels=out_channels,\n        is_bottleneck=is_bottleneck,\n        block_fn=block_fn,\n        num_blocks=num_blocks,\n    )\n    input_tensor = torch.rand(2, first_in_channels, img_height, img_width)\n    output_tensor = resnet_block_layer(input_tensor)\n    assert output_tensor.shape[1:] == resnet_block_layer.output_shape\n\n\n@pytest.mark.parametrize(\"img_height,img_width,first_in_channels,out_channels\", [(224, 224, 3, 64)])\n@pytest.mark.parametrize(\"resnet_size\", [18, 34, 50])\ndef test_resnet(\n    img_height: int,\n    img_width: int,\n    first_in_channels: int,\n    out_channels: int,\n    resnet_size: int,\n):\n    # make repeatable\n    torch.manual_seed(RANDOM_SEED)\n\n    resnet = ResNet(\n        img_height=img_height,\n        img_width=img_width,\n        first_in_channels=first_in_channels,\n        out_channels=out_channels,\n        resnet_size=resnet_size,\n    )\n    input_tensor = torch.rand(2, first_in_channels, img_height, img_width)\n    output_tensor = resnet(input_tensor)\n    assert output_tensor.shape[1:] == resnet.output_shape\n\n    # check for parameter updates\n    target = torch.randn(output_tensor.shape)\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(resnet, (input_tensor,), target)\n    # all trainable parameters should be updated\n    assert tpc == upc, (\n        f\"All parameter not updated. Parameters not updated: {not_updated}\" f\"\\nModule structure:\\n{resnet}\"\n    )\n"
  },
  {
    "path": "tests/ludwig/modules/test_embedding_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules.embedding_modules import Embed, EmbedSequence, EmbedSet, EmbedWeighted, TokenAndPositionEmbedding\nfrom ludwig.utils.torch_utils import get_torch_device\n\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\"]])\n@pytest.mark.parametrize(\"embedding_size\", [2])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_embed(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n):\n    embed = Embed(\n        vocab=vocab,\n        embedding_size=embedding_size,\n        representation=representation,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, 1)).bool().to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs.shape[1:] == embed.output_shape\n\n\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\", \"d\"]])\n@pytest.mark.parametrize(\"embedding_size\", [3])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_embed_set(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n):\n    embed = EmbedSet(\n        vocab=vocab,\n        embedding_size=embedding_size,\n        representation=representation,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs.shape[1:] == embed.output_shape\n\n\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"]])\n@pytest.mark.parametrize(\"embedding_size\", [5, 10])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_embed_weighted(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n):\n    embed_weighted = EmbedWeighted(vocab=vocab, embedding_size=embedding_size, representation=representation).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, len(vocab))).bool().to(DEVICE)\n    outputs = embed_weighted(inputs)\n    assert outputs.shape[1:] == embed_weighted.output_shape\n\n\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\"]])\n@pytest.mark.parametrize(\"embedding_size\", [2])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_embed_sequence(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n):\n    embed = EmbedSequence(\n        vocab=vocab,\n        embedding_size=embedding_size,\n        max_sequence_length=10,\n        representation=representation,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, 10)).to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs.shape[1:] == embed.output_shape\n\n\n@pytest.mark.parametrize(\"vocab\", [[\"a\", \"b\", \"c\"]])\n@pytest.mark.parametrize(\"embedding_size\", [10])\n@pytest.mark.parametrize(\"representation\", [\"dense\", \"sparse\"])\ndef test_token_and_position_embedding(\n    vocab: list[str],\n    embedding_size: int,\n    representation: str,\n):\n    embed = TokenAndPositionEmbedding(\n        vocab=vocab,\n        embedding_size=embedding_size,\n        max_sequence_length=10,\n        representation=representation,\n    ).to(DEVICE)\n    inputs = torch.randint(0, 2, size=(2, 10)).to(DEVICE)\n    outputs = embed(inputs)\n    assert outputs.shape[1:] == embed.output_shape\n"
  },
  {
    "path": "tests/ludwig/modules/test_encoder.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport random\n\nimport numpy as np\nimport pytest\nimport torch\n\nfrom ludwig.constants import ENCODER_OUTPUT\nfrom ludwig.data.dataset_synthesizer import build_vocab\nfrom ludwig.encoders.base import Encoder\nfrom ludwig.encoders.image.base import MLPMixerEncoder, Stacked2DCNN\nfrom ludwig.encoders.sequence_encoders import (\n    ParallelCNN,\n    SequenceEmbedEncoder,\n    StackedCNN,\n    StackedCNNRNN,\n    StackedParallelCNN,\n    StackedRNN,\n)\nfrom ludwig.utils.torch_utils import get_torch_device\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nDROPOUT = 0.5\nDEVICE = get_torch_device()\nRANDOM_SEED = 1919\n\n\ndef create_encoder(encoder_type, **encoder_kwargs):\n    encoder = encoder_type(**encoder_kwargs)\n    return encoder\n\n\ndef _generate_image(image_size):\n    return np.random.randint(0, 1, image_size).astype(np.float32)\n\n\ndef generate_images(image_size, num_images):\n    return np.array([_generate_image(image_size) for _ in range(num_images)])\n\n\ndef _generate_sentence(vocab_size, max_len):\n    sentence = np.zeros(max_len, dtype=np.int32)\n    random_length = random.randint(1, max_len)\n    sentence[:random_length] = [random.randint(0, vocab_size - 1) for _ in range(random_length)]\n\n    return sentence\n\n\ndef generate_random_sentences(num_sentences=10, max_len=10, vocab_size=10):\n    # Generate some random text\n    vocab = build_vocab(vocab_size)\n\n    text = np.array([_generate_sentence(vocab_size, max_len) for _ in range(num_sentences)])\n\n    return text, vocab\n\n\ndef encoder_test(\n    encoder,\n    input_data,\n    output_dtype,\n    output_shape,\n    output_data=None,\n):\n    \"\"\"Helper method to test different kinds of encoders.\n\n    :param encoder: encoder object\n    :param input_data: data to encode\n    :param output_dtype: expected data type of the output (optional)\n    :param output_shape: expected shape of the encoder output (optional)\n    :param output_data: expected output data (optional)\n    :return: returns the encoder object for the caller to run extra checks\n    \"\"\"\n    encoder = encoder.to(DEVICE)\n\n    # Run the encoder\n    input_data = torch.from_numpy(input_data).to(DEVICE)\n\n    hidden = encoder(input_data)[ENCODER_OUTPUT]\n\n    # Check output shape and type\n    assert hidden.dtype == output_dtype\n    assert list(hidden.shape) == output_shape\n\n    if output_data is not None:\n        # todo the hidden output is actually a tensor. May need modification\n        assert np.allclose(hidden, output_data)\n\n\ndef test_image_encoders_stacked_2dcnn():\n    # make repeatable\n    np.random.seed(RANDOM_SEED)\n    torch.manual_seed(RANDOM_SEED)\n\n    # Test the resnet encoder for images\n    encoder_kwargs = {\"num_conv_layers\": 2, \"num_filters\": 16, \"output_size\": 28, \"dropout\": DROPOUT}\n    image_size = (3, 10, 10)\n\n    encoder = create_encoder(\n        Stacked2DCNN, num_channels=image_size[0], height=image_size[1], width=image_size[2], **encoder_kwargs\n    )\n\n    assert encoder is not None\n    assert encoder.conv_stack_2d is not None\n    assert list(encoder.conv_stack_2d.output_shape) == [32, 1, 1]\n    assert len(encoder.fc_stack.layers) == 1\n    assert encoder.conv_stack_2d.layers[0][\"pool_kernel_size\"] == 2\n    assert encoder.conv_stack_2d.layers[0][\"stride\"] == 1\n    assert encoder.conv_stack_2d.layers[0][\"pool_stride\"] == 2\n    assert encoder.conv_stack_2d.layers[0][\"norm\"] is None\n    assert encoder.conv_stack_2d.layers[0][\"activation\"] == \"relu\"\n    assert encoder.conv_stack_2d.layers[0][\"dropout\"] == 0\n\n    output_shape = [1, 28]\n    input_image = generate_images(image_size, 1)\n\n    encoder_test(\n        encoder=encoder, input_data=input_image, output_dtype=torch.float32, output_shape=output_shape, output_data=None\n    )\n\n    output_shape = [5, 28]\n    input_images = generate_images(image_size, 5)\n\n    encoder_test(\n        encoder=encoder,\n        input_data=input_images,\n        output_dtype=torch.float32,\n        output_shape=output_shape,\n        output_data=None,\n    )\n\n    # test for parameter updates\n    # generate tensors for parameter update test\n    target = torch.rand(output_shape)\n    image_tensor = torch.rand(input_image.shape)\n\n    # check for parameter updates\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(encoder, (image_tensor,), target)\n    assert upc == tpc, (\n        f\"Not all trainable parameters updated.  Parameters not updated: {not_updated}.\"\n        f\"  Module structure\\n{encoder}\"\n    )\n\n\ndef test_image_encoders_mlpmixer():\n    # make repeatable\n    np.random.seed(RANDOM_SEED)\n    torch.manual_seed(RANDOM_SEED)\n\n    # Test the resnet encoder for images\n    encoder_kwargs = {\n        \"patch_size\": 5,\n        \"embed_size\": 8,\n        \"token_size\": 32,\n        \"channel_dim\": 16,\n        \"num_layers\": 2,\n        \"dropout\": DROPOUT,\n    }\n    image_size = (3, 10, 10)\n\n    output_shape = [1, 8]\n    input_image = generate_images(image_size, 1)\n\n    encoder = create_encoder(\n        MLPMixerEncoder, num_channels=image_size[0], height=image_size[1], width=image_size[2], **encoder_kwargs\n    )\n    encoder_test(\n        encoder=encoder, input_data=input_image, output_dtype=torch.float32, output_shape=output_shape, output_data=None\n    )\n\n    output_shape = [5, 8]\n    input_images = generate_images(image_size, 5)\n\n    encoder_test(\n        encoder=encoder,\n        input_data=input_images,\n        output_dtype=torch.float32,\n        output_shape=output_shape,\n        output_data=None,\n    )\n\n    assert encoder is not None\n    assert encoder.mlp_mixer.__class__.__name__ == \"MLPMixer\"\n    assert len(encoder.mlp_mixer.mixer_blocks) == 2\n    assert list(encoder.mlp_mixer.mixer_blocks[0].mlp1.output_shape) == [4]\n    assert encoder.mlp_mixer.patch_conv.__class__.__name__ == \"Conv2d\"\n    assert encoder.mlp_mixer.patch_conv.kernel_size == (5, 5)\n\n    # test for parameter updates\n    # generate tensors for parameter update test\n    target = torch.rand(output_shape)\n    image_tensor = torch.rand(input_image.shape)\n\n    # check for parameter updates\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(encoder, (image_tensor,), target)\n    assert upc == tpc, (\n        f\"Not all trainable parameters updated.  Parameters not updated: {not_updated}.\"\n        f\"  Module structure\\n{encoder}\"\n    )\n\n\ndef test_sequence_encoder_embed():\n    num_sentences = 4\n    embedding_size = 5\n    max_len = 6\n\n    # make repeatable\n    np.random.seed(RANDOM_SEED)\n    torch.manual_seed(RANDOM_SEED)\n\n    # Generate data\n    text, vocab = generate_random_sentences(\n        num_sentences=num_sentences,\n        max_len=max_len,\n    )\n\n    encoder_kwargs = {\"embedding_size\": embedding_size, \"vocab\": vocab}\n\n    # Different values for reduce_output and the corresponding expected size\n    reduce_outputs = [\"sum\", None, \"concat\"]\n    output_shapes = [\n        [num_sentences, embedding_size],\n        [num_sentences, max_len, embedding_size],\n        [num_sentences, max_len * embedding_size],\n    ]\n\n    for reduce_output, output_shape in zip(reduce_outputs, output_shapes):\n        for trainable in [True, False]:\n            encoder_kwargs[\"reduce_output\"] = reduce_output\n            encoder_kwargs[\"embeddings_trainable\"] = trainable\n            encoder_kwargs[\"dropout\"] = DROPOUT\n            encoder = create_encoder(SequenceEmbedEncoder, max_sequence_length=max_len, **encoder_kwargs)\n\n            encoder_test(\n                encoder=encoder,\n                input_data=text,\n                output_dtype=torch.float32,\n                output_shape=output_shape,\n                output_data=None,\n            )\n\n            assert encoder.embed_sequence.dropout is not None\n\n            # test for parameter updates\n            # generate tensors for parameter update test\n            target = torch.rand(output_shape)\n\n            # check for parameter updates\n            fpc, tpc, upc, not_updated = check_module_parameters_updated(\n                encoder, (torch.tensor(text, dtype=torch.int32),), target\n            )\n            assert upc == tpc, (\n                f\"Not all trainable parameters updated.  Parameters not updated: {not_updated}.\"\n                f\"  Module structure\\n{encoder}\"\n            )\n\n\n@pytest.mark.parametrize(\"encoder_type\", [ParallelCNN, StackedCNN, StackedParallelCNN, StackedRNN, StackedCNNRNN])\n@pytest.mark.parametrize(\"trainable\", [True, False])\n@pytest.mark.parametrize(\"reduce_output\", [\"sum\", \"max\"])\ndef test_sequence_encoders(encoder_type: Encoder, trainable: bool, reduce_output: str):\n    num_sentences = 4\n    embedding_size = 5\n    max_len = 7\n    output_size = 3\n\n    # make repeatable\n    np.random.seed(RANDOM_SEED)\n    torch.manual_seed(RANDOM_SEED)\n\n    # Generate data\n    text, vocab = generate_random_sentences(\n        num_sentences=num_sentences,\n        max_len=max_len,\n    )\n\n    encoder_kwargs = {\n        \"embedding_size\": embedding_size,\n        \"vocab\": vocab,\n        \"output_size\": output_size,\n        \"num_fc_layers\": 1,\n        \"filter_size\": 3,\n        \"num_filters\": 8,\n        \"state_size\": output_size,\n    }\n\n    # todo figure out the output size for parallel 1d conv\n    output_shape = [num_sentences, output_size]\n\n    encoder_kwargs[\"embeddings_trainable\"] = trainable\n    encoder_kwargs[\"dropout\"] = DROPOUT\n    encoder_kwargs[\"recurrent_dropout\"] = DROPOUT\n    encoder_kwargs[\"fc_dropout\"] = DROPOUT\n    encoder_kwargs[\"reduce_output\"] = reduce_output\n    encoder = create_encoder(encoder_type, max_sequence_length=max_len, **encoder_kwargs)\n\n    encoder_test(\n        encoder=encoder, input_data=text, output_dtype=torch.float32, output_shape=output_shape, output_data=None\n    )\n\n    assert isinstance(encoder, encoder_type)\n\n    # test for parameter updates\n    # generate tensors for parameter update test\n    target = torch.rand(output_shape)\n\n    # check for parameter updates\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(\n        encoder, (torch.tensor(text, dtype=torch.int32),), target\n    )\n\n    if trainable:\n        assert fpc == 0, \"Embedding layer expected to be trainable but found to be frozen\"\n    else:\n        assert fpc == 1, \"Embedding layer expected to be frozen, but found to be trainable.\"\n\n    # With dropout=0.5 and small sequences (4 sentences, max_len=7), many parameters\n    # may legitimately not receive gradients in a single step. ParallelCNN with max\n    # reduction is especially susceptible since max selects sparse gradients, and dropout\n    # can zero out entire channels.\n    if trainable:\n        # At least some trainable parameters should update\n        assert upc > 0 or encoder_type == ParallelCNN, (\n            f\"No trainable parameters updated. Parameters not updated: {not_updated}.\" f\"  Module structure\\n{encoder}\"\n        )\n"
  },
  {
    "path": "tests/ludwig/modules/test_fully_connected_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules.fully_connected_modules import FCLayer, FCStack\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.torch_utils import get_torch_device\n\nBATCH_SIZE = 2\nDEVICE = get_torch_device()\nRANDOM_SEED = 1919\n\n\n@pytest.mark.parametrize(\"input_size\", [2, 3])\n@pytest.mark.parametrize(\"output_size\", [3, 4])\n@pytest.mark.parametrize(\"activation\", [\"relu\", \"sigmoid\", \"tanh\"])\n@pytest.mark.parametrize(\"dropout\", [0.0, 0.6])\n@pytest.mark.parametrize(\"batch_size\", [1, 2])\n@pytest.mark.parametrize(\"norm\", [None, \"layer\", \"batch\", \"ghost\"])\ndef test_fc_layer(\n    input_size: int,\n    output_size: int,\n    activation: str,\n    dropout: float,\n    batch_size: int,\n    norm: str | None,\n):\n    set_random_seed(RANDOM_SEED)  # make repeatable\n    fc_layer = FCLayer(\n        input_size=input_size, output_size=output_size, activation=activation, dropout=dropout, norm=norm\n    ).to(DEVICE)\n    input_tensor = torch.randn(batch_size, input_size, device=DEVICE)\n    output_tensor = fc_layer(input_tensor)\n    assert output_tensor.shape[1:] == fc_layer.output_shape\n\n\n@pytest.mark.parametrize(\n    \"first_layer_input_size,layers,num_layers\",\n    [\n        (2, None, 3),\n        (2, [{\"output_size\": 4}, {\"output_size\": 8}], None),\n        (2, [{\"input_size\": 2, \"output_size\": 4}, {\"output_size\": 8}], None),\n    ],\n)\ndef test_fc_stack(\n    first_layer_input_size: int | None,\n    layers: list | None,\n    num_layers: int | None,\n):\n    set_random_seed(RANDOM_SEED)\n    fc_stack = FCStack(first_layer_input_size=first_layer_input_size, layers=layers, num_layers=num_layers).to(DEVICE)\n    input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE)\n    output_tensor = fc_stack(input_tensor)\n    assert output_tensor.shape[1:] == fc_stack.output_shape\n\n\ndef test_fc_stack_input_size_mismatch_fails():\n    first_layer_input_size = 10\n    layers = [{\"input_size\": 2, \"output_size\": 4}, {\"output_size\": 8}]\n\n    fc_stack = FCStack(\n        first_layer_input_size=first_layer_input_size,\n        layers=layers,\n    ).to(DEVICE)\n    input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE)\n\n    with pytest.raises(RuntimeError):\n        fc_stack(input_tensor)\n\n\ndef test_fc_stack_no_layers_behaves_like_passthrough():\n    first_layer_input_size = 10\n    layers = None\n    num_layers = 0\n    output_size = 15\n\n    fc_stack = FCStack(\n        first_layer_input_size=first_layer_input_size,\n        layers=layers,\n        num_layers=num_layers,\n        default_output_size=output_size,\n    ).to(DEVICE)\n    input_tensor = torch.randn(BATCH_SIZE, first_layer_input_size, device=DEVICE)\n    output_tensor = fc_stack(input_tensor)\n\n    assert list(output_tensor.shape[1:]) == [first_layer_input_size]\n    assert output_tensor.shape[1:] == fc_stack.output_shape\n    assert torch.allclose(input_tensor, output_tensor)\n"
  },
  {
    "path": "tests/ludwig/modules/test_initializer_modules.py",
    "content": "import torch\nimport torch.nn as nn\n\nfrom ludwig.modules.initializer_modules import get_initializer\nfrom ludwig.utils.torch_utils import get_torch_device\n\nDEVICE = \"cuda:0\" if get_torch_device() == \"cuda\" else \"cpu\"\n\n\ndef test_get_initializer():\n    \"\"\"Currently only checks for when the parameters are default case.\"\"\"\n    tensor_size = (2, 3)\n\n    # Test for when the parameters are default\n    torch.random.manual_seed(0)\n    initialized_tensor = get_initializer(\"xavier_uniform\")(*tensor_size, device=DEVICE)\n\n    # Check that the tensor using the expected initialization and the same seed is identical\n    default_initializer = nn.init.xavier_uniform_\n    torch.random.manual_seed(0)\n    default_tensor = default_initializer(torch.empty(*tensor_size, device=DEVICE))\n    assert torch.equal(initialized_tensor, default_tensor)\n"
  },
  {
    "path": "tests/ludwig/modules/test_loss_modules.py",
    "content": "import contextlib\n\nimport pytest\nimport torch\nfrom pydantic import ValidationError\n\nfrom ludwig.features.category_feature import CategoryOutputFeature\nfrom ludwig.features.set_feature import SetOutputFeature\nfrom ludwig.features.text_feature import TextOutputFeature\nfrom ludwig.modules import loss_modules\nfrom ludwig.schema.features.loss.loss import (\n    BWCEWLossConfig,\n    CORNLossConfig,\n    HuberLossConfig,\n    MAELossConfig,\n    MAPELossConfig,\n    MSELossConfig,\n    RMSELossConfig,\n    RMSPELossConfig,\n    SigmoidCrossEntropyLossConfig,\n    SoftmaxCrossEntropyLossConfig,\n)\nfrom ludwig.schema.model_config import ModelConfig\nfrom tests.integration_tests.utils import category_feature, set_feature, text_feature\n\n\ndef from_float(v: float) -> torch.Tensor:\n    return torch.tensor(v).float()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(36).float()])\ndef test_mse_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.MSELoss(MSELossConfig())\n    assert loss(preds, target) == output\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(6).float()])\ndef test_mae_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.MAELoss(MAELossConfig())\n    assert loss(preds, target) == output\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7365440726280212)])\ndef test_mape_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.MAPELoss(MAPELossConfig())\n    assert loss(preds, target) == output\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(6).float()])\ndef test_rmse_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.RMSELoss(RMSELossConfig())\n    assert loss(preds, target) == output\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7527).float()])\ndef test_rmspe_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.RMSPELoss(RMSPELossConfig())\n    assert torch.isclose(loss(preds, target), output, rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([[0.1, 0.2]]).float()])\n@pytest.mark.parametrize(\"target\", [torch.tensor([[0.0, 0.2]]).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(707.1068).float()])\ndef test_rmspe_loss_zero_targets(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.RMSPELoss(RMSPELossConfig())\n    assert torch.isclose(loss(preds, target), output, rtol=0.0001)\n\n\n@pytest.mark.parametrize(\n    \"confidence_penalty,positive_class_weight,robust_lambda,output\",\n    [\n        (0.0, None, 0, from_float(-21.4655)),\n        (2.0, None, 0, from_float(-21.1263)),\n        (0.0, 2.0, 0, from_float(-20.1222)),\n        (0.0, None, 2, from_float(22.4655)),\n        (2, 2, 2, from_float(21.4614)),\n    ],\n)\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\ndef test_bwcew_loss(\n    preds: torch.Tensor,\n    target: torch.Tensor,\n    confidence_penalty: float,\n    positive_class_weight: float | None,\n    robust_lambda: int,\n    output: torch.Tensor,\n):\n    loss = loss_modules.BWCEWLoss(\n        BWCEWLossConfig(\n            positive_class_weight=positive_class_weight,\n            robust_lambda=robust_lambda,\n            confidence_penalty=confidence_penalty,\n        )\n    )\n    assert torch.isclose(loss(preds, target), output)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([[0.5, 0.5], [0.2, 0.8], [0.6, 0.4]])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([1, 1, 0])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.5763)])\ndef test_softmax_cross_entropy_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.SoftmaxCrossEntropyLoss(SoftmaxCrossEntropyLossConfig())\n    assert torch.isclose(loss(preds, target), output, rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(-21.4655).float()])\ndef test_sigmoid_cross_entropy_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.SigmoidCrossEntropyLoss(SigmoidCrossEntropyLossConfig())\n    assert torch.isclose(loss(preds, target), output)\n\n\n@pytest.mark.parametrize(\n    \"delta,output\",\n    [\n        (1.0, from_float(5.5000)),\n        (0.5, from_float(2.8750)),\n        (2.0, from_float(10.0)),\n        (0.0, ValidationError),\n    ],\n)\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\ndef test_huber_loss(preds: torch.Tensor, target: torch.Tensor, delta: float, output: torch.Tensor | type[Exception]):\n    with pytest.raises(output) if not isinstance(output, torch.Tensor) else contextlib.nullcontext():\n        loss = loss_modules.HuberLoss(HuberLossConfig.from_dict({\"delta\": delta}))\n        value = loss(preds, target)\n        assert value == output\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([[0.25, 0.2, 0.55], [0.2, 0.35, 0.45], [0.8, 0.1, 0.1]])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([2, 1, 0])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7653)])\ndef test_corn_loss(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    loss = loss_modules.CORNLoss(CORNLossConfig())\n    assert torch.isclose(loss(preds, target), output, rtol=0.0001)\n\n\ndef test_dict_class_weights_category():\n    input_features = [text_feature()]\n    output_features = [category_feature(decoder={\"vocab_size\": 3})]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    # Set class weights as dictionary on config\n    class_weights_dict = {\"token_1\": 0.1, \"token_2\": 0.2, \"token_3\": 0.3}\n    config[\"output_features\"][0][\"loss\"] = {\"type\": \"softmax_cross_entropy\", \"class_weights\": class_weights_dict}\n\n    # Mock feature metadata\n    feature_metadata = {\n        \"idx2str\": [\"token_1\", \"token_2\", \"token_3\"],\n        \"str2idx\": {\"token_1\": 0, \"token_2\": 1, \"token_3\": 2},\n        \"str2freq\": {\"token_1\": 300, \"token_2\": 200, \"token_3\": 100},\n        \"vocab_size\": 3,\n        \"preprocessing\": {\n            \"missing_value_strategy\": \"drop_row\",\n            \"fill_value\": \"<UNK>\",\n            \"computed_fill_value\": \"<UNK>\",\n            \"lowercase\": False,\n            \"most_common\": 10000,\n            \"cache_encoder_embeddings\": False,\n        },\n    }\n\n    model_config = ModelConfig.from_dict(config)\n\n    CategoryOutputFeature.update_config_with_metadata(\n        feature_config=model_config.output_features[0],\n        feature_metadata=feature_metadata,\n    )\n\n    assert model_config.output_features[0].loss.class_weights == [0.1, 0.2, 0.3]\n\n\ndef test_dict_class_weights_text():\n    input_features = [text_feature()]\n    output_features = [text_feature(decoder={\"vocab_size\": 3, \"max_sequence_length\": 10})]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    # Set class weights as dictionary on config\n    class_weights_dict = {\n        \"<EOS>\": 0,\n        \"<SOS>\": 0,\n        \"<PAD>\": 0,\n        \"<UNK>\": 0,\n        \"token_1\": 0.5,\n        \"token_2\": 0.4,\n        \"token_3\": 0.1,\n    }\n    config[\"output_features\"][0][\"loss\"] = {\n        \"type\": \"sequence_softmax_cross_entropy\",\n        \"class_weights\": class_weights_dict,\n    }\n\n    # Mock feature metadata\n    feature_metadata = {\n        \"idx2str\": [\"<EOS>\", \"<SOS>\", \"<PAD>\", \"<UNK>\", \"token_1\", \"token_2\", \"token_3\"],\n        \"str2idx\": {\"<EOS>\": 0, \"<SOS>\": 1, \"<PAD>\": 2, \"<UNK>\": 3, \"token_1\": 4, \"token_2\": 5, \"token_3\": 6},\n        \"str2freq\": {\"<EOS>\": 0, \"<SOS>\": 0, \"<PAD>\": 0, \"<UNK>\": 0, \"token_1\": 300, \"token_2\": 200, \"token_3\": 100},\n        \"str2idf\": None,\n        \"vocab_size\": 7,\n        \"max_sequence_length\": 9,\n        \"max_sequence_length_99ptile\": 9.0,\n        \"pad_idx\": 2,\n        \"padding_symbol\": \"<PAD>\",\n        \"unknown_symbol\": \"<UNK>\",\n        \"index_name\": None,\n        \"preprocessing\": {\n            \"prompt\": {\n                \"retrieval\": {\"type\": None, \"index_name\": None, \"model_name\": None, \"k\": 0},\n                \"task\": None,\n                \"template\": None,\n            },\n            \"pretrained_model_name_or_path\": None,\n            \"tokenizer\": \"space_punct\",\n            \"vocab_file\": None,\n            \"sequence_length\": None,\n            \"max_sequence_length\": 256,\n            \"most_common\": 20000,\n            \"padding_symbol\": \"<PAD>\",\n            \"unknown_symbol\": \"<UNK>\",\n            \"padding\": \"right\",\n            \"lowercase\": True,\n            \"missing_value_strategy\": \"drop_row\",\n            \"fill_value\": \"<UNK>\",\n            \"computed_fill_value\": \"<UNK>\",\n            \"ngram_size\": 2,\n            \"cache_encoder_embeddings\": False,\n            \"compute_idf\": False,\n        },\n    }\n\n    model_config = ModelConfig.from_dict(config)\n\n    TextOutputFeature.update_config_with_metadata(\n        feature_config=model_config.output_features[0],\n        feature_metadata=feature_metadata,\n    )\n\n    assert model_config.output_features[0].loss.class_weights == [0, 0, 0, 0, 0.5, 0.4, 0.1]\n\n\ndef test_dict_class_weights_set():\n    input_features = [category_feature()]\n    output_features = [set_feature()]\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n    }\n\n    # Set class weights as dictionary on config\n    class_weights_dict = {\"token_1\": 0.1, \"token_2\": 0.2, \"token_3\": 0.3, \"<UNK>\": 0}\n    config[\"output_features\"][0][\"loss\"] = {\"type\": \"sigmoid_cross_entropy\", \"class_weights\": class_weights_dict}\n\n    # Mock feature metadata\n    feature_metadata = {\n        \"idx2str\": [\"token_1\", \"token_2\", \"token_3\", \"<UNK>\"],\n        \"str2idx\": {\"token_1\": 0, \"token_2\": 1, \"token_3\": 2, \"<UNK>\": 3},\n        \"str2freq\": {\"token_1\": 300, \"token_2\": 200, \"token_3\": 100, \"<UNK>\": 0},\n        \"vocab_size\": 4,\n        \"max_set_size\": 3,\n        \"preprocessing\": {\n            \"tokenizer\": \"space\",\n            \"missing_value_strategy\": \"drop_row\",\n            \"fill_value\": \"<UNK>\",\n            \"computed_fill_value\": \"<UNK>\",\n            \"lowercase\": False,\n            \"most_common\": 10000,\n        },\n    }\n\n    model_config = ModelConfig.from_dict(config)\n\n    SetOutputFeature.update_config_with_metadata(\n        feature_config=model_config.output_features[0],\n        feature_metadata=feature_metadata,\n    )\n\n    assert model_config.output_features[0].loss.class_weights == [0.1, 0.2, 0.3, 0]\n"
  },
  {
    "path": "tests/ludwig/modules/test_lr_scheduler.py",
    "content": "import math\nimport sys\n\nimport numpy as np\nfrom torch.optim import SGD\n\nfrom ludwig.features.number_feature import NumberInputFeature, NumberOutputFeature\nfrom ludwig.modules.lr_scheduler import LRScheduler\nfrom ludwig.schema.encoders.base import DenseEncoderConfig\nfrom ludwig.schema.features.number_feature import ECDNumberOutputFeatureConfig, NumberInputFeatureConfig\nfrom ludwig.schema.lr_scheduler import LRSchedulerConfig\nfrom ludwig.utils.metric_utils import TrainerMetric\nfrom ludwig.utils.trainer_utils import get_new_progress_tracker\n\n\ndef test_lr_scheduler_warmup_decay():\n    total_steps = 10000\n    steps_per_checkpoint = 1000\n    base_lr = 1.0\n    warmup_fraction = 0.1\n\n    module = NumberInputFeature(NumberInputFeatureConfig(name=\"num1\", encoder=DenseEncoderConfig()))\n\n    const_optimizer = SGD(module.parameters(), lr=base_lr)\n    const_config = LRSchedulerConfig(warmup_evaluations=0)\n    const_scheduler = LRScheduler(\n        config=const_config,\n        optimizer=const_optimizer,\n        steps_per_checkpoint=steps_per_checkpoint,\n        total_steps=total_steps,\n    )\n\n    linear_optimizer = SGD(module.parameters(), lr=base_lr)\n    linear_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay=\"linear\")\n    linear_scheduler = LRScheduler(\n        config=linear_config,\n        optimizer=linear_optimizer,\n        steps_per_checkpoint=steps_per_checkpoint,\n        total_steps=total_steps,\n    )\n\n    exp_optimizer = SGD(module.parameters(), lr=base_lr)\n    exp_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay=\"exponential\")\n    exp_scheduler = LRScheduler(\n        config=exp_config, optimizer=exp_optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps\n    )\n\n    cosine_optimizer = SGD(module.parameters(), lr=base_lr)\n    cosine_config = LRSchedulerConfig(warmup_fraction=warmup_fraction, decay=\"cosine\", t_0=steps_per_checkpoint)\n    cosine_scheduler = LRScheduler(\n        config=cosine_config,\n        optimizer=cosine_optimizer,\n        steps_per_checkpoint=steps_per_checkpoint,\n        total_steps=total_steps,\n    )\n\n    warmup_steps = total_steps * warmup_fraction\n    for i in range(total_steps):\n        # Offset by 1\n        step = i + 1\n\n        const_scheduler.step()\n        const_lr = const_optimizer.param_groups[0][\"lr\"]\n        assert const_lr == base_lr, f\"step: {step}\"\n\n        linear_scheduler.step()\n        linear_lr = linear_optimizer.param_groups[0][\"lr\"]\n\n        exp_scheduler.step()\n        exp_lr = exp_optimizer.param_groups[0][\"lr\"]\n\n        cosine_scheduler.step()\n        cosine_lr = cosine_optimizer.param_groups[0][\"lr\"]\n\n        if step < warmup_steps:\n            assert linear_lr == exp_lr, f\"step: {step}\"\n            assert linear_lr == cosine_lr, f\"step: {step}\"\n            assert linear_lr < base_lr, f\"step: {step}\"\n        elif step == warmup_steps:\n            assert linear_lr == base_lr, f\"step: {step}\"\n            assert cosine_lr == base_lr, f\"step: {step}\"\n            assert exp_lr < base_lr, f\"step: {step}\"\n        else:\n            assert linear_lr < base_lr, f\"step: {step}\"\n            assert exp_lr < base_lr, f\"step: {step}\"\n            assert cosine_lr <= base_lr, f\"step: {step}\"\n\n    assert linear_lr < exp_lr\n    assert exp_lr < cosine_lr\n    assert cosine_lr == base_lr\n\n\ndef test_lr_scheduler_reduce_on_plateau():\n    total_eval_steps = 100\n    base_lr = 1.0\n    reduce_limit = 3\n\n    module = NumberInputFeature(NumberInputFeatureConfig(name=\"num1\", encoder=DenseEncoderConfig()))\n    output1 = NumberOutputFeature(ECDNumberOutputFeatureConfig(name=\"output1\", input_size=10), output_features={})\n\n    optimizer = SGD(module.parameters(), lr=base_lr)\n    config = LRSchedulerConfig(\n        warmup_evaluations=0,\n        decay=None,\n        reduce_on_plateau=reduce_limit,\n        reduce_on_plateau_patience=10,\n        reduce_on_plateau_rate=0.1,\n    )\n    scheduler = LRScheduler(config=config, optimizer=optimizer, steps_per_checkpoint=0, total_steps=0)\n\n    progress_tracker = get_new_progress_tracker(\n        batch_size=64,\n        best_eval_metric_value=sys.float_info.max,\n        best_increase_batch_size_eval_metric=sys.float_info.max,\n        learning_rate=base_lr,\n        output_features={\"output1\": output1},\n    )\n\n    num_reductions = 0\n\n    last_lr = optimizer.param_groups[0][\"lr\"]\n    steps_to_plateau = 5\n    loss = 10.0\n    for epoch in range(total_eval_steps):\n        for i in range(100):\n            # Simulate batch-wise steps. If we make a mistake, then this will reset\n            # the learning rate.\n            scheduler.step()\n\n        steps_to_plateau -= 1\n        if steps_to_plateau > 0:\n            loss -= 0.1\n\n        progress_tracker.train_metrics[\"output1\"][\"loss\"].append(\n            TrainerMetric(epoch=epoch, step=epoch * 100, value=loss)\n        )\n        scheduler.eval_step(progress_tracker, \"output1\")\n        lr = optimizer.param_groups[0][\"lr\"]\n        if lr < last_lr:\n            # Reset steps to plateau\n            steps_to_plateau = 5\n            num_reductions += 1\n        last_lr = lr\n\n    assert num_reductions == reduce_limit\n\n    # 3 reductions that multiply by 0.1 each time\n    assert np.isclose(lr, 0.001)\n\n\ndef test_lr_scheduler_cosine_decay_fixed_period():\n    total_steps = 10000\n    steps_per_checkpoint = 1000\n    base_lr = 1.0\n\n    module = NumberInputFeature(NumberInputFeatureConfig(name=\"num1\", encoder=DenseEncoderConfig()))\n\n    optimizer = SGD(module.parameters(), lr=base_lr)\n    config = LRSchedulerConfig(decay=\"cosine\", t_0=steps_per_checkpoint, decay_rate=0, reduce_on_plateau=0)\n    scheduler = LRScheduler(config=config, optimizer=optimizer, steps_per_checkpoint=0, total_steps=0)\n\n    curr_lr = base_lr\n    prev_lr = base_lr\n    num_restarts = 0\n    for step in range(total_steps + 1):\n        # Cosine annealing formula\n        expected_lr = base_lr * 0.5 * (1 + math.cos(math.pi * (step % steps_per_checkpoint) / steps_per_checkpoint))\n        assert np.isclose(curr_lr, expected_lr), f\"step: {step}\"\n\n        if prev_lr < curr_lr:\n            # Since Cosine decay is periodic, we should see the learning rate\n            # decrease and then increase again.\n            num_restarts += 1\n\n        prev_lr = curr_lr\n        scheduler.step()\n\n        curr_lr = optimizer.param_groups[0][\"lr\"]\n\n    assert num_restarts == 10, f\"num_restarts: {num_restarts}\"\n\n\ndef test_lr_scheduler_cosine_decay_increasing_period():\n    total_steps = 20000\n    steps_per_checkpoint = 1000\n    base_lr = 1.0\n\n    module = NumberInputFeature(NumberInputFeatureConfig(name=\"num1\", encoder=DenseEncoderConfig()))\n\n    optimizer = SGD(module.parameters(), lr=base_lr)\n    config = LRSchedulerConfig(\n        decay=\"cosine\",\n        t_0=steps_per_checkpoint,\n        t_mult=2,\n        decay_rate=0,\n        reduce_on_plateau=0,\n    )\n    scheduler = LRScheduler(\n        config=config, optimizer=optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps\n    )\n\n    curr_lr = base_lr\n    prev_lr = base_lr\n    num_restarts = 0\n    for _ in range(total_steps + 1):\n        if prev_lr < curr_lr:\n            # Since Cosine decay is periodic, we should see the learning rate\n            # decrease and then increase again.\n            num_restarts += 1\n\n        prev_lr = curr_lr\n        scheduler.step()\n\n        curr_lr = optimizer.param_groups[0][\"lr\"]\n\n    # 1000, 3000, 6000, 12000, 24000 (but we stop at 20000)\n    assert num_restarts == 4, f\"num_restarts: {num_restarts}\"\n\n\ndef test_lr_scheduler_save_load():\n    steps_per_checkpoint = 10\n    total_steps = 100\n    base_lr = 1.0\n    reduce_limit = 3\n\n    module = NumberInputFeature(NumberInputFeatureConfig(name=\"num1\", encoder=DenseEncoderConfig()))\n    output1 = NumberOutputFeature(ECDNumberOutputFeatureConfig(name=\"output1\", input_size=10), output_features={})\n\n    optimizer = SGD(module.parameters(), lr=base_lr)\n    config = LRSchedulerConfig(warmup_fraction=0.2, reduce_on_plateau=reduce_limit)\n    scheduler = LRScheduler(\n        config=config, optimizer=optimizer, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps\n    )\n\n    progress_tracker = get_new_progress_tracker(\n        batch_size=64,\n        best_eval_metric_value=sys.float_info.max,\n        best_increase_batch_size_eval_metric=sys.float_info.max,\n        learning_rate=base_lr,\n        output_features={\"output1\": output1},\n    )\n\n    for _ in range(10):\n        scheduler.step()\n\n    progress_tracker.train_metrics[\"output1\"][\"loss\"].append(TrainerMetric(epoch=0, step=10, value=1.0))\n    scheduler.eval_step(progress_tracker, \"output1\")\n\n    optimizer_state = optimizer.state_dict()\n    scheduler_state = scheduler.state_dict()\n\n    optimizer2 = SGD(module.parameters(), lr=base_lr)\n    scheduler2 = LRScheduler(\n        config=config, optimizer=optimizer2, steps_per_checkpoint=steps_per_checkpoint, total_steps=total_steps\n    )\n\n    # Important: state needs to be loaded after init of optimizer and scheduler, otherwise\n    # it can override loaded state\n    optimizer2.load_state_dict(optimizer_state)\n    scheduler2.load_state_dict(scheduler_state)\n\n    lr = optimizer.param_groups[0][\"lr\"]\n    assert lr == optimizer2.param_groups[0][\"lr\"]\n    assert scheduler.state_dict() == scheduler2.state_dict()\n\n    for _ in range(10):\n        scheduler.step()\n        scheduler2.step()\n\n    progress_tracker.train_metrics[\"output1\"][\"loss\"].append(TrainerMetric(epoch=1, step=20, value=0.8))\n    scheduler.eval_step(progress_tracker, \"output1\")\n    scheduler2.eval_step(progress_tracker, \"output1\")\n\n    assert lr != optimizer.param_groups[0][\"lr\"]\n    assert optimizer.param_groups[0][\"lr\"] == optimizer2.param_groups[0][\"lr\"]\n    assert scheduler.state_dict() == scheduler2.state_dict()\n"
  },
  {
    "path": "tests/ludwig/modules/test_metric_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.distributed import init_dist_strategy\nfrom ludwig.modules import metric_modules\nfrom ludwig.schema.features.loss.loss import (\n    BWCEWLossConfig,\n    SigmoidCrossEntropyLossConfig,\n    SoftmaxCrossEntropyLossConfig,\n)\n\n# Required for local testing.\ninit_dist_strategy(\"local\")\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(6).float()])\ndef test_rmse_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.RMSEMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output == metric.compute()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([0.2, 0.3, 0.8, 0.1])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([0, 0, 1, 1])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.5)])\ndef test_roc_auc_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.BinaryAUROCMetric(task=\"binary\")\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output == metric.compute()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([0.2, 0.3, 0.8, 0.1, 0.8])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([0, 0, 1, 1, 0])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.6667).float()])\ndef test_specificity_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.SpecificityMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7527).float()])\ndef test_rmspe_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.RMSPEMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\n    \"preds,target,num_outputs,output\",\n    [\n        (torch.arange(3), torch.arange(3, 6), 1, torch.tensor(-12.5)),\n        (torch.arange(6).reshape(3, 2), torch.arange(6, 12).reshape(3, 2), 2, torch.tensor(-12.5)),\n    ],\n)\ndef test_r2_score(preds: torch.Tensor, target: torch.Tensor, num_outputs: int, output: torch.Tensor):\n    metric = metric_modules.R2Score(num_outputs=num_outputs)\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert metric.compute() == output\n\n\ndef test_r2_score_single_sample():\n    metric = metric_modules.R2Score(num_outputs=1)\n    with metric.sync_context():\n        metric.update(preds=torch.tensor([0.8]), target=torch.arange(1))\n        assert torch.isnan(metric.compute())\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(-21.4655).float()])\ndef test_bwcewl_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.BWCEWLMetric(BWCEWLossConfig())\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([[0.5, 0.5], [0.2, 0.8], [0.6, 0.4]])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([1, 1, 0])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.5763)])\ndef test_softmax_cross_entropy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.SoftmaxCrossEntropyMetric(SoftmaxCrossEntropyLossConfig())\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(-21.4655).float()])\ndef test_sigmoid_cross_entropy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.SigmoidCrossEntropyMetric(SigmoidCrossEntropyLossConfig())\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\n    \"preds,target,output\",\n    [\n        (\n            torch.tensor([[0, 1], [3, 2], [4, 5]]),\n            torch.tensor([[0, 1], [1, 2], [4, 5]]),\n            torch.tensor(0.8),\n        ),\n        (\n            torch.tensor([[0, 1, 2], [1, 3, 4], [3, 4, 5]]),\n            torch.tensor([[0, 1, 2], [1, 1, 4], [3, 4, 5]]),\n            torch.tensor(0.8750),\n        ),\n        (\n            torch.tensor([[1, 5, 1, 5, 1, 5, 12, 12, 12], [10, 1, 5, 1, 5, 12, 12, 12, 12]]),\n            torch.tensor([[1, 9, 5, 7, 5, 9, 13, 6, 0], [1, 9, 7, 13, 4, 7, 7, 7, 0]]),\n            torch.tensor(0.05555555),\n        ),\n    ],\n)\ndef test_token_accuracy_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.TokenAccuracyMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.allclose(metric.compute(), output)\n\n\ndef test_sequence_accuracy_metric():\n    target = torch.tensor(\n        [\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n        ]\n    )\n    preds = torch.tensor(\n        [\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 6, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n            [1, 4, 5, 4, 0],\n        ]\n    )\n    metric = metric_modules.SequenceAccuracyMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(metric.compute(), torch.tensor(0.8438), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6)])\n@pytest.mark.parametrize(\"target\", [torch.tensor([0, 1, 2, 1, 4, 5]).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7500).float()])\n@pytest.mark.parametrize(\"one_hot\", [False, True])\ndef test_category_accuracy(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, one_hot: bool):\n    if one_hot:\n        target = torch.nn.functional.one_hot(target.long(), num_classes=6).float()\n    metric = metric_modules.CategoryAccuracy(num_classes=6)\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6)])\n@pytest.mark.parametrize(\"target\", [torch.tensor([0, 1, 2, 1, 4, 5]).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.8333).float()])\n@pytest.mark.parametrize(\"one_hot\", [False, True])\ndef test_category_accuracy_micro(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, one_hot: bool):\n    if one_hot:\n        target = torch.nn.functional.one_hot(target.long(), num_classes=6).float()\n    metric = metric_modules.CategoryAccuracyMicro(num_classes=6)\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\n    \"preds,target,output,k\",\n    [\n        (\n            torch.tensor([[0.1, 0.9, 0], [0.3, 0.1, 0.6], [0.2, 0.5, 0.3]]),\n            torch.tensor([0, 1, 2]),\n            torch.tensor(0.6667).float(),\n            2,\n        )\n    ],\n)\ndef test_hits_at_k_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor, k: int):\n    metric = metric_modules.HitsAtKMetric(num_classes=3, top_k=k)\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert torch.isclose(output, metric.compute(), rtol=0.0001)\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(6).float()])\ndef test_mae_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.MAEMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output == metric.compute()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(36).float()])\ndef test_mse_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.MSEMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output == metric.compute()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.arange(6).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"target\", [torch.arange(6, 12).reshape(3, 2).float()])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.7365440726280212)])\ndef test_mape_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.MAPEMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output.item() == metric.compute().item()\n\n\n@pytest.mark.parametrize(\"preds\", [torch.tensor([[0, 1], [1, 1]])])\n@pytest.mark.parametrize(\"target\", [torch.tensor([[1, 0], [1, 1]])])\n@pytest.mark.parametrize(\"output\", [torch.tensor(0.5)])\ndef test_jaccard_metric(preds: torch.Tensor, target: torch.Tensor, output: torch.Tensor):\n    metric = metric_modules.JaccardMetric()\n    with metric.sync_context():\n        metric.update(preds, target)\n        assert output == metric.compute()\n\n\ndef test_char_error_rate():\n    metric = metric_modules.CharErrorRateMetric()\n    with metric.sync_context():\n        metric.update(\n            [\"this is the prediction\", \"there is an other sample\"], [\"this is the reference\", \"there is another one\"]\n        )\n        assert torch.isclose(torch.tensor(0.3415), metric.compute(), rtol=0.5)\n"
  },
  {
    "path": "tests/ludwig/modules/test_mlp_mixer_modules.py",
    "content": "import pytest\n\nfrom ludwig.modules.mlp_mixer_modules import MixerBlock, MLP, MLPMixer\n\nfrom .test_utils import assert_output_shapes\n\n\n@pytest.mark.parametrize(\"in_features,hidden_size,out_features\", [(3, 8, 8), (8, 64, 32)])\ndef test_mlp(in_features: int, hidden_size: int, out_features: int):\n    assert_output_shapes(module=MLP(in_features, hidden_size, out_features), input_shape=(in_features,))\n\n\n@pytest.mark.parametrize(\"embed_size,n_patches,token_dim,channel_dim\", [(512, 49, 2048, 256)])\ndef test_mixer_block(\n    embed_size: int,\n    n_patches: int,\n    token_dim: int,\n    channel_dim: int,\n):\n    assert_output_shapes(\n        module=MixerBlock(embed_size, n_patches, token_dim, channel_dim), input_shape=(n_patches, embed_size)\n    )\n\n\n@pytest.mark.parametrize(\"img_height,img_width,in_channels\", [(224, 224, 3)])\ndef test_mlp_mixer(img_height: int, img_width: int, in_channels: int):\n    assert_output_shapes(module=MLPMixer(img_height, img_width, in_channels), input_shape=(3, img_height, img_width))\n"
  },
  {
    "path": "tests/ludwig/modules/test_normalization_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules.normalization_modules import GhostBatchNormalization\n\nBATCH_SIZE = 16\nOUTPUT_SIZE = 8\n\n\n@pytest.mark.parametrize(\"virtual_batch_size\", [None, BATCH_SIZE // 2, BATCH_SIZE - 14, BATCH_SIZE - 10])\n@pytest.mark.parametrize(\"mode\", [True, False])  # training (True) or eval(False)\ndef test_ghostbatchnormalization(mode: bool, virtual_batch_size: int | None) -> None:\n    # setup up GhostBatchNormalization layer\n    ghost_batch_norm = GhostBatchNormalization(OUTPUT_SIZE, virtual_batch_size=virtual_batch_size)\n\n    # set training or eval mode\n    ghost_batch_norm.train(mode=mode)\n\n    # setup inputs to test\n    inputs = torch.randn([BATCH_SIZE, OUTPUT_SIZE], dtype=torch.float32)\n\n    # run tensor through\n    norm_tensor = ghost_batch_norm(inputs)\n\n    # check for correctness of output\n    assert isinstance(norm_tensor, torch.Tensor)\n    assert norm_tensor.shape == (BATCH_SIZE, OUTPUT_SIZE)\n\n    # check for required properties\n    assert ghost_batch_norm.input_shape == inputs.shape[1:]\n    assert ghost_batch_norm.output_shape == inputs.shape[1:]\n    assert ghost_batch_norm.input_dtype == torch.float32\n\n    assert isinstance(ghost_batch_norm.moving_mean, torch.Tensor)\n    assert ghost_batch_norm.moving_mean.shape == (OUTPUT_SIZE,)\n\n    assert isinstance(ghost_batch_norm.moving_variance, torch.Tensor)\n    assert ghost_batch_norm.moving_variance.shape == (OUTPUT_SIZE,)\n\n\ndef test_ghostbatchnormalization_chunk_size_2() -> None:\n    \"\"\"Test GhostBatchNormalization with virtual_batch_size=2 and batch_size=7 This creates chunks of size 2, 2, 2,\n    1 which should be handled correctly since we should skip applying batch norm to the last chunk since it is size\n    1.\"\"\"\n    # setup up GhostBatchNormalization layer\n    ghost_batch_norm = GhostBatchNormalization(6, virtual_batch_size=2)\n\n    # setup inputs to test\n    inputs = torch.randn([7, 6], dtype=torch.float32)\n\n    # Set to training mode\n    ghost_batch_norm.train(mode=True)\n\n    # run tensor through\n    ghost_batch_norm(inputs)\n"
  },
  {
    "path": "tests/ludwig/modules/test_recurrent_modules.py",
    "content": "import logging\n\nimport pytest\nimport torch\n\nfrom ludwig.modules import recurrent_modules\n\nlogger = logging.getLogger(__name__)\n\n\n@pytest.mark.parametrize(\"max_sequence_length,expected_output_shape\", [(19, [19, 256]), (None, [256])])\ndef test_recurrent_stack(max_sequence_length, expected_output_shape):\n    recurrent_stack = recurrent_modules.RecurrentStack(\n        input_size=10, max_sequence_length=max_sequence_length, hidden_size=256\n    )\n    assert recurrent_stack.output_shape == torch.Size(expected_output_shape)\n\n    # Batch (N), Length (L), Input (H)\n    inputs = torch.rand(2, 19, 10)\n    hidden, final_state = recurrent_stack(inputs)\n\n    assert hidden.shape == torch.Size([2, 19, 256])\n    assert final_state.shape == torch.Size([2, 256])\n"
  },
  {
    "path": "tests/ludwig/modules/test_reduction_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules import reduction_modules\nfrom ludwig.utils.torch_utils import get_torch_device\n\nDEVICE = get_torch_device()\n\n\n@pytest.mark.parametrize(\"reduce_mode\", [\"last\", \"sum\", \"mean\", \"avg\", \"max\", \"concat\", \"attention\", None])\n@pytest.mark.parametrize(\"test_input_shape\", [(16, 1, 4), (4, 10, 16)])\ndef test_sequence_reducer(reduce_mode: str, test_input_shape: tuple[int, ...]):\n    batch_size, max_sequence_length, encoding_size = test_input_shape\n    sequence_reducer = reduction_modules.SequenceReducer(\n        reduce_mode=reduce_mode, max_sequence_length=max_sequence_length, encoding_size=encoding_size\n    ).to(DEVICE)\n    inputs = torch.zeros(test_input_shape)\n    # Generates random sequence of random length for each instance in batch.\n    for batch_index in range(batch_size):\n        sequence_length = torch.randint(max_sequence_length, (1,))\n        inputs[batch_index, :sequence_length] = torch.rand((sequence_length, encoding_size))\n    outputs = sequence_reducer(inputs.to(DEVICE))\n    assert outputs.shape[1:] == sequence_reducer.output_shape\n"
  },
  {
    "path": "tests/ludwig/modules/test_regex_freezing.py",
    "content": "import logging\nimport os\nimport re\nfrom contextlib import nullcontext as no_error_raised\n\nimport pytest\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.constants import (\n    BASE_MODEL,\n    BATCH_SIZE,\n    EPOCHS,\n    GENERATION,\n    INPUT_FEATURES,\n    MODEL_LLM,\n    MODEL_TYPE,\n    OUTPUT_FEATURES,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.encoders.image.torchvision import TVEfficientNetEncoder\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.utils.misc_utils import set_random_seed\nfrom ludwig.utils.trainer_utils import freeze_layers_regex\nfrom tests.integration_tests.utils import category_feature, generate_data, image_feature, text_feature\n\nRANDOM_SEED = 130\n\n\n@pytest.mark.parametrize(\n    \"regex\",\n    [\n        r\"(features\\.1.*|features\\.2.*|features\\.3.*|model\\.features\\.4\\.1\\.block\\.3\\.0\\.weight)\",\n        r\"(features\\.1.*|features\\.2\\.*|features\\.3.*)\",\n        r\"(features\\.4\\.0\\.block|features\\.4\\.\\d+\\.block)\",\n        r\"(features\\.5\\.*|features\\.6\\.*|features\\.7\\.*)\",\n        r\"(features\\.8\\.\\d+\\.weight|features\\.8\\.\\d+\\.bias)\",\n    ],\n)\ndef test_tv_efficientnet_freezing(regex):\n    set_random_seed(RANDOM_SEED)\n\n    pretrained_model = TVEfficientNetEncoder(\n        model_variant=\"b0\", use_pretrained=False, saved_weights_in_checkpoint=True, trainable=True\n    )\n\n    config = ECDTrainerConfig(layers_to_freeze_regex=regex)\n    freeze_layers_regex(config, pretrained_model)\n    for name, param in pretrained_model.named_parameters():\n        if re.search(re.compile(regex), name):\n            assert not param.requires_grad\n        else:\n            assert param.requires_grad\n\n\ndef test_llm_freezing(tmpdir, csv_filename):\n    # Force CPU to avoid CUBLAS errors with tiny random LLM models on GPU.\n    old_val = os.environ.get(\"CUDA_VISIBLE_DEVICES\")\n    os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"\"\n    try:\n        _run_llm_freezing(tmpdir, csv_filename)\n    finally:\n        if old_val is None:\n            os.environ.pop(\"CUDA_VISIBLE_DEVICES\", None)\n        else:\n            os.environ[\"CUDA_VISIBLE_DEVICES\"] = old_val\n\n\ndef _run_llm_freezing(tmpdir, csv_filename):\n    input_features = [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})]\n    output_features = [text_feature(name=\"output\")]\n\n    train_df = generate_data(input_features, output_features, filename=csv_filename, num_examples=25)\n\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"hf-internal-testing/tiny-random-GPTJForCausalLM\",\n        INPUT_FEATURES: [text_feature(name=\"input\", encoder={\"type\": \"passthrough\"})],\n        OUTPUT_FEATURES: [text_feature(name=\"output\")],\n        TRAINER: {TYPE: \"finetune\", BATCH_SIZE: 8, EPOCHS: 1, \"layers_to_freeze_regex\": r\"(h\\.0\\.attn\\.*)\"},\n        GENERATION: {\"pad_token_id\": 0},\n    }\n\n    model = LudwigModel(config, logging_level=logging.INFO)\n\n    output_directory: str = str(tmpdir)\n    model.train(dataset=train_df, output_directory=output_directory, skip_save_processed_input=False)\n\n    for name, p in model.model.named_parameters():\n        if \"h.0.attn\" in name:\n            assert not p.requires_grad\n        else:\n            assert p.requires_grad\n\n\ndef test_frozen_tv_training(tmpdir, csv_filename):\n    input_features = [\n        image_feature(tmpdir, encoder={\"type\": \"efficientnet\", \"use_pretrained\": False, \"model_variant\": \"b0\"})\n    ]\n    output_features = [category_feature()]\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": output_features,\n        TRAINER: {\n            \"layers_to_freeze_regex\": r\"(features\\.1.*|features\\.2\\.*|features\\.3.*)\",\n            \"epochs\": 1,\n            \"train_steps\": 1,\n        },\n    }\n\n    training_data_csv_path = generate_data(config[\"input_features\"], config[\"output_features\"], csv_filename)\n    model = LudwigModel(config)\n\n    with no_error_raised():\n        model.experiment(\n            dataset=training_data_csv_path,\n            skip_save_training_description=True,\n            skip_save_training_statistics=True,\n            skip_save_model=True,\n            skip_save_progress=True,\n            skip_save_log=True,\n            skip_save_processed_input=True,\n        )\n"
  },
  {
    "path": "tests/ludwig/modules/test_tabnet_modules.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.modules.tabnet_modules import AttentiveTransformer, FeatureBlock, FeatureTransformer, TabNet\nfrom ludwig.utils.entmax import sparsemax\nfrom tests.integration_tests.parameter_update_utils import check_module_parameters_updated\n\nRANDOM_SEED = 67\n\n\n@pytest.mark.parametrize(\n    \"input_tensor\",\n    [\n        torch.tensor([[-1.0, 0.0, 1.0], [5.01, 4.0, -2.0]], dtype=torch.float32),\n        torch.tensor(\n            [[1.36762051e8, -1.36762051e8, 1.59594639e20], [1.59594639e37, 1.36762051e7, 1.26e6]], dtype=torch.float32\n        ),\n    ],\n)\ndef test_sparsemax(input_tensor: torch.Tensor) -> None:\n    output_tensor = sparsemax(input_tensor)\n\n    assert isinstance(output_tensor, torch.Tensor)\n    assert output_tensor.equal(torch.tensor([[0, 0, 1], [1, 0, 0]], dtype=torch.float32))\n\n\n@pytest.mark.parametrize(\"bn_virtual_bs\", [None, 7])\n@pytest.mark.parametrize(\"external_shared_fc_layer\", [True, False])\n@pytest.mark.parametrize(\"apply_glu\", [True, False])\n@pytest.mark.parametrize(\"size\", [4, 12])\n@pytest.mark.parametrize(\"input_size\", [2, 6])\n@pytest.mark.parametrize(\"batch_size\", [1, 16])\ndef test_feature_block(\n    input_size,\n    size: int,\n    apply_glu: bool,\n    external_shared_fc_layer: bool,\n    bn_virtual_bs: int | None,\n    batch_size: int,\n) -> None:\n    # setup synthetic tensor\n    torch.manual_seed(RANDOM_SEED)\n    input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32)\n\n    if external_shared_fc_layer:\n        shared_fc_layer = torch.nn.Linear(input_size, size * 2 if apply_glu else size, bias=False)\n    else:\n        shared_fc_layer = None\n\n    feature_block = FeatureBlock(\n        input_size, size, apply_glu=apply_glu, shared_fc_layer=shared_fc_layer, bn_virtual_bs=bn_virtual_bs\n    )\n\n    output_tensor = feature_block(input_tensor)\n\n    # check for expected structure and properties\n    assert isinstance(output_tensor, torch.Tensor)\n    assert output_tensor.shape == (batch_size, size)\n\n    assert feature_block.input_shape[-1] == input_size\n    assert feature_block.output_shape[-1] == size\n    assert feature_block.input_dtype == torch.float32\n\n\n@pytest.mark.parametrize(\"num_total_blocks, num_shared_blocks\", [(4, 2), (6, 4), (3, 1)])\n@pytest.mark.parametrize(\"virtual_batch_size\", [None, 7])\n@pytest.mark.parametrize(\"size\", [4, 12])\n@pytest.mark.parametrize(\"input_size\", [2, 6])\n@pytest.mark.parametrize(\"batch_size\", [1, 16])\ndef test_feature_transformer(\n    input_size: int,\n    size: int,\n    virtual_batch_size: int | None,\n    num_total_blocks: int,\n    num_shared_blocks: int,\n    batch_size: int,\n) -> None:\n    # setup synthetic tensor\n    torch.manual_seed(RANDOM_SEED)\n    input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32)\n\n    feature_transformer = FeatureTransformer(\n        input_size,\n        size,\n        bn_virtual_bs=virtual_batch_size,\n        num_total_blocks=num_total_blocks,\n        num_shared_blocks=num_shared_blocks,\n    )\n\n    output_tensor = feature_transformer(input_tensor)\n\n    # check for expected structure and properties\n    assert isinstance(output_tensor, torch.Tensor)\n    assert output_tensor.shape == (batch_size, size)\n\n    assert feature_transformer.input_shape[-1] == input_size\n    assert feature_transformer.output_shape[-1] == size\n    assert feature_transformer.input_dtype == torch.float32\n\n\n@pytest.mark.parametrize(\"virtual_batch_size\", [None, 7])\n@pytest.mark.parametrize(\"output_size\", [10, 12])\n@pytest.mark.parametrize(\"size\", [4, 8])\n@pytest.mark.parametrize(\"input_size\", [2, 6])\n@pytest.mark.parametrize(\"entmax_mode\", [None, \"entmax15\", \"adaptive\", \"constant\"])\n@pytest.mark.parametrize(\"batch_size\", [1, 16])\ndef test_attentive_transformer(\n    entmax_mode: str | None,\n    input_size: int,\n    size: int,\n    output_size: int,\n    virtual_batch_size: int | None,\n    batch_size: int,\n) -> None:\n    # setup synthetic tensors\n    torch.manual_seed(RANDOM_SEED)\n    input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32)\n    prior_scales = torch.ones([batch_size, input_size])\n\n    # setup required transformers for test\n    feature_transformer = FeatureTransformer(input_size, size + output_size, bn_virtual_bs=virtual_batch_size)\n    attentive_transformer = AttentiveTransformer(\n        size, input_size, bn_virtual_bs=virtual_batch_size, entmax_mode=entmax_mode\n    )\n\n    # process synthetic tensor through transformers\n    x = feature_transformer(input_tensor)\n    output_tensor = attentive_transformer(x[:, output_size:], prior_scales)\n\n    # check for expected shape and properties\n    assert isinstance(output_tensor, torch.Tensor)\n    assert output_tensor.shape == (batch_size, input_size)\n\n    assert attentive_transformer.input_shape[-1] == size\n    assert attentive_transformer.output_shape[-1] == input_size\n    assert attentive_transformer.input_dtype == torch.float32\n    if entmax_mode == \"adaptive\":\n        assert isinstance(attentive_transformer.trainable_alpha, torch.Tensor)\n\n    # TODO:  Need variant of assert_model_parameters_updated() to account for the two step calling sequence\n    #        of AttentiveTransformer\n\n\n@pytest.mark.parametrize(\"virtual_batch_size\", [None, 7])\n@pytest.mark.parametrize(\"size\", [2, 4, 8])\n@pytest.mark.parametrize(\"output_size\", [2, 4, 12])\n@pytest.mark.parametrize(\"input_size\", [2])\n@pytest.mark.parametrize(\"entmax_mode\", [None, \"entmax15\", \"adaptive\", \"constant\"])\n@pytest.mark.parametrize(\"batch_size\", [1, 16])\ndef test_tabnet(\n    entmax_mode: str | None,\n    input_size: int,\n    output_size: int,\n    size: int,\n    virtual_batch_size: int | None,\n    batch_size: int,\n) -> None:\n    # setup synthetic tensor\n    torch.manual_seed(RANDOM_SEED)\n    input_tensor = torch.randn([batch_size, input_size], dtype=torch.float32)\n\n    tabnet = TabNet(\n        input_size, size, output_size, num_steps=3, num_total_blocks=4, num_shared_blocks=2, entmax_mode=entmax_mode\n    )\n\n    output = tabnet(input_tensor)\n\n    # check for expected shape and properties\n    assert isinstance(output, tuple)\n    assert output[0].shape == (batch_size, output_size)\n\n    assert tabnet.input_shape[-1] == input_size\n    assert tabnet.output_shape[-1] == output_size\n    assert tabnet.input_dtype == torch.float32\n\n    # check for parameter updates\n    target = torch.randn([batch_size, 1])\n    fpc, tpc, upc, not_updated = check_module_parameters_updated(tabnet, (input_tensor,), target)\n\n    if batch_size == 1:\n        # for single record batches, batchnorm layer is bypassed, only a subset of parameters are updated\n        assert upc == 17, (\n            f\"Updated parameter count not expected value. Parameters not updated: {not_updated}\"\n            f\"\\nModule structure:\\n{tabnet}\"\n        )\n    else:\n        # update count should equal trainable number of parameters\n        assert tpc == upc, (\n            f\"All parameter not updated. Parameters not updated: {not_updated}\" f\"\\nModule structure:\\n{tabnet}\"\n        )\n"
  },
  {
    "path": "tests/ludwig/modules/test_utils.py",
    "content": "import torch\n\nfrom ludwig.utils.torch_utils import LudwigModule\n\n\ndef assert_output_shapes(module: LudwigModule, input_shape: tuple[int]):\n    \"\"\"Runs a unit test to confirm that the out shape matches expected output.\n\n    module: Module to be tested.\n    input_shape: List of integers of the expected input shape (w/o batch dim).\n    \"\"\"\n\n    inputs = torch.rand(2, *input_shape, dtype=module.input_dtype)\n    output_tensor = module(inputs)\n    assert output_tensor.shape[1:] == module.output_shape\n"
  },
  {
    "path": "tests/ludwig/schema/hyperopt/test_scheduler.py",
    "content": "import pytest\n\nfrom ludwig.schema.hyperopt.scheduler import BaseSchedulerConfig\nfrom ludwig.schema.hyperopt.utils import register_scheduler_config, scheduler_config_registry\nfrom ludwig.schema.utils import ludwig_dataclass, ProtectedString\n\n\n@pytest.fixture(\n    params=[  # Tuples of SA name, dependency list, whether it should raise an exception\n        (\"no_deps\", None, False),\n        (\"installed\", [(\"ludwig\", \"ludwig\")], False),\n        (\"multiple_installed\", [(\"ludwig\", \"ludwig\"), (\"marshmallow\", \"marshmallow\")], False),\n        (\"not_installed\", [(\"fake_dependency\", \"fake_dependency\")], True),\n        (\"mixed_installed\", [(\"fake_dependency\", \"fake_dependency\"), (\"ludwig\", \"ludwig\")], True),\n    ]\n)\ndef dependency_check_config(request):\n    key, deps, raises_exception = request.param\n\n    @register_scheduler_config(key, dependencies=deps)\n    @ludwig_dataclass\n    class DependencyCheckConfig(BaseSchedulerConfig):\n        type: str = ProtectedString(key)\n\n    yield DependencyCheckConfig(), raises_exception\n    del scheduler_config_registry[key]\n\n\ndef test_dependency_check(dependency_check_config):\n    \"\"\"Test that the hyperopt scheduler dependency check properly identifies missing dependencies.\n\n    Some schedulers supported by Ray Tune have additional dependencies that may not be installed. The schema records\n    these dependencies and can be used to verify they are installed at run time.\n    \"\"\"\n    config, raises_exception = dependency_check_config\n    if raises_exception:\n        with pytest.raises(ImportError):\n            config.dependencies_installed()\n    else:\n        assert config.dependencies_installed()\n"
  },
  {
    "path": "tests/ludwig/schema/hyperopt/test_search_algorithm.py",
    "content": "import pytest\n\nfrom ludwig.schema.hyperopt.search_algorithm import BaseSearchAlgorithmConfig\nfrom ludwig.schema.hyperopt.utils import register_search_algorithm_config, search_algorithm_config_registry\nfrom ludwig.schema.utils import ludwig_dataclass, ProtectedString\n\n\n@pytest.fixture(\n    params=[  # Tuples of SA name, dependency list, whether it should raise an exception\n        (\"no_deps\", None, False),\n        (\"installed\", [(\"ludwig\", \"ludwig\")], False),\n        (\"multiple_installed\", [(\"ludwig\", \"ludwig\"), (\"marshmallow\", \"marshmallow\")], False),\n        (\"not_installed\", [(\"fake_dependency\", \"fake_dependency\")], True),\n        (\"mixed_installed\", [(\"fake_dependency\", \"fake_dependency\"), (\"ludwig\", \"ludwig\")], True),\n    ]\n)\ndef dependency_check_config(request):\n    key, deps, raises_exception = request.param\n\n    @register_search_algorithm_config(key, dependencies=deps)\n    @ludwig_dataclass\n    class DependencyCheckConfig(BaseSearchAlgorithmConfig):\n        type: str = ProtectedString(key)\n\n    yield DependencyCheckConfig(), raises_exception\n    del search_algorithm_config_registry[key]\n\n\ndef test_dependency_check(dependency_check_config):\n    \"\"\"Test that the hyperopt search alg dependency check properly identifies missing dependencies.\n\n    Most search algorithms supported by Ray Tune have additional dependencies that may not be installed. The schema\n    records these dependencies and can be used to verify they are installed at run time.\n    \"\"\"\n    config, raises_exception = dependency_check_config\n    if raises_exception:\n        with pytest.raises(ImportError):\n            config.dependencies_installed()\n    else:\n        assert config.dependencies_installed()\n"
  },
  {
    "path": "tests/ludwig/schema/test_model_config.py",
    "content": "import os\nfrom tempfile import TemporaryDirectory\nfrom typing import Any\n\nimport pytest\nimport yaml\n\nfrom ludwig.constants import (\n    ACTIVE,\n    BASE_MODEL,\n    CLIP,\n    COLUMN,\n    COMBINER,\n    DECODER,\n    DEFAULT_VALIDATION_METRIC,\n    DEFAULTS,\n    DEPENDENCIES,\n    ENCODER,\n    HYPEROPT,\n    INPUT_FEATURES,\n    INPUT_SIZE,\n    LOSS,\n    MODEL_ECD,\n    MODEL_LLM,\n    MODEL_TYPE,\n    NAME,\n    NUM_CLASSES,\n    OPTIMIZER,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    PROC_COLUMN,\n    REDUCE_DEPENDENCIES,\n    REDUCE_INPUT,\n    TIED,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.error import ConfigValidationError\nfrom ludwig.schema.decoders.base import ClassifierConfig\nfrom ludwig.schema.encoders.text_encoders import BERTConfig\nfrom ludwig.schema.features.augmentation.image import RandomBlurConfig, RandomRotateConfig\nfrom ludwig.schema.features.image_feature import AUGMENTATION_DEFAULT_OPERATIONS\nfrom ludwig.schema.features.number_feature import NumberOutputFeatureConfig\nfrom ludwig.schema.features.text_feature import TextOutputFeatureConfig\nfrom ludwig.schema.llms.quantization import QuantizationConfig\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.schema.utils import BaseMarshmallowConfig, convert_submodules\n\nconfig_sections = {INPUT_FEATURES, OUTPUT_FEATURES, PREPROCESSING, TRAINER, COMBINER, DEFAULTS, HYPEROPT}\n\n\ndef test_config_object():\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"text_feature\",\n                \"type\": \"text\",\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"drop_row\",\n                },\n                \"encoder\": {\n                    \"type\": \"rnn\",\n                    \"bidirectional\": True,\n                    \"representation\": \"dense\",\n                    \"num_layers\": 2,\n                },\n            },\n            {\n                \"name\": \"image_feature_1\",\n                \"type\": \"image\",\n                \"preprocessing\": {\n                    \"height\": 32,\n                    \"width\": 32,\n                    \"num_channels\": 4,\n                },\n                \"encoder\": {\n                    \"type\": \"stacked_cnn\",\n                    \"num_channels\": 4,\n                    \"dropout\": 0.1,\n                },\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"category_feature\",\n                \"type\": \"category\",\n                \"top_k\": 3,\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"bfill\",\n                },\n                \"decoder\": {\n                    \"type\": \"classifier\",\n                    \"num_classes\": 10,\n                    \"use_bias\": False,\n                },\n            },\n        ],\n        \"combiner\": {\n            \"type\": \"concat\",\n            \"output_size\": 512,\n            \"weights_initializer\": \"xavier_uniform\",\n            \"dropout\": 0.2,\n        },\n        \"trainer\": {\n            \"epochs\": 50,\n            \"batch_size\": \"auto\",\n            \"optimizer\": {\n                \"type\": \"adam\",\n                \"betas\": [0.8, 0.999],\n                \"eps\": 5e-09,\n            },\n        },\n    }\n\n    config_object = ModelConfig.from_dict(config)\n    assert config_object.input_features.text_feature.encoder.type == \"rnn\"\n    assert config_object.input_features.text_feature.encoder.num_layers == 2\n    assert config_object.input_features.text_feature.preprocessing.missing_value_strategy == \"drop_row\"\n\n    assert config_object.defaults.text.encoder.type != \"rnn\"\n    assert config_object.defaults.text.preprocessing.missing_value_strategy != \"drop_row\"\n\n    assert config_object.output_features.category_feature.decoder.num_classes == 10\n    assert config_object.output_features.category_feature.top_k == 3\n\n    assert config_object.combiner.output_size == 512\n    assert config_object.combiner.weights_initializer == \"xavier_uniform\"\n    assert config_object.combiner.fc_layers is None\n\n    assert config_object.trainer.epochs == 50\n    assert config_object.trainer.batch_size == \"auto\"\n\n    assert config_object.trainer.optimizer.type == \"adam\"\n    assert config_object.trainer.optimizer.betas[0] == 0.8\n    assert config_object.trainer.optimizer.betas[1] == 0.999\n    assert config_object.trainer.optimizer.eps == 5e-09\n\n\ndef test_config_object_defaults():\n    config = {\n        \"input_features\": [\n            {\"name\": \"number_feature\", \"type\": \"number\"},\n            {\n                \"name\": \"text_feature_1\",\n                \"type\": \"text\",\n                \"encoder\": {\n                    \"type\": \"rnn\",\n                    \"activation\": \"sigmoid\",\n                },\n            },\n            {\n                \"name\": \"text_feature_2\",\n                \"type\": \"text\",\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n        \"defaults\": {\n            \"number\": {\"preprocessing\": {\"missing_value_strategy\": \"drop_row\"}, \"encoder\": {\"type\": \"dense\"}},\n            \"text\": {\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"drop_row\",\n                },\n                \"encoder\": {\n                    \"type\": \"stacked_parallel_cnn\",\n                    \"activation\": \"tanh\",\n                },\n            },\n        },\n    }\n\n    config_object = ModelConfig.from_dict(config)\n    assert config_object.input_features.number_feature.preprocessing.missing_value_strategy == \"drop_row\"\n    assert config_object.input_features.number_feature.encoder.type == \"dense\"\n\n    assert config_object.input_features.text_feature_1.encoder.type == \"rnn\"\n    assert config_object.input_features.text_feature_1.encoder.activation == \"sigmoid\"\n    assert config_object.input_features.text_feature_1.preprocessing.missing_value_strategy == \"drop_row\"\n\n    assert config_object.input_features.text_feature_2.encoder.type == \"stacked_parallel_cnn\"\n    assert config_object.input_features.text_feature_2.encoder.activation == \"tanh\"\n    assert config_object.input_features.text_feature_2.preprocessing.missing_value_strategy == \"drop_row\"\n\n\ndef test_config_object_to_config_dict():\n    config = {\n        \"input_features\": [\n            {\"name\": \"number_feature\", \"type\": \"number\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_object = ModelConfig.from_dict(config)\n    config_dict = config_object.to_dict()\n\n    for section in config_sections:\n        assert section in config_dict\n    assert len(config_dict[DEFAULTS]) == 13\n    assert set(config_dict[INPUT_FEATURES][0].keys()) == {\n        NAME,\n        ACTIVE,\n        TYPE,\n        COLUMN,\n        PROC_COLUMN,\n        TIED,\n        PREPROCESSING,\n        ENCODER,\n    }\n    assert set(config_dict[OUTPUT_FEATURES][0].keys()) == {\n        NAME,\n        ACTIVE,\n        TYPE,\n        COLUMN,\n        PROC_COLUMN,\n        PREPROCESSING,\n        DECODER,\n        LOSS,\n        REDUCE_INPUT,\n        DEPENDENCIES,\n        INPUT_SIZE,\n        CLIP,\n        REDUCE_DEPENDENCIES,\n        NUM_CLASSES,\n        DEFAULT_VALIDATION_METRIC,\n    }\n\n\ndef test_update_config_object():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_object = ModelConfig.from_dict(config)\n\n    assert config_object.input_features.text_feature.encoder.type == \"parallel_cnn\"\n    assert config_object.input_features.text_feature.encoder.max_sequence_length is None\n\n    temp_config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\", \"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10}},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_object = ModelConfig.from_dict(temp_config)\n\n    assert config_object.input_features.text_feature.encoder.max_sequence_length == 10\n\n\n@pytest.mark.parametrize(\"model_type\", [MODEL_ECD])\ndef test_config_object_validation_parameters_defaults(model_type: str):\n    config = {\n        \"input_features\": [\n            {\"name\": \"category_feature\", \"type\": \"category\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n        \"model_type\": model_type,\n    }\n\n    config_object = ModelConfig.from_dict(config)\n\n    assert config_object.trainer.validation_field == \"number_output_feature\"\n    assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric\n\n\ndef test_config_object_validation_parameters_multiple_output_features():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"text_output_feature\",\n                \"type\": \"text\",\n            },\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_object = ModelConfig.from_dict(config)\n\n    assert config_object.trainer.validation_field == \"text_output_feature\"\n    assert config_object.trainer.validation_metric == TextOutputFeatureConfig.default_validation_metric\n\n    # swap features\n    tmp = config[\"output_features\"][0]\n    config[\"output_features\"][0] = config[\"output_features\"][1]\n    config[\"output_features\"][1] = tmp\n\n    config_object = ModelConfig.from_dict(config)\n\n    assert config_object.trainer.validation_field == \"number_output_feature\"\n    assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric\n\n\ndef test_config_object_validation_parameters_explicitly_set_validation_field():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"text_output_feature\",\n                \"type\": \"text\",\n            },\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n        \"trainer\": {\n            \"validation_field\": \"combined\",\n        },\n    }\n\n    config_object = ModelConfig.from_dict(config)\n\n    assert config_object.trainer.validation_field == \"combined\"\n    assert config_object.trainer.validation_metric == \"loss\"\n\n\ndef test_config_object_validation_parameters_explicitly_set_validation_metric():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"text_output_feature\",\n                \"type\": \"text\",\n            },\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n        \"trainer\": {\n            \"validation_metric\": NumberOutputFeatureConfig.default_validation_metric,\n        },\n    }\n\n    config_object = ModelConfig.from_dict(config)\n\n    # We find the output feature that the validation_metric corresponds to.\n    assert config_object.trainer.validation_field == \"number_output_feature\"\n    assert config_object.trainer.validation_metric == NumberOutputFeatureConfig.default_validation_metric\n\n\ndef test_config_object_validation_parameters_invalid_metric():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"text_output_feature\",\n                \"type\": \"text\",\n            },\n        ],\n        \"trainer\": {\n            \"validation_metric\": NumberOutputFeatureConfig.default_validation_metric,\n        },\n    }\n\n    with pytest.raises(Exception):\n        ModelConfig.from_dict(config)\n\n\ndef test_config_object_validation_parameters_metric_conflict():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature1\",\n                \"type\": \"number\",\n            },\n            {\n                \"name\": \"number_output_feature2\",\n                \"type\": \"number\",\n            },\n        ],\n        \"trainer\": {\n            \"validation_metric\": NumberOutputFeatureConfig.default_validation_metric,\n        },\n    }\n\n    with pytest.raises(Exception):\n        ModelConfig.from_dict(config)\n\n\ndef test_constructors_yaml():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\", \"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10}},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    with TemporaryDirectory() as tmpdir:\n        file_path = os.path.join(tmpdir, \"test.yaml\")\n        with open(file_path, \"w\") as file:\n            yaml.dump(config, file)\n\n        config_obj = ModelConfig.from_yaml(file_path)\n\n    for section in config_sections:\n        assert hasattr(config_obj, section)\n\n\ndef test_constructors_dict():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\", \"encoder\": {\"type\": \"parallel_cnn\", \"max_sequence_length\": 10}},\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    for section in config_sections:\n        assert hasattr(config_obj, section)\n\n\ndef test_feature_enabling_disabling():\n    config = {\n        \"input_features\": [{\"name\": \"text_feature\", \"type\": \"text\"}, {\"name\": \"category_feature\", \"type\": \"number\"}],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.input_features.text_feature.active\n    assert config_obj.input_features.category_feature.active\n\n    config_obj.input_features.text_feature.disable()\n\n    assert not config_obj.input_features.text_feature.active\n\n\ndef test_sequence_combiner():\n    config = {\n        \"input_features\": [{\"name\": \"text_feature\", \"type\": \"text\"}],\n        \"output_features\": [{\"name\": \"number_output_feature\", \"type\": \"number\"}],\n        \"combiner\": {\"type\": \"sequence\", \"encoder\": {\"type\": \"rnn\"}},\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.combiner.type == \"sequence\"\n    assert config_obj.combiner.encoder.type == \"rnn\"\n    assert config_obj.combiner.encoder.cell_type == \"rnn\"\n\n\n@pytest.mark.parametrize(\n    \"session\",\n    [\n        {\"sess_id\": 0, \"encoder\": \"parallel_cnn\", \"loss\": {\"type\": \"mean_squared_error\"}},\n        {\"sess_id\": 1, \"encoder\": \"cnnrnn\", \"loss\": {\"type\": \"mean_absolute_error\"}},\n        {\"sess_id\": 2, \"encoder\": \"parallel_cnn\", \"loss\": {\"type\": \"mean_absolute_error\"}},\n    ],\n)\ndef test_shared_state(session):\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\", \"encoder\": {\"type\": session[\"encoder\"]}},\n            {\"name\": \"text_feature_2\", \"type\": \"text\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"number_output_feature\", \"type\": \"number\"},\n            {\"name\": \"category_feature\", \"type\": \"category\", \"preprocessing\": {\"missing_value_strategy\": \"bfill\"}},\n        ],\n        \"defaults\": {\"text\": {\"encoder\": {\"type\": session[\"encoder\"]}}},\n    }\n\n    if session[\"sess_id\"] == 1:\n        del config[OUTPUT_FEATURES][1][\"preprocessing\"]\n\n    if session[\"sess_id\"] == 2:\n        del config[INPUT_FEATURES][0][\"encoder\"]\n        del config[DEFAULTS]\n\n    config_obj = ModelConfig.from_dict(config)\n\n    if session[\"sess_id\"] == 0:\n        config_obj.input_features.text_feature.encoder.max_sequence_length = 10\n        config_obj.input_features.text_feature.tied = \"text_feature_2\"\n\n        assert config_obj.defaults.text.encoder.max_sequence_length is None  # Test no link w/ defaults config\n        assert config_obj.input_features.text_feature.tied == \"text_feature_2\"  # Test tied set as expected\n\n    if session[\"sess_id\"] == 1:\n        config_obj.output_features.number_output_feature.loss.weight = 2.0\n\n        # Test previous edits to config don't carry over\n        assert config_obj.output_features.category_feature.preprocessing.missing_value_strategy == \"drop_row\"\n        assert config_obj.defaults.text.encoder.max_sequence_length is None  # Test no link w/ previous encoder config\n        assert config_obj.input_features.text_feature.tied is None  # Test no link w/ previous text feature config\n        assert config_obj.output_features.number_output_feature.loss.weight == 2.0  # Test loss weight set as expected\n\n    if session[\"sess_id\"] == 2:\n        assert config_obj.input_features.text_feature.encoder.type == \"parallel_cnn\"\n        assert config_obj.output_features.number_output_feature.loss.weight == 1.0  # Test no link previous loss config\n        assert config_obj.defaults.text.encoder.max_sequence_length is None  # Test no link w/ first encoder config\n        assert config_obj.input_features.text_feature.tied is None  # Test no link w/ first tied setting\n\n\ndef test_convert_submodules():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [{\"name\": \"number_output_feature\", \"type\": \"number\"}],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n    trainer = convert_submodules(config_obj.trainer.__dict__)\n    input_features = config_obj.input_features.to_list()\n\n    assert not isinstance(trainer[OPTIMIZER], BaseMarshmallowConfig)\n    assert not isinstance(input_features[0][PREPROCESSING], BaseMarshmallowConfig)\n\n\ndef test_defaults_mixins():\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_feature\", \"type\": \"text\"},\n        ],\n        \"output_features\": [{\"name\": \"number_output_feature\", \"type\": \"number\"}],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.defaults.audio.to_dict().keys() == {ENCODER, PREPROCESSING}\n    assert config_obj.defaults.category.to_dict().keys() == {ENCODER, PREPROCESSING, DECODER, LOSS}\n\n\ndef test_initializer_recursion():\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"category_B9834\",\n                \"type\": \"category\",\n                \"encoder\": {\n                    \"type\": \"dense\",\n                    \"vocab_size\": 2,\n                    \"embedding_size\": 5,\n                },\n                \"reduce_input\": \"sum\",\n                \"column\": \"category_B9834\",\n                \"proc_column\": \"category_B9834_mZFLky\",\n            },\n            {\n                \"name\": \"number_0F633\",\n                \"type\": \"number\",\n                \"encoder\": {\n                    \"type\": \"dense\",\n                    \"norm\": \"batch\",\n                    \"norm_params\": {\"momentum\": 0.2},\n                },\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"binary_52912\",\n                \"type\": \"binary\",\n                \"column\": \"binary_52912\",\n                \"proc_column\": \"binary_52912_mZFLky\",\n            }\n        ],\n        \"combiner\": {\"type\": \"concat\", \"weights_initializer\": {\"type\": \"normal\", \"stddev\": 0}},\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert isinstance(config_obj.combiner.weights_initializer, dict)\n\n\ndef test_number_feature_zscore_preprocessing_default():\n    \"\"\"Tests that the default value for the number feature preprocessing is 'zscore'.\"\"\"\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"number_input_feature1\",\n                \"type\": \"number\",\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number_output_feature1\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.input_features.number_input_feature1.preprocessing.normalization == \"zscore\"\n\n\n@pytest.mark.parametrize(\n    \"augmentation,expected\",\n    [\n        (None, []),\n        (False, []),\n        (True, AUGMENTATION_DEFAULT_OPERATIONS),\n        (\n            [{\"type\": \"random_blur\"}, {\"type\": \"random_rotate\", \"degree\": 30}],\n            [RandomBlurConfig(), RandomRotateConfig(degree=30)],\n        ),\n    ],\n)\ndef test_augmentation_pipeline(augmentation, expected):\n    \"\"\"Tests that augmentation pipeline is correctly deserialized and serialized between config.\"\"\"\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"input1\",\n                \"type\": \"image\",\n                \"augmentation\": augmentation,\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"output1\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n\n    if augmentation is None:\n        del config[\"input_features\"][0][\"augmentation\"]\n\n    config_obj = ModelConfig.from_dict(config)\n    assert config_obj.input_features[0].augmentation == expected\n\n    # Test serialized dict form is fully rendered\n    config_dict = config_obj.to_dict()\n    assert len(config_dict[\"input_features\"][0][\"augmentation\"]) == len(expected)\n    for aug in config_dict[\"input_features\"][0][\"augmentation\"]:\n        assert isinstance(aug, dict)\n\n    # Test the serializing and reloading yields the same results\n    config_obj2 = ModelConfig.from_dict(config_dict)\n    assert config_obj2.input_features[0].augmentation == config_obj.input_features[0].augmentation\n\n\n@pytest.mark.parametrize(\n    \"sequence_length, max_sequence_length, max_sequence_length_expected\",\n    [\n        (None, 100, 100),\n        (50, 100, 100),\n        (100, 50, 100),\n    ],\n)\ndef test_preprocessing_max_sequence_length(sequence_length, max_sequence_length, max_sequence_length_expected):\n    config = {\n        \"input_features\": [\n            {\n                \"name\": \"text1\",\n                \"type\": \"text\",\n                \"preprocessing\": {\n                    \"sequence_length\": sequence_length,\n                    \"max_sequence_length\": max_sequence_length,\n                },\n            },\n            {\n                \"name\": \"sequence1\",\n                \"type\": \"sequence\",\n                \"preprocessing\": {\n                    \"sequence_length\": sequence_length,\n                    \"max_sequence_length\": max_sequence_length,\n                },\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"number1\",\n                \"type\": \"number\",\n            },\n        ],\n    }\n    config_obj = ModelConfig.from_dict(config)\n    assert config_obj.input_features[0].preprocessing.max_sequence_length == max_sequence_length_expected\n    assert config_obj.input_features[1].preprocessing.max_sequence_length == max_sequence_length_expected\n\n\ndef test_encoder_decoder_values_as_str():\n    \"\"\"Tests that encoder / decoder params provided as strings are properly converted to the correct type.\"\"\"\n    config = {\n        \"input_features\": [\n            {\"name\": \"text_input\", \"type\": \"text\", \"encoder\": \"bert\"},\n        ],\n        \"output_features\": [{\"name\": \"cat_output\", \"type\": \"category\", \"decoder\": \"classifier\"}],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert isinstance(config_obj.input_features[0].encoder, BERTConfig)\n    assert isinstance(config_obj.output_features[0].decoder, ClassifierConfig)\n\n\n@pytest.mark.parametrize(\n    \"base_model_config,model_name\",\n    [\n        (\"bloomz-3b\", \"bigscience/bloomz-3b\"),\n        (\"vicuna-7b\", \"lmsys/vicuna-7b-v1.3\"),\n        (\"huggyllama/llama-7b\", \"huggyllama/llama-7b\"),\n    ],\n)\ndef test_llm_base_model_config(base_model_config, model_name):\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: base_model_config,\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.base_model == model_name\n\n\n@pytest.mark.parametrize(\n    \"base_model_config\",\n    [\n        None,\n        \"invalid/model/name\",\n    ],\n)\ndef test_llm_base_model_config_error(base_model_config):\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: base_model_config,\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\n@pytest.mark.parametrize(\n    \"bits,expected_qconfig\",\n    [\n        (None, None),\n        (4, QuantizationConfig(bits=4)),\n        (8, QuantizationConfig(bits=8)),\n    ],\n)\ndef test_llm_quantization_config(bits: int | None, expected_qconfig: QuantizationConfig | None):\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"bigscience/bloomz-3b\",\n        \"quantization\": {\"bits\": bits},\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n    }\n\n    if bits is None:\n        del config[\"quantization\"]\n\n    config_obj = ModelConfig.from_dict(config)\n\n    assert config_obj.quantization == expected_qconfig\n\n\n@pytest.mark.parametrize(\n    \"rope_scaling_config\",\n    [\n        ({\"type\": \"linear\"}),\n        ({\"factor\": 2.0}),\n        ({\"type\": \"linear\", \"factor\": 1.0}),\n    ],\n)\ndef test_llm_rope_scaling_failure_modes(\n    rope_scaling_config: None | dict[str, Any],\n):\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n        \"model_parameters\": {\n            \"rope_scaling\": rope_scaling_config,\n        },\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n\ndef test_llm_model_parameters_no_rope_scaling():\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n        \"model_parameters\": {},\n    }\n\n    config_obj = ModelConfig.from_dict(config)\n    assert config_obj.model_parameters.rope_scaling is None\n    assert config_obj.model_parameters.to_dict() == {}\n\n\ndef test_llm_finetuning_output_feature_config():\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"category_output\", TYPE: \"category\"}],\n        \"trainer\": {\n            \"type\": \"finetune\",\n        },\n    }\n\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    config[OUTPUT_FEATURES] = [{NAME: \"text_output\", TYPE: \"text\"}]\n    ModelConfig.from_dict(config)\n\n\n@pytest.mark.skip(\n    reason=\"TODO(geoffrey, arnav): re-enable this when we have reconciled the config with the backend kwarg in api.py\"\n)\n@pytest.mark.distributed\ndef test_llm_quantization_backend_compatibility():\n    config = {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n        OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n        \"quantization\": {\"bits\": 4},\n    }\n\n    # Backend not set - defaults to local backend\n    ModelConfig.from_dict(config)\n\n    # Backend explicitly set to local backend\n    config[\"backend\"] = {\"type\": \"local\"}\n    ModelConfig.from_dict(config)\n\n    # Backend explicitly set to Ray backend\n    config[\"backend\"] = {\"type\": \"ray\"}\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    # Start local ray process\n    import ray\n\n    ray.init()\n\n    # Backend not set, but local Ray process is running\n    config.pop(\"backend\")\n    with pytest.raises(ConfigValidationError):\n        ModelConfig.from_dict(config)\n\n    ray.shutdown()\n\n\nclass TestMaxNewTokensOverride:\n    def test_max_new_tokens_override_no_changes_to_max_new_tokens(self):\n        \"\"\"Tests that the default value for max_new_tokens is respected when explicitly set in the config.\"\"\"\n        config = {\n            MODEL_TYPE: MODEL_LLM,\n            BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n            INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n            # Default value for generation.max_sequence_length is 32\n            OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n            \"generation\": {\"max_new_tokens\": 64},\n        }\n\n        config_obj = ModelConfig.from_dict(config)\n        assert config_obj.generation.max_new_tokens == 64\n\n    def test_max_new_tokens_override_large_max_sequence_length(self):\n        \"\"\"Tests that the default value for max_new_tokens is overridden when max_sequence_length is set to a large\n        value than the default max_new_tokens.\"\"\"\n        config = {\n            MODEL_TYPE: MODEL_LLM,\n            BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n            INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n            # Default value for generation.max_sequence_length is 32\n            OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\", \"preprocessing\": {\"max_sequence_length\": 100}}],\n        }\n\n        config_obj = ModelConfig.from_dict(config)\n        assert config_obj.generation.max_new_tokens == 100\n\n    def test_max_new_tokens_override_large_global_max_sequence_length(self):\n        \"\"\"Tests that the default value for max_new_tokens is overridden when global_max_sequence_length is set to\n        a larger value than the default max_new_tokens.\"\"\"\n        config = {\n            MODEL_TYPE: MODEL_LLM,\n            BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n            INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n            # Default value for generation.max_sequence_length is 32\n            OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n            PREPROCESSING: {\"global_max_sequence_length\": 100},\n        }\n\n        config_obj = ModelConfig.from_dict(config)\n        assert config_obj.generation.max_new_tokens == 100\n\n    def test_max_new_tokens_override_fallback_to_model_window_size(self):\n        config = {\n            MODEL_TYPE: MODEL_LLM,\n            BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n            INPUT_FEATURES: [{NAME: \"text_input\", TYPE: \"text\"}],\n            # Default value for generation.max_sequence_length is 32\n            OUTPUT_FEATURES: [{NAME: \"text_output\", TYPE: \"text\"}],\n        }\n\n        config_obj = ModelConfig.from_dict(config)\n        # Base model context length is 2048 tokens by default\n        # Since we fallback to setting max_new_tokens to the model context length / 2, we expect it to be 1024 tokens\n        assert config_obj.generation.max_new_tokens == 1024\n"
  },
  {
    "path": "tests/ludwig/schema/test_schema_utils.py",
    "content": "from ludwig.constants import TYPE\nfrom ludwig.schema import utils as schema_utils\n\n\ndef test_remove_duplicate_fields():\n    props = {TYPE: \"random\", \"probabilities\": [0.7, 0.1, 0.2]}\n    schema_utils.remove_duplicate_fields(props, [TYPE])\n    assert TYPE not in props\n    assert \"probabilities\" in props\n"
  },
  {
    "path": "tests/ludwig/schema_fields/test_fields_misc.py",
    "content": "import pytest\nfrom pydantic import ValidationError as PydanticValidationError\n\nfrom ludwig.config_validation.validation import get_validator, validate\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\ndef get_marshmallow_field_from_metadata(dfield):\n    \"\"\"Helper method for extracting the marshmallow field from pydantic field metadata.\"\"\"\n    metadata = dfield.metadata\n    if isinstance(metadata, dict):\n        return metadata.get(\"marshmallow_field\")\n    if isinstance(metadata, (list, tuple)):\n        for item in metadata:\n            if hasattr(item, \"_deserialize\"):\n                return item\n    return None\n\n\n# Simple marshmallow fields:\n\n\ndef test_StringOptions():\n    # Test case of default conflicting with allowed options:\n    test_options = [\"one\"]\n    with pytest.raises(AssertionError):\n        schema_utils.StringOptions(test_options, default=None, allow_none=False)\n\n    # Test creating a schema with simple option, null not allowed:\n    test_options = [\"one\"]\n\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: str = schema_utils.StringOptions(test_options, \"one\", allow_none=False)\n\n    with pytest.raises(PydanticValidationError):\n        CustomTestSchema.Schema().load({\"foo\": None})\n\n\n# Complex, custom marshmallow fields:\n\n\ndef test_Embed():\n    # Test simple schema creation:\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: str | int | None = schema_utils.Embed()\n\n    # Test null/empty loading cases:\n    assert CustomTestSchema.Schema().load({}).foo is None\n    assert CustomTestSchema.Schema().load({\"foo\": None}).foo is None\n\n    # Test valid strings/numbers:\n    assert CustomTestSchema.Schema().load({\"foo\": \"add\"}).foo == \"add\"\n    assert CustomTestSchema.Schema().load({\"foo\": 1}).foo == 1\n\n\ndef test_InitializerOrDict():\n    # Test default value validation:\n    with pytest.raises(Exception):\n        schema_utils.InitializerOrDict(\"test\")\n\n    # Test simple schema creation:\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: str | dict | None = schema_utils.InitializerOrDict()\n\n    # Test valid loads:\n    assert CustomTestSchema.Schema().load({}).foo == \"xavier_uniform\"\n    assert CustomTestSchema.Schema().load({\"foo\": \"zeros\"}).foo == \"zeros\"\n\n    # Test valid dict loads:\n    assert CustomTestSchema.Schema().load({\"foo\": {\"type\": \"zeros\"}}).foo == {\"type\": \"zeros\"}\n\n\ndef test_FloatRangeTupleDataclassField():\n    # Test dimensional mismatch:\n    with pytest.raises(Exception):\n        schema_utils.FloatRangeTupleDataclassField(n=3, default=(1, 1))\n\n    # Test default schema creation:\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: tuple[float, float] | None = schema_utils.FloatRangeTupleDataclassField(allow_none=True)\n\n    # Test empty load:\n    assert CustomTestSchema.Schema().load({}).foo == (0.9, 0.999)\n    assert CustomTestSchema.Schema().load({\"foo\": None}).foo is None\n\n    # Test valid loads:\n    assert CustomTestSchema.Schema().load({\"foo\": [0.5, 0.6]}).foo == (0.5, 0.6)\n\n    # Test non-default schema (N=3, other custom metadata):\n    @ludwig_dataclass\n    class CustomTestSchema2(schema_utils.BaseMarshmallowConfig):\n        foo: tuple[float, float, float] | None = schema_utils.FloatRangeTupleDataclassField(\n            n=3, default=(1, 1, 1), min=-10, max=10\n        )\n\n    assert CustomTestSchema2.Schema().load({}).foo == (1, 1, 1)\n    assert CustomTestSchema2.Schema().load({\"foo\": [2, 2, 2]}).foo == (2, 2, 2)\n\n\ndef test_OneOfOptionsField():\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: float | str = schema_utils.OneOfOptionsField(\n            default=0.1,\n            description=\"\",\n            allow_none=False,\n            field_options=[\n                schema_utils.FloatRange(default=0.001, min=0, max=1, allow_none=False),\n                schema_utils.StringOptions(options=[\"placeholder\"], default=\"placeholder\", allow_none=False),\n            ],\n        )\n\n    # Test valid loads:\n    assert CustomTestSchema.Schema().load({}).foo == 0.1\n    assert CustomTestSchema().foo == 0.1\n\n    # Reverse the order and allow none (via StringOptions):\n    @ludwig_dataclass\n    class CustomTestSchema2(schema_utils.BaseMarshmallowConfig):\n        foo: float | str | None = schema_utils.OneOfOptionsField(\n            default=\"placeholder\",\n            description=\"\",\n            field_options=[\n                schema_utils.FloatRange(default=0.001, min=0, max=1, allow_none=False),\n                schema_utils.StringOptions(options=[\"placeholder\"], default=\"placeholder\", allow_none=False),\n            ],\n            allow_none=True,\n        )\n\n    # Test valid loads:\n    assert CustomTestSchema2.Schema().load({}).foo == \"placeholder\"\n    assert CustomTestSchema2.Schema().load({\"foo\": 0.1}).foo == 0.1\n    assert CustomTestSchema2().foo == \"placeholder\"\n    CustomTestSchema2.Schema().load({\"foo\": None})\n\n    # Test JSON schema generation:\n    json = schema_utils.unload_jsonschema_from_marshmallow_class(CustomTestSchema2)\n    assert \"foo\" in json[\"properties\"]\n\n\ndef test_OneOfOptionsField_allows_none():\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: float | str | None = schema_utils.OneOfOptionsField(\n            default=None,\n            allow_none=True,\n            description=\"\",\n            field_options=[\n                schema_utils.PositiveInteger(description=\"\", default=1, allow_none=False),\n                schema_utils.List(list_type=int, allow_none=False),\n            ],\n        )\n\n    json = schema_utils.unload_jsonschema_from_marshmallow_class(CustomTestSchema)\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"hello\": json,\n        },\n        \"definitions\": {},\n    }\n    validate(instance={\"hello\": {\"foo\": None}}, schema=schema, cls=get_validator())\n\n\ndef test_OneOfOptionsField_multiple_fields_allow_none():\n    # With pydantic, multiple fields allowing none is handled by union validation.\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: float | str | None = schema_utils.OneOfOptionsField(\n            default=None,\n            description=\"\",\n            field_options=[\n                schema_utils.PositiveInteger(description=\"\", default=1, allow_none=True),\n                schema_utils.List(list_type=int, allow_none=True),\n            ],\n        )\n\n    assert CustomTestSchema().foo is None\n\n\ndef test_OneOfOptionsField_allows_none_one_field_allows_none():\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: float | str | None = schema_utils.OneOfOptionsField(\n            default=None,\n            description=\"\",\n            field_options=[\n                schema_utils.PositiveInteger(description=\"\", default=1, allow_none=False),\n                schema_utils.List(list_type=int, allow_none=True),\n            ],\n        )\n\n    json = schema_utils.unload_jsonschema_from_marshmallow_class(CustomTestSchema)\n    schema = {\n        \"type\": \"object\",\n        \"properties\": {\n            \"hello\": json,\n        },\n        \"definitions\": {},\n    }\n    validate(instance={\"hello\": {\"foo\": None}}, schema=schema, cls=get_validator())\n"
  },
  {
    "path": "tests/ludwig/schema_fields/test_fields_optimization.py",
    "content": "#! /usr/bin/env python\n\nimport pytest\nfrom pydantic import ValidationError as PydanticValidationError\n\nimport ludwig.schema.optimizers as lso\nfrom ludwig.schema import utils as schema_utils\nfrom ludwig.schema.utils import ludwig_dataclass\n\n\ndef test_torch_description_pull():\n    example_empty_desc_prop = schema_utils.unload_jsonschema_from_marshmallow_class(lso.AdamOptimizerConfig)[\n        \"properties\"\n    ][\"eps\"]\n    assert (\n        isinstance(example_empty_desc_prop, dict)\n        and \"description\" in example_empty_desc_prop\n        and isinstance(example_empty_desc_prop[\"description\"], str)\n        and len(example_empty_desc_prop[\"description\"]) > 3\n    )\n\n\ndef test_OptimizerDataclassField():\n    # Test default case:\n    default_optimizer_field = lso.OptimizerDataclassField()\n    assert default_optimizer_field.default_factory is not None\n    assert default_optimizer_field.default_factory() == lso.AdamOptimizerConfig()\n\n    # Test normal cases:\n    optimizer_field = lso.OptimizerDataclassField(\"adamax\")\n    assert optimizer_field.default_factory is not None\n    assert optimizer_field.default_factory() == lso.AdamaxOptimizerConfig()\n\n    # Test invalid default case:\n    with pytest.raises(AttributeError):\n        lso.OptimizerDataclassField({})\n    with pytest.raises(KeyError):\n        lso.OptimizerDataclassField(\"test\")\n    with pytest.raises(AttributeError):\n        lso.OptimizerDataclassField(1)\n\n    # Test creating a schema with default options:\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: lso.BaseOptimizerConfig | None = lso.OptimizerDataclassField()\n\n    with pytest.raises((PydanticValidationError, Exception)):\n        CustomTestSchema.Schema().load({\"foo\": \"test\"})\n\n    assert CustomTestSchema.Schema().load({}).foo == lso.AdamOptimizerConfig()\n\n    # Test creating a schema with set default:\n    @ludwig_dataclass\n    class CustomTestSchema2(schema_utils.BaseMarshmallowConfig):\n        foo: lso.BaseOptimizerConfig | None = lso.OptimizerDataclassField(\"adamax\")\n\n    with pytest.raises((PydanticValidationError, Exception)):\n        CustomTestSchema2.Schema().load({\"foo\": \"test\"})\n\n    assert CustomTestSchema2.Schema().load(\n        {\"foo\": {\"type\": \"adamax\", \"betas\": (0.2, 0.2)}}\n    ).foo == lso.AdamaxOptimizerConfig(betas=(0.2, 0.2))\n\n\ndef test_ClipperDataclassField():\n    # Test default case:\n    default_clipper_field = lso.GradientClippingDataclassField(description=\"\", default={})\n    assert default_clipper_field.default_factory is not None\n    assert default_clipper_field.default_factory() == lso.GradientClippingConfig()\n\n    # Test normal cases:\n    clipper_field = lso.GradientClippingDataclassField(description=\"\", default={\"clipglobalnorm\": 0.1})\n    assert clipper_field.default_factory is not None\n    assert clipper_field.default_factory() == lso.GradientClippingConfig(clipglobalnorm=0.1)\n\n    clipper_field = lso.GradientClippingDataclassField(description=\"\", default={\"clipglobalnorm\": None})\n    assert clipper_field.default_factory is not None\n    assert clipper_field.default_factory() == lso.GradientClippingConfig(clipglobalnorm=None)\n\n    # Test invalid default case:\n    with pytest.raises(Exception):\n        lso.GradientClippingDataclassField(description=\"\", default=\"test\")\n    with pytest.raises(Exception):\n        lso.GradientClippingDataclassField(description=\"\", default=None)\n    with pytest.raises(Exception):\n        lso.GradientClippingDataclassField(description=\"\", default=1)\n\n    # Test creating a schema with set default:\n    @ludwig_dataclass\n    class CustomTestSchema(schema_utils.BaseMarshmallowConfig):\n        foo: lso.GradientClippingConfig | None = lso.GradientClippingDataclassField(\n            description=\"\", default={\"clipglobalnorm\": 0.1}\n        )\n\n    with pytest.raises((PydanticValidationError, Exception)):\n        CustomTestSchema.Schema().load({\"foo\": \"test\"})\n\n    assert CustomTestSchema.Schema().load({}).foo == lso.GradientClippingConfig(clipglobalnorm=0.1)\n    assert CustomTestSchema.Schema().load({\"foo\": {\"clipglobalnorm\": 1}}).foo == lso.GradientClippingConfig(\n        clipglobalnorm=1\n    )\n"
  },
  {
    "path": "tests/ludwig/schema_fields/test_fields_preprocessing.py",
    "content": "#! /usr/bin/env python\n\n\nfrom ludwig.schema.features.preprocessing.binary import BinaryPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.category import CategoryPreprocessingConfig\nfrom ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField\n\n\ndef get_marshmallow_from_dataclass_field(dfield):\n    \"\"\"Helper method for checking marshmallow metadata succinctly.\"\"\"\n    return dfield.metadata[\"marshmallow_field\"]\n\n\ndef test_preprocessing_dataclass_field():\n    binary_preproc_dataclass = PreprocessingDataclassField(\"binary\")\n    assert binary_preproc_dataclass.default_factory is not None\n    assert get_marshmallow_from_dataclass_field(binary_preproc_dataclass).allow_none is False\n    assert binary_preproc_dataclass.default_factory() == BinaryPreprocessingConfig()\n\n    category_preproc_dataclass = PreprocessingDataclassField(\"category\")\n    assert category_preproc_dataclass.default_factory is not None\n    assert get_marshmallow_from_dataclass_field(category_preproc_dataclass).allow_none is False\n    assert category_preproc_dataclass.default_factory() == CategoryPreprocessingConfig()\n"
  },
  {
    "path": "tests/ludwig/schema_fields/test_marshmallow_misc.py",
    "content": "import pytest\n\nimport ludwig.combiners.combiners as lcc\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.schema.utils import (\n    assert_is_a_marshmallow_class,\n    BaseMarshmallowConfig,\n    load_config_with_kwargs,\n    ludwig_dataclass,\n)\n\n\n@ludwig_dataclass\nclass CustomTestSchema(BaseMarshmallowConfig):\n    \"\"\"Sample docstring.\"\"\"\n\n    foo: int = 5\n    \"foo (default: 5)\"\n\n\ndef test_assert_is_a_marshmallow_clas():\n    assert_is_a_marshmallow_class(ECDTrainerConfig)\n    with pytest.raises(AssertionError, match=r\"Expected.*config class\"):\n        assert_is_a_marshmallow_class(lcc.ConcatCombiner)\n\n\ndef test_load_config_with_kwargs():\n    test_kwargs = {\n        \"foo\": 6,\n        \"bar\": 6,\n    }\n    initialized_class, leftover = load_config_with_kwargs(CustomTestSchema, test_kwargs)\n\n    assert initialized_class.foo == 6\n    assert leftover == {\"bar\": 6}\n\n    # TransformerCombiner has no required/non-default arguments:\n    initialized_class, leftover = load_config_with_kwargs(lcc.TransformerCombinerConfig, test_kwargs)\n    assert initialized_class.bias_initializer == \"zeros\"\n    assert leftover == test_kwargs\n    initialized_class, leftover = load_config_with_kwargs(lcc.TransformerCombinerConfig, {})\n    assert leftover == {}\n"
  },
  {
    "path": "tests/ludwig/utils/__init__.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n"
  },
  {
    "path": "tests/ludwig/utils/automl/test_type_inference.py",
    "content": "import random\n\nimport pytest\n\nfrom ludwig.constants import AUDIO, BINARY, CATEGORY, DATE, IMAGE, NUMBER, TEXT\nfrom ludwig.data.dataset_synthesizer import generate_string\nfrom ludwig.utils.automl.field_info import FieldInfo\nfrom ludwig.utils.automl.type_inference import infer_type, should_exclude\n\nROW_COUNT = 100\nTARGET_NAME = \"target\"\n\n\n@pytest.mark.parametrize(\n    \"num_distinct_values,distinct_values,img_values,audio_values,avg_words,missing_vals,expected\",\n    [\n        # Random numbers.\n        (ROW_COUNT, [str(random.random()) for _ in range(ROW_COUNT)], 0, 0, None, 0.0, NUMBER),\n        # Random numbers with NaNs.\n        (ROW_COUNT, [str(random.random()) for _ in range(ROW_COUNT - 1)] + [\"NaN\"], 0, 0, None, 0.0, NUMBER),\n        # Finite list of numbers.\n        (10, [\"0\", \"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\", \"9\"], 0, 0, None, 0.0, CATEGORY),\n        (2, [\"1.5\", \"3.7\"], 0, 0, None, 0.1, NUMBER),\n        (2, [\"1.5\", \"3.7\", \"nan\"], 0, 0, None, 0.1, NUMBER),\n        # Bool-like values.\n        (2, [\"0\", \"1\"], 0, 0, None, 0.0, BINARY),\n        # Mostly bool-like values.\n        (3, [\"0\", \"1\", \"True\"], 0, 0, None, 0.0, CATEGORY),\n        # Non-conventional booleans are treated as categories since we cannot infer true/false labels.\n        pytest.param(2, [\"<=50K\", \">50K\"], 0, 0, None, 0.0, CATEGORY, id=\"non-conventional-bools\"),\n        # Finite list of strings.\n        (2, [\"human\", \"bot\"], 0, 0, None, 0.0, CATEGORY),\n        (10, [generate_string(5) for _ in range(10)], 0, 0, None, 0.0, CATEGORY),\n        (40, [generate_string(5) for _ in range(40)], 0, 0, None, 0.0, CATEGORY),\n        # Mostly random strings.\n        (90, [generate_string(5) for _ in range(90)], 0, 0, None, 0.0, TEXT),\n        # Mostly random strings with capped distinct values.\n        (90, [generate_string(5) for _ in range(10)], 0, 0, None, 0.0, TEXT),\n        # All random strings.\n        (ROW_COUNT, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, None, 0.0, TEXT),\n        # Images.\n        (ROW_COUNT, [], ROW_COUNT, 0, None, 0.0, IMAGE),\n        # Audio.\n        (ROW_COUNT, [], 0, ROW_COUNT, None, 0.0, AUDIO),\n        # Text with low distinct value percent / high missing value percent\n        (ROW_COUNT // 4, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, 5, 0.75, TEXT),\n        (ROW_COUNT // 4, [generate_string(5) for _ in range(ROW_COUNT)], 0, 0, 5, 0.25, CATEGORY),\n    ],\n)\ndef test_infer_type(num_distinct_values, distinct_values, img_values, audio_values, avg_words, missing_vals, expected):\n    field = FieldInfo(\n        name=\"foo\",\n        dtype=\"object\",\n        num_distinct_values=num_distinct_values,\n        distinct_values=distinct_values,\n        image_values=img_values,\n        audio_values=audio_values,\n        avg_words=avg_words,\n    )\n    assert infer_type(field, missing_vals, ROW_COUNT) == expected\n\n\ndef test_infer_type_explicit_date():\n    field = FieldInfo(\n        name=\"foo\",\n        distinct_values=[\"1\", \"2\"],\n        num_distinct_values=2,\n        dtype=DATE,\n    )\n    assert infer_type(field, 0, ROW_COUNT) == DATE\n\n\n@pytest.mark.parametrize(\n    \"idx,num_distinct_values,dtype,name,expected\",\n    [\n        (3, ROW_COUNT, NUMBER, \"id\", True),\n        (0, ROW_COUNT, NUMBER, \"index\", True),\n        (1, ROW_COUNT, NUMBER, \"index\", False),\n        (0, ROW_COUNT, NUMBER, \"foo\", False),\n        (3, ROW_COUNT, TEXT, \"uuid\", True),\n        (0, ROW_COUNT, TEXT, \"name\", False),\n        (0, ROW_COUNT, NUMBER, TARGET_NAME, False),\n        (0, ROW_COUNT - 1, NUMBER, \"id\", False),\n        (0, 0, CATEGORY, \"empty_col\", True),\n    ],\n)\ndef test_should_exclude(idx, num_distinct_values, dtype, name, expected):\n    column_count = 10\n    field = FieldInfo(name=name, dtype=dtype, num_distinct_values=num_distinct_values, avg_words=10)\n    assert should_exclude(idx, field, dtype, column_count, ROW_COUNT, {TARGET_NAME}) == expected\n\n\ndef test_auto_type_inference_single_value_binary_feature():\n    field = FieldInfo(\n        name=\"foo\", dtype=\"object\", num_distinct_values=1, distinct_values=[\"1\" for i in range(ROW_COUNT)]\n    )\n    assert infer_type(field=field, missing_value_percent=0, row_count=ROW_COUNT) == CATEGORY\n    assert should_exclude(\n        idx=3, field=field, dtype=\"object\", column_count=10, row_count=ROW_COUNT, targets={TARGET_NAME}\n    )\n\n\n@pytest.mark.parametrize(\n    \"column_count,avg_words,expected\",\n    [\n        (1, 10, False),\n        (1, 2, False),\n        (5, 2, True),\n        (5, 10, False),\n    ],\n)\ndef test_should_exclude_text(column_count, avg_words, expected):\n    field = FieldInfo(name=\"sentence\", dtype=TEXT, avg_words=avg_words, num_distinct_values=ROW_COUNT)\n    assert should_exclude(0, field, TEXT, column_count, ROW_COUNT, {TARGET_NAME}) == expected\n\n\n@pytest.mark.parametrize(\"negative_class\", (\"-1\", \"-1.0\"), ids=[\"-1\", \"-1.0\"])\ndef test_type_inference_with_negative_positive_binary_values(negative_class):\n    \"\"\"This test ensures that we infer binary type for a feature with negative and positive values, specifically -1\n    and 1.\"\"\"\n    field = FieldInfo(\n        name=\"foo\",\n        dtype=\"object\",\n        num_distinct_values=2,\n        distinct_values=[\"1\", negative_class],\n    )\n    assert infer_type(field=field, missing_value_percent=0, row_count=ROW_COUNT) == BINARY\n"
  },
  {
    "path": "tests/ludwig/utils/automl/test_utils.py",
    "content": "import pandas as pd\nimport pytest\n\nfrom ludwig.utils.automl.utils import avg_num_tokens\n\n\n@pytest.mark.parametrize(\n    \"field,expected\",\n    [\n        (pd.Series([None]), 0),\n        (pd.Series([\"string1\", \"string2\", \"string3\"]), 1),\n        (pd.Series([b\"string1\", b\"string2\", b\"string3\"]), 1),\n        (pd.Series([b\"string1 string1\", b\"string2 string2\", b\"string3 string3\"]), 2),\n        (pd.Series([1, 2, 3]), 1),\n    ],\n)\ndef test_avg_num_tokens(field, expected):\n    assert avg_num_tokens(field) == expected\n"
  },
  {
    "path": "tests/ludwig/utils/entmax/test_losses.py",
    "content": "from functools import partial\n\nimport pytest\nimport torch\nfrom torch.autograd import gradcheck\n\nfrom ludwig.constants import IGNORE_INDEX_TOKEN_ID\nfrom ludwig.utils.entmax.losses import Entmax15Loss, EntmaxBisectLoss, SparsemaxBisectLoss, SparsemaxLoss\n\n# make data\nXs = [torch.randn(4, 10, dtype=torch.float64, requires_grad=True) for _ in range(5)]\n\nys = [torch.max(torch.randn_like(X), dim=1)[1] for X in Xs]\n\n\nlosses = [\n    SparsemaxLoss,\n    partial(SparsemaxLoss, k=5),\n    Entmax15Loss,\n    partial(Entmax15Loss, k=5),\n    SparsemaxBisectLoss,\n    EntmaxBisectLoss,\n]\n\n\n@pytest.mark.parametrize(\"Loss\", losses)\ndef test_non_neg(Loss):\n    for X, y in zip(Xs, ys):\n        ls = Loss(reduction=\"none\")\n        lval = ls(X, y)\n        assert torch.all(lval >= 0)\n\n\n@pytest.mark.parametrize(\"Loss\", losses)\n@pytest.mark.parametrize(\"ignore_index\", (False, True))\n@pytest.mark.parametrize(\"reduction\", (\"sum\", \"elementwise_mean\"))\ndef test_loss(Loss, ignore_index, reduction):\n    for X, y in zip(Xs, ys):\n        iix = y[0] if ignore_index else -100\n        ls = Loss(ignore_index=iix, reduction=reduction)\n        gradcheck(ls, (X, y), eps=1e-5)\n\n\n@pytest.mark.parametrize(\"Loss\", losses)\ndef test_index_ignored(Loss):\n    x = torch.randn(20, 6, dtype=torch.float64, requires_grad=True)\n    _, y = torch.max(torch.randn_like(x), dim=1)\n\n    loss_ignore = Loss(reduction=\"sum\", ignore_index=y[0])\n    loss_noignore = Loss(reduction=\"sum\", ignore_index=IGNORE_INDEX_TOKEN_ID)\n\n    # Note: since these are sparse losses, it is possible that an element makes no contribution to the loss.\n    assert loss_ignore(x, y) <= loss_noignore(x, y)\n"
  },
  {
    "path": "tests/ludwig/utils/entmax/test_mask.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.utils.entmax.activations import Entmax15, Sparsemax\nfrom ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect\n\nfuncs = [\n    Sparsemax(dim=1),\n    Entmax15(dim=1),\n    Sparsemax(dim=1, k=512),\n    Entmax15(dim=1, k=512),\n    sparsemax_bisect,\n    entmax_bisect,\n]\n\n\n@pytest.mark.parametrize(\"func\", funcs)\n@pytest.mark.parametrize(\"dtype\", (torch.float32, torch.float64))\ndef test_mask(func, dtype):\n    torch.manual_seed(42)\n    x = torch.randn(2, 6, dtype=dtype)\n    x[:, 3:] = -float(\"inf\")\n    x0 = x[:, :3]\n\n    y = func(x)\n    y0 = func(x0)\n\n    y[:, :3] -= y0\n\n    assert torch.allclose(y, torch.zeros_like(y))\n\n\n@pytest.mark.parametrize(\"alpha\", (1.25, 1.5, 1.75, 2.25))\ndef test_mask_alphas(alpha):\n    torch.manual_seed(42)\n    x = torch.randn(2, 6)\n    x[:, 3:] = -float(\"inf\")\n    x0 = x[:, :3]\n\n    y = entmax_bisect(x, alpha)\n    y0 = entmax_bisect(x0, alpha)\n\n    y[:, :3] -= y0\n\n    assert torch.allclose(y, torch.zeros_like(y))\n"
  },
  {
    "path": "tests/ludwig/utils/entmax/test_root_finding.py",
    "content": "from functools import partial\nfrom itertools import product\n\nimport pytest\nimport torch\nfrom torch.autograd import gradcheck\n\nfrom ludwig.utils.entmax.activations import entmax15, sparsemax\nfrom ludwig.utils.entmax.root_finding import entmax_bisect, sparsemax_bisect\n\n# @pytest.mark.parametrize(\"dim\", (0, 1, 2))\n# def test_dim(dim, Map):\n# for _ in range(10):\n# x = torch.randn(5, 6, 7, requires_grad=True, dtype=torch.float64)\n# # gradcheck(f, (x,))\n\n\n@pytest.mark.parametrize(\"training\", [True, False])\n@pytest.mark.parametrize(\"bisect_training\", [True, False])\ndef test_sparsemax(training, bisect_training):\n    x = 0.5 * torch.randn(4, 6, dtype=torch.float32)\n    p1 = sparsemax(x, 1, training=training)\n    p2 = sparsemax_bisect(x, training=bisect_training)\n    assert torch.sum((p1 - p2) ** 2) < 1e-7\n\n\n@pytest.mark.parametrize(\"training\", [True, False])\n@pytest.mark.parametrize(\"bisect_training\", [True, False])\ndef test_entmax15(training, bisect_training):\n    x = 0.5 * torch.randn(4, 6, dtype=torch.float32)\n    p1 = entmax15(x, 1, training=training)\n    p2 = entmax_bisect(x, alpha=1.5, training=bisect_training)\n    assert torch.sum((p1 - p2) ** 2) < 1e-7\n\n\ndef test_sparsemax_grad():\n    x = torch.randn(4, 6, dtype=torch.float64, requires_grad=True)\n    gradcheck(sparsemax_bisect, (x,), eps=1e-5)\n\n\n@pytest.mark.parametrize(\"alpha\", (0.2, 0.5, 0.75, 1.2, 1.5, 1.75, 2.25))\ndef test_entmax_grad(alpha):\n    alpha = torch.tensor(alpha, dtype=torch.float64, requires_grad=True)\n    x = torch.randn(4, 6, dtype=torch.float64, requires_grad=True)\n    gradcheck(entmax_bisect, (x, alpha), eps=1e-5)\n\n\ndef test_entmax_correct_multiple_alphas():\n    n = 4\n    x = torch.randn(n, 6, dtype=torch.float64, requires_grad=True)\n    alpha = 0.05 + 2.5 * torch.rand((n, 1), dtype=torch.float64, requires_grad=True)\n\n    p1 = entmax_bisect(x, alpha)\n    p2_ = [entmax_bisect(x[i].unsqueeze(0), alpha[i].item()).squeeze() for i in range(n)]\n    p2 = torch.stack(p2_)\n\n    assert torch.allclose(p1, p2)\n\n\ndef test_entmax_grad_multiple_alphas():\n    n = 4\n    x = torch.randn(n, 6, dtype=torch.float64, requires_grad=True)\n    alpha = 0.05 + 2.5 * torch.rand((n, 1), dtype=torch.float64, requires_grad=True)\n    gradcheck(entmax_bisect, (x, alpha), eps=1e-5)\n\n\n@pytest.mark.parametrize(\"dim\", (0, 1, 2, 3))\ndef test_arbitrary_dimension(dim):\n    shape = [3, 4, 2, 5]\n    X = torch.randn(*shape, dtype=torch.float64)\n\n    alpha_shape = shape\n    alpha_shape[dim] = 1\n\n    alphas = 0.05 + 2.5 * torch.rand(alpha_shape, dtype=torch.float64)\n\n    P = entmax_bisect(X, alpha=alphas, dim=dim)\n\n    ranges = [list(range(k)) if i != dim else [slice(None)] for i, k in enumerate(shape)]\n\n    for ix in product(*ranges):\n        x = X[ix].unsqueeze(0)\n        alpha = alphas[ix].item()\n        p_true = entmax_bisect(x, alpha=alpha, dim=-1)\n        assert torch.allclose(P[ix], p_true)\n\n\n@pytest.mark.parametrize(\"dim\", (0, 1, 2, 3))\ndef test_arbitrary_dimension_grad(dim):\n    shape = [3, 4, 2, 5]\n\n    alpha_shape = shape\n    alpha_shape[dim] = 1\n\n    f = partial(entmax_bisect, dim=dim)\n\n    X = torch.randn(*shape, dtype=torch.float64, requires_grad=True)\n    alphas = 0.05 + 2.5 * torch.rand(alpha_shape, dtype=torch.float64, requires_grad=True)\n    gradcheck(f, (X, alphas), eps=1e-5)\n"
  },
  {
    "path": "tests/ludwig/utils/entmax/test_topk.py",
    "content": "import pytest\nimport torch\nfrom torch.autograd import gradcheck\n\nfrom ludwig.utils.entmax.activations import (\n    _entmax_threshold_and_support,\n    _sparsemax_threshold_and_support,\n    Entmax15,\n    Sparsemax,\n)\n\n\n@pytest.mark.parametrize(\"dim\", (0, 1, 2))\n@pytest.mark.parametrize(\"Map\", (Sparsemax, Entmax15))\ndef test_mapping(dim, Map):\n    f = Map(dim=dim, k=3)\n    x = torch.randn(3, 4, 5, requires_grad=True, dtype=torch.float64)\n    gradcheck(f, (x,))\n\n\n@pytest.mark.parametrize(\"dim\", (0, 1, 2))\n@pytest.mark.parametrize(\"coef\", (0.00001, 0.5, 10000))\ndef test_entmax_topk(dim, coef):\n    x = coef * torch.randn(3, 4, 5)\n    tau1, supp1 = _entmax_threshold_and_support(x, dim=dim, k=None)\n    tau2, supp2 = _entmax_threshold_and_support(x, dim=dim, k=5)\n\n    assert torch.all(tau1 == tau2)\n    assert torch.all(supp1 == supp2)\n\n\n@pytest.mark.parametrize(\"dim\", (0, 1, 2))\n@pytest.mark.parametrize(\"coef\", (0.00001, 0.5, 10000))\n@pytest.mark.parametrize(\"k\", (5, 30))\ndef test_sparsemax_topk(dim, coef, k):\n    x = coef * torch.randn(3, 4, 5)\n    tau1, supp1 = _sparsemax_threshold_and_support(x, dim=dim, k=None)\n    tau2, supp2 = _sparsemax_threshold_and_support(x, dim=dim, k=k)\n\n    assert torch.all(tau1 == tau2)\n    assert torch.all(supp1 == supp2)\n"
  },
  {
    "path": "tests/ludwig/utils/test_algorithm_utils.py",
    "content": "import pytest\n\nfrom ludwig.utils.algorithms_utils import topological_sort\n\n\n@pytest.mark.parametrize(\n    \"unsorted,sorted\",\n    [\n        (\n            [(2, []), (5, [11]), (11, [2, 9, 10]), (7, [11, 8]), (9, []), (10, []), (8, [9]), (3, [10, 8])],\n            [(2, []), (9, []), (10, []), (8, [9]), (3, [10, 8]), (11, [2, 9, 10]), (7, [11, 8]), (5, [11])],\n        ),\n        (\n            [(\"macro\", [\"action\", \"contact_type\"]), (\"contact_type\", None), (\"action\", [\"contact_type\"])],\n            [(\"contact_type\", []), (\"action\", [\"contact_type\"]), (\"macro\", [\"action\", \"contact_type\"])],\n        ),\n    ],\n)\ndef test_topological_sort(unsorted: list, sorted: list) -> None:\n    assert topological_sort(unsorted) == sorted\n"
  },
  {
    "path": "tests/ludwig/utils/test_audio_utils.py",
    "content": "import pytest\n\nfrom ludwig.utils.audio_utils import is_audio_score\n\n\n@pytest.mark.parametrize(\n    \"path, score\",\n    [\n        (\"data.wav\", 1),\n        (\"/home/peter/file.amb\", 1),\n        (\"my.mp3\", 1),\n        (\"data.ogg\", 1),\n        (\"data.vorbis\", 1),\n        (\"data.flac\", 1),\n        (\"data.opus\", 1),\n        (\"data.sphere\", 1),\n        (\"video.mp4\", 0),\n        (\"image.png\", 0),\n        (\".wav/image.png\", 0),\n    ],\n)\ndef test_is_audio_score(path: str, score: int):\n    assert is_audio_score(path) == score\n"
  },
  {
    "path": "tests/ludwig/utils/test_backward_compatibility.py",
    "content": "import copy\nimport math\nfrom typing import Any\n\nimport pytest\n\nfrom ludwig.constants import (\n    BATCH_SIZE,\n    BFILL,\n    CLASS_WEIGHTS,\n    DEFAULTS,\n    EVAL_BATCH_SIZE,\n    EXECUTOR,\n    HYPEROPT,\n    INPUT_FEATURES,\n    LEARNING_RATE_SCHEDULER,\n    LOSS,\n    NUMBER,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    SCHEDULER,\n    SPLIT,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.utils.backward_compatibility import (\n    _update_backend_cache_credentials,\n    _upgrade_encoder_decoder_params,\n    _upgrade_feature,\n    _upgrade_preprocessing_split,\n    upgrade_config_dict_to_latest_version,\n    upgrade_missing_value_strategy,\n    upgrade_model_progress,\n)\nfrom ludwig.utils.trainer_utils import TrainerMetric\n\n\ndef test_preprocessing_backward_compatibility():\n    # From v0.5.3.\n    preprocessing_config = {\n        \"force_split\": False,\n        \"split_probabilities\": [0.7, 0.1, 0.2],\n        \"stratify\": None,\n    }\n\n    _upgrade_preprocessing_split(preprocessing_config)\n\n    assert preprocessing_config == {\n        \"split\": {\"probabilities\": [0.7, 0.1, 0.2], \"type\": \"random\"},\n    }\n\n\ndef test_audio_feature_backward_compatibility():\n    # From v0.5.3.\n\n    audio_feature_preprocessing_config = {\n        \"name\": \"audio_feature\",\n        \"type\": \"audio\",\n        \"preprocessing\": {\n            \"audio_file_length_limit_in_s\": 7.5,\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"padding_value\": 0,\n            \"norm\": None,\n            \"audio_feature\": {\n                \"type\": \"fbank\",\n                \"window_length_in_s\": 0.04,\n                \"window_shift_in_s\": 0.02,\n                \"num_fft_points\": None,\n                \"window_type\": \"hamming\",\n                \"num_filter_bands\": 80,\n            },\n        },\n    }\n\n    global_preprocessing_config = {\n        \"audio\": {\n            \"audio_file_length_limit_in_s\": 7.5,\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"padding_value\": 0,\n            \"norm\": None,\n            \"audio_feature\": {\n                \"type\": \"fbank\",\n                \"window_length_in_s\": 0.04,\n                \"window_shift_in_s\": 0.02,\n                \"num_fft_points\": None,\n                \"window_type\": \"hamming\",\n                \"num_filter_bands\": 80,\n            },\n        },\n    }\n\n    _upgrade_feature(audio_feature_preprocessing_config)\n    _upgrade_preprocessing_split(global_preprocessing_config)\n\n    assert global_preprocessing_config == {\n        \"audio\": {\n            \"audio_file_length_limit_in_s\": 7.5,\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"padding_value\": 0,\n            \"norm\": None,\n            \"type\": \"fbank\",\n            \"window_length_in_s\": 0.04,\n            \"window_shift_in_s\": 0.02,\n            \"num_fft_points\": None,\n            \"window_type\": \"hamming\",\n            \"num_filter_bands\": 80,\n        }\n    }\n\n    assert audio_feature_preprocessing_config == {\n        \"name\": \"audio_feature\",\n        \"type\": \"audio\",\n        \"preprocessing\": {\n            \"audio_file_length_limit_in_s\": 7.5,\n            \"missing_value_strategy\": BFILL,\n            \"in_memory\": True,\n            \"padding_value\": 0,\n            \"norm\": None,\n            \"type\": \"fbank\",\n            \"window_length_in_s\": 0.04,\n            \"window_shift_in_s\": 0.02,\n            \"num_fft_points\": None,\n            \"window_type\": \"hamming\",\n            \"num_filter_bands\": 80,\n        },\n    }\n\n\ndef test_encoder_decoder_backwards_compatibility():\n    old_config = {\n        \"input_features\": [\n            {\n                \"name\": \"text_feature\",\n                \"type\": \"text\",\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"drop_row\",\n                },\n                \"encoder\": \"rnn\",\n                \"bidirectional\": True,\n                \"representation\": \"dense\",\n                \"num_layers\": 2,\n            },\n            {\n                \"name\": \"image_feature_1\",\n                \"type\": \"image\",\n                \"preprocessing\": {\n                    \"height\": 7.5,\n                    \"width\": 7.5,\n                    \"num_channels\": 4,\n                },\n                \"encoder\": \"resnet\",\n                \"num_channels\": 4,\n                \"dropout\": 0.1,\n                \"resnet_size\": 100,\n            },\n            {\n                \"name\": \"image_feature_2\",\n                \"type\": \"image\",\n                \"tied\": \"image_feature_1\",\n                \"preprocessing\": {\n                    \"height\": 7.5,\n                    \"width\": 7.5,\n                    \"num_channels\": 4,\n                },\n                \"encoder\": \"resnet\",\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"category_feature\",\n                \"type\": \"category\",\n                \"top_k\": 3,\n                \"preprocessing\": {\n                    \"missing_value_strategy\": BFILL,\n                },\n                \"decoder\": \"classifier\",\n                \"num_classes\": 10,\n                \"use_bias\": False,\n            },\n            {\n                \"name\": \"binary_feature\",\n                \"type\": \"binary\",\n                \"dependencies\": [\"category_feature\"],\n                \"loss\": {\n                    \"type\": \"cross_entropy\",\n                },\n                \"reduce_dependencies\": \"mean\",\n                \"decoder\": \"regressor\",\n                \"use_bias\": True,\n                \"bias_initializer\": \"constant\",\n            },\n            {\n                \"name\": \"vector_feature\",\n                \"type\": \"vector\",\n                \"decoder\": \"projector\",\n                \"num_fc_layers\": 5,\n                \"output_size\": 128,\n                \"activation\": \"tanh\",\n                \"dropout\": 0.1,\n            },\n        ],\n    }\n\n    for feature in old_config[INPUT_FEATURES]:\n        _upgrade_encoder_decoder_params(feature, True)\n\n    for feature in old_config[OUTPUT_FEATURES]:\n        _upgrade_encoder_decoder_params(feature, False)\n\n    assert old_config == {\n        \"input_features\": [\n            {\n                \"name\": \"text_feature\",\n                \"type\": \"text\",\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"drop_row\",\n                },\n                \"encoder\": {\n                    \"type\": \"rnn\",\n                    \"bidirectional\": True,\n                    \"representation\": \"dense\",\n                    \"num_layers\": 2,\n                },\n            },\n            {\n                \"name\": \"image_feature_1\",\n                \"type\": \"image\",\n                \"preprocessing\": {\n                    \"height\": 7.5,\n                    \"width\": 7.5,\n                    \"num_channels\": 4,\n                },\n                \"encoder\": {\n                    \"type\": \"resnet\",\n                    \"num_channels\": 4,\n                    \"dropout\": 0.1,\n                    \"resnet_size\": 100,\n                },\n            },\n            {\n                \"name\": \"image_feature_2\",\n                \"type\": \"image\",\n                \"tied\": \"image_feature_1\",\n                \"preprocessing\": {\n                    \"height\": 7.5,\n                    \"width\": 7.5,\n                    \"num_channels\": 4,\n                },\n                \"encoder\": {\"type\": \"resnet\"},\n            },\n        ],\n        \"output_features\": [\n            {\n                \"name\": \"category_feature\",\n                \"type\": \"category\",\n                \"num_classes\": 10,\n                \"top_k\": 3,\n                \"preprocessing\": {\n                    \"missing_value_strategy\": BFILL,\n                },\n                \"decoder\": {\n                    \"type\": \"classifier\",\n                    \"fc_use_bias\": False,\n                    \"use_bias\": False,\n                },\n            },\n            {\n                \"name\": \"binary_feature\",\n                \"type\": \"binary\",\n                \"dependencies\": [\"category_feature\"],\n                \"loss\": {\n                    \"type\": \"cross_entropy\",\n                },\n                \"reduce_dependencies\": \"mean\",\n                \"decoder\": {\n                    \"type\": \"regressor\",\n                    \"fc_use_bias\": True,\n                    \"fc_bias_initializer\": \"constant\",\n                    \"bias_initializer\": \"constant\",\n                    \"use_bias\": True,\n                },\n            },\n            {\n                \"name\": \"vector_feature\",\n                \"type\": \"vector\",\n                \"decoder\": {\n                    \"type\": \"projector\",\n                    \"num_fc_layers\": 5,\n                    \"fc_output_size\": 128,\n                    \"fc_activation\": \"tanh\",\n                    \"fc_dropout\": 0.1,\n                    \"output_size\": 128,\n                    \"activation\": \"tanh\",\n                    \"dropout\": 0.1,\n                },\n            },\n        ],\n    }\n\n\ndef test_deprecated_field_aliases():\n    config = {\n        \"ludwig_version\": \"0.4\",\n        INPUT_FEATURES: [{\"name\": \"num_in\", \"type\": \"numerical\"}],\n        OUTPUT_FEATURES: [{\"name\": \"num_out\", \"type\": \"numerical\"}],\n        HYPEROPT: {\n            \"parameters\": {\n                \"training.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.001,\n                    \"upper\": 0.1,\n                },\n            },\n            \"goal\": \"minimize\",\n            \"sampler\": {\"type\": \"grid\", \"num_samples\": 2, \"scheduler\": {\"type\": \"fifo\"}},\n            \"executor\": {\n                \"type\": \"grid\",\n                \"search_alg\": \"bohb\",\n            },\n        },\n        PREPROCESSING: {\n            \"numerical\": {\n                \"fill_value\": 2,\n                \"missing_value_strategy\": \"fill_with_const\",\n            },\n        },\n        \"training\": {\n            \"epochs\": 2,\n            \"eval_batch_size\": 0,\n            \"reduce_learning_rate_on_plateau\": 2,\n            \"reduce_learning_rate_on_plateau_patience\": 5,\n            \"decay\": True,\n            \"learning_rate_warmup_epochs\": 2,\n        },\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(config)\n\n    assert updated_config[\"input_features\"][0][TYPE] == NUMBER\n    assert updated_config[\"output_features\"][0][TYPE] == NUMBER\n\n    # \"numerical\" preprocssing directive should be translated to \"number\" and moved into the defaults section.\n    assert PREPROCESSING not in updated_config\n    assert updated_config[DEFAULTS][NUMBER][PREPROCESSING][\"fill_value\"] == 2\n\n    assert \"training\" not in updated_config\n    assert updated_config[TRAINER][\"epochs\"] == 2\n    assert updated_config[TRAINER][EVAL_BATCH_SIZE] is None\n\n    assert LEARNING_RATE_SCHEDULER in updated_config[TRAINER]\n    assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER][\"reduce_on_plateau\"] == 2\n    assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER][\"reduce_on_plateau_patience\"] == 5\n    assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER][\"decay\"] == \"exponential\"\n    assert updated_config[TRAINER][LEARNING_RATE_SCHEDULER][\"warmup_evaluations\"] == 2\n\n    hparams = updated_config[HYPEROPT][\"parameters\"]\n    assert \"training.learning_rate\" not in hparams\n    assert \"trainer.learning_rate\" in hparams\n\n    assert \"sampler\" not in updated_config[HYPEROPT]\n\n    assert updated_config[HYPEROPT][\"executor\"][\"type\"] == \"ray\"\n    assert \"num_samples\" in updated_config[HYPEROPT][\"executor\"]\n    assert \"scheduler\" in updated_config[HYPEROPT][\"executor\"]\n\n    ModelConfig.from_dict(updated_config)\n\n\n@pytest.mark.parametrize(\"force_split\", [None, False, True])\n@pytest.mark.parametrize(\"stratify\", [None, \"cat_in\"])\ndef test_deprecated_split_aliases(stratify, force_split):\n    split_probabilities = [0.6, 0.2, 0.2]\n    config = {\n        \"ludwig_version\": \"0.4\",\n        INPUT_FEATURES: [{\"name\": \"num_in\", \"type\": \"number\"}, {\"name\": \"cat_in\", \"type\": \"category\"}],\n        OUTPUT_FEATURES: [{\"name\": \"num_out\", \"type\": \"number\"}],\n        PREPROCESSING: {\n            \"force_split\": force_split,\n            \"split_probabilities\": split_probabilities,\n            \"stratify\": stratify,\n        },\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(config)\n\n    assert \"force_split\" not in updated_config[PREPROCESSING]\n    assert \"split_probabilities\" not in updated_config[PREPROCESSING]\n    assert \"stratify\" not in updated_config[PREPROCESSING]\n\n    assert SPLIT in updated_config[PREPROCESSING]\n    split = updated_config[PREPROCESSING][SPLIT]\n\n    assert split[\"probabilities\"] == split_probabilities\n    if stratify is None:\n        if force_split:\n            assert split.get(TYPE) == \"random\"\n    else:\n        assert split.get(TYPE) == \"stratify\"\n        assert split.get(\"column\") == stratify\n\n\n@pytest.mark.parametrize(\"use_scheduler\", [True, False])\ndef test_deprecated_hyperopt_sampler_early_stopping(use_scheduler):\n    sampler = {\n        \"type\": \"ray\",\n        \"num_samples\": 2,\n    }\n\n    if use_scheduler:\n        sampler[SCHEDULER] = {\n            \"type\": \"async_hyperband\",\n            \"max_t\": 200,\n            \"time_attr\": \"time_total_s\",\n            \"grace_period\": 72,\n            \"reduction_factor\": 5,\n        }\n\n    config = {\n        INPUT_FEATURES: [\n            {\n                \"type\": \"category\",\n                \"name\": \"cat_input_feature\",\n            },\n        ],\n        OUTPUT_FEATURES: [\n            {\n                \"type\": \"number\",\n                \"name\": \"num_output_feature\",\n            },\n        ],\n        \"hyperopt\": {\n            \"search_alg\": {\n                \"type\": \"variant_generator\",\n            },\n            \"executor\": {\n                \"type\": \"ray\",\n                \"time_budget_s\": 200,\n                \"cpu_resources_per_trial\": 1,\n            },\n            \"sampler\": sampler,\n            \"parameters\": {\n                \"trainer.batch_size\": {\n                    \"space\": \"choice\",\n                    \"categories\": [64, 128, 256],\n                },\n                \"trainer.learning_rate\": {\n                    \"space\": \"loguniform\",\n                    \"lower\": 0.001,\n                    \"upper\": 0.1,\n                },\n            },\n        },\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(config)\n    if use_scheduler:\n        assert SCHEDULER in updated_config[HYPEROPT][EXECUTOR]\n\n    merged_config = ModelConfig.from_dict(updated_config).to_dict()\n\n    # When a scheulder is provided, early stopping in the rendered config needs to be disabled to allow the\n    # hyperopt scheduler to manage trial lifecycle.\n    expected_early_stop = -1 if use_scheduler else ECDTrainerConfig().early_stop\n    assert merged_config[TRAINER][\"early_stop\"] == expected_early_stop\n\n\ndef test_validate_old_model_config():\n    old_valid_config = {\n        \"input_features\": [\n            {\"name\": \"feature_1\", \"type\": \"category\"},\n            {\"name\": \"Sex\", \"type\": \"category\", \"encoder\": \"dense\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"Survived\", \"type\": \"category\"},\n        ],\n    }\n\n    old_invalid_config = {\n        \"input_features\": [\n            {\"name\": \"feature_1\", \"type\": \"category\"},\n            {\"name\": \"Sex\", \"type\": \"category\", \"encoder\": \"fake_encoder\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"Survived\", \"type\": \"category\"},\n        ],\n    }\n\n    ModelConfig.from_dict(old_valid_config)\n\n    with pytest.raises(Exception):\n        ModelConfig.from_dict(old_invalid_config)\n\n\n@pytest.mark.parametrize(\"missing_value_strategy\", [\"backfill\", \"pad\"])\ndef test_update_missing_value_strategy(missing_value_strategy: str):\n    old_valid_config = {\n        \"input_features\": [\n            {\n                \"name\": \"input_feature_1\",\n                \"type\": \"category\",\n                \"preprocessing\": {\"missing_value_strategy\": missing_value_strategy},\n            }\n        ],\n        \"output_features\": [\n            {\"name\": \"output_feature_1\", \"type\": \"category\"},\n        ],\n    }\n\n    updated_config = upgrade_missing_value_strategy(old_valid_config)\n\n    expected_config = copy.deepcopy(old_valid_config)\n    if missing_value_strategy == \"backfill\":\n        expected_config[\"input_features\"][0][\"preprocessing\"][\"missing_value_strategy\"] == \"bfill\"\n    else:\n        expected_config[\"input_features\"][0][\"preprocessing\"][\"missing_value_strategy\"] == \"ffill\"\n\n    assert updated_config == expected_config\n\n\ndef test_update_increase_batch_size_on_plateau_max():\n    old_valid_config = {\n        \"input_features\": [{\"name\": \"input_feature_1\", \"type\": \"category\"}],\n        \"output_features\": [{\"name\": \"output_feature_1\", \"type\": \"category\"}],\n        \"trainer\": {\n            \"increase_batch_size_on_plateau_max\": 256,\n        },\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(old_valid_config)\n    del updated_config[\"ludwig_version\"]\n\n    expected_config = copy.deepcopy(old_valid_config)\n    del expected_config[\"trainer\"][\"increase_batch_size_on_plateau_max\"]\n    expected_config[\"trainer\"][\"max_batch_size\"] = 256\n\n    assert updated_config == expected_config\n\n\ndef test_old_class_weights_default():\n    old_config = {\n        \"input_features\": [\n            {\n                \"name\": \"input_feature_1\",\n                \"type\": \"category\",\n            }\n        ],\n        \"output_features\": [\n            {\"name\": \"output_feature_1\", \"type\": \"category\", \"loss\": {\"class_weights\": 1}},\n        ],\n    }\n\n    new_config = {\n        \"input_features\": [\n            {\n                \"name\": \"input_feature_1\",\n                \"type\": \"category\",\n            }\n        ],\n        \"output_features\": [\n            {\"name\": \"output_feature_1\", \"type\": \"category\", \"loss\": {\"class_weights\": None}},\n        ],\n    }\n\n    upgraded_config = upgrade_config_dict_to_latest_version(old_config)\n    del upgraded_config[\"ludwig_version\"]\n    assert new_config == upgraded_config\n\n    old_config[OUTPUT_FEATURES][0][LOSS][CLASS_WEIGHTS] = [0.5, 0.8, 1]\n    new_config[OUTPUT_FEATURES][0][LOSS][CLASS_WEIGHTS] = [0.5, 0.8, 1]\n\n    upgraded_config = upgrade_config_dict_to_latest_version(old_config)\n    del upgraded_config[\"ludwig_version\"]\n    assert new_config == upgraded_config\n\n\ndef test_upgrade_model_progress():\n    old_model_progress = {\n        \"batch_size\": 64,\n        \"best_eval_metric\": 0.5,\n        \"best_increase_batch_size_eval_metric\": math.inf,\n        \"best_reduce_learning_rate_eval_metric\": math.inf,\n        \"epoch\": 2,\n        \"last_improvement\": 1,\n        \"last_improvement_epoch\": 1,\n        \"best_eval_metric_epoch\": 1,\n        \"last_increase_batch_size\": 0,\n        \"last_increase_batch_size_epoch\": 0,\n        \"last_increase_batch_size_eval_metric_improvement\": 0,\n        \"last_learning_rate_reduction\": 0,\n        \"last_learning_rate_reduction_epoch\": 0,\n        \"last_reduce_learning_rate_eval_metric_improvement\": 0,\n        \"learning_rate\": 0.001,\n        \"num_increases_batch_size\": 0,\n        \"num_reductions_learning_rate\": 0,\n        \"steps\": 224,\n        \"test_metrics\": {\n            \"combined\": {\"loss\": [0.59, 0.56]},\n            \"delinquent\": {\n                \"accuracy\": [0.77, 0.78],\n            },\n        },\n        \"train_metrics\": {\"combined\": {\"loss\": [0.58, 0.55]}, \"delinquent\": {\"roc_auc\": [0.53, 0.54]}},\n        \"vali_metrics\": {\"combined\": {\"loss\": [0.59, 0.60]}, \"delinquent\": {\"roc_auc\": [0.53, 0.44]}},\n    }\n\n    new_model_progress = upgrade_model_progress(old_model_progress)\n\n    assert new_model_progress == {\n        \"batch_size\": 64,\n        \"best_eval_metric_value\": 0.5,\n        \"best_increase_batch_size_eval_metric\": float(\"inf\"),\n        \"epoch\": 2,\n        \"last_improvement_steps\": 64,\n        \"best_eval_metric_steps\": 0,\n        \"best_eval_metric_epoch\": 1,\n        \"last_increase_batch_size\": 0,\n        \"last_increase_batch_size_eval_metric_improvement\": 0,\n        \"last_learning_rate_reduction\": 0,\n        \"learning_rate\": 0.001,\n        \"num_increases_batch_size\": 0,\n        \"num_reductions_learning_rate\": 0,\n        \"steps\": 224,\n        \"test_metrics\": {\n            \"combined\": {\n                \"loss\": [TrainerMetric(epoch=1, step=64, value=0.59), TrainerMetric(epoch=2, step=128, value=0.56)]\n            },\n            \"delinquent\": {\n                \"accuracy\": [TrainerMetric(epoch=1, step=64, value=0.77), TrainerMetric(epoch=2, step=128, value=0.78)]\n            },\n        },\n        \"train_metrics\": {\n            \"combined\": {\n                \"loss\": [TrainerMetric(epoch=1, step=64, value=0.58), TrainerMetric(epoch=2, step=128, value=0.55)]\n            },\n            \"delinquent\": {\n                \"roc_auc\": [TrainerMetric(epoch=1, step=64, value=0.53), TrainerMetric(epoch=2, step=128, value=0.54)]\n            },\n        },\n        \"last_learning_rate_reduction_steps\": 0,\n        \"last_increase_batch_size_steps\": 0,\n        \"validation_metrics\": {\n            \"combined\": {\n                \"loss\": [TrainerMetric(epoch=1, step=64, value=0.59), TrainerMetric(epoch=2, step=128, value=0.6)]\n            },\n            \"delinquent\": {\n                \"roc_auc\": [TrainerMetric(epoch=1, step=64, value=0.53), TrainerMetric(epoch=2, step=128, value=0.44)]\n            },\n        },\n        \"tune_checkpoint_num\": 0,\n        \"checkpoint_number\": 0,\n        \"best_eval_metric_checkpoint_number\": 0,\n        \"best_eval_train_metrics\": {},\n        \"best_eval_validation_metrics\": {},\n        \"best_eval_test_metrics\": {},\n    }\n\n    # Verify that we don't make changes to already-valid model progress dicts.\n    # To do so, we modify the batch size value and re-run the upgrade on the otherwise-valid `new_model_progress` dict.\n    new_model_progress[\"batch_size\"] = 1\n    unchanged_model_progress = upgrade_model_progress(new_model_progress)\n    assert unchanged_model_progress == new_model_progress\n\n\ndef test_upgrade_model_progress_already_valid():\n    # Verify that we don't make changes to already-valid model progress dicts.\n    valid_model_progress = {\n        BATCH_SIZE: 128,\n        \"best_eval_metric_checkpoint_number\": 7,\n        \"best_eval_metric_epoch\": 6,\n        \"best_eval_metric_steps\": 35,\n        \"best_eval_metric_value\": 0.719,\n        \"best_eval_test_metrics\": {\n            \"Survived\": {\"accuracy\": 0.634, \"loss\": 3.820, \"roc_auc\": 0.598},\n            \"combined\": {\"loss\": 3.820},\n        },\n        \"best_eval_train_metrics\": {\n            \"Survived\": {\"accuracy\": 0.682, \"loss\": 4.006, \"roc_auc\": 0.634},\n            \"combined\": {\"loss\": 4.006},\n        },\n        \"best_eval_validation_metrics\": {\n            \"Survived\": {\"accuracy\": 0.719, \"loss\": 4.396, \"roc_auc\": 0.667},\n            \"combined\": {\"loss\": 4.396},\n        },\n        \"best_increase_batch_size_eval_metric\": float(\"inf\"),\n        \"checkpoint_number\": 12,\n        \"epoch\": 12,\n        \"last_increase_batch_size\": 0,\n        \"last_increase_batch_size_eval_metric_improvement\": 0,\n        \"last_increase_batch_size_steps\": 0,\n        \"last_learning_rate_reduction\": 0,\n        \"last_learning_rate_reduction_steps\": 0,\n        \"learning_rate\": 0.001,\n        \"num_increases_batch_size\": 0,\n        \"num_reductions_learning_rate\": 0,\n        \"steps\": 60,\n        \"test_metrics\": {\n            \"Survived\": {\n                \"accuracy\": [\n                    [0, 5, 0.651],\n                    [1, 10, 0.651],\n                ],\n                \"loss\": [\n                    [0, 5, 4.130],\n                    [1, 10, 4.074],\n                ],\n                \"roc_auc\": [\n                    [0, 5, 0.574],\n                    [1, 10, 0.595],\n                ],\n            },\n            \"combined\": {\n                \"loss\": [\n                    [0, 5, 4.130],\n                    [1, 10, 4.074],\n                ]\n            },\n        },\n        \"train_metrics\": {\n            \"Survived\": {\n                \"accuracy\": [\n                    [0, 5, 0.6875],\n                    [1, 10, 0.6875],\n                ],\n                \"loss\": [\n                    [0, 5, 4.417],\n                    [1, 10, 4.344],\n                ],\n                \"roc_auc\": [\n                    [0, 5, 0.628],\n                    [1, 10, 0.629],\n                ],\n            },\n            \"combined\": {\n                \"loss\": [\n                    [0, 5, 4.417],\n                    [1, 10, 4.344],\n                ]\n            },\n        },\n        \"tune_checkpoint_num\": 0,\n        \"validation_metrics\": {\n            \"Survived\": {\n                \"accuracy\": [\n                    [0, 5, 0.696],\n                    [1, 10, 0.696],\n                ],\n                \"loss\": [\n                    [0, 5, 4.494],\n                    [1, 10, 4.473],\n                ],\n                \"roc_auc\": [\n                    [0, 5, 0.675],\n                    [1, 10, 0.671],\n                ],\n            },\n            \"combined\": {\n                \"loss\": [\n                    [0, 5, 4.494],\n                    [1, 10, 4.473],\n                ]\n            },\n        },\n    }\n\n    unchanged_model_progress = upgrade_model_progress(valid_model_progress)\n    assert unchanged_model_progress == valid_model_progress\n\n\ndef test_cache_credentials_backward_compatibility():\n    # From v0.6.3.\n    creds = {\"s3\": {\"client_kwargs\": {}}}\n    backend = {\"type\": \"local\", \"cache_dir\": \"/foo/bar\", \"cache_credentials\": creds}\n\n    _update_backend_cache_credentials(backend)\n\n    assert backend == {\"type\": \"local\", \"cache_dir\": \"/foo/bar\", \"credentials\": {\"cache\": creds}}\n\n\n@pytest.mark.parametrize(\n    \"encoder,upgraded_type\",\n    [\n        ({\"type\": \"resnet\"}, \"resnet\"),\n        ({\"type\": \"vit\"}, \"vit\"),\n        ({\"type\": \"resnet\", \"resnet_size\": 50}, \"_resnet_legacy\"),\n        ({\"type\": \"vit\", \"num_hidden_layers\": 12}, \"_vit_legacy\"),\n    ],\n    ids=[\"resnet\", \"vit\", \"resnet_legacy\", \"vit_legacy\"],\n)\ndef test_legacy_image_encoders(encoder: dict[str, Any], upgraded_type: str):\n    config = {\n        \"input_features\": [{\"name\": \"image1\", \"type\": \"image\", \"encoder\": encoder}],\n        \"output_features\": [{\"name\": \"binary1\", \"type\": \"binary\"}],\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(config)\n\n    expected_encoder = {\n        **encoder,\n        **{\"type\": upgraded_type},\n    }\n    assert updated_config[\"input_features\"][0][\"encoder\"] == expected_encoder\n\n\ndef test_load_config_missing_hyperopt():\n    old_valid_config = {\n        \"input_features\": [\n            {\"name\": \"feature_1\", \"type\": \"category\"},\n            {\"name\": \"Sex\", \"type\": \"category\", \"encoder\": \"dense\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"Survived\", \"type\": \"category\"},\n        ],\n        \"combiner\": {\"type\": \"concat\"},\n        \"trainer\": {},\n        \"hyperopt\": {},\n    }\n\n    config_obj = ModelConfig.from_dict(old_valid_config)\n    assert config_obj.hyperopt is None\n    assert config_obj.to_dict()[HYPEROPT] is None\n\n\ndef test_type_removed_from_defaults_config():\n    config = {\n        \"input_features\": [\n            {\"name\": \"feature_1\", \"type\": \"category\"},\n            {\"name\": \"Sex\", \"type\": \"category\"},\n        ],\n        \"output_features\": [\n            {\"name\": \"Survived\", \"type\": \"category\"},\n        ],\n        \"defaults\": {\n            \"binary\": {\n                \"encoder\": {\n                    \"type\": \"passthrough\",\n                },\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"fill_with_false\",\n                },\n                \"type\": \"binary\",\n            },\n            \"category\": {\n                \"encoder\": {\n                    \"type\": \"onehot\",\n                },\n                \"preprocessing\": {\n                    \"missing_value_strategy\": \"fill_with_const\",\n                    \"most_common\": 10000,\n                },\n                \"type\": \"category\",\n            },\n        },\n        \"model_type\": \"ecd\",\n    }\n\n    config_obj = ModelConfig.from_dict(config).to_dict()\n\n    for feature_type in config_obj.get(\"defaults\"):\n        assert \"type\" not in config_obj[\"defaults\"][feature_type]\n"
  },
  {
    "path": "tests/ludwig/utils/test_calibration.py",
    "content": "import numpy as np\nimport pytest\n\nfrom ludwig.utils import calibration\n\n\n@pytest.fixture\ndef uncalibrated_logits_and_labels():\n    \"\"\"Returns a pair of logits (10x3) and labels (10).\"\"\"\n    return (\n        np.array(\n            [\n                [-3.596756, 6.728981, 6.3807454],\n                [-16.818138, -3.5217745, -1.7786252],\n                [-16.060827, 4.7207646, 3.5336719],\n                [-4.784969, 5.062503, 3.515455],\n                [-4.669478, 7.171067, 6.5137157],\n                [-32.596764, -3.5582566, -5.2003713],\n                [-4.4035864, 6.3911495, 4.7273974],\n                [-4.2035627, 7.846533, 6.0476217],\n                [-20.748848, -3.1521742, -4.873552],\n                [-4.8901286, 4.726167, 3.208372],\n            ]\n        ),\n        np.array([2, 0, 2, 1, 1, 2, 0, 1, 0, 1]),\n    )\n\n\nEPSILON = 0.1  # maximum relative precision error allowed.\n\n\ndef test_temperature_scaling_binary(uncalibrated_logits_and_labels):\n    logits, labels = uncalibrated_logits_and_labels\n    # Selects one category of the 3-class test fixture to treat as a binary classifier.\n    binary_logits = logits[:, 1]\n    binary_labels = labels == 1\n    temperature_scaling = calibration.TemperatureScaling(binary=True)\n    calibration_result = temperature_scaling.train_calibration(binary_logits, binary_labels)\n    # Checks that we got close to optimal temperature\n    assert temperature_scaling.temperature.item() == pytest.approx(8.3, EPSILON)\n    # Checks that negative log-likelhood and expected calibration error are the same or lower post-calibration.\n    assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll\n    assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece\n\n\ndef test_temperature_scaling_category(uncalibrated_logits_and_labels):\n    logits, labels = uncalibrated_logits_and_labels\n    temperature_scaling = calibration.TemperatureScaling(num_classes=logits.shape[-1])\n    calibration_result = temperature_scaling.train_calibration(logits, labels)\n    # Checks that we got close to optimal temperature\n    # The exact temperature depends on optimizer internals (PyTorch version).\n    # Check it's in a reasonable range and that calibration improved metrics.\n    assert temperature_scaling.temperature.item() > 5.0\n    assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll\n    assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece\n\n\ndef test_matrix_scaling_category(uncalibrated_logits_and_labels):\n    logits, labels = uncalibrated_logits_and_labels\n    matrix_scaling = calibration.MatrixScaling(num_classes=logits.shape[-1])\n    calibration_result = matrix_scaling.train_calibration(logits, labels)\n    # Matrix scaling may not have a single optimum, so multiple runs could give different results.\n    # In this case we don't check any specific values\n    # Checks that negative log-likelhood and expected calibration error are the same or lower post-calibration.\n    assert calibration_result.after_calibration_nll <= calibration_result.before_calibration_nll\n    assert calibration_result.after_calibration_ece <= calibration_result.before_calibration_ece\n"
  },
  {
    "path": "tests/ludwig/utils/test_class_balancing.py",
    "content": "import numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.backend.base import LocalBackend\nfrom ludwig.constants import BALANCE_PERCENTAGE_TOLERANCE, NAME\nfrom ludwig.data.preprocessing import balance_data\n\n\n@pytest.mark.parametrize(\n    \"method, balance\",\n    [\n        (\"oversample_minority\", 0.25),\n        (\"oversample_minority\", 0.5),\n        (\"oversample_minority\", 0.75),\n        (\"undersample_majority\", 0.25),\n        (\"undersample_majority\", 0.5),\n        (\"undersample_majority\", 0.75),\n        (\"undersample_majority\", 0.9),\n    ],\n)\ndef test_balance(method, balance):\n    config = {\n        \"input_features\": [\n            {\"name\": \"Index\", \"proc_column\": \"Index\", \"type\": \"number\"},\n            {\"name\": \"random_1\", \"proc_column\": \"random_1\", \"type\": \"number\"},\n            {\"name\": \"random_2\", \"proc_column\": \"random_2\", \"type\": \"number\"},\n        ],\n        \"output_features\": [{\"name\": \"Label\", \"proc_column\": \"Label\", \"type\": \"binary\"}],\n        \"preprocessing\": {\"oversample_minority\": None, \"undersample_majority\": None},\n    }\n    input_df = pd.DataFrame(\n        {\n            \"Index\": np.arange(0, 200, 1),\n            \"random_1\": np.random.randint(0, 50, 200),\n            \"random_2\": np.random.choice([\"Type A\", \"Type B\", \"Type C\", \"Type D\"], 200),\n            \"Label\": np.concatenate((np.zeros(180), np.ones(20))),\n            \"split\": np.zeros(200),\n        }\n    )\n\n    config[\"preprocessing\"][method] = balance\n    backend = LocalBackend()\n\n    test_df = balance_data(input_df, config[\"output_features\"], config[\"preprocessing\"], backend, 42)\n    target = config[\"output_features\"][0][NAME]\n    majority_class = test_df[target].value_counts()[test_df[target].value_counts().idxmax()]\n    minority_class = test_df[target].value_counts()[test_df[target].value_counts().idxmin()]\n    new_class_balance = round(minority_class / majority_class, 2)\n\n    assert abs(balance - new_class_balance) < BALANCE_PERCENTAGE_TOLERANCE\n"
  },
  {
    "path": "tests/ludwig/utils/test_config_utils.py",
    "content": "import copy\nfrom typing import Any\n\nimport pytest\n\nfrom ludwig.constants import (\n    BASE_MODEL,\n    BINARY,\n    ENCODER,\n    INPUT_FEATURES,\n    MODEL_ECD,\n    MODEL_LLM,\n    MODEL_TYPE,\n    NAME,\n    OUTPUT_FEATURES,\n    TEXT,\n    TYPE,\n)\nfrom ludwig.schema.encoders.text_encoders import BERTConfig\nfrom ludwig.schema.encoders.utils import get_encoder_cls\nfrom ludwig.schema.features.preprocessing.text import TextPreprocessingConfig\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.config_utils import config_uses_llm, get_quantization\n\n\n@pytest.mark.parametrize(\n    \"pretrained_model_name_or_path\",\n    [None, \"bert-large-uncased\"],\n    ids=[\"default_model\", \"override_model\"],\n)\ndef test_set_fixed_preprocessing_params(pretrained_model_name_or_path: str):\n    expected_model_name = \"bert-base-uncased\"\n\n    preprocessing = TextPreprocessingConfig.from_dict(\n        {\n            \"tokenizer\": \"space\",\n            \"lowercase\": True,\n        }\n    )\n\n    encoder_params = {}\n    if pretrained_model_name_or_path is not None:\n        encoder_params[\"pretrained_model_name_or_path\"] = pretrained_model_name_or_path\n        expected_model_name = pretrained_model_name_or_path\n\n    encoder = BERTConfig.from_dict(encoder_params)\n    encoder.set_fixed_preprocessing_params(MODEL_ECD, preprocessing)\n\n    assert preprocessing.tokenizer == \"hf_tokenizer\"\n    assert preprocessing.lowercase\n    assert preprocessing.pretrained_model_name_or_path == expected_model_name\n\n\n@pytest.mark.parametrize(\n    \"encoder_params,expected\",\n    [\n        ({\"type\": \"parallel_cnn\"}, False),\n        ({\"type\": \"bert\", \"trainable\": False}, True),\n        ({\"type\": \"bert\", \"trainable\": True}, False),\n    ],\n    ids=[\"parallel_cnn\", \"bert_fixed\", \"bert_trainable\"],\n)\ndef test_set_fixed_preprocessing_params_cache_embeddings(encoder_params: dict[str, Any], expected: bool | None):\n    preprocessing = TextPreprocessingConfig.from_dict(\n        {\n            \"tokenizer\": \"space\",\n            \"lowercase\": True,\n            \"cache_encoder_embeddings\": True,\n        }\n    )\n\n    encoder = get_encoder_cls(MODEL_ECD, TEXT, encoder_params[TYPE]).from_dict(encoder_params)\n    encoder.set_fixed_preprocessing_params(MODEL_ECD, preprocessing)\n    assert preprocessing.cache_encoder_embeddings == expected\n\n\n@pytest.fixture(scope=\"module\")\ndef llm_config_dict() -> dict[str, Any]:\n    return {\n        MODEL_TYPE: MODEL_LLM,\n        BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n        INPUT_FEATURES: [{TYPE: TEXT, NAME: \"in1\"}],\n        OUTPUT_FEATURES: [{TYPE: TEXT, NAME: \"out1\"}],\n    }\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_llm_encoder() -> dict[str, Any]:\n    return {\n        MODEL_TYPE: MODEL_ECD,\n        INPUT_FEATURES: [\n            {\n                TYPE: TEXT,\n                NAME: \"in1\",\n                ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"},\n            }\n        ],\n        OUTPUT_FEATURES: [{TYPE: BINARY, NAME: \"out1\"}],\n    }\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_llm_encoder_multiple_features() -> dict[str, Any]:\n    return {\n        MODEL_TYPE: MODEL_ECD,\n        INPUT_FEATURES: [\n            {TYPE: BINARY, NAME: \"in1\"},\n            {\n                TYPE: TEXT,\n                NAME: \"in2\",\n                ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"},\n            },\n        ],\n        OUTPUT_FEATURES: [{TYPE: BINARY, NAME: \"out1\"}],\n    }\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_no_llm_encoder() -> dict[str, Any]:\n    return {\n        MODEL_TYPE: MODEL_ECD,\n        INPUT_FEATURES: [{TYPE: TEXT, NAME: \"in1\", ENCODER: {TYPE: \"parallel_cnn\"}}],\n        OUTPUT_FEATURES: [{TYPE: BINARY, NAME: \"out1\"}],\n    }\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_no_text_features() -> dict[str, Any]:\n    return {\n        MODEL_TYPE: MODEL_ECD,\n        INPUT_FEATURES: [{TYPE: BINARY, NAME: \"in1\"}],\n        OUTPUT_FEATURES: [{TYPE: BINARY, NAME: \"out1\"}],\n    }\n\n\n@pytest.mark.parametrize(\n    \"config,expectation\",\n    [\n        # LLM configurations\n        (\"llm_config_dict\", True),\n        # LLM encoder configurations\n        (\"ecd_config_dict_llm_encoder\", True),\n        # LLM encoder configurations, multiple features\n        (\"ecd_config_dict_llm_encoder_multiple_features\", True),\n        # ECD configuration with text feature and non-LLM encoder\n        (\"ecd_config_dict_no_llm_encoder\", False),\n        # ECD configuration with no text features\n        (\"ecd_config_dict_no_text_features\", False),\n    ],\n)\n@pytest.mark.parametrize(\"config_type\", [\"dict\", \"object\"])\ndef test_is_or_uses_llm(config: dict[str, Any], expectation: bool, config_type, request):\n    \"\"\"Test LLM detection on a variety of configs. Configs that use an LLM anywhere should return True, otherwise\n    False.\n\n    Args:\n        config: The name of the config fixture to test\n        expectation: The expected result\n        request: pytest `request` fixture\n    \"\"\"\n    config = request.getfixturevalue(config)\n    if config_type == \"object\":\n        config = ModelConfig.from_dict(config)\n    assert config_uses_llm(config) == expectation\n\n\n@pytest.mark.parametrize(\"invalid_config\", [1, 1.0, \"foo\", True, False, None, [], {}, {\"foo\": \"bar\"}])\ndef test_is_or_uses_llm_invalid_input(invalid_config):\n    \"\"\"Sanity checks for invalid config handling.\n\n    These should all raise an exception.\n\n    Args:\n        invalid_config: An invalid argument to `config_uses_llm`\n    \"\"\"\n    with pytest.raises(ValueError):\n        config_uses_llm(invalid_config)\n\n\n@pytest.fixture(scope=\"module\")\ndef quantization_4bit_config() -> dict[str, Any]:\n    return {\"quantization\": {\"bits\": 4}}\n\n\n@pytest.fixture(scope=\"module\")\ndef quantization_8bit_config() -> dict[str, Any]:\n    return {\"quantization\": {\"bits\": 8}}\n\n\n@pytest.fixture(scope=\"module\")\ndef llm_config_dict_4bit(llm_config_dict: dict[str, Any], quantization_4bit_config: dict[str, Any]) -> dict[str, Any]:\n    config = copy.deepcopy(llm_config_dict)\n    config.update(quantization_4bit_config)\n    return config\n\n\n@pytest.fixture(scope=\"module\")\ndef llm_config_dict_8bit(llm_config_dict: dict[str, Any], quantization_8bit_config: dict[str, Any]) -> dict[str, Any]:\n    config = copy.deepcopy(llm_config_dict)\n    config.update(quantization_8bit_config)\n    return config\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_llm_encoder_4bit(\n    ecd_config_dict_llm_encoder: dict[str, Any], quantization_4bit_config: dict[str, Any]\n) -> dict[str, Any]:\n    config = copy.deepcopy(ecd_config_dict_llm_encoder)\n    config[INPUT_FEATURES][0][ENCODER].update(quantization_4bit_config)\n    return config\n\n\n@pytest.fixture(scope=\"module\")\ndef ecd_config_dict_llm_encoder_8bit(\n    ecd_config_dict_llm_encoder: dict[str, Any], quantization_8bit_config: dict[str, Any]\n) -> dict[str, Any]:\n    config = copy.deepcopy(ecd_config_dict_llm_encoder)\n    config[INPUT_FEATURES][0][ENCODER].update(quantization_8bit_config)\n    return config\n\n\n@pytest.mark.parametrize(\n    \"config,expectation\",\n    [\n        # LLM configurations\n        (\"llm_config_dict\", [None]),\n        (\"llm_config_dict_4bit\", [4]),\n        (\"llm_config_dict_8bit\", [8]),\n        # LLM encoder configurations with one feature\n        (\"ecd_config_dict_llm_encoder\", [None]),\n        (\"ecd_config_dict_llm_encoder_4bit\", [4]),\n        (\"ecd_config_dict_llm_encoder_8bit\", [8]),\n    ],\n)\n@pytest.mark.parametrize(\"config_type\", [\"dict\", \"object\"])\ndef test_get_quantization(\n    config: dict[str, Any], expectation: int | list[int] | None | list[None], config_type: str, request\n):\n    \"\"\"Test get_quantization with LLM and single-feature ECD configs.\n\n    Args:\n        config: The configuration to test\n        expectation: The expected quantization\n        config_type: Whether to test the config as a dict or object\n        request: pytest builtin fixture\n    \"\"\"\n    config = request.getfixturevalue(config)\n    if config_type == \"object\":\n        config = ModelConfig.from_dict(config)\n    assert get_quantization(config) == expectation\n\n\nTEST_FEATURE_CONFIGS = [\n    (\n        {\n            TYPE: BINARY,\n        },\n        None,\n    ),\n    (\n        {\n            TYPE: TEXT,\n        },\n        None,\n    ),\n    ({TYPE: TEXT, ENCODER: {TYPE: MODEL_LLM, BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\"}}, None),\n    (\n        {\n            TYPE: TEXT,\n            ENCODER: {\n                TYPE: MODEL_LLM,\n                BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n                \"quantization\": {\"bits\": 4},\n            },\n        },\n        4,\n    ),\n    (\n        {\n            TYPE: TEXT,\n            ENCODER: {\n                TYPE: MODEL_LLM,\n                BASE_MODEL: \"HuggingFaceH4/tiny-random-LlamaForCausalLM\",\n                \"quantization\": {\"bits\": 8},\n            },\n        },\n        8,\n    ),\n]\n\nTEST_FEATURE_CONFIGS_IDS = [BINARY, TEXT, MODEL_LLM, f\"{MODEL_LLM}-4bit\", f\"{MODEL_LLM}-8bit\"]\n\n\n@pytest.mark.parametrize(\"feature1,quantization1\", TEST_FEATURE_CONFIGS, ids=TEST_FEATURE_CONFIGS_IDS)\n@pytest.mark.parametrize(\"feature2,quantization2\", TEST_FEATURE_CONFIGS, ids=TEST_FEATURE_CONFIGS_IDS)\n@pytest.mark.parametrize(\"config_type\", [\"dict\", \"object\"])\ndef test_get_quantization_multiple_features(\n    ecd_config_dict_llm_encoder_multiple_features: dict[str, Any],\n    feature1: dict[str, Any],\n    quantization1: int,\n    feature2: dict[str, Any],\n    quantization2: int,\n    config_type: str,\n):\n    \"\"\"Test get_quantization with multiple features.\n\n    Args:\n        ecd_config_dict_llm_encoder_multiple_features: Baseline config to add features to.\n        feature1: First input feature config dict\n        quantization1: First input feature expected quantization\n        feature2: Second input feature config dict\n        quantization2: Second input feature expected quantization\n        config_type: Whether to test the config as a dict or object\n    \"\"\"\n    config = copy.deepcopy(ecd_config_dict_llm_encoder_multiple_features)\n    feature1 = dict(name=\"in1\", **feature1)\n    feature2 = dict(name=\"in2\", **feature2)\n    config[INPUT_FEATURES] = [feature1, feature2]\n\n    if config_type == \"object\":\n        config = ModelConfig.from_dict(config)\n\n    assert get_quantization(config) == [quantization1, quantization2]\n\n\n@pytest.mark.parametrize(\"invalid_config\", [1, 1.0, \"foo\", True, False, None, [], {}, {\"foo\": \"bar\"}])\ndef test_get_quantization_invalid_input(invalid_config):\n    \"\"\"Test get_quantization with invalid configs. These should always raise a ValueError.\n\n    Args:\n        invalid_config: The invalid config to test\n    \"\"\"\n    with pytest.raises(ValueError):\n        get_quantization(invalid_config)\n"
  },
  {
    "path": "tests/ludwig/utils/test_data_utils.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport functools\nimport json\nimport logging\n\nimport numpy as np\nimport pandas as pd\nimport pytest\nfrom fsspec.config import conf\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.cache.types import CacheableDataframe\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.utils.data_utils import (\n    add_sequence_feature_column,\n    figure_data_format_dataset,\n    get_abs_path,\n    hash_dict,\n    NumpyEncoder,\n    PANDAS_DF,\n    read_csv,\n    read_html,\n    read_parquet,\n    sanitize_column_names,\n    use_credentials,\n)\n\ntry:\n    import dask.dataframe as dd\nexcept ImportError:\n    dd = None\n\n\ndef test_add_sequence_feature_column():\n    df = pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"])\n\n    add_sequence_feature_column(df, \"x\", 2)\n    assert df.equals(\n        pd.DataFrame(\n            [\n                [1, \"1 2\"],\n                [2, \"1 2\"],\n                [3, \"1 2\"],\n                [4, \"2 3\"],\n                [5, \"3 4\"],\n            ],\n            columns=[\"x\", \"x_feature\"],\n        )\n    )\n\n    add_sequence_feature_column(df, \"x\", 1)\n    assert df.equals(\n        pd.DataFrame(\n            [\n                [1, \"1\"],\n                [2, \"1\"],\n                [3, \"2\"],\n                [4, \"3\"],\n                [5, \"4\"],\n            ],\n            columns=[\"x\", \"x_feature\"],\n        )\n    )\n\n    df = pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"])\n\n    add_sequence_feature_column(df, \"y\", 2)\n    assert df.equals(pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"]))\n\n\ndef test_get_abs_path():\n    assert get_abs_path(\"a\", \"b.jpg\") == \"a/b.jpg\"\n    assert get_abs_path(None, \"b.jpg\") == \"b.jpg\"\n\n\n@pytest.mark.parametrize(\n    \"path, expected_format\", [(\"s3://path/to.parquet \", \"parquet\"), (\"/Users/path/to.csv \\n\", \"csv\")]\n)\ndef test_figure_data_format_dataset_strip(path, expected_format):\n    assert figure_data_format_dataset(path) == expected_format\n\n\n@pytest.mark.distributed\ndef test_figure_data_format_dataset():\n    assert figure_data_format_dataset({\"a\": \"b\"}) == dict\n    assert figure_data_format_dataset(pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"])) == pd.DataFrame\n    assert (\n        figure_data_format_dataset(\n            dd.from_pandas(pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"]), npartitions=1).reset_index()\n        )\n        == dd.DataFrame\n    )\n    assert (\n        figure_data_format_dataset(\n            CacheableDataframe(df=pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"]), name=\"test\", checksum=\"test123\")\n        )\n        == pd.DataFrame\n    )\n    assert (\n        figure_data_format_dataset(\n            CacheableDataframe(\n                df=dd.from_pandas(pd.DataFrame([1, 2, 3, 4, 5], columns=[\"x\"]), npartitions=1).reset_index(),\n                name=\"test\",\n                checksum=\"test123\",\n            )\n        )\n        == dd.DataFrame\n    )\n\n\ndef test_hash_dict_numpy_types():\n    d = {\"float32\": np.float32(1)}\n    assert hash_dict(d) == b\"uqtgWB\"\n\n\ndef test_use_credentials():\n    conf.clear()\n    with use_credentials(None):\n        assert len(conf) == 0\n\n    s3_creds = {\n        \"s3\": {\n            \"client_kwargs\": {\n                \"endpoint_url\": \"http://localhost:9000\",\n                \"aws_access_key_id\": \"test\",\n                \"aws_secret_access_key\": \"test\",\n            }\n        }\n    }\n    with use_credentials(s3_creds):\n        assert len(conf) == 1\n        assert conf == s3_creds\n\n    assert len(conf) == 0\n\n\ndef test_numpy_encoder():\n    # Test Python builtin data type encoding.\n    assert json.dumps(None, cls=NumpyEncoder) == \"null\"\n    assert json.dumps({}, cls=NumpyEncoder) == \"{}\"\n    assert json.dumps(1, cls=NumpyEncoder) == \"1\"\n    assert json.dumps(1.0, cls=NumpyEncoder) == \"1.0\"\n    assert json.dumps(\"a\", cls=NumpyEncoder) == '\"a\"'\n    assert json.dumps([0, 1, 2, 3, 4], cls=NumpyEncoder) == \"[0, 1, 2, 3, 4]\"\n    assert json.dumps((0, 1, 2, 3, 4), cls=NumpyEncoder) == \"[0, 1, 2, 3, 4]\"\n    assert json.dumps({0, 1, 2, 3, 4}, cls=NumpyEncoder) == \"[0, 1, 2, 3, 4]\"\n    assert json.dumps({\"a\": \"b\"}, cls=NumpyEncoder) == '{\"a\": \"b\"}'\n\n    # Test numpy data type encoding\n    for dtype in [np.byte, np.ubyte, np.short, np.ushort, np.int32, np.int64, np.uint, np.longlong, np.ulonglong]:\n        x = np.arange(5, dtype=dtype)\n        assert json.dumps(x, cls=NumpyEncoder) == \"[0, 1, 2, 3, 4]\"\n        for i in x:\n            assert json.dumps(i, cls=NumpyEncoder) == f\"{i}\"\n\n    for dtype in [np.half, np.single, np.double, np.longdouble]:\n        x = np.arange(5, dtype=dtype)\n        assert json.dumps(x, cls=NumpyEncoder) == \"[0.0, 1.0, 2.0, 3.0, 4.0]\"\n        for i in x:\n            assert json.dumps(i, cls=NumpyEncoder) == f\"{i}\"\n\n\ndef test_dataset_synthesizer_output_feature_decoder():\n    config = {\n        \"input_features\": [{\"name\": \"sentence\", \"type\": \"text\"}],\n        \"output_features\": [{\"name\": \"product\", \"type\": \"category\"}],\n        \"trainer\": {\"epochs\": 5},\n        \"model_type\": \"ecd\",\n    }\n    build_synthetic_dataset_df(dataset_size=100, config=config)\n    LudwigModel(config=config, logging_level=logging.INFO)\n\n\n@pytest.fixture\ndef synthetic_1k_files(tmp_path):\n    \"\"\"Create synthetic 1000-row CSV and Parquet files for chunking tests.\"\"\"\n    df = pd.DataFrame({f\"col_{i}\": range(1000) for i in range(5)})\n    csv_path = str(tmp_path / \"synthetic_1k.csv\")\n    parquet_path = str(tmp_path / \"synthetic_1k.parquet\")\n    df.to_csv(csv_path, index=False)\n    df.to_parquet(parquet_path, index=False)\n    return csv_path, parquet_path\n\n\n@pytest.mark.parametrize(\"fmt_idx\", [0, 1], ids=[\"csv\", \"parquet\"])\n@pytest.mark.parametrize(\"nrows\", [None, 100])\ndef test_chunking(synthetic_1k_files, fmt_idx, nrows):\n    dataset_path = synthetic_1k_files[fmt_idx]\n    reader_fn = {\"csv\": read_csv, \"parquet\": functools.partial(read_parquet, df_lib=PANDAS_DF)}\n\n    fmt = figure_data_format_dataset(dataset_path)\n\n    assert reader_fn[fmt](dataset_path, nrows=nrows).shape[0] == (nrows if nrows else 1000)\n\n\n@pytest.mark.parametrize(\n    \"df_lib\", [pytest.param(pd, id=\"pandas\"), pytest.param(dd, marks=pytest.mark.distributed, id=\"dask\")]\n)\n@pytest.mark.parametrize(\"nrows\", [None, 10])\ndef test_read_html(df_lib, nrows):\n    HTML_DOCUMENT = \"\"\"\n    <!DOCTYPE html>\n    <html>\n    <head><title>TITLE</title></head>\n    <body>\n    <table>\n    <th><td>Col 1</td><td>Col 2</td></th>\n    <tr><td>1</td><td>2</td></tt>\n    </table>\n    </body>\n    </html>\n    \"\"\"\n\n    kwargs = {}\n    if not nrows:\n        kwargs[\"nrows\"] = nrows\n\n    read_html(HTML_DOCUMENT, df_lib, **kwargs)\n\n\ndef test_sanitize_column_names():\n    df = pd.DataFrame(\n        {\n            \"col.one\": [1, 2, 3, 4],\n            \"col(two)\": [4, 5, 6, 7],\n            \"col[]:three\": [7, 8, 9, 10],\n            \"col 'one' (new)\": [1, 2, 3, 4],\n        }\n    )\n\n    df = sanitize_column_names(df)\n\n    assert list(df.columns) == [\"col_one\", \"col_two_\", \"col___three\", \"col _one_ _new_\"]\n"
  },
  {
    "path": "tests/ludwig/utils/test_dataframe_utils.py",
    "content": "import numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.backend import create_backend, LOCAL_BACKEND\nfrom ludwig.utils.dataframe_utils import to_numpy_dataset, to_scalar_df\n\ntry:\n    import dask.dataframe as dd\nexcept ImportError:\n    pass\n\n\n@pytest.mark.distributed\ndef test_to_numpy_dataset_with_dask(ray_cluster_2cpu):\n    dd_df = dd.from_pandas(pd.DataFrame([[1, 2, 3]], columns=[\"col1\", \"col2\", \"col3\"]), npartitions=1)\n    ray_backend = create_backend(\"ray\")\n\n    np_df = to_numpy_dataset(dd_df, backend=ray_backend)\n\n    assert np_df == {\"col1\": np.array([1]), \"col2\": np.array([2]), \"col3\": np.array([3])}\n\n\n@pytest.mark.distributed\ndef test_to_numpy_dataset_with_dask_backend_mismatch():\n    dd_df = dd.from_pandas(pd.DataFrame([[1, 2, 3]], columns=[\"col1\", \"col2\", \"col3\"]), npartitions=1)\n\n    with pytest.raises(AttributeError):\n        to_numpy_dataset(dd_df, backend=LOCAL_BACKEND)\n\n\ndef test_to_numpy_dataset_with_pandas():\n    pd_df = pd.DataFrame([[1, 2, 3]], columns=[\"col1\", \"col2\", \"col3\"])\n\n    np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND)\n\n    assert np_df == {\"col1\": np.array([1]), \"col2\": np.array([2]), \"col3\": np.array([3])}\n\n\ndef test_to_numpy_dataset_empty_with_columns():\n    pd_df = pd.DataFrame(columns=[\"col1\", \"col2\", \"col3\"])\n\n    np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND)\n\n    assert np_df == {\"col1\": [], \"col2\": [], \"col3\": []}\n\n\ndef test_to_numpy_dataset_empty():\n    pd_df = pd.DataFrame()\n\n    np_df = to_numpy_dataset(pd_df, backend=LOCAL_BACKEND)\n\n    assert np_df == {}\n\n\n@pytest.mark.distributed\ndef test_to_numpy_dataset_with_pandas_backend_mismatch(ray_cluster_2cpu):\n    pd_df = pd.DataFrame([[1, 2, 3]], columns=[\"col1\", \"col2\", \"col3\"])\n    ray_backend = create_backend(\"ray\")\n\n    with pytest.raises(AttributeError):\n        to_numpy_dataset(pd_df, backend=ray_backend)\n\n\ndef test_to_scalar_df():\n    data = [\n        [True, np.array([1, 2, 3]), 42],\n        [False, np.array([4, 5, 6]), 28],\n        [True, np.array([7, 8, 9]), 99],\n    ]\n    df = pd.DataFrame(data, columns=[\"bin\", \"cat_encoded\", \"num\"])\n\n    scalar_data = [\n        [True, 1, 2, 3, 42],\n        [False, 4, 5, 6, 28],\n        [True, 7, 8, 9, 99],\n    ]\n    expected_df = pd.DataFrame(scalar_data, columns=[\"bin\", \"cat_encoded_0\", \"cat_encoded_1\", \"cat_encoded_2\", \"num\"])\n\n    scalar_df = to_scalar_df(df)\n    assert scalar_df.columns.tolist() == expected_df.columns.tolist()\n    assert scalar_df.equals(expected_df)\n"
  },
  {
    "path": "tests/ludwig/utils/test_dataset_utils.py",
    "content": "import pandas as pd\n\nfrom ludwig.utils.dataset_utils import get_repeatable_train_val_test_split\n\n\ndef test_get_repeatable_train_val_test_split():\n    # Test adding split with stratify\n    df = pd.DataFrame(\n        [\n            [0, 0],\n            [1, 0],\n            [2, 0],\n            [3, 0],\n            [4, 0],\n            [5, 1],\n            [6, 1],\n            [7, 1],\n            [8, 1],\n            [9, 1],\n            [10, 0],\n            [11, 0],\n            [12, 0],\n            [13, 0],\n            [14, 0],\n            [15, 1],\n            [16, 1],\n            [17, 1],\n            [18, 1],\n            [19, 1],\n        ],\n        columns=[\"input\", \"target\"],\n    )\n    split_df = get_repeatable_train_val_test_split(df, \"target\", random_seed=42)\n    assert split_df.equals(\n        pd.DataFrame(\n            [\n                [7, 1, 0],\n                [16, 1, 0],\n                [5, 1, 0],\n                [14, 0, 0],\n                [19, 1, 0],\n                [6, 1, 0],\n                [11, 0, 0],\n                [18, 1, 0],\n                [1, 0, 0],\n                [10, 0, 0],\n                [2, 0, 0],\n                [15, 1, 0],\n                [0, 0, 0],\n                [17, 1, 1],\n                [12, 0, 1],\n                [8, 1, 2],\n                [4, 0, 2],\n                [13, 0, 2],\n                [3, 0, 2],\n                [9, 1, 2],\n            ],\n            columns=[\"input\", \"target\", \"split\"],\n        )\n    )\n\n    # Test adding split without stratify\n    df = pd.DataFrame(\n        [\n            [0, 0],\n            [1, 0],\n            [2, 0],\n            [3, 0],\n            [4, 0],\n            [5, 1],\n            [6, 1],\n            [7, 1],\n            [8, 1],\n            [9, 1],\n            [10, 0],\n            [11, 0],\n            [12, 0],\n            [13, 0],\n            [14, 0],\n            [15, 1],\n            [16, 1],\n            [17, 1],\n            [18, 1],\n            [19, 1],\n        ],\n        columns=[\"input\", \"target\"],\n    )\n    split_df = get_repeatable_train_val_test_split(df, random_seed=42)\n    assert split_df.equals(\n        pd.DataFrame(\n            [\n                [3, 0, 0],\n                [4, 0, 0],\n                [5, 1, 0],\n                [7, 1, 0],\n                [8, 1, 0],\n                [10, 0, 0],\n                [11, 0, 0],\n                [12, 0, 0],\n                [13, 0, 0],\n                [14, 0, 0],\n                [15, 1, 0],\n                [16, 1, 0],\n                [18, 1, 0],\n                [19, 1, 0],\n                [0, 0, 1],\n                [17, 1, 1],\n                [1, 0, 2],\n                [2, 0, 2],\n                [9, 1, 2],\n                [6, 1, 2],\n            ],\n            columns=[\"input\", \"target\", \"split\"],\n        )\n    )\n\n    # Test needing no change\n    df = pd.DataFrame(\n        [\n            [0, 0, 0],\n            [1, 0, 0],\n            [2, 0, 0],\n            [5, 1, 0],\n            [6, 1, 0],\n            [7, 1, 0],\n            [10, 0, 0],\n            [11, 0, 0],\n            [14, 0, 0],\n            [15, 1, 0],\n            [16, 1, 0],\n            [18, 1, 0],\n            [19, 1, 0],\n            [12, 0, 1],\n            [17, 1, 1],\n            [3, 0, 2],\n            [4, 0, 2],\n            [8, 1, 2],\n            [9, 1, 2],\n            [13, 0, 2],\n        ],\n        columns=[\"input\", \"target\", \"split\"],\n    )\n    split_df = get_repeatable_train_val_test_split(df, \"target\", random_seed=42)\n    assert split_df.equals(\n        pd.DataFrame(\n            [\n                [0, 0, 0],\n                [1, 0, 0],\n                [2, 0, 0],\n                [5, 1, 0],\n                [6, 1, 0],\n                [7, 1, 0],\n                [10, 0, 0],\n                [11, 0, 0],\n                [14, 0, 0],\n                [15, 1, 0],\n                [16, 1, 0],\n                [18, 1, 0],\n                [19, 1, 0],\n                [12, 0, 1],\n                [17, 1, 1],\n                [3, 0, 2],\n                [4, 0, 2],\n                [8, 1, 2],\n                [9, 1, 2],\n                [13, 0, 2],\n            ],\n            columns=[\"input\", \"target\", \"split\"],\n        )\n    )\n\n    # Test adding only validation split\n    df = pd.DataFrame(\n        [\n            [0, 0, 0],\n            [1, 0, 0],\n            [2, 0, 0],\n            [5, 1, 0],\n            [6, 1, 0],\n            [7, 1, 0],\n            [10, 0, 0],\n            [11, 0, 0],\n            [14, 0, 0],\n            [15, 1, 0],\n            [16, 1, 0],\n            [18, 1, 0],\n            [19, 1, 0],\n            [12, 0, 0],\n            [17, 1, 0],\n            [3, 0, 2],\n            [4, 0, 2],\n            [8, 1, 2],\n            [9, 1, 2],\n            [13, 0, 2],\n        ],\n        columns=[\"input\", \"target\", \"split\"],\n    )\n    split_df = get_repeatable_train_val_test_split(df, \"target\", random_seed=42)\n    assert split_df.equals(\n        pd.DataFrame(\n            [\n                [0, 0, 0],\n                [1, 0, 0],\n                [2, 0, 0],\n                [5, 1, 0],\n                [6, 1, 0],\n                [7, 1, 0],\n                [10, 0, 0],\n                [11, 0, 0],\n                [14, 0, 0],\n                [16, 1, 0],\n                [19, 1, 0],\n                [12, 0, 0],\n                [17, 1, 0],\n                [15, 1, 1],\n                [18, 1, 1],\n                [3, 0, 2],\n                [4, 0, 2],\n                [8, 1, 2],\n                [9, 1, 2],\n                [13, 0, 2],\n            ],\n            columns=[\"input\", \"target\", \"split\"],\n        )\n    )\n"
  },
  {
    "path": "tests/ludwig/utils/test_date_utils.py",
    "content": "import datetime\nfrom contextlib import nullcontext as does_not_raise\nfrom typing import Any, ContextManager\n\nimport pytest\n\nfrom ludwig.utils.date_utils import convert_number_to_datetime\n\n\n@pytest.fixture(scope=\"module\")\ndef reference_datetime() -> datetime.datetime:\n    return datetime.datetime.fromtimestamp(1691600953.443032, tz=datetime.UTC).replace(tzinfo=None)\n\n\n@pytest.mark.parametrize(\n    \"timestamp,raises\",\n    [\n        pytest.param(1691600953.443032, does_not_raise(), id=\"float-s\"),\n        pytest.param(1691600953443.032, does_not_raise(), id=\"float-ms\"),\n        pytest.param(1691600953, does_not_raise(), id=\"int-s\"),\n        pytest.param(1691600953443, does_not_raise(), id=\"int-ms\"),\n        pytest.param(\"1691600953.443032\", does_not_raise(), id=\"string[float]-s\"),\n        pytest.param(\"1691600953443.0032\", does_not_raise(), id=\"string[float]-ms\"),\n        pytest.param(\"1691600953\", does_not_raise(), id=\"string[int]-s\"),\n        pytest.param(\"1691600953443\", does_not_raise(), id=\"string[int]-ms\"),\n        pytest.param(\"foo\", pytest.raises(ValueError), id=\"string error\"),\n        pytest.param([1691600953.443032], pytest.raises(ValueError), id=\"list error\"),\n        pytest.param(datetime.datetime(2023, 8, 9, 13, 9, 13), pytest.raises(ValueError), id=\"datetime error\"),\n        pytest.param(None, pytest.raises(ValueError), id=\"NoneType error\"),\n    ],\n)\ndef test_convert_number_to_datetime(reference_datetime: datetime.datetime, timestamp: Any, raises: ContextManager):\n    \"\"\"Ensure that numeric timestamps are correctly converted to datetime objects.\n\n    Args:\n        reference_datetime: A datetime object with the expected date/time\n        timestamp: The timestamp to convert in s or ms\n        raises: context manager to check for expected exceptions\n    \"\"\"\n    with raises:\n        dt = convert_number_to_datetime(timestamp)\n\n        # Check that the returned datetime is accurate to the scale of seconds.\n        assert dt.year == reference_datetime.year\n        assert dt.month == reference_datetime.month\n        assert dt.day == reference_datetime.day\n        assert dt.hour == reference_datetime.hour\n        assert dt.minute == reference_datetime.minute\n        assert dt.second == reference_datetime.second\n"
  },
  {
    "path": "tests/ludwig/utils/test_defaults.py",
    "content": "import copy\n\nimport pytest\n\nfrom ludwig.constants import (\n    CATEGORY,\n    COMBINER,\n    DECODER,\n    DEFAULTS,\n    DEPENDENCIES,\n    DROP_ROW,\n    EARLY_STOP,\n    ENCODER,\n    EXECUTOR,\n    FILL_WITH_MODE,\n    HYPEROPT,\n    INPUT_FEATURES,\n    LOSS,\n    MISSING_VALUE_STRATEGY,\n    MODEL_ECD,\n    MODEL_TYPE,\n    OUTPUT_FEATURES,\n    PREPROCESSING,\n    REDUCE_DEPENDENCIES,\n    REDUCE_INPUT,\n    SCHEDULER,\n    SUM,\n    TIED,\n    TOP_K,\n    TRAINER,\n    TYPE,\n)\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.utils.backward_compatibility import upgrade_config_dict_to_latest_version\nfrom ludwig.utils.misc_utils import merge_dict, set_default_values\nfrom tests.integration_tests.utils import (\n    binary_feature,\n    category_feature,\n    number_feature,\n    sequence_feature,\n    text_feature,\n    vector_feature,\n)\n\nHYPEROPT_CONFIG = {\n    \"parameters\": {\n        \"trainer.learning_rate\": {\n            \"space\": \"loguniform\",\n            \"lower\": 0.001,\n            \"upper\": 0.1,\n        },\n        \"combiner.num_fc_layers\": {\"space\": \"randint\", \"lower\": 2, \"upper\": 6},\n        \"utterance.encoder.norm\": {\"space\": \"grid_search\", \"values\": [\"layer\", \"batch\"]},\n        \"utterance.encoder.dropout\": {\"space\": \"choice\", \"categories\": [0.0001, 0.001, 0.01]},\n        \"utterance.encoder.fc_layers\": {\n            \"space\": \"choice\",\n            \"categories\": [\n                [{\"output_size\": 512}, {\"output_size\": 256}],\n                [{\"output_size\": 512}],\n                [{\"output_size\": 256}],\n            ],\n        },\n    },\n    \"search_alg\": {\"type\": \"variant_generator\"},\n    \"executor\": {\"type\": \"ray\"},\n    \"goal\": \"minimize\",\n}\n\nSCHEDULER_DICT = {\"type\": \"async_hyperband\", \"time_attr\": \"time_total_s\"}\n\n\n@pytest.mark.parametrize(\n    \"use_train,use_hyperopt_scheduler\",\n    [\n        (True, True),\n        (False, True),\n        (True, False),\n        (False, False),\n    ],\n)\ndef test_merge_with_defaults_early_stop(use_train, use_hyperopt_scheduler):\n    all_input_features = [\n        binary_feature(),\n        category_feature(),\n        number_feature(),\n        text_feature(name=\"utterance\"),\n    ]\n    all_output_features = [\n        category_feature(output_feature=True),\n        sequence_feature(output_feature=True),\n        vector_feature(),\n    ]\n\n    # validate config with all features\n    config = {\n        INPUT_FEATURES: all_input_features,\n        OUTPUT_FEATURES: all_output_features,\n        HYPEROPT: HYPEROPT_CONFIG,\n    }\n    config = copy.deepcopy(config)\n\n    if use_train:\n        config[TRAINER] = {\"batch_size\": 42}\n\n    if use_hyperopt_scheduler:\n        # hyperopt scheduler cannot be used with early stopping\n        config[HYPEROPT][EXECUTOR][SCHEDULER] = SCHEDULER_DICT\n\n    merged_config = ModelConfig.from_dict(config).to_dict()\n\n    # When a scheulder is provided, early stopping in the rendered config needs to be disabled to allow the\n    # hyperopt scheduler to manage trial lifecycle.\n    expected = -1 if use_hyperopt_scheduler else ECDTrainerConfig().early_stop\n    assert merged_config[TRAINER][\"early_stop\"] == expected\n\n\ndef test_missing_outputs_drop_rows():\n    config = {\n        INPUT_FEATURES: [category_feature()],\n        OUTPUT_FEATURES: [category_feature(output_feature=True)],\n        DEFAULTS: {CATEGORY: {PREPROCESSING: {MISSING_VALUE_STRATEGY: FILL_WITH_MODE}}},\n    }\n\n    merged_config = ModelConfig.from_dict(config).to_dict()\n\n    global_preprocessing = merged_config[DEFAULTS]\n    input_feature_config = merged_config[INPUT_FEATURES][0]\n    output_feature_config = merged_config[OUTPUT_FEATURES][0]\n\n    assert output_feature_config[PREPROCESSING][MISSING_VALUE_STRATEGY] == DROP_ROW\n\n    assert global_preprocessing[input_feature_config[TYPE]][PREPROCESSING][MISSING_VALUE_STRATEGY] == FILL_WITH_MODE\n    feature_preprocessing = merge_dict(\n        global_preprocessing[output_feature_config[TYPE]][PREPROCESSING], output_feature_config[PREPROCESSING]\n    )\n    assert feature_preprocessing[MISSING_VALUE_STRATEGY] == DROP_ROW\n\n\ndef test_default_model_type():\n    config = {\n        INPUT_FEATURES: [category_feature()],\n        OUTPUT_FEATURES: [category_feature(output_feature=True)],\n    }\n\n    merged_config = ModelConfig.from_dict(config).to_dict()\n\n    assert merged_config[MODEL_TYPE] == MODEL_ECD\n\n\ndef test_set_default_values():\n    config = {\n        INPUT_FEATURES: [number_feature(encoder={\"max_sequence_length\": 10})],\n        OUTPUT_FEATURES: [category_feature(decoder={})],\n    }\n\n    assert TIED not in config[INPUT_FEATURES][0]\n    assert TOP_K not in config[OUTPUT_FEATURES][0]\n    assert DEPENDENCIES not in config[OUTPUT_FEATURES][0]\n    assert REDUCE_INPUT not in config[OUTPUT_FEATURES][0]\n    assert REDUCE_DEPENDENCIES not in config[OUTPUT_FEATURES][0]\n\n    set_default_values(config[INPUT_FEATURES][0], {ENCODER: {TYPE: \"passthrough\"}, TIED: None})\n\n    set_default_values(\n        config[OUTPUT_FEATURES][0],\n        {\n            DECODER: {\n                TYPE: \"classifier\",\n            },\n            TOP_K: 3,\n            DEPENDENCIES: [],\n            REDUCE_INPUT: SUM,\n            REDUCE_DEPENDENCIES: SUM,\n        },\n    )\n\n    assert config[INPUT_FEATURES][0][ENCODER][TYPE] == \"passthrough\"\n    assert config[INPUT_FEATURES][0][TIED] is None\n    assert config[OUTPUT_FEATURES][0][DECODER][TYPE] == \"classifier\"\n    assert config[OUTPUT_FEATURES][0][TOP_K] == 3\n    assert config[OUTPUT_FEATURES][0][DEPENDENCIES] == []\n    assert config[OUTPUT_FEATURES][0][REDUCE_INPUT] == SUM\n    assert config[OUTPUT_FEATURES][0][REDUCE_DEPENDENCIES] == SUM\n\n\ndef test_merge_with_defaults():\n    # configuration with legacy parameters\n    legacy_config_format = {\n        \"ludwig_version\": \"0.4\",\n        INPUT_FEATURES: [\n            {\"type\": \"numerical\", \"name\": \"number_input_feature\", \"encoder\": {\"type\": \"dense\"}},\n            {\n                \"type\": \"image\",\n                \"name\": \"image_input_feature\",\n                \"encoder\": \"stacked_cnn\",\n                \"conv_bias\": True,\n                \"conv_layers\": [\n                    {\"num_filters\": 32, \"pool_size\": 2, \"pool_stride\": 2, \"bias\": False},\n                    {\n                        \"num_filters\": 64,\n                        \"pool_size\": 2,\n                        \"pool_stride\": 2,\n                    },\n                ],\n            },\n        ],\n        OUTPUT_FEATURES: [\n            {\n                \"type\": \"numerical\",\n                \"name\": \"number_output_feature\",\n            },\n        ],\n        \"training\": {\"eval_batch_size\": 0, \"optimizer\": {\"type\": \"adadelta\"}},\n        HYPEROPT: {\n            \"parameters\": {\n                \"training.learning_rate\": {\"space\": \"choice\", \"categories\": [0.0001, 0.001, 0.01]},\n                \"training.early_stop\": {\"space\": \"choice\", \"categories\": [5, 10, 15]},\n                \"number_input_feature.encoder.num_layers\": {\"space\": \"choice\", \"categories\": [2, 3, 4]},\n                \"number_output_feature.decoder.fc_output_size\": {\"space\": \"choice\", \"categories\": [128, 256, 512]},\n                \"number_output_feature.decoder.fc_dropout\": {\"space\": \"uniform\", \"lower\": 0, \"upper\": 1},\n            },\n            \"executor\": {\n                \"type\": \"serial\",\n                \"search_alg\": {TYPE: \"variant_generator\"},\n            },\n            \"sampler\": {\n                \"num_samples\": 99,\n                \"scheduler\": {},\n            },\n        },\n    }\n\n    updated_config = upgrade_config_dict_to_latest_version(legacy_config_format)\n    merged_config = ModelConfig.from_dict(updated_config).to_dict()\n\n    assert len(merged_config[DEFAULTS]) == 13\n    assert ENCODER in merged_config[DEFAULTS][CATEGORY]\n    assert PREPROCESSING in merged_config[DEFAULTS][CATEGORY]\n    assert DECODER in merged_config[DEFAULTS][CATEGORY]\n    assert LOSS in merged_config[DEFAULTS][CATEGORY]\n    assert COMBINER in merged_config\n    assert merged_config[TRAINER][EARLY_STOP] == 5\n    assert SCHEDULER in merged_config[HYPEROPT][EXECUTOR]\n    assert merged_config[HYPEROPT][EXECUTOR][SCHEDULER][\"type\"] == \"fifo\"\n    assert TYPE in merged_config[INPUT_FEATURES][1][ENCODER]\n    assert TYPE in merged_config[OUTPUT_FEATURES][0][DECODER]\n"
  },
  {
    "path": "tests/ludwig/utils/test_error_handling_utils.py",
    "content": "import pytest\n\nfrom ludwig.constants import TRIES\nfrom ludwig.utils.error_handling_utils import default_retry\n\n\ndef test_default_retry_success():\n    ctr = 0\n\n    @default_retry()\n    def flaky_function():\n        nonlocal ctr\n        if ctr < TRIES - 1:\n            ctr += 1\n            raise Exception(f\"Ctr: {ctr} too low.\")\n\n        return\n\n    flaky_function()\n\n\ndef test_default_retry_failure():\n    ctr = 0\n\n    @default_retry()\n    def flaky_function():\n        nonlocal ctr\n        if ctr < TRIES:\n            ctr += 1\n            raise Exception(f\"Ctr: {ctr} too low.\")\n\n        return\n\n    with pytest.raises(Exception):\n        flaky_function()\n\n\ndef test_default_retry_success_custom_num_tries():\n    CUSTOM_TRIES = 3\n    ctr = 0\n\n    @default_retry(tries=CUSTOM_TRIES)\n    def flaky_function():\n        nonlocal ctr\n        if ctr < CUSTOM_TRIES - 1:\n            ctr += 1\n            raise Exception(f\"Ctr: {ctr} too low.\")\n\n        return\n\n    flaky_function()\n"
  },
  {
    "path": "tests/ludwig/utils/test_errors.py",
    "content": "import pickle\n\nfrom ludwig.error import ConfigValidationError, InputDataError\n\n\ndef test_input_data_error_serializeable():\n    err = InputDataError(\n        \"location\", \"category\", \"At least 2 distinct values are required, column only contains ['here']\"\n    )\n\n    loaded_err: InputDataError = pickle.loads(pickle.dumps(err))\n\n    assert loaded_err.column_name == err.column_name\n    assert loaded_err.feature_type == err.feature_type\n    assert loaded_err.message == err.message\n    assert str(err) == str(loaded_err)\n\n\ndef test_config_validation_error_serializeable():\n    err = ConfigValidationError(message=\"At least 2 distinct values are required, column only contains ['here']\")\n\n    loaded_err: ConfigValidationError = pickle.loads(pickle.dumps(err))\n\n    assert loaded_err.message == err.message\n    assert str(err) == str(loaded_err)\n"
  },
  {
    "path": "tests/ludwig/utils/test_fs_utils.py",
    "content": "import logging\nimport os\nimport platform\nimport tempfile\nfrom urllib.parse import quote\n\nimport pytest\n\nfrom ludwig.utils.fs_utils import get_fs_and_path, list_file_names_in_directory, safe_move_directory\n\nlogger = logging.getLogger(__name__)\n\n\ndef create_file(url):\n    _, path = get_fs_and_path(url)\n    logger.info(f\"saving url '{url}' to path '{path}'\")\n    with tempfile.TemporaryDirectory() as tmpdir:\n        file_path = os.path.join(tmpdir, path)\n        os.makedirs(os.path.dirname(file_path))\n        with open(file_path, \"w\"):\n            return path\n\n\n@pytest.mark.filesystem\ndef test_get_fs_and_path_simple():\n    assert create_file(\"http://a/b.jpg\") == os.path.join(\"a\", \"b.jpg\")\n\n\n@pytest.mark.filesystem\ndef test_get_fs_and_path_query_string():\n    assert create_file(\"http://a/b.jpg?c=d\") == os.path.join(\"a\", \"b.jpg\")\n\n\n@pytest.mark.filesystem\ndef test_get_fs_and_path_decode():\n    assert create_file(\"http://a//b%20c.jpg\") == os.path.join(\"a\", \"b c.jpg\")\n\n\n@pytest.mark.filesystem\ndef test_get_fs_and_path_unicode():\n    assert create_file(\"http://a/æ.jpg\") == \"a/æ.jpg\"\n\n\n@pytest.mark.filesystem\n@pytest.mark.skipif(platform.system() == \"Windows\", reason=\"Skipping if windows.\")\ndef test_get_fs_and_path_invalid_linux():\n    invalid_chars = {\n        \"\\x00\": ValueError,\n        \"/\": FileExistsError,\n    }\n    for c, e in invalid_chars.items():\n        url = f\"http://a/{quote(c)}\"\n        with pytest.raises(e):\n            create_file(url)\n\n\n@pytest.mark.filesystem\n@pytest.mark.skipif(platform.system() != \"Windows\", reason=\"Skipping if not windows.\")\ndef test_get_fs_and_path_invalid_windows():\n    invalid_chars = {\n        \"\\x00\": ValueError,\n        \"\\\\\": FileExistsError,\n        \"/\": OSError,\n        \":\": OSError,\n        \"*\": OSError,\n        \"?\": OSError,\n        '\"': OSError,\n        \"<\": OSError,\n        \">\": OSError,\n        \"|\": OSError,\n    }\n    for c, e in invalid_chars.items():\n        url = f\"http://a/{quote(c)}\"\n        with pytest.raises(e):\n            create_file(url)\n\n\n@pytest.mark.filesystem\ndef test_safe_move_directory(tmpdir):\n    src_dir = os.path.join(tmpdir, \"src\")\n    dst_dir = os.path.join(tmpdir, \"dst\")\n\n    os.mkdir(src_dir)\n    os.mkdir(dst_dir)\n\n    with open(os.path.join(src_dir, \"file.txt\"), \"w\") as f:\n        f.write(\"test\")\n\n    safe_move_directory(src_dir, dst_dir)\n\n    assert not os.path.exists(src_dir)\n    assert os.path.exists(os.path.join(dst_dir, \"file.txt\"))\n    with open(os.path.join(dst_dir, \"file.txt\")) as f:\n        assert f.read() == \"test\"\n\n\n@pytest.mark.filesystem\ndef test_list_file_names_in_directory(tmpdir):\n    my_dir = os.path.join(tmpdir, \"my_dir\")\n\n    os.mkdir(my_dir)\n\n    with open(os.path.join(my_dir, \"my_file.txt\"), \"w\") as f:\n        f.write(\"test_0\")\n\n    with open(os.path.join(my_dir, \"my_other_file.txt\"), \"w\") as f:\n        f.write(\"test_1\")\n\n    assert set(list_file_names_in_directory(directory_name=my_dir)) == {\"my_file.txt\", \"my_other_file.txt\"}\n"
  },
  {
    "path": "tests/ludwig/utils/test_heuristics.py",
    "content": "from typing import Any\n\nimport pytest\n\nfrom ludwig.constants import DEFAULTS, ENCODER, TEXT, TRAINABLE, TRAINER, TYPE\nfrom ludwig.schema.model_config import ModelConfig\nfrom ludwig.utils.heuristics import get_auto_learning_rate\n\n\n@pytest.mark.parametrize(\n    \"text_encoder,expected_lr\",\n    [\n        (None, 0.001),\n        ({}, 0.00001),\n        ({\"type\": \"parallel_cnn\"}, 0.0001),\n        ({\"type\": \"bert\"}, 0.00002),\n        ({\"type\": \"bert\", \"trainable\": True}, 0.00001),\n        ({\"type\": \"bert\", \"trainable\": True, \"use_pretrained\": False}, 0.0001),\n    ],\n    ids=[\"no_text\", \"default_electra\", \"parallel_cnn\", \"bert_fixed\", \"bert_trainable\", \"bert_untrained\"],\n)\ndef test_get_auto_learning_rate(text_encoder: dict[str, Any] | None, expected_lr: float):\n    input_features = [{\"name\": \"bin1\", \"type\": \"binary\"}]\n    if text_encoder is not None:\n        input_features.append({\"name\": \"text1\", \"type\": \"text\", \"encoder\": text_encoder})\n\n    config = {\n        \"input_features\": input_features,\n        \"output_features\": [{\"name\": \"bin2\", \"type\": \"binary\"}],\n        TRAINER: {\n            \"train_steps\": 1,\n            \"learning_rate\": \"auto\",\n        },\n        DEFAULTS: {\n            TEXT: {\n                ENCODER: {\n                    # Note that encoder defaults are all or nothing: if the encoder type is overridden, trainable\n                    # here is ignored\n                    TYPE: \"electra\",\n                    TRAINABLE: True,\n                }\n            }\n        },\n    }\n\n    config = ModelConfig.from_dict(config)\n    lr = get_auto_learning_rate(config)\n    assert lr == expected_lr\n"
  },
  {
    "path": "tests/ludwig/utils/test_hf_utils.py",
    "content": "import os\nimport shutil\n\nimport pytest\nfrom transformers import AlbertModel, BertModel, BertTokenizer\n\nfrom ludwig.encoders.text_encoders import ALBERTEncoder, BERTEncoder\nfrom ludwig.utils.hf_utils import (\n    load_pretrained_hf_model_from_hub,\n    load_pretrained_hf_model_with_hub_fallback,\n    upload_folder_to_hfhub,\n)\n\n\n@pytest.mark.parametrize(\n    (\"model\", \"name\"),\n    [\n        (AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME),\n        (BertTokenizer, \"bert-base-uncased\"),\n    ],\n)\ndef test_load_pretrained_hf_model_from_hub(model: type, name: str, tmpdir: os.PathLike):\n    \"\"\"Ensure that the HF models used in ludwig download correctly.\"\"\"\n    cache_dir = os.path.join(tmpdir, name.replace(os.path.sep, \"_\") if name else str(model.__name__))\n    os.makedirs(cache_dir, exist_ok=True)\n    loaded_model = load_pretrained_hf_model_from_hub(model, name, cache_dir=cache_dir, force_download=True)\n    assert isinstance(loaded_model, model)\n    assert os.listdir(cache_dir)\n\n\ndef test_load_pretrained_hf_model_with_hub_fallback(tmpdir):\n    \"\"\"Ensure that the HF models used in ludwig download correctly with S3 or hub fallback.\"\"\"\n    # Don't set env var.\n    _, used_fallback = load_pretrained_hf_model_with_hub_fallback(AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME)\n    assert used_fallback\n\n    # Download the model, load it from tmpdir, and set env var.\n    load_pretrained_hf_model_from_hub(AlbertModel, \"albert-base-v2\").save_pretrained(\n        os.path.join(tmpdir, \"albert-base-v2\")\n    )\n    os.environ[\"LUDWIG_PRETRAINED_MODELS_DIR\"] = f\"file://{tmpdir}\"  # noqa: E231  # Needs to be an absolute path.\n    _, used_fallback = load_pretrained_hf_model_with_hub_fallback(AlbertModel, ALBERTEncoder.DEFAULT_MODEL_NAME)\n    assert not used_fallback\n\n    # Fallback is used for a model that doesn't exist in models directory.\n    _, used_fallback = load_pretrained_hf_model_with_hub_fallback(BertModel, BERTEncoder.DEFAULT_MODEL_NAME)\n    assert used_fallback\n\n    # Clean up.\n    del os.environ[\"LUDWIG_PRETRAINED_MODELS_DIR\"]\n\n\n@pytest.fixture\ndef tmp_folder_with_file(tmpdir):\n    # Create a temporary folder\n    tmp_folder = str(tmpdir.mkdir(\"tmp_folder\"))\n\n    # Create a file within the temporary folder\n    file_path = os.path.join(tmp_folder, \"test_file.txt\")\n    with open(file_path, \"w\") as f:\n        f.write(\"Test content\")\n\n    yield tmp_folder\n\n    # Clean up: Remove the temporary folder and its contents\n    shutil.rmtree(tmp_folder)\n\n\ndef test_upload_folder_to_hfhub_folder_not_exist():\n    with pytest.raises(FileNotFoundError, match=r\"Folder .* does not exist.\"):\n        upload_folder_to_hfhub(\"test_repo\", \"/nonexistent_folder\")\n\n\ndef test_upload_folder_to_hfhub_folder_empty(tmpdir):\n    empty_folder = str(tmpdir.mkdir(\"empty_folder\"))\n    with pytest.raises(ValueError, match=r\"Folder .* is empty.\"):\n        upload_folder_to_hfhub(\"test_repo\", empty_folder)\n\n\ndef test_upload_folder_to_hfhub_folder_is_file(tmpdir):\n    file_path = str(tmpdir.join(\"test_file.txt\"))\n    with open(file_path, \"w\") as f:\n        f.write(\"Test content\")\n    with pytest.raises(ValueError, match=r\"Folder .* is a file. Please provide a folder.\"):\n        upload_folder_to_hfhub(\"test_repo\", file_path)\n\n\ndef test_upload_folder_to_hfhub_invalid_repo_type(tmp_folder_with_file):\n    with pytest.raises(ValueError, match=r\"Invalid repo_type .*\"):\n        upload_folder_to_hfhub(\"test_repo\", tmp_folder_with_file, repo_type=\"invalid_type\")\n"
  },
  {
    "path": "tests/ludwig/utils/test_hyperopt_ray_utils.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nimport pytest\n\ntry:\n    from ray import tune\n\n    from ludwig.hyperopt.execution import get_build_hyperopt_executor\nexcept ImportError:\n    RAY_AVAILABLE = False\nelse:\n    RAY_AVAILABLE = True\n\n# from ludwig.hyperopt.sampling import RayTuneSampler   TDOO: remove\nfrom ludwig.constants import RAY, TYPE\n\nHYPEROPT_PARAMS = {\n    \"test_1\": {\n        \"parameters\": {\n            \"trainer.learning_rate\": {\"space\": \"uniform\", \"lower\": 0.001, \"upper\": 0.1},\n            \"combiner.num_fc_layers\": {\"space\": \"qrandint\", \"lower\": 3, \"upper\": 6, \"q\": 3},\n            \"utterance.cell_type\": {\"space\": \"grid_search\", \"values\": [\"rnn\", \"gru\", \"lstm\"]},\n        },\n    },\n    \"test_2\": {\n        \"parameters\": {\n            \"trainer.learning_rate\": {\n                \"space\": \"loguniform\",\n                \"lower\": 0.001,\n                \"upper\": 0.1,\n                \"base\": 10,\n            },\n            \"combiner.num_fc_layers\": {\"space\": \"randint\", \"lower\": 2, \"upper\": 6},\n            \"utterance.cell_type\": {\"space\": \"choice\", \"categories\": [\"rnn\", \"gru\", \"lstm\"]},\n        },\n    },\n}\n\nif RAY_AVAILABLE:\n    EXPECTED_SEARCH_SPACE = {\n        \"test_1\": {\n            \"trainer.learning_rate\": tune.uniform(0.001, 0.1),\n            \"combiner.num_fc_layers\": tune.qrandint(3, 6, 3),\n            \"utterance.cell_type\": tune.grid_search([\"rnn\", \"gru\", \"lstm\"]),\n        },\n        \"test_2\": {\n            \"trainer.learning_rate\": tune.loguniform(0.001, 0.1),\n            \"combiner.num_fc_layers\": tune.randint(2, 6),\n            \"utterance.cell_type\": tune.choice([\"rnn\", \"gru\", \"lstm\"]),\n        },\n    }\n\n\n@pytest.mark.skipif(not RAY_AVAILABLE, reason=\"Ray is not installed for testing\")\n@pytest.mark.parametrize(\"key\", [\"test_1\", \"test_2\"])\ndef test_grid_strategy(key):\n    hyperopt_test_params = HYPEROPT_PARAMS[key]\n    expected_search_space = EXPECTED_SEARCH_SPACE[key]\n\n    tune_sampler_params = hyperopt_test_params[\"parameters\"]\n\n    hyperopt_executor = get_build_hyperopt_executor(RAY)(\n        tune_sampler_params,\n        \"output_feature\",\n        \"mse\",\n        \"minimize\",\n        \"validation\",\n        search_alg={TYPE: \"variant_generator\"},\n        **{\"type\": \"ray\", \"num_samples\": 2, \"scheduler\": {\"type\": \"fifo\"}}\n    )\n\n    search_space = hyperopt_executor.search_space\n\n    actual_params_keys = search_space.keys()\n    expected_params_keys = expected_search_space.keys()\n\n    for param in search_space:\n        assert isinstance(search_space[param], type(expected_search_space[param]))\n\n    assert actual_params_keys == expected_params_keys\n"
  },
  {
    "path": "tests/ludwig/utils/test_image_utils.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\nfrom collections.abc import Callable\n\nimport pytest\nimport torch\nimport torchvision.transforms.functional as F\n\nfrom ludwig.utils.image_utils import (\n    crop,\n    crop_or_pad,\n    get_class_mask_from_image,\n    get_image_from_class_mask,\n    get_unique_channels,\n    grayscale,\n    is_image_score,\n    num_channels_in_image,\n    pad,\n    read_image_as_tif,\n    resize_image,\n    ResizeChannels,\n)\n\n\n@pytest.mark.parametrize(\"pad_fn\", [pad, torch.jit.script(pad)])\n@pytest.mark.parametrize(\n    \"img,size,padded_img\",\n    [\n        (\n            torch.arange(12, dtype=torch.int).reshape(3, 2, 2),\n            4,\n            torch.Tensor(\n                [\n                    0,\n                    0,\n                    1,\n                    1,\n                    0,\n                    0,\n                    1,\n                    1,\n                    2,\n                    2,\n                    3,\n                    3,\n                    2,\n                    2,\n                    3,\n                    3,\n                    4,\n                    4,\n                    5,\n                    5,\n                    4,\n                    4,\n                    5,\n                    5,\n                    6,\n                    6,\n                    7,\n                    7,\n                    6,\n                    6,\n                    7,\n                    7,\n                    8,\n                    8,\n                    9,\n                    9,\n                    8,\n                    8,\n                    9,\n                    9,\n                    10,\n                    10,\n                    11,\n                    11,\n                    10,\n                    10,\n                    11,\n                    11,\n                ]\n            )\n            .type(torch.int)\n            .reshape(3, 4, 4),\n        )\n    ],\n)\ndef test_pad(pad_fn: Callable, img: torch.Tensor, size: int, padded_img: torch.Tensor):\n    output_img = pad_fn(img, size)\n    assert torch.equal(output_img, padded_img)\n\n\n@pytest.mark.parametrize(\"crop_fn\", [crop, torch.jit.script(crop)])\n@pytest.mark.parametrize(\n    \"img,size,cropped_img\",\n    [\n        (\n            torch.arange(27, dtype=torch.int).reshape(3, 3, 3),\n            2,\n            torch.Tensor([0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22]).type(torch.int).reshape(3, 2, 2),\n        )\n    ],\n)\ndef test_crop(crop_fn: Callable, img: torch.Tensor, size: int, cropped_img: torch.Tensor):\n    output_img = crop_fn(img, size)\n    assert torch.equal(output_img, cropped_img)\n\n\n@pytest.mark.parametrize(\"crop_or_pad_fn\", [crop_or_pad, torch.jit.script(crop_or_pad)])\n@pytest.mark.parametrize(\n    \"img,new_size,expected_img\",\n    [\n        (\n            torch.arange(12, dtype=torch.int).reshape(3, 2, 2),\n            4,\n            torch.Tensor(\n                [\n                    0,\n                    0,\n                    1,\n                    1,\n                    0,\n                    0,\n                    1,\n                    1,\n                    2,\n                    2,\n                    3,\n                    3,\n                    2,\n                    2,\n                    3,\n                    3,\n                    4,\n                    4,\n                    5,\n                    5,\n                    4,\n                    4,\n                    5,\n                    5,\n                    6,\n                    6,\n                    7,\n                    7,\n                    6,\n                    6,\n                    7,\n                    7,\n                    8,\n                    8,\n                    9,\n                    9,\n                    8,\n                    8,\n                    9,\n                    9,\n                    10,\n                    10,\n                    11,\n                    11,\n                    10,\n                    10,\n                    11,\n                    11,\n                ]\n            )\n            .type(torch.int)\n            .reshape(3, 4, 4),\n        ),\n        (\n            torch.arange(27, dtype=torch.int).reshape(3, 3, 3),\n            2,\n            torch.Tensor([0, 1, 3, 4, 9, 10, 12, 13, 18, 19, 21, 22]).type(torch.int).reshape(3, 2, 2),\n        ),\n    ],\n)\ndef test_crop_or_pad(crop_or_pad_fn: Callable, img: torch.Tensor, new_size: int, expected_img: torch.Tensor):\n    output_image = crop_or_pad_fn(img, new_size)\n    assert torch.equal(output_image, expected_img)\n\n\n@pytest.mark.parametrize(\"resize_image_fn\", [resize_image, torch.jit.script(resize_image)])\n@pytest.mark.parametrize(\n    \"img,new_size,resize_method\",\n    [\n        (\n            torch.arange(27, dtype=torch.int).reshape(3, 3, 3),\n            2,\n            \"crop_or_pad\",\n        ),\n        (\n            torch.arange(27, dtype=torch.int).reshape(3, 3, 3),\n            2,\n            \"interpolate\",\n        ),\n    ],\n)\ndef test_resize_image(resize_image_fn: Callable, img: torch.Tensor, new_size: int, resize_method: str):\n    # Get the expected output from the underlying function\n    if resize_method == \"crop_or_pad\":\n        expected_img = crop_or_pad(img, new_size)\n    else:\n        expected_img = F.resize(img, new_size)\n\n    output_img = resize_image_fn(img, new_size, resize_method)\n\n    # Test that resize_image is equivalent to the underlying function output\n    assert torch.equal(output_img, expected_img)\n\n\n@pytest.mark.parametrize(\"grayscale_fn\", [grayscale, torch.jit.script(grayscale)])\n@pytest.mark.parametrize(\n    \"input_img,grayscale_img\",\n    [(torch.arange(12).reshape(3, 2, 2).type(torch.int), torch.Tensor([[[3, 4], [5, 6]]]).type(torch.int))],\n)\ndef test_grayscale(grayscale_fn: Callable, input_img: torch.Tensor, grayscale_img: torch.Tensor):\n    output_img = grayscale_fn(input_img)\n    assert torch.equal(output_img, grayscale_img)\n\n\ndef test_num_channels_in_image():\n    image_2d = torch.randint(0, 1, (10, 10))\n    image_3d = torch.randint(0, 1, (3, 10, 10))\n    assert num_channels_in_image(image_2d) == 1\n    assert num_channels_in_image(image_3d) == 3\n\n    with pytest.raises(ValueError):\n        num_channels_in_image(torch.rand(5))\n        num_channels_in_image(None)\n\n\n@pytest.mark.parametrize(\"image_shape\", [(1, 10, 10), (3, 10, 10), (5, 10, 10)])\n@pytest.mark.parametrize(\"num_channels_expected\", [1, 2, 3, 4])\ndef test_ResizeChannels_module(image_shape, num_channels_expected):\n    image = torch.randint(0, 1, image_shape)\n    fn = ResizeChannels(num_channels_expected)\n    assert fn(image).shape == tuple([num_channels_expected] + list(image_shape[1:]))\n\n\n@pytest.mark.parametrize(\"image_shape\", [(2, 1, 10, 10), (2, 3, 10, 10), (2, 5, 10, 10)])\n@pytest.mark.parametrize(\"num_channels_expected\", [1, 2, 3, 4])\ndef test_ResizeChannels_module_with_batch_dim(image_shape, num_channels_expected):\n    image = torch.randint(0, 1, image_shape)\n    fn = ResizeChannels(num_channels_expected)\n    assert fn(image).shape == tuple([image_shape[0], num_channels_expected] + list(image_shape[2:]))\n\n\ndef test_read_image_as_tif():\n    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\"  # noqa: E501\n    tensor = read_image_as_tif(img_bytes)\n    assert tensor is not None\n    assert tensor.equal(torch.tensor([[[5, 32], [140, 229]]], dtype=torch.uint8))\n\n\n@pytest.mark.parametrize(\n    \"extension, score\",\n    [\n        (\"data.png\", 1),\n        (\"/home/peter/data.jpg\", 1),\n        (\"./data/file.jpeg\", 1),\n        (\"new.tiff\", 1),\n        (\"b.tif\", 1),\n        (\".bmp\", 1),\n        (\"a.gif\", 1),\n        (\"b.tif\", 1),\n        (\"audio.wav\", 0),\n        (\".png/video.mp4\", 0),\n    ],\n)\ndef test_is_image_score(extension: str, score: int):\n    assert is_image_score(extension) == score\n\n\n@pytest.mark.parametrize(\n    \"img_list,num_channels,num_classes,expected_class_map\",\n    [\n        (\n            [\n                torch.Tensor([0, 0, 8, 8, 120, 120, 180, 180, 230, 230, 255, 255]).type(torch.uint8).reshape(3, 2, 2),\n                torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2),\n            ],\n            3,\n            None,\n            torch.Tensor(\n                [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]]\n            ).type(torch.uint8),\n        ),\n        (\n            [\n                torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255])\n                .type(torch.uint8)\n                .reshape(1, 3, 5),\n            ],\n            1,\n            None,\n            torch.Tensor([[0], [255]]).type(torch.uint8),\n        ),\n        (\n            [\n                torch.Tensor([0, 31, 17, 185, 192, 173, 55, 76, 24, 128, 255, 238]).type(torch.uint8).reshape(3, 4),\n            ],\n            1,\n            2,\n            torch.Tensor([[0], [255]]).type(torch.uint8),\n        ),\n    ],\n)\ndef test_unique_channels(\n    img_list: list[torch.Tensor], num_channels: int, num_classes: int, expected_class_map: torch.Tensor\n):\n    channel_class_map = get_unique_channels(img_list, num_channels, num_classes)\n\n    channel_class_map, _ = channel_class_map.sort(dim=0)\n    expected_class_map, _ = expected_class_map.sort(dim=0)\n    assert torch.equal(channel_class_map, expected_class_map)\n\n\n@pytest.mark.parametrize(\n    \"img,channel_class_map,expected_mask\",\n    [\n        (\n            torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2),\n            torch.Tensor(\n                [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]]\n            ).type(torch.uint8),\n            torch.Tensor([2, 3, 4, 5]).type(torch.uint8).reshape(2, 2),\n        ),\n        (\n            torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255])\n            .type(torch.uint8)\n            .reshape(1, 3, 5),\n            torch.Tensor([[0], [255]]).type(torch.uint8),\n            torch.Tensor([0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1]).type(torch.uint8).reshape(3, 5),\n        ),\n        (\n            torch.Tensor([0, 31, 17, 185, 192, 173, 55, 76, 24, 128, 255, 238]).type(torch.uint8).reshape(3, 4),\n            torch.Tensor([[0], [255]]).type(torch.uint8),\n            torch.Tensor([0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1]).type(torch.uint8).reshape(3, 4),\n        ),\n    ],\n)\ndef test_class_mask_from_image(img: torch.Tensor, channel_class_map: torch.Tensor, expected_mask: torch.Tensor):\n    mask = get_class_mask_from_image(channel_class_map, img)\n    assert torch.equal(mask, expected_mask)\n\n\n@pytest.mark.parametrize(\n    \"mask,channel_class_map,expected_img\",\n    [\n        (\n            torch.Tensor([0, 0, 1, 1]).type(torch.uint8).reshape(2, 2),\n            torch.Tensor(\n                [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]]\n            ).type(torch.uint8),\n            torch.Tensor([0, 0, 8, 8, 120, 120, 180, 180, 230, 230, 255, 255]).type(torch.uint8).reshape(3, 2, 2),\n        ),\n        (\n            torch.Tensor([2, 3, 4, 5]).type(torch.uint8).reshape(2, 2),\n            torch.Tensor(\n                [[0, 120, 230], [8, 180, 255], [1, 131, 241], [2, 132, 242], [3, 133, 243], [4, 134, 244]]\n            ).type(torch.uint8),\n            torch.Tensor([1, 2, 3, 4, 131, 132, 133, 134, 241, 242, 243, 244]).type(torch.uint8).reshape(3, 2, 2),\n        ),\n        (\n            torch.Tensor([0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1]).type(torch.uint8).reshape(3, 5),\n            torch.Tensor([[0], [255]]).type(torch.uint8),\n            torch.Tensor([0, 255, 255, 0, 255, 255, 255, 0, 0, 0, 255, 255, 0, 255, 255])\n            .type(torch.uint8)\n            .reshape(1, 3, 5),\n        ),\n    ],\n)\ndef test_image_from_class_mask(mask: torch.Tensor, channel_class_map: torch.Tensor, expected_img: torch.Tensor):\n    img = get_image_from_class_mask(channel_class_map, mask.numpy())\n    assert torch.equal(torch.from_numpy(img), expected_img)\n"
  },
  {
    "path": "tests/ludwig/utils/test_llm_utils.py",
    "content": "import pytest\nimport torch\nfrom transformers import AutoConfig, AutoModelForCausalLM\n\nfrom ludwig.constants import LOGITS, PREDICTIONS, PROBABILITIES\nfrom ludwig.modules.training_hooks import NEFTuneHook\nfrom ludwig.utils.llm_utils import (\n    add_left_padding,\n    create_attention_mask,\n    FALLBACK_CONTEXT_LEN,\n    find_last_matching_index,\n    generate_merged_ids,\n    get_context_len,\n    get_realigned_target_and_prediction_tensors_for_inference,\n    has_padding_token,\n    pad_target_tensor_for_fine_tuning,\n    remove_left_padding,\n)\nfrom ludwig.utils.tokenizers import HFTokenizer\n\npytestmark = [pytest.mark.llm]\n\n# Pad token ID is 1 for OPT even though it uses the GPT2 tokenizer\n# BOS token ID is 2\nTEST_MODEL_NAME = \"hf-internal-testing/tiny-random-OPTForCausalLM\"\n\n\n@pytest.fixture\ndef tokenizer():\n    return HFTokenizer(TEST_MODEL_NAME).tokenizer\n\n\n@pytest.fixture\ndef input_ids():\n    # Provide sample input IDs tensor\n    return torch.tensor([[3, 4, 5], [6, 7, 8]])\n\n\n@pytest.fixture\ndef target_ids():\n    # Provide sample target IDs tensor\n    return torch.tensor([[9, 10, 11], [12, 13, 14]])\n\n\nclass TestSetContextLen:\n    def test_max_sequence_length(self):\n        # Test when 'max_sequence_length' is present in the model configuration\n        config = AutoConfig.from_pretrained(\"huggyllama/llama-7b\")\n        assert get_context_len(config) == config.max_sequence_length\n\n    def test_max_position_embeddings(self):\n        # Test when 'max_position_embeddings' is present in the model configuration\n        config = AutoConfig.from_pretrained(\"huggyllama/llama-7b\")\n        del config.max_sequence_length\n        assert get_context_len(config) == config.max_position_embeddings\n\n    def test_n_positions(self):\n        # Test when 'n_positions' is present in the model configuration\n        config = AutoConfig.from_pretrained(\"hf-internal-testing/tiny-random-GPTJForCausalLM\")\n        assert get_context_len(config) == config.n_positions\n\n    def test_default_value(self):\n        config = AutoConfig.from_pretrained(\"hf-internal-testing/tiny-random-GPTJForCausalLM\")\n        del config.n_positions\n        assert get_context_len(config) == FALLBACK_CONTEXT_LEN\n\n\ndef test_has_padding_token_with_padding_tokens(tokenizer):\n    input_sentence = \"This is an example sentence.\"\n    input_ids = tokenizer([input_sentence])\n    input_ids[\"input_ids\"] = torch.tensor(input_ids[\"input_ids\"])\n    padded_input_ids = torch.nn.functional.pad(input_ids[\"input_ids\"], (10 - len(input_ids[\"input_ids\"]), 1), value=1)\n\n    assert has_padding_token(padded_input_ids, tokenizer)\n\n\ndef test_has_padding_token_without_padding_tokens(tokenizer):\n    input_sentence = \"This is an example sentence.\"\n    input_ids = tokenizer([input_sentence])\n    input_ids[\"input_ids\"] = torch.tensor(input_ids[\"input_ids\"])\n\n    assert not has_padding_token(input_ids[\"input_ids\"], tokenizer)\n\n\n@pytest.mark.parametrize(\n    \"input_ids, expected\",\n    [\n        # No padding\n        (torch.tensor([5]), torch.tensor([5])),\n        (torch.tensor([5, 3]), torch.tensor([5, 3])),\n        # Padding\n        (torch.tensor([1, 5, 5, 3]), torch.tensor([5, 5, 3])),\n        # EOS token\n        (torch.tensor([2, 5, 5, 3]), torch.tensor([2, 5, 5, 3])),\n        # Padding + EOS token\n        (torch.tensor([1, 2, 5, 5, 3]), torch.tensor([2, 5, 5, 3])),\n    ],\n)\ndef test_remove_left_padding(input_ids, expected, tokenizer):\n    assert torch.equal(remove_left_padding(input_ids, tokenizer).squeeze(0), expected)\n\n\n@pytest.mark.parametrize(\n    \"input_ids, max_length, pad_value, expected\",\n    [\n        (torch.tensor([1, 2, 3]), 3, 0, torch.tensor([1, 2, 3])),\n        (torch.tensor([1, 2, 3]), 5, 0, torch.tensor([0, 0, 1, 2, 3])),\n        (torch.tensor([4, 5, 6, 7]), 6, 2, torch.tensor([2, 2, 4, 5, 6, 7])),\n        (torch.tensor([8, 9]), 3, 1, torch.tensor([1, 8, 9])),\n    ],\n)\ndef test_add_left_padding(input_ids, max_length, pad_value, expected):\n    padded = add_left_padding(input_ids, max_length, pad_value).squeeze(0)\n\n    assert torch.equal(padded, expected)\n\n\ndef test_create_attention_mask_last_token_padding(tokenizer):\n    input_ids = torch.tensor([3, 4, tokenizer.pad_token_id])\n    attention_mask = create_attention_mask(input_ids, tokenizer)\n    assert attention_mask[-1] == 1\n\n\n@pytest.mark.parametrize(\n    \"input_ids, expected_output\",\n    [\n        # No padding\n        (torch.tensor([3, 4, 5]), torch.tensor([1, 1, 1])),\n        (torch.tensor([1, 1, 4, 6, 8]), torch.tensor([0, 0, 1, 1, 1])),\n        # All padding\n        (torch.tensor([1, 1, 1]), torch.tensor([0, 0, 1])),\n    ],\n)\ndef test_create_attention_mask(input_ids, expected_output, tokenizer):\n    attention_mask = create_attention_mask(input_ids, tokenizer)\n\n    assert torch.equal(attention_mask, expected_output)\n\n\n@pytest.mark.parametrize(\n    \"tensor_a, tensor_b, expected_index\",\n    [\n        # Matching index at the end\n        (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([6, 7, 8]), 5),\n        # No matching index\n        (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([9, 10]), -1),\n        # Matching index in the middle. Fails because we're only checking the last X elements.\n        (torch.tensor([1, 2, 3, 4, 5, 6, 7, 8]), torch.tensor([4, 5, 6]), -1),\n    ],\n)\ndef test_find_last_matching_index(tensor_a, tensor_b, expected_index):\n    last_matching_index = find_last_matching_index(tensor_a, tensor_b)\n    assert last_matching_index == expected_index\n\n\ndef test_generate_merged_ids_with_target(tokenizer, input_ids, target_ids):\n    # Test case when target_ids is not None\n    merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer)\n    assert torch.equal(merged_ids, torch.tensor([[3, 4, 5, 9, 10, 11, 2], [6, 7, 8, 12, 13, 14, 2]]))\n    assert merged_ids.shape == (2, 7)  # Check the shape of merged_ids\n    assert attention_masks.shape == (2, 7)  # Check the shape of attention_masks\n\n\ndef test_generate_merged_ids_with_max_sequence_length(tokenizer, input_ids, target_ids):\n    # Test case when max_sequence_length is provided\n    max_sequence_length = 5\n    merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer, max_sequence_length)\n    assert merged_ids.shape == (2, 5)  # Check the shape of merged_ids with truncation\n    assert attention_masks.shape == (2, 5)  # Check the shape of attention_masks\n\n\ndef test_generate_merged_ids_padding_removal(tokenizer, input_ids, target_ids):\n    # Test case to check removal of left padding from inputs and targets during merge\n    padding_tokens = torch.tensor([tokenizer.pad_token_id, tokenizer.pad_token_id])\n\n    # Adds 2 padding tokens to the left of input_ids and target_ids individually. Typically, if we just merged this\n    # 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\n    # except for the padding token at the end.\n    input_ids_with_padding = torch.cat((padding_tokens.unsqueeze(0).expand(input_ids.size(0), -1), input_ids), dim=1)\n    target_ids_with_padding = torch.cat((padding_tokens.unsqueeze(0).expand(target_ids.size(0), -1), target_ids), dim=1)\n\n    merged_ids, attention_masks = generate_merged_ids(input_ids_with_padding, target_ids_with_padding, tokenizer)\n\n    assert merged_ids.shape == (2, 7)  # Check the shape of merged_ids\n    assert attention_masks.shape == (2, 7)  # Check the shape of attention_masks\n\n    assert torch.equal(merged_ids[0][:3], input_ids[0])  # Check the input_ids part without padding\n    assert torch.equal(merged_ids[0][3:-1], target_ids[0])  # Check the target_ids part without padding\n    assert torch.equal(merged_ids[0][-1], torch.tensor(tokenizer.eos_token_id))  # Check the padding tokens\n\n    assert torch.all(attention_masks == 1)\n\n\ndef test_generate_merged_ids_returns_tensor(tokenizer, input_ids, target_ids):\n    # Test that the function returns torch.Tensor objects\n    merged_ids, attention_masks = generate_merged_ids(input_ids, target_ids, tokenizer)\n    assert isinstance(merged_ids, torch.Tensor)\n    assert isinstance(attention_masks, torch.Tensor)\n\n\ndef test_pad_target_tensor_for_fine_tuning():\n    of_name = \"out_1\"\n    prediction = {\n        of_name: {PREDICTIONS: torch.tensor([[764, 764, 764, 764, 764, 764, 764, 578, 619, 841, 182, 905, 483, 764]])}\n    }\n\n    # Scenario 1: Entire target tensor was passed into model inputs\n    model_input = torch.tensor([[0, 0, 24, 52, 654, 529, 221, 78, 79, 504, 76, 397, 84, 0]])\n    target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])}\n    expected_target = {of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, 78, 79, 504, 76, 397, 84, 0]])}\n    updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name)\n    assert torch.equal(expected_target[of_name], updated_targets[of_name])\n\n    # Scenario 2: Entire target tensor was not passed into model inputs\n    model_input = torch.tensor([[13, 24, 395, 13, 46, 57, 52, 41, 45, 37, 51, 14, 380, 435]])\n    target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])}\n    expected_target = {\n        of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100]])\n    }\n    updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name)\n    assert torch.equal(expected_target[of_name], updated_targets[of_name])\n\n    # Scenario 3: Partial target tensor was passed into model inputs\n    model_input = torch.tensor([[0, 0, 24, 52, 654, 529, 221, 78, 79, 504, 76, 78, 79, 504]])\n    target = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])}\n    expected_target = {\n        of_name: torch.tensor([[-100, -100, -100, -100, -100, -100, -100, -100, -100, -100, -100, 78, 79, 504]])\n    }\n    updated_targets = pad_target_tensor_for_fine_tuning(target, prediction, model_input, of_name)\n    assert torch.equal(expected_target[of_name], updated_targets[of_name])\n\n\ndef test_get_realigned_target_and_prediction_tensors_for_inference(tokenizer):\n    of_name = \"out_1\"\n    vocab_size = 8\n\n    # Scenario 1: Prediction and target tensors have the same length, so nothing should change\n    targets = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])}\n    predictions = {\n        of_name: {\n            PREDICTIONS: torch.tensor([[78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64),\n            PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32),\n            LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32),\n        }\n    }\n    updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference(\n        targets, predictions, of_name, tokenizer\n    )\n\n    assert targets == updated_targets\n    assert predictions == updated_predictions\n    assert predictions[of_name][PREDICTIONS].shape[1] == targets[of_name].shape[1]\n    assert predictions[of_name][PROBABILITIES].shape[1] == targets[of_name].shape[1]\n    assert predictions[of_name][LOGITS].shape[1] == targets[of_name].shape[1]\n\n    # Scenario 2: Prediction length is longer than the target tensor, so we need to realign the target tensor\n    targets = {of_name: torch.tensor([[78, 79, 504, 76, 397, 84, 0]])}\n    predictions = {\n        of_name: {\n            PREDICTIONS: torch.tensor([[98, 47, 78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64),\n            PROBABILITIES: torch.randn(1, 9, vocab_size).to(torch.float32),\n            LOGITS: torch.randn(1, 9, vocab_size).to(torch.float32),\n        }\n    }\n    updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference(\n        targets, predictions, of_name, tokenizer\n    )\n\n    for key in updated_predictions.keys():\n        assert torch.equal(updated_predictions[key][PREDICTIONS], predictions[key][PREDICTIONS])\n        assert torch.equal(updated_predictions[key][PROBABILITIES], predictions[key][PROBABILITIES])\n        assert torch.equal(updated_predictions[key][LOGITS], predictions[key][LOGITS])\n\n    assert torch.equal(updated_targets[of_name], torch.tensor([[78, 79, 504, 76, 397, 84, 0, 1, 1]]))\n\n    # Scenario 3: Target length is longer than the prediction tensor, so we need to realign them\n    targets = {of_name: torch.tensor([[98, 47, 78, 79, 504, 76, 397, 84, 0]])}\n    predictions = {\n        of_name: {\n            PREDICTIONS: torch.tensor([[78, 79, 504, 76, 397, 84, 0]], dtype=torch.int64),\n            PROBABILITIES: torch.randn(1, 7, vocab_size).to(torch.float32),\n            LOGITS: torch.randn(1, 7, vocab_size).to(torch.float32),\n        }\n    }\n    updated_targets, updated_predictions = get_realigned_target_and_prediction_tensors_for_inference(\n        targets, predictions, of_name, tokenizer\n    )\n\n    assert torch.equal(updated_targets[of_name], targets[of_name])\n\n    assert torch.equal(updated_predictions[of_name][PREDICTIONS], torch.tensor([[78, 79, 504, 76, 397, 84, 0, 1, 1]]))\n    assert updated_predictions[of_name][PROBABILITIES].shape[1] == targets[of_name].shape[1]\n    assert updated_predictions[of_name][LOGITS].shape[1] == targets[of_name].shape[1]\n\n    assert torch.equal(updated_predictions[of_name][PROBABILITIES][0][-1], torch.zeros(vocab_size))\n    assert torch.equal(updated_predictions[of_name][PROBABILITIES][0][-2], torch.zeros(vocab_size))\n    assert not torch.equal(updated_predictions[of_name][PROBABILITIES][0][-3], torch.zeros(vocab_size))\n\n    assert torch.equal(updated_predictions[of_name][LOGITS][0][-1], torch.zeros(vocab_size))\n    assert torch.equal(updated_predictions[of_name][LOGITS][0][-2], torch.zeros(vocab_size))\n    assert not torch.equal(updated_predictions[of_name][LOGITS][0][-3], torch.zeros(vocab_size))\n\n\ndef _setup_models_for_neftune():\n    module_without_hook = AutoModelForCausalLM.from_pretrained(TEST_MODEL_NAME)\n    module_with_hook = AutoModelForCausalLM.from_pretrained(TEST_MODEL_NAME)\n\n    # Only module_with_hook should have the NEFTuneHook\n    neftune_hook = NEFTuneHook(neftune_noise_alpha=5)\n    module_with_hook = neftune_hook.activate_hook(module_with_hook)\n\n    return module_without_hook, module_with_hook\n\n\ndef _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode):\n    assert module_with_hook.get_input_embeddings()._forward_hooks\n    assert not module_without_hook.get_input_embeddings()._forward_hooks\n\n    if mode == \"train\":\n        module_without_hook.train()\n        module_with_hook.train()\n    elif mode == \"eval\":\n        module_without_hook.eval()\n        module_with_hook.eval()\n\n    input_tensor = torch.tensor([[1, 2, 3]])\n    output_tensor_with_noise = module_with_hook.get_input_embeddings()(input_tensor)\n    output_tensor_without_noise = module_without_hook.get_input_embeddings()(input_tensor)\n\n    if mode == \"train\":\n        assert not torch.equal(output_tensor_with_noise, output_tensor_without_noise)\n    elif mode == \"eval\":\n        assert torch.equal(output_tensor_with_noise, output_tensor_without_noise)\n\n\ndef test_neftune_hook_with_noise_alpha_train_mode():\n    \"\"\"Test that the NEFTuneHook is only applied when the module is in training mode.\"\"\"\n    module_without_hook, module_with_hook = _setup_models_for_neftune()\n    _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode=\"train\")\n\n\ndef test_neftune_hook_with_noise_alpha_eval_mode():\n    \"\"\"Test that the NEFTuneHook is not applied when the module is in eval mode.\"\"\"\n    module_without_hook, module_with_hook = _setup_models_for_neftune()\n    _forward_pass_and_assert_neftune_hook(module_without_hook, module_with_hook, mode=\"eval\")\n"
  },
  {
    "path": "tests/ludwig/utils/test_metric_utils.py",
    "content": "from collections import OrderedDict\n\nimport torch\n\nfrom ludwig.utils import metric_utils\nfrom ludwig.utils.metric_utils import TrainerMetric\n\n\ndef test_dynamic_partition():\n    data = torch.Tensor([10, 20, 30, 40, 50])\n    partitions = torch.Tensor([0, 0, 1, 1, 0])\n\n    partitioned_data = metric_utils.dynamic_partition(data, partitions, 2)\n\n    assert torch.equal(partitioned_data[0], torch.Tensor([10.0, 20.0, 50.0]))\n    assert torch.equal(partitioned_data[1], torch.Tensor([30.0, 40.0]))\n\n\ndef test_dynamic_partition_2D():\n    data = torch.Tensor(\n        [\n            [1, 2, 3, 4, 5, 6, 7, 8, 9],\n            [10, 11, 12, 13, 14, 15, 16, 17, 18],\n        ]\n    )\n    partitions = torch.Tensor([[1, 1, 1, 1, 1, 1, 1, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 0]])\n\n    partitioned_data = metric_utils.dynamic_partition(data, partitions, 2)\n\n    assert torch.equal(partitioned_data[0], torch.Tensor([9, 18]))\n    assert torch.equal(\n        partitioned_data[1],\n        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]),\n    )\n\n\ndef test_masked_correct_predictions():\n    preds = torch.tensor([[1, 5, 1, 5, 1, 5, 12, 12, 12], [10, 1, 5, 1, 5, 12, 12, 12, 12]])\n    targets = torch.tensor([[1, 9, 5, 7, 5, 9, 13, 6, 0], [1, 9, 7, 13, 4, 7, 7, 7, 0]])\n    targets_sequence_length = torch.tensor([8, 8])\n\n    result = metric_utils.masked_correct_predictions(targets, preds, targets_sequence_length)\n\n    assert torch.equal(\n        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])\n    )\n\n\ndef test_reduce_trainer_metrics_dict():\n    dict_dict_trainer_metrics = {\n        \"feature_name\": {\"metric_name\": [metric_utils.TrainerMetric(epoch=1, step=2, value=10)]}\n    }\n\n    result = metric_utils.reduce_trainer_metrics_dict(dict_dict_trainer_metrics)\n\n    assert result == {\"feature_name\": {\"metric_name\": [10]}}\n\n\ndef test_reduce_trainer_metrics_dict_ordered_dict():\n    dict_dict_trainer_metrics = OrderedDict(\n        [\n            (\n                \"category_5B6BF\",\n                OrderedDict(\n                    [\n                        (\"loss\", [TrainerMetric(epoch=0, step=1, value=0.0)]),\n                        (\"accuracy\", [TrainerMetric(epoch=0, step=1, value=1.0)]),\n                    ]\n                ),\n            ),\n            (\"combined\", {\"loss\": [TrainerMetric(epoch=0, step=1, value=0.0)]}),\n        ]\n    )\n\n    result = metric_utils.reduce_trainer_metrics_dict(dict_dict_trainer_metrics)\n\n    assert result == {\"category_5B6BF\": {\"accuracy\": [1.0], \"loss\": [0.0]}, \"combined\": {\"loss\": [0.0]}}\n"
  },
  {
    "path": "tests/ludwig/utils/test_model_utils.py",
    "content": "import pytest\nimport torch\nfrom transformers import AutoModelForCausalLM\n\nfrom ludwig.utils.model_utils import (\n    contains_nan_or_inf_tensors,\n    extract_tensors,\n    find_embedding_layer_with_path,\n    replace_tensors,\n)\n\n\nclass SampleModel(torch.nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.conv = torch.nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1)\n        self.relu = torch.nn.ReLU()\n\n\ndef test_extract_tensors():\n    # Create a sample model\n    model = SampleModel()\n\n    # Call extract_tensors function\n    stripped_model, tensors = extract_tensors(model)\n\n    # Assert that the model and tensors are returned\n    assert isinstance(stripped_model, torch.nn.Module)\n    assert isinstance(tensors, list)\n\n    # Assert that the tensors contain the expected keys\n    for tensor_dict in tensors:\n        assert \"params\" in tensor_dict\n        assert \"buffers\" in tensor_dict\n\n    # Assert that all model parameters are set to None\n    for module in stripped_model.modules():\n        for name, param in module.named_parameters(recurse=False):\n            assert param is None\n\n        for name, buf in module.named_buffers(recurse=False):\n            assert buf is None\n\n\ndef test_replace_tensors():\n    # Create a sample model\n    model = SampleModel()\n\n    # Call extract_tensors function to get the tensors\n    _, tensors = extract_tensors(model)\n\n    # Create a new device for testing\n    device = torch.device(\"cpu\")\n\n    # Call replace_tensors function\n    replace_tensors(model, tensors, device)\n\n    # Assert that the tensors are restored\n    for module, tensor_dict in zip(model.modules(), tensors):\n        for name, array in tensor_dict[\"params\"].items():\n            assert name in module._parameters\n            assert torch.allclose(module._parameters[name], torch.as_tensor(array, device=device))\n\n        for name, array in tensor_dict[\"buffers\"].items():\n            assert name in module._buffers\n            assert torch.allclose(module._buffers[name], torch.as_tensor(array, device=device))\n\n\nclass SampleModule(torch.nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.embedding = torch.nn.Embedding(10, 20)\n        self.rnn = torch.nn.LSTM(20, 30)\n\n\ndef test_find_embedding_layer_with_path_simple():\n    # Test case 1: Test the function with a simple module structure\n    module = SampleModule()\n    embedding_layer, path = find_embedding_layer_with_path(module)\n    assert embedding_layer is not None\n    assert isinstance(embedding_layer, torch.nn.Embedding)\n    assert path == \"embedding\"\n\n\ndef test_find_embedding_layer_with_path_complex():\n    # Test case 2: Test the function with a more complex module structure including AutoModelForCausalLM\n    model = AutoModelForCausalLM.from_pretrained(\"HuggingFaceM4/tiny-random-LlamaForCausalLM\")\n\n    embedding_layer, path = find_embedding_layer_with_path(model)\n    assert embedding_layer is not None\n    assert isinstance(embedding_layer, torch.nn.Embedding)\n    assert path == \"model.embed_tokens\"\n\n\ndef test_no_embedding_layer():\n    # Test case 3: Embedding layer is not present\n    no_embedding_model = torch.nn.Sequential(torch.nn.Linear(10, 10), torch.nn.Linear(10, 10))\n    embedding_layer, path = find_embedding_layer_with_path(no_embedding_model)\n    assert embedding_layer is None\n    assert path is None\n\n\nclass TestHasNanOrInfTensors:\n    \"\"\"Test suite for the 'has_nan_or_inf_tensors' function, which checks for NaN or infinity (inf) values in\n    PyTorch tensors.\"\"\"\n\n    class SampleModel(torch.nn.Module):\n        def __init__(self):\n            super().__init__()\n            self.param = torch.nn.Parameter(torch.tensor(1.0, requires_grad=True))\n            self.buffer = torch.nn.Parameter(torch.tensor(1.0, requires_grad=True))\n\n    @pytest.fixture(autouse=True)\n    def setup(self):\n        self.model_with_nan_or_inf = self.SampleModel()\n        self.model_without_nan_or_inf = self.SampleModel()\n        self.transformer_model = AutoModelForCausalLM.from_pretrained(\"HuggingFaceM4/tiny-random-LlamaForCausalLM\")\n\n    def test_has_nan_or_inf_tensors_without_nan_or_inf(self):\n        assert contains_nan_or_inf_tensors(self.model_without_nan_or_inf) is False\n\n    def test_has_nan_or_inf_tensors_with_nan(self):\n        self.model_with_nan_or_inf.param.data = torch.tensor(float(\"nan\"))\n        assert contains_nan_or_inf_tensors(self.model_with_nan_or_inf) is True\n\n    def test_has_nan_or_inf_tensors_without_nan(self):\n        self.model_with_nan_or_inf.buffer.data = torch.tensor(float(\"inf\"))\n        assert contains_nan_or_inf_tensors(self.model_with_nan_or_inf) is True\n\n    def test_has_nan_or_inf_tensors_transformer_model(self):\n        assert contains_nan_or_inf_tensors(self.transformer_model) is False\n\n    def test_has_nan_or_inf_tensors_transformer_model_with_nan(self):\n        self.transformer_model.model.embed_tokens.weight.data[0][0] = float(\"nan\")\n        assert contains_nan_or_inf_tensors(self.transformer_model) is True\n\n    def test_has_nan_or_inf_tensors_transformer_model_with_inf(self):\n        self.transformer_model.model.embed_tokens.weight.data[0][0] = float(\"inf\")\n        assert contains_nan_or_inf_tensors(self.transformer_model) is True\n"
  },
  {
    "path": "tests/ludwig/utils/test_normalization.py",
    "content": "# Copyright (c) 2023 Predibase, Inc., 2019 Uber Technologies, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n# ==============================================================================\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.backend import initialize_backend\nfrom ludwig.constants import COLUMN, NAME, PROC_COLUMN\nfrom ludwig.features.feature_utils import compute_feature_hash\nfrom ludwig.features.number_feature import NumberFeatureMixin, numeric_transformation_registry\nfrom ludwig.utils.types import DataFrame\n\n\ndef number_feature():\n    feature = {NAME: \"x\", COLUMN: \"x\", \"type\": \"number\"}\n    feature[PROC_COLUMN] = compute_feature_hash(feature)\n    return feature\n\n\ndef get_test_data(backend: str) -> tuple[DataFrame, DataFrame]:\n    \"\"\"Returns test data for the given backend.\"\"\"\n    data_df = pd.DataFrame(pd.Series([2, 4, 6, 8, 10]), columns=[\"x\"])\n    proc_df = pd.DataFrame(columns=[\"x\"])\n    if backend == \"ray\":\n        import dask.dataframe as dd\n\n        data_df = dd.from_pandas(data_df, npartitions=1).reset_index()\n        proc_df = dd.from_pandas(proc_df, npartitions=1).reset_index()\n    return data_df, proc_df\n\n\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_norm(backend, ray_cluster_2cpu):\n    data_df, proc_df = get_test_data(backend)\n    backend = initialize_backend(backend)\n\n    feature_1_meta = NumberFeatureMixin.get_feature_meta({}, data_df[\"x\"], {\"normalization\": \"zscore\"}, backend, True)\n    feature_2_meta = NumberFeatureMixin.get_feature_meta({}, data_df[\"x\"], {\"normalization\": \"minmax\"}, backend, True)\n    feature_3_meta = NumberFeatureMixin.get_feature_meta({}, data_df[\"x\"], {\"normalization\": \"iq\"}, backend, True)\n\n    assert feature_1_meta[\"mean\"] == 6\n    assert feature_2_meta[\"min\"] == 2\n    assert feature_2_meta[\"max\"] == 10\n    assert feature_3_meta[\"q1\"] == 4\n    assert feature_3_meta[\"q2\"] == 6\n    assert feature_3_meta[\"q3\"] == 8\n\n    # value checks after normalization\n    num_feature = number_feature()\n\n    NumberFeatureMixin.add_feature_data(\n        feature_config=num_feature,\n        input_df=data_df,\n        proc_df=proc_df,\n        metadata={num_feature[NAME]: feature_1_meta},\n        preprocessing_parameters={\"normalization\": \"zscore\"},\n        backend=backend,\n        skip_save_processed_input=False,\n    )\n    assert np.allclose(\n        np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([-1.26491106, -0.63245553, 0, 0.63245553, 1.26491106])\n    )\n\n    NumberFeatureMixin.add_feature_data(\n        feature_config=num_feature,\n        input_df=data_df,\n        proc_df=proc_df,\n        metadata={num_feature[NAME]: feature_2_meta},\n        preprocessing_parameters={\"normalization\": \"minmax\"},\n        backend=backend,\n        skip_save_processed_input=False,\n    )\n    assert np.allclose(np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([0, 0.25, 0.5, 0.75, 1]))\n\n    NumberFeatureMixin.add_feature_data(\n        feature_config=num_feature,\n        input_df=data_df,\n        proc_df=proc_df,\n        metadata={num_feature[NAME]: feature_3_meta},\n        preprocessing_parameters={\"normalization\": \"iq\"},\n        backend=backend,\n        skip_save_processed_input=False,\n    )\n    assert np.allclose(np.array(proc_df[num_feature[PROC_COLUMN]]), np.array([-1, -0.5, 0, 0.5, 1]))\n\n\n@pytest.mark.parametrize(\"transformation\", numeric_transformation_registry.keys())\n@pytest.mark.parametrize(\n    \"backend\",\n    [\n        pytest.param(\"local\", id=\"local\"),\n        pytest.param(\"ray\", id=\"ray\", marks=pytest.mark.distributed),\n    ],\n)\ndef test_numeric_transformation_registry(transformation, backend, ray_cluster_2cpu):\n    data_df, proc_df = get_test_data(backend)\n    backend = initialize_backend(backend)\n\n    feature_meta = NumberFeatureMixin.get_feature_meta(\n        {}, data_df[\"x\"], {\"normalization\": transformation}, backend, True\n    )\n\n    num_feature = number_feature()\n\n    NumberFeatureMixin.add_feature_data(\n        feature_config=num_feature,\n        input_df=data_df,\n        proc_df=proc_df,\n        metadata={num_feature[NAME]: feature_meta},\n        preprocessing_parameters={\"normalization\": transformation},\n        backend=backend,\n        skip_save_processed_input=False,\n    )\n"
  },
  {
    "path": "tests/ludwig/utils/test_numerical_test_utils.py",
    "content": "import numpy as np\nimport pytest\n\nfrom ludwig.utils.numerical_test_utils import assert_all_finite\n\n\n@pytest.fixture\ndef finite_valued_dict():\n    return {\n        \"scalar\": 1,\n        \"metrics\": {\"val\": 0.2, \"series\": [0.1, 0.2, 0.3], \"ndarray\": np.ones((8, 4, 2))},\n    }\n\n\ndef test_assert_all_finite(finite_valued_dict):\n    assert_all_finite(finite_valued_dict)\n\n\ndef test_fail_with_nan(finite_valued_dict):\n    finite_valued_dict[\"scalar\"] = float(\"nan\")\n    with pytest.raises(Exception):\n        assert_all_finite(finite_valued_dict)\n\n\ndef test_fail_with_inf(finite_valued_dict):\n    finite_valued_dict[\"scalar\"] = float(\"inf\")\n    with pytest.raises(Exception):\n        assert_all_finite(finite_valued_dict)\n\n\ndef test_fail_with_nan_in_list(finite_valued_dict):\n    finite_valued_dict[\"scalar\"] = float(\"nan\")\n    with pytest.raises(Exception):\n        assert_all_finite(finite_valued_dict)\n\n\ndef test_fail_with_nan_in_ndarray(finite_valued_dict):\n    finite_valued_dict[\"metrics\"][\"ndarray\"][0, 0, 1] = np.nan\n    with pytest.raises(Exception):\n        assert_all_finite(finite_valued_dict)\n"
  },
  {
    "path": "tests/ludwig/utils/test_output_feature_utils.py",
    "content": "import pytest\nimport torch\n\nfrom ludwig.utils import output_feature_utils\n\n\ndef test_output_feature_utils():\n    tensor_dict = {}\n    output_feature_utils.set_output_feature_tensor(tensor_dict, \"feature_1\", \"1\", torch.Tensor([1]))\n    output_feature_utils.set_output_feature_tensor(tensor_dict, \"feature_1\", \"10\", torch.Tensor([10]))\n    output_feature_utils.set_output_feature_tensor(tensor_dict, \"feature_2\", \"2\", torch.Tensor([2]))\n    output_feature_utils.set_output_feature_tensor(tensor_dict, \"feature_2\", \"20\", torch.Tensor([20]))\n\n    assert list(tensor_dict.keys()) == [\"feature_1::1\", \"feature_1::10\", \"feature_2::2\", \"feature_2::20\"]\n    assert output_feature_utils.get_output_feature_tensor(tensor_dict, \"feature_1\", \"1\") == torch.Tensor([1])\n    assert list(output_feature_utils.get_single_output_feature_tensors(tensor_dict, \"feature_1\").keys()) == [\"1\", \"10\"]\n    assert list(output_feature_utils.get_single_output_feature_tensors(tensor_dict, \"feature_3\").keys()) == []\n    with pytest.raises(Exception):\n        output_feature_utils.get_output_feature_tensor(tensor_dict, \"feature_1\", \"2\")\n"
  },
  {
    "path": "tests/ludwig/utils/test_server_utils.py",
    "content": "import numpy as np\n\nfrom ludwig.utils.server_utils import NumpyJSONResponse\n\n\ndef test_numpy_json_response():\n    response = NumpyJSONResponse({\"message\": \"Ludwig server is up\"})\n\n    # Test Python builtin data type encoding.\n    assert response.render(None) == b\"null\"\n    assert response.render({}) == b\"{}\"\n    assert response.render(1) == b\"1\"\n    assert response.render(1.0) == b\"1.0\"\n    assert response.render(\"a\") == b'\"a\"'\n    assert response.render([0, 1, 2, 3, 4]) == b\"[0,1,2,3,4]\"\n    assert response.render((0, 1, 2, 3, 4)) == b\"[0,1,2,3,4]\"\n    assert response.render({0, 1, 2, 3, 4}) == b\"[0,1,2,3,4]\"\n    assert response.render({\"a\": \"b\"}) == b'{\"a\":\"b\"}'\n\n    # Test numpy data type encoding\n    for dtype in [np.byte, np.ubyte, np.short, np.ushort, np.int32, np.int64, np.uint, np.longlong, np.ulonglong]:\n        x = np.arange(5, dtype=dtype)\n        assert response.render(x) == b\"[0,1,2,3,4]\"\n        for i in x:\n            assert response.render(i) == f\"{i}\".encode()\n\n    for dtype in [np.half, np.single, np.double, np.longdouble]:\n        x = np.arange(5, dtype=dtype)\n        assert response.render(x) == b\"[0.0,1.0,2.0,3.0,4.0]\"\n        for i in x:\n            assert response.render(i) == f\"{i}\".encode()\n"
  },
  {
    "path": "tests/ludwig/utils/test_state_dict_backward_compatibility.py",
    "content": "from ludwig.utils.state_dict_backward_compatibility import update_state_dict\n\n\ndef test_update_transformer_module_keys():\n    state_dict_with_old_keys = {\n        \"input_features.module_dict.sentence__ludwig.encoder_obj.transformer.embeddings.LayerNorm.bias\": 0.0,\n        \"sentence__ludwig.encoder_obj.transformer.encoder.layer.0.attention.output.LayerNorm.weight\": 0.0,\n        \"module_dict.sentence__ludwig.encoder_obj.transformer.embeddings.word_embeddings.weight\": 0.0,\n    }\n\n    expected_state_dict = {\n        \"input_features.module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.LayerNorm.bias\": 0.0,\n        \"sentence__ludwig.encoder_obj.transformer.module.encoder.layer.0.attention.output.LayerNorm.weight\": 0.0,\n        \"module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.word_embeddings.weight\": 0.0,\n    }\n\n    # Ensures that, for models saved before FreezeModule was added, 'module' is added to the key path.\n    updated_state_dict = update_state_dict(state_dict_with_old_keys)\n    assert updated_state_dict == expected_state_dict\n\n\ndef test_does_not_update_freeze_module():\n    state_dict = {\n        \"module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.LayerNorm.bias\": 0.0,\n        \"sentence__ludwig.encoder_obj.transformer.module.encoder.layer.0.attention.output.LayerNorm.weight\": 0.0,\n        \"module_dict.sentence__ludwig.encoder_obj.transformer.module.embeddings.word_embeddings.weight\": 0.0,\n    }\n\n    # Ensures that models saved with FreezeModule aren't modified.\n    updated_state_dict = update_state_dict(state_dict)\n    assert updated_state_dict == state_dict\n"
  },
  {
    "path": "tests/ludwig/utils/test_strings_utils.py",
    "content": "from collections import defaultdict\n\nimport numpy as np\nimport pandas as pd\nimport pytest\n\nfrom ludwig.schema.features.preprocessing.text import TextPreprocessingConfig\nfrom ludwig.utils import strings_utils\n\n\ndef test_is_number():\n    assert strings_utils.is_number(\"1.1\")\n    assert strings_utils.is_number(\"1.000001\")\n    assert strings_utils.is_number(\"1000001\")\n    assert strings_utils.is_number(\"Nan\")\n    assert strings_utils.is_number(\"NaN\")\n    assert strings_utils.is_number(1)\n    assert strings_utils.is_number(1.1)\n    assert not strings_utils.is_number(\"NaNaaa\")\n\n\ndef test_are_sequential_integers():\n    assert strings_utils.are_sequential_integers([\"1.0\", \"2\", \"3\"])\n    assert strings_utils.are_sequential_integers([\"1\", \"2\", \"3\"])\n    assert not strings_utils.are_sequential_integers([\"1\", \"2\", \"4\"])\n    assert not strings_utils.are_sequential_integers([\"1.1\", \"2\", \"3\"])\n    assert not strings_utils.are_sequential_integers([\"a\", \"2\", \"3\"])\n\n\ndef test_str_to_bool():\n    # Global bool mappings are used.\n    assert strings_utils.str2bool(\"True\")\n    assert strings_utils.str2bool(True)\n    assert strings_utils.str2bool(\"true\")\n    assert not strings_utils.str2bool(\"0\")\n\n    # Error raised if non-mapped value is encountered and no fallback is specified.\n    with pytest.raises(Exception):\n        strings_utils.str2bool(\"bot\")\n\n    # Fallback label is used.\n    assert strings_utils.str2bool(\"bot\", fallback_true_label=\"bot\")\n    assert not strings_utils.str2bool(\"human\", fallback_true_label=\"bot\")\n    assert strings_utils.str2bool(\"human\", fallback_true_label=\"human\")\n    assert not strings_utils.str2bool(\"human\", fallback_true_label=\"Human\")\n\n    # Fallback label is used, strictly as a fallback.\n    assert strings_utils.str2bool(\"True\", fallback_true_label=\"False\")\n\n\ndef test_are_conventional_bools():\n    assert strings_utils.are_conventional_bools([\"True\", \"False\"])\n    assert strings_utils.are_conventional_bools([True, False])\n    assert strings_utils.are_conventional_bools([\"True\", False, True])\n    assert strings_utils.are_conventional_bools([\"T\", \"F\"])\n    assert strings_utils.are_conventional_bools([\"t\", \"f\"])\n    assert not strings_utils.are_conventional_bools([\"True\", \"Fails\"])\n    assert strings_utils.are_conventional_bools([\"0\", \"1\"])\n    assert not strings_utils.are_conventional_bools([\"0\", \"2\"])\n    assert strings_utils.are_conventional_bools([\"1.0\", \"0.0\"])\n    assert not strings_utils.are_conventional_bools([\"high\", \"low\"])\n    assert not strings_utils.are_conventional_bools([\"human\", \"bot\"])\n\n\ndef test_create_vocabulary_chars():\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = TextPreprocessingConfig().to_dict()\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=\"characters\",\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n    )\n    vocab = vocabulary.vocab\n\n    assert len(vocab) == 27\n    assert vocab[strings_utils.SpecialSymbol.START.value] == strings_utils.START_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.STOP.value] == strings_utils.STOP_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.PADDING.value] == strings_utils.PADDING_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL\n\n\ndef test_create_vocabulary_word():\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = TextPreprocessingConfig().to_dict()\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        vocab_file=preprocessing_parameters[\"vocab_file\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n    )\n    vocab = vocabulary.vocab\n\n    assert len(vocab) == 19\n    assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.STOP.value] == strings_utils.STOP_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.PADDING.value] == strings_utils.PADDING_SYMBOL\n    assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL\n\n\ndef test_create_vocabulary_no_special_symbols():\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = TextPreprocessingConfig().to_dict()\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        vocab_file=preprocessing_parameters[\"vocab_file\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n        add_special_symbols=False,\n    )\n    vocab = vocabulary.vocab\n\n    assert len(vocab) == 16\n    assert vocab[strings_utils.SpecialSymbol.UNKNOWN.value] == strings_utils.UNKNOWN_SYMBOL\n\n\ndef test_create_vocabulary_from_hf():\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = TextPreprocessingConfig().to_dict()\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=\"hf_tokenizer\",\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=\"albert-base-v2\",\n    )\n    vocab = vocabulary.vocab\n\n    assert len(vocab) == 30000\n\n\ndef test_create_vocabulary_single_token():\n    data = pd.DataFrame([\"dog\", \"cat\", \"bird\", \"dog\", \"cat\", \"super cat\"])\n    column = data[0]\n\n    vocab, str2idx, str2freq = strings_utils.create_vocabulary_single_token(\n        column,\n        num_most_frequent=10000,\n    )\n\n    assert set(vocab) == {\"dog\", \"cat\", \"bird\", \"super cat\"}\n    assert str2freq == {\"dog\": 2, \"cat\": 2, \"bird\": 1, \"super cat\": 1}\n    assert strings_utils.UNKNOWN_SYMBOL not in str2idx\n\n\ndef test_create_vocabulary_single_token_small_most_frequent():\n    data = pd.DataFrame([\"dog\", \"cat\", \"bird\", \"dog\", \"cat\", \"super cat\"])\n    column = data[0]\n\n    vocab, str2idx, str2freq = strings_utils.create_vocabulary_single_token(column, num_most_frequent=2)\n\n    assert set(vocab) == {\"dog\", \"cat\", strings_utils.UNKNOWN_SYMBOL}\n    assert str2idx[strings_utils.UNKNOWN_SYMBOL] == 0\n    assert str2freq == {\"dog\": 2, \"cat\": 2, strings_utils.UNKNOWN_SYMBOL: 0}\n\n\ndef test_build_sequence_matrix():\n    inverse_vocabulary = {\n        \"<EOS>\": 0,\n        \"<SOS>\": 1,\n        \"<PAD>\": 2,\n        \"<UNK>\": 3,\n        \"a\": 4,\n        \"b\": 5,\n        \"c\": 6,\n    }\n    sequences = pd.core.series.Series([\"a b c\", \"c b a\"])\n    sequence_matrix = strings_utils.build_sequence_matrix(\n        sequences, inverse_vocabulary, tokenizer_type=\"space\", length_limit=10\n    )\n    assert not (\n        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]])\n    ).any()\n\n\n@pytest.mark.parametrize(\n    \"pretrained_model_name_or_path\",\n    [\n        \"bert-base-uncased\",\n        \"gpt2\",\n        \"HuggingFaceH4/zephyr-7b-beta\",\n    ],\n)\ndef test_get_vocabulary_hf(pretrained_model_name_or_path):\n    tokenizer_type = \"hf_tokenizer\"\n    vocab_file = None\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = (\n        TextPreprocessingConfig()\n        .from_dict(\n            {\n                \"tokenizer\": tokenizer_type,\n                \"vocab_file\": vocab_file,\n                \"pretrained_model_name_or_path\": pretrained_model_name_or_path,\n            }\n        )\n        .to_dict()\n    )\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        vocab_file=preprocessing_parameters[\"vocab_file\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n        compute_idf=False,\n        add_special_symbols=False,\n    )\n\n    tokenizer = strings_utils.get_tokenizer(\n        tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n        tokenizer_vocab_file=preprocessing_parameters[\"vocab_file\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n    )\n\n    # check special tokens\n    assert vocabulary.padding_symbol == tokenizer.get_pad_token()\n    assert vocabulary.pad_idx == tokenizer.convert_token_to_id(tokenizer.get_pad_token())\n    assert vocabulary.unknown_symbol == tokenizer.get_unk_token()\n\n    # check all tokens\n    for token, idx in tokenizer.get_vocab().items():\n        assert vocabulary.str2idx[token] == idx\n\n\n@pytest.mark.parametrize(\"compute_idf\", [False, True])\ndef test_create_vocabulary_idf(compute_idf: bool):\n    data = pd.DataFrame([\"Hello, I'm a single sentence!\", \"And another sentence\", \"And the very very last one\"])\n    column = data[0]\n    preprocessing_parameters = TextPreprocessingConfig().to_dict()\n\n    vocabulary = strings_utils.create_vocabulary(\n        column,\n        tokenizer_type=preprocessing_parameters[\"tokenizer\"],\n        num_most_frequent=preprocessing_parameters[\"most_common\"],\n        lowercase=preprocessing_parameters[\"lowercase\"],\n        vocab_file=preprocessing_parameters[\"vocab_file\"],\n        unknown_symbol=preprocessing_parameters[\"unknown_symbol\"],\n        padding_symbol=preprocessing_parameters[\"padding_symbol\"],\n        pretrained_model_name_or_path=preprocessing_parameters[\"pretrained_model_name_or_path\"],\n        compute_idf=compute_idf,\n        add_special_symbols=False,\n    )\n\n    str2idf = vocabulary.str2idf\n\n    if not compute_idf:\n        assert str2idf is None\n        return\n\n    idf2str = defaultdict(set)\n    for k, v in str2idf.items():\n        idf2str[v].add(k)\n    idf_sorted = sorted(idf2str.items(), key=lambda x: x[0])\n    assert len(idf_sorted) == 3\n\n    # Unknown symbol should have the lowest idf as it never appears in any documents\n    assert idf_sorted[0][0] == 0\n    assert idf_sorted[0][1] == {\"<UNK>\"}\n\n    # \"sentence\" and \"and\" should be next, as they appear in two docs each\n    assert idf_sorted[1][0] > idf_sorted[0][0]\n    assert idf_sorted[1][1] == {\"sentence\", \"And\"}\n\n    # finally, every token that only appears once\n    assert idf_sorted[2][0] > idf_sorted[1][0]\n    assert idf_sorted[2][1] == {\n        \",\",\n        \"I\",\n        \"'\",\n        \"one\",\n        \"very\",\n        \"single\",\n        \"the\",\n        \"m\",\n        \"!\",\n        \"last\",\n        \"Hello\",\n        \"a\",\n        \"another\",\n    }\n"
  },
  {
    "path": "tests/ludwig/utils/test_tokenizers.py",
    "content": "from ludwig.utils.tokenizers import EnglishLemmatizeFilterTokenizer, NgramTokenizer, StringSplitTokenizer\n\n\ndef test_ngram_tokenizer():\n    inputs = \"Hello, I'm a single sentence!\"\n    tokenizer = NgramTokenizer(n=2)\n    tokens_expected = [\n        \"Hello,\",\n        \"I'm\",\n        \"a\",\n        \"single\",\n        \"sentence!\",\n        \"Hello, I'm\",\n        \"I'm a\",\n        \"a single\",\n        \"single sentence!\",\n    ]\n    tokens = tokenizer(inputs)\n    assert tokens == tokens_expected\n\n\ndef test_string_split_tokenizer():\n    inputs = \"Multiple,Elements,Are here!\"\n    tokenizer = StringSplitTokenizer(\",\")\n    tokens = tokenizer(inputs)\n    assert tokens == [\"Multiple\", \"Elements\", \"Are here!\"]\n\n\ndef test_english_lemmatize_filter_tokenizer():\n    inputs = \"Hello, I'm a single sentence!\"\n    tokenizer = EnglishLemmatizeFilterTokenizer()\n    tokens = tokenizer(inputs)\n    assert len(tokens) > 0\n"
  },
  {
    "path": "tests/ludwig/utils/test_torch_utils.py",
    "content": "import contextlib\nimport os\nfrom unittest.mock import patch\n\nimport pytest\nimport torch\n\nfrom ludwig.utils.torch_utils import (\n    _get_torch_init_params,\n    _set_torch_init_params,\n    initialize_pytorch,\n    sequence_length_2D,\n    sequence_length_3D,\n)\n\n\n@pytest.mark.parametrize(\"input_sequence\", [[[0, 1, 1], [2, 0, 0], [3, 3, 3]]])\n@pytest.mark.parametrize(\"expected_output\", [[3, 2, 3]])\ndef test_sequence_length_2D(input_sequence: list[list[int]], expected_output: list[int]):\n    output_seq_length = sequence_length_2D(torch.tensor(input_sequence))\n    assert torch.equal(torch.tensor(expected_output), output_seq_length)\n\n\n@pytest.mark.parametrize(\"input_sequence\", [[[[-1, 0, 1], [1, -2, 0]], [[0, 0, 0], [3, 0, -2]]]])\n@pytest.mark.parametrize(\"expected_output\", [[2, 1]])\ndef test_sequence_length_3D(input_sequence: list[list[list[int]]], expected_output: list[int]):\n    input_sequence = torch.tensor(input_sequence, dtype=torch.int32)\n    expected_output = torch.tensor(expected_output, dtype=torch.int32)\n    output_seq_length = sequence_length_3D(input_sequence)\n    assert torch.equal(expected_output, output_seq_length)\n\n\n@contextlib.contextmanager\ndef clean_params():\n    prev = _get_torch_init_params()\n    prev_cuda = os.environ.get(\"CUDA_VISIBLE_DEVICES\")\n    try:\n        _set_torch_init_params(None)\n        if \"CUDA_VISIBLE_DEVICES\" in os.environ:\n            del os.environ[\"CUDA_VISIBLE_DEVICES\"]\n        yield\n    finally:\n        _set_torch_init_params(prev)\n        # Restore CUDA_VISIBLE_DEVICES to prevent contaminating other tests\n        if prev_cuda is not None:\n            os.environ[\"CUDA_VISIBLE_DEVICES\"] = prev_cuda\n        elif \"CUDA_VISIBLE_DEVICES\" in os.environ:\n            del os.environ[\"CUDA_VISIBLE_DEVICES\"]\n\n\n@patch(\"ludwig.utils.torch_utils.torch\")\ndef test_initialize_pytorch_only_once(mock_torch):\n    mock_torch.cuda.is_available.return_value = True\n    mock_torch.cuda.device_count.return_value = 4\n    with clean_params():\n        # During first time initialization, set pytorch parallelism\n        initialize_pytorch(allow_parallel_threads=False)\n        mock_torch.set_num_threads.assert_called_once()\n        mock_torch.set_num_interop_threads.assert_called_once()\n\n        # Reset call counts on all threading calls\n        mock_torch.reset_mock()\n\n        # In the second call to initialization, avoid calling these methods again, as pytorch\n        # will raise an exception\n        initialize_pytorch(allow_parallel_threads=False)\n        mock_torch.set_num_threads.assert_not_called()\n        mock_torch.set_num_interop_threads.assert_not_called()\n\n    # No GPUs were specified, so this should not have been called even once\n    mock_torch.cuda.memory.set_per_process_memory_fraction.assert_not_called()\n\n\n@patch(\"ludwig.utils.torch_utils.torch\")\ndef test_initialize_pytorch_with_gpu_list(mock_torch):\n    # For test purposes, these devices can be anything, we just need to be able to uniquely\n    # identify them.\n    mock_torch.cuda.is_available.return_value = True\n    mock_torch.cuda.device_count.return_value = 4\n    with clean_params():\n        initialize_pytorch(gpus=[1, 2])\n        assert os.environ[\"CUDA_VISIBLE_DEVICES\"] == \"1,2\"\n\n\n@patch(\"ludwig.utils.torch_utils.torch\")\ndef test_initialize_pytorch_with_gpu_string(mock_torch):\n    mock_torch.cuda.is_available.return_value = True\n    mock_torch.cuda.device_count.return_value = 4\n    with clean_params():\n        initialize_pytorch(gpus=\"1,2\")\n        assert os.environ[\"CUDA_VISIBLE_DEVICES\"] == \"1,2\"\n\n\n@patch(\"ludwig.utils.torch_utils.torch\")\ndef test_initialize_pytorch_with_gpu_int(mock_torch):\n    mock_torch.cuda.is_available.return_value = True\n    mock_torch.cuda.device_count.return_value = 4\n    with clean_params():\n        initialize_pytorch(gpus=1)\n        mock_torch.cuda.set_device.assert_called_with(1)\n        assert \"CUDA_VISIBLE_DEVICES\" not in os.environ\n\n\n@patch(\"ludwig.utils.torch_utils.torch\")\ndef test_initialize_pytorch_without_gpu(mock_torch):\n    mock_torch.cuda.is_available.return_value = True\n    mock_torch.cuda.device_count.return_value = 4\n    with clean_params():\n        initialize_pytorch(gpus=-1)\n        assert os.environ[\"CUDA_VISIBLE_DEVICES\"] == \"\"\n"
  },
  {
    "path": "tests/ludwig/utils/test_trainer_utils.py",
    "content": "import sys\nfrom collections import OrderedDict\n\nimport pytest\n\nfrom ludwig.constants import AUTO, BATCH_SIZE, COMBINED, LOSS\nfrom ludwig.features.category_feature import CategoryOutputFeature\nfrom ludwig.features.feature_utils import LudwigFeatureDict\nfrom ludwig.schema.features.category_feature import ECDCategoryOutputFeatureConfig\nfrom ludwig.schema.trainer import ECDTrainerConfig\nfrom ludwig.schema.utils import load_config_with_kwargs\nfrom ludwig.utils import trainer_utils\nfrom ludwig.utils.metric_utils import TrainerMetric\n\n\ndef test_get_latest_metrics_dict():\n    progress_tracker_metrics = OrderedDict(\n        [\n            (\n                \"category_92E9E\",\n                OrderedDict(\n                    [\n                        (\n                            \"loss\",\n                            [\n                                TrainerMetric(epoch=0, step=1, value=0.7929425835609436),\n                                TrainerMetric(epoch=1, step=2, value=0.7906522750854492),\n                            ],\n                        ),\n                        (\n                            \"accuracy\",\n                            [\n                                TrainerMetric(epoch=0, step=1, value=0.4117647111415863),\n                                TrainerMetric(epoch=1, step=2, value=0.4117647111415863),\n                            ],\n                        ),\n                    ]\n                ),\n            ),\n            (\n                \"combined\",\n                {\n                    \"loss\": [\n                        TrainerMetric(epoch=0, step=1, value=0.7929425835609436),\n                        TrainerMetric(epoch=1, step=2, value=0.7906522750854492),\n                    ]\n                },\n            ),\n        ]\n    )\n\n    latest_metrics_dict = trainer_utils.get_latest_metrics_dict(progress_tracker_metrics)\n\n    assert latest_metrics_dict == {\n        \"category_92E9E\": {\"accuracy\": 0.4117647111415863, \"loss\": 0.7906522750854492},\n        \"combined\": {\"loss\": 0.7906522750854492},\n    }\n\n\ndef test_get_latest_metrics_dict_empty():\n    progress_tracker_metrics = OrderedDict(\n        [(\"category_F18D1\", OrderedDict([(\"loss\", []), (\"accuracy\", [])])), (\"combined\", {\"loss\": []})]\n    )\n\n    latest_metrics_dict = trainer_utils.get_latest_metrics_dict(progress_tracker_metrics)\n\n    assert not latest_metrics_dict\n\n\ndef test_progress_tracker_empty():\n    output_features = LudwigFeatureDict()\n    category_feature, _ = load_config_with_kwargs(\n        ECDCategoryOutputFeatureConfig,\n        {\n            \"name\": \"category_feature\",\n            \"type\": \"category\",\n            \"decoder\": {\n                \"type\": \"classifier\",\n            },\n            \"num_classes\": 3,\n            \"input_size\": 10,\n        },\n    )\n    output_features.set(\"category_feature\", CategoryOutputFeature(category_feature, {}))\n\n    progress_tracker = trainer_utils.get_new_progress_tracker(\n        batch_size=5,\n        best_eval_metric_value=0,\n        best_increase_batch_size_eval_metric=0,\n        learning_rate=0.01,\n        output_features=output_features,\n    )\n\n    assert progress_tracker.log_metrics() == {\n        \"batch_size\": 5,\n        \"best_valid_metric\": 0,\n        \"epoch\": 0,\n        \"best_eval_metric_steps\": 0,\n        \"learning_rate\": 0.01,\n        \"num_increases_bs\": 0,\n        \"num_reductions_lr\": 0,\n        \"steps\": 0,\n        \"tune_checkpoint_num\": 0,\n        \"best_eval_metric_checkpoint_number\": 0,\n        \"best_eval_metric_epoch\": 0,\n        \"checkpoint_number\": 0,\n        \"last_improvement_steps\": 0,\n        \"total_tokens_used\": 0,\n    }\n\n\ndef test_progress_tracker():\n    output_features = LudwigFeatureDict()\n    category_feature, _ = load_config_with_kwargs(\n        ECDCategoryOutputFeatureConfig,\n        {\n            \"name\": \"category_feature\",\n            \"type\": \"category\",\n            \"decoder\": {\n                \"type\": \"classifier\",\n            },\n            \"num_classes\": 3,\n            \"input_size\": 10,\n        },\n    )\n    output_features.set(\"category_feature\", CategoryOutputFeature(category_feature, {}))\n\n    progress_tracker = trainer_utils.get_new_progress_tracker(\n        batch_size=5,\n        best_eval_metric_value=0,\n        best_increase_batch_size_eval_metric=0,\n        learning_rate=0.01,\n        output_features=output_features,\n    )\n\n    progress_tracker.validation_metrics[COMBINED][LOSS].append(TrainerMetric(epoch=1, step=10, value=0.1))\n    progress_tracker.validation_metrics[COMBINED][LOSS].append(TrainerMetric(epoch=1, step=20, value=0.2))\n\n    assert progress_tracker.log_metrics() == {\n        \"batch_size\": 5,\n        \"best_eval_metric_checkpoint_number\": 0,\n        \"best_eval_metric_epoch\": 0,\n        \"best_valid_metric\": 0,\n        \"checkpoint_number\": 0,\n        \"epoch\": 0,\n        \"best_eval_metric_steps\": 0,\n        \"learning_rate\": 0.01,\n        \"num_increases_bs\": 0,\n        \"num_reductions_lr\": 0,\n        \"steps\": 0,\n        \"tune_checkpoint_num\": 0,\n        \"validation_metrics.combined.loss\": 0.2,\n        \"last_improvement_steps\": 0,\n        \"total_tokens_used\": 0,\n    }\n\n\ndef test_full_progress_tracker():\n    llm_eval_examples = {\n        \"inputs\": {\"input\": [1, 2, 3]},\n        \"targets\": {\"output\": [1, 2, 3]},\n        \"outputs\": {\"output\": [1, 2, 3]},\n    }\n    progress_tracker = trainer_utils.ProgressTracker(\n        **{\n            BATCH_SIZE: 128,\n            \"best_eval_metric_checkpoint_number\": 7,\n            \"best_eval_metric_epoch\": 6,\n            \"best_eval_metric_steps\": 35,\n            \"best_eval_metric_value\": 0.719,\n            \"last_improvement_steps\": 35,\n            \"best_eval_test_metrics\": {\n                \"Survived\": {\"accuracy\": 0.634, \"loss\": 3.820, \"roc_auc\": 0.598},\n                \"combined\": {\"loss\": 3.820},\n            },\n            \"best_eval_train_metrics\": {\n                \"Survived\": {\"accuracy\": 0.682, \"loss\": 4.006, \"roc_auc\": 0.634},\n                \"combined\": {\"loss\": 4.006},\n            },\n            \"best_eval_validation_metrics\": {\n                \"Survived\": {\"accuracy\": 0.719, \"loss\": 4.396, \"roc_auc\": 0.667},\n                \"combined\": {\"loss\": 4.396},\n            },\n            \"best_increase_batch_size_eval_metric\": sys.float_info.max,\n            \"checkpoint_number\": 12,\n            \"epoch\": 12,\n            \"last_increase_batch_size\": 0,\n            \"last_increase_batch_size_eval_metric_improvement\": 0,\n            \"last_increase_batch_size_steps\": 0,\n            \"last_learning_rate_reduction\": 0,\n            \"last_learning_rate_reduction_steps\": 0,\n            \"learning_rate\": 0.001,\n            \"num_increases_batch_size\": 0,\n            \"num_reductions_learning_rate\": 0,\n            \"steps\": 60,\n            \"test_metrics\": {\n                \"Survived\": {\n                    \"accuracy\": [\n                        [0, 5, 0.651],\n                        [1, 10, 0.651],\n                    ],\n                    \"loss\": [\n                        [0, 5, 4.130],\n                        [1, 10, 4.074],\n                    ],\n                    \"roc_auc\": [\n                        [0, 5, 0.574],\n                        [1, 10, 0.595],\n                    ],\n                },\n                \"combined\": {\n                    \"loss\": [\n                        [0, 5, 4.130],\n                        [1, 10, 4.074],\n                    ]\n                },\n            },\n            \"train_metrics\": {\n                \"Survived\": {\n                    \"accuracy\": [\n                        [0, 5, 0.6875],\n                        [1, 10, 0.6875],\n                    ],\n                    \"loss\": [\n                        [0, 5, 4.417],\n                        [1, 10, 4.344],\n                    ],\n                    \"roc_auc\": [\n                        [0, 5, 0.628],\n                        [1, 10, 0.629],\n                    ],\n                },\n                \"combined\": {\n                    \"loss\": [\n                        [0, 5, 4.417],\n                        [1, 10, 4.344],\n                    ]\n                },\n            },\n            \"tune_checkpoint_num\": 0,\n            \"validation_metrics\": {\n                \"Survived\": {\n                    \"accuracy\": [\n                        [0, 5, 0.696],\n                        [1, 10, 0.696],\n                    ],\n                    \"loss\": [\n                        [0, 5, 4.494],\n                        [1, 10, 4.473],\n                    ],\n                    \"roc_auc\": [\n                        [0, 5, 0.675],\n                        [1, 10, 0.671],\n                    ],\n                },\n                \"combined\": {\n                    \"loss\": [\n                        [0, 5, 4.494],\n                        [1, 10, 4.473],\n                    ]\n                },\n            },\n            \"llm_eval_examples\": llm_eval_examples,\n        }\n    )\n\n    assert progress_tracker.log_metrics() == {\n        BATCH_SIZE: 128,\n        \"best.train_metrics.Survived.accuracy\": 0.682,\n        \"best.train_metrics.Survived.loss\": 4.006,\n        \"best.train_metrics.Survived.roc_auc\": 0.634,\n        \"best.train_metrics.combined.loss\": 4.006,\n        \"best.test_metrics.Survived.accuracy\": 0.634,\n        \"best.test_metrics.Survived.loss\": 3.82,\n        \"best.test_metrics.Survived.roc_auc\": 0.598,\n        \"best.test_metrics.combined.loss\": 3.82,\n        \"best.validation_metrics.Survived.accuracy\": 0.719,\n        \"best.validation_metrics.Survived.loss\": 4.396,\n        \"best.validation_metrics.Survived.roc_auc\": 0.667,\n        \"best.validation_metrics.combined.loss\": 4.396,\n        \"best_eval_metric_checkpoint_number\": 7,\n        \"best_eval_metric_epoch\": 6,\n        \"best_eval_metric_steps\": 35,\n        \"best_valid_metric\": 0.719,\n        \"checkpoint_number\": 12,\n        \"epoch\": 12,\n        \"last_improvement_steps\": 35,\n        \"learning_rate\": 0.001,\n        \"num_increases_bs\": 0,\n        \"num_reductions_lr\": 0,\n        \"steps\": 60,\n        \"test_metrics.Survived.accuracy\": 0.651,\n        \"test_metrics.Survived.loss\": 4.074,\n        \"test_metrics.Survived.roc_auc\": 0.595,\n        \"test_metrics.combined.loss\": 4.074,\n        \"train_metrics.Survived.accuracy\": 0.6875,\n        \"train_metrics.Survived.loss\": 4.344,\n        \"train_metrics.Survived.roc_auc\": 0.629,\n        \"train_metrics.combined.loss\": 4.344,\n        \"tune_checkpoint_num\": 0,\n        \"validation_metrics.Survived.accuracy\": 0.696,\n        \"validation_metrics.Survived.loss\": 4.473,\n        \"validation_metrics.Survived.roc_auc\": 0.671,\n        \"validation_metrics.combined.loss\": 4.473,\n        \"llm_eval_examples\": {\n            \"inputs\": {\"input\": [1, 2, 3]},\n            \"targets\": {\"output\": [1, 2, 3]},\n            \"outputs\": {\"output\": [1, 2, 3]},\n        },\n        \"total_tokens_used\": 0,\n    }\n\n\ndef test_get_final_steps_per_checkpoint():\n    # steps_per_checkpoint and checkpoints_per_epoch cannot both be specified.\n    with pytest.raises(Exception):\n        trainer_utils.get_final_steps_per_checkpoint(\n            steps_per_epoch=1024,\n            steps_per_checkpoint=1,\n            checkpoints_per_epoch=1,\n        )\n\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, steps_per_checkpoint=100) == 100\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, steps_per_checkpoint=2048) == 1024\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=2) == 512\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=2.5) == 409\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024, checkpoints_per_epoch=0.5) == 1024\n    assert trainer_utils.get_final_steps_per_checkpoint(steps_per_epoch=1024) == 1024\n    assert (\n        trainer_utils.get_final_steps_per_checkpoint(\n            steps_per_epoch=1024, steps_per_checkpoint=0, checkpoints_per_epoch=0\n        )\n        == 1024\n    )\n\n\n@pytest.mark.parametrize(\n    \"effective_batch_size,batch_size,gradient_accumulation_steps,num_workers,expected_batch_size,expected_grad_accum\",\n    [\n        (128, 16, 4, 2, 16, 4),\n        (AUTO, 16, 4, 2, 16, 4),\n        (128, 16, AUTO, 2, 16, 4),\n        (128, AUTO, 4, 2, 16, 4),\n        (128, AUTO, AUTO, 2, AUTO, AUTO),\n        (AUTO, AUTO, AUTO, 2, AUTO, AUTO),\n        (AUTO, 16, AUTO, 2, 16, 1),\n        (AUTO, AUTO, 4, 2, AUTO, 4),\n    ],\n)\ndef test_get_rendered_batch_size_grad_accum(\n    effective_batch_size: str | int,\n    batch_size: str | int,\n    gradient_accumulation_steps: str | int,\n    num_workers: int,\n    expected_batch_size: int,\n    expected_grad_accum: int,\n):\n    config = ECDTrainerConfig.from_dict(\n        {\n            \"effective_batch_size\": effective_batch_size,\n            \"batch_size\": batch_size,\n            \"gradient_accumulation_steps\": gradient_accumulation_steps,\n        }\n    )\n    rendered_batch_size, rendered_grad_accum = trainer_utils.get_rendered_batch_size_grad_accum(config, num_workers)\n    assert rendered_batch_size == expected_batch_size\n    assert rendered_grad_accum == expected_grad_accum\n"
  },
  {
    "path": "tests/ludwig/utils/test_upload_utils.py",
    "content": "from __future__ import annotations\n\nimport logging\nimport pathlib\nimport shutil\n\nimport pytest\n\nfrom ludwig.globals import MODEL_FILE_NAME, MODEL_HYPERPARAMETERS_FILE_NAME, MODEL_WEIGHTS_FILE_NAME\nfrom ludwig.utils.upload_utils import HuggingFaceHub\n\nlogger = logging.getLogger(__name__)\n\n\ndef _build_fake_model_repo(\n    destination_directory: str,\n    experiment_name: str,\n    file_names: list[str],\n    *,\n    model_directory_name: str = MODEL_FILE_NAME,\n    model_weights_directory_name: str = MODEL_WEIGHTS_FILE_NAME,\n) -> None:\n    \"\"\"This utility function accepts the \"destination_directory\" and list of file names on input.\n\n    It then makes directory hierarchy \"my_simple_experiment_run\" / \"model\" / \"model_weights\" under\n    \"destination_directory\" and creates empty files for each file name specified in bottom-most (leaf) directory (file\n    names must be leaf file names, not paths).\n    \"\"\"\n    # Create a temporary folder designating training output directory.\n    model_directory: pathlib.Path = pathlib.Path(destination_directory) / experiment_name / model_directory_name\n    model_weights_directory: pathlib.Path = model_directory / model_weights_directory_name\n    model_weights_directory.mkdir(parents=True, exist_ok=True)\n\n    # Create files within the \"model_weights\" subdirectory.\n    file_name: str\n    for file_name in file_names:\n        pathlib.Path(model_weights_directory / file_name).touch()\n    pathlib.Path(model_directory / MODEL_HYPERPARAMETERS_FILE_NAME).touch()\n\n\n@pytest.fixture\ndef output_directory_manager(tmpdir) -> str:\n    \"\"\"This convenience fixture creates temporary directory \"training_results_output\" and yields it to user test\n    functions.\n\n    When the user test functions complete their execution, this fixture resumes and cleans up the temporary directory.\n    \"\"\"\n    # Create a temporary folder designating training output directory.\n    output_directory: str = str(tmpdir.mkdir(\"training_results_output\"))\n\n    yield output_directory\n\n    # Clean up: Remove the temporary output directory and its contents.\n    shutil.rmtree(output_directory)\n\n\n@pytest.mark.parametrize(\n    \"file_names,error_raised\",\n    [\n        pytest.param(\n            [\n                \"pytorch_model.bin\",\n            ],\n            None,\n            id=\"pretrained_model_weights_bin\",\n        ),\n        pytest.param(\n            [\n                \"adapter_model.bin\",\n            ],\n            None,\n            id=\"adapter_model_weights_bin_unmerged\",  # backward compatibility for peft versions < 0.7.0\n        ),\n        pytest.param(\n            [\n                \"adapter_model.safetensors\",\n            ],\n            None,\n            id=\"adapter_model_weights_safetensors_unmerged\",\n        ),\n        pytest.param(\n            [\n                \"adapter_model.bin\",\n                \"adapter_model.safetensors\",\n            ],\n            None,\n            id=\"adapter_model_weights_bin_and_safetensors_unmerged\",  # backward compatibility for peft versions < 0.7.0\n        ),\n        pytest.param(\n            [\n                \"pytorch_model.bin\",\n                \"adapter_model.safetensors\",\n            ],\n            None,\n            id=\"pretrained_model_weights_bin_and_adapter_model_weights_safetensors_merged\",\n        ),\n        pytest.param(\n            [],\n            (\n                ValueError,\n                \"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.\",  # noqa E501\n            ),\n            id=\"model_weights_missing\",\n        ),\n        pytest.param(\n            [\n                \"pytorch_model.safetensors\",\n            ],\n            (\n                ValueError,\n                \"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.\",  # noqa E501\n            ),\n            id=\"model_weights_unexpected_name_format_combination\",\n        ),\n        pytest.param(\n            [\n                \"pytorch_model.unkn\",\n            ],\n            (\n                ValueError,\n                \"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.\",  # noqa E501\n            ),\n            id=\"model_weights_unrecognized_format\",\n        ),\n        pytest.param(\n            [\n                \"unknown_model.safetensors\",\n            ],\n            (\n                ValueError,\n                \"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.\",  # noqa E501\n            ),\n            id=\"model_weights_unrecognized_name\",\n        ),\n    ],\n)\n@pytest.mark.unit\ndef test_upload_to_hf_hub__validate_upload_parameters(\n    output_directory_manager, file_names: list[str], error_raised: tuple[type, str] | None\n):\n    \"\"\"Test \"HuggingFaceHub._validate_upload_parameters()\", which is executed in the path of upload to HuggingFace\n    Hub; for example: `upload hf_hub -repo_id \"hf-account/repo-name\" --model_path.\n\n    /content/results/api_experiment_run`.\n\n    Each test case consists of: 1) Populating the temporary output directory (\"training_results_output) with zero or\n    more test model weights file; 2) Executing \"HuggingFaceHub._validate_upload_parameters()\"; and 3) Asserting on\n    presence/absence of errors.\n    \"\"\"\n    output_directory: str = output_directory_manager\n    _build_fake_model_repo(\n        destination_directory=output_directory, experiment_name=\"my_simple_experiment_run\", file_names=file_names\n    )\n\n    model_path: pathlib.Path = pathlib.Path(output_directory) / \"my_simple_experiment_run\"\n    model_weights_path: pathlib.Path = pathlib.Path(model_path / MODEL_FILE_NAME / MODEL_WEIGHTS_FILE_NAME)\n\n    repo_id: str = \"test_account/test_repo\"\n    model_path: str = str(model_path)\n    if error_raised:\n        error_class: type  # noqa [F842]  # incorrect flagging of \"local variable is annotated but never used\n        error_message: str  # noqa [F842]  # incorrect flagging of \"local variable is annotated but never used\n        error_class, error_message = error_raised\n        with pytest.raises(error_class) as excinfo:\n            HuggingFaceHub._validate_upload_parameters(\n                repo_id=repo_id,\n                model_path=model_path,\n            )\n\n        assert str(excinfo.value) == error_message.format(model_weights_path=model_weights_path)\n    else:\n        try:\n            HuggingFaceHub._validate_upload_parameters(\n                repo_id=repo_id,\n                model_path=model_path,\n            )\n        except Exception as exc:\n            assert False, f'\"HuggingFaceHub._validate_upload_parameters()\" raised an exception: \"{exc}\".'\n"
  },
  {
    "path": "tests/ludwig/utils/test_version_transformation.py",
    "content": "from ludwig.utils.version_transformation import VersionTransformation, VersionTransformationRegistry\n\n\ndef test_version_transformation_registry():\n    def transform_a(config):\n        config[\"b\"] = config[\"a\"]\n        del config[\"a\"]\n        return config\n\n    def transform_b(config):\n        config[\"c\"] = config[\"b\"]\n        del config[\"b\"]\n        return config\n\n    def transform_e(e):\n        e[\"g\"] = e[\"f\"]\n        del e[\"f\"]\n        return e\n\n    transformation_registry = VersionTransformationRegistry()\n    transformation_registry.register(VersionTransformation(transform=transform_a, version=\"0.1\"))\n    transformation_registry.register(VersionTransformation(transform=transform_b, version=\"0.2\"))\n    transformation_registry.register(VersionTransformation(transform=transform_e, version=\"0.2\", prefixes=[\"e\"]))\n    input_config = {\"a\": \"a value\", \"e\": {\"f\": \"f_value\"}}\n\n    transformed_0_1 = transformation_registry.update_config(input_config, from_version=\"0.0\", to_version=\"0.1\")\n    assert \"a\" not in transformed_0_1\n    assert transformed_0_1[\"b\"] == \"a value\"\n\n    transformed_0_2 = transformation_registry.update_config(input_config, from_version=\"0.0\", to_version=\"0.2\")\n    assert \"a\" not in transformed_0_2\n    assert \"b\" not in transformed_0_2\n    assert transformed_0_2[\"c\"] == \"a value\"\n    assert \"e\" in transformed_0_2\n    assert \"f\" not in transformed_0_2[\"e\"]\n    assert transformed_0_2[\"e\"][\"g\"] == \"f_value\"\n\n\ndef test_version_transformation_order():\n    v1 = VersionTransformation(transform=lambda x: x, version=\"0.1\")\n    v2 = VersionTransformation(transform=lambda x: x, version=\"0.2\")\n    v3 = VersionTransformation(transform=lambda x: x, version=\"0.10\")\n\n    assert v1 < v2\n    assert v1 < v3\n    assert v2 < v3\n"
  },
  {
    "path": "tests/regression_tests/automl/golden/adult_census_income.types.json",
    "content": "[\n    {\n        \"column\": \"age\",\n        \"name\": \"age\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"workclass\",\n        \"name\": \"workclass\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"fnlwgt\",\n        \"name\": \"fnlwgt\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"education\",\n        \"name\": \"education\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"education-num\",\n        \"name\": \"education-num\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"marital-status\",\n        \"name\": \"marital-status\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"occupation\",\n        \"name\": \"occupation\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"relationship\",\n        \"name\": \"relationship\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"race\",\n        \"name\": \"race\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"sex\",\n        \"name\": \"sex\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"capital-gain\",\n        \"name\": \"capital-gain\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"capital-loss\",\n        \"name\": \"capital-loss\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"hours-per-week\",\n        \"name\": \"hours-per-week\",\n        \"type\": \"number\"\n    },\n    {\n        \"column\": \"native-country\",\n        \"name\": \"native-country\",\n        \"type\": \"category\"\n    },\n    {\n        \"column\": \"income\",\n        \"name\": \"income\",\n        \"type\": \"category\"\n    }\n]\n"
  },
  {
    "path": "tests/regression_tests/automl/golden/mnist.types.json",
    "content": "[\n    {\n        \"column\": \"image_path\",\n        \"encoder\": {\n            \"type\": \"stacked_cnn\"\n        },\n        \"name\": \"image_path\",\n        \"type\": \"image\"\n    },\n    {\n        \"column\": \"label\",\n        \"name\": \"label\",\n        \"type\": \"category\"\n    }\n]\n"
  },
  {
    "path": "tests/regression_tests/automl/scripts/update_golden_types.py",
    "content": "#!/usr/bin/env python\n\"\"\"This script updates all golden JSON files containing expected data types.\"\"\"\n\nimport json\n\nfrom ludwig.automl import create_auto_config\nfrom tests.regression_tests.automl.utils import get_dataset_golden_types_path, get_dataset_object, TEST_DATASET_REGISTRY\n\n\ndef write_json_files():\n    for dataset_name in TEST_DATASET_REGISTRY:\n        dataset_obj = get_dataset_object(dataset_name)\n        dataset = dataset_obj.load(split=False)\n\n        # NOTE: assuming type inference for input and output features is the same\n        config = create_auto_config(\n            dataset=dataset,\n            target=[],\n            time_limit_s=3600,\n        )\n\n        golden_types_path = get_dataset_golden_types_path(dataset_name)\n        with open(golden_types_path, \"w\") as f:\n            json.dump(config[\"input_features\"], f, indent=4, sort_keys=True)\n            f.write(\"\\n\")\n\n\nif __name__ == \"__main__\":\n    write_json_files()\n"
  },
  {
    "path": "tests/regression_tests/automl/test_auto_type_inference.py",
    "content": "import json\n\nimport pytest\n\nfrom tests.regression_tests.automl.utils import get_dataset_golden_types_path, get_dataset_object, TEST_DATASET_REGISTRY\n\ntry:\n    from ludwig.automl import create_auto_config\nexcept ImportError:\n    pass\n\n\n@pytest.mark.slow\n@pytest.mark.distributed  # ludwig.automl has a dependency on ray\n@pytest.mark.parametrize(\"dataset_name\", TEST_DATASET_REGISTRY)\ndef test_auto_type_inference_regression(dataset_name):\n    golden_types_path = get_dataset_golden_types_path(dataset_name)\n    with open(golden_types_path) as f:\n        golden_types = json.load(f)\n\n    dataset_obj = get_dataset_object(dataset_name)\n    dataset = dataset_obj.load(split=False)\n\n    # NOTE: assuming type inference for input and output features is the same\n    config = create_auto_config(\n        dataset=dataset,\n        target=[],\n        time_limit_s=3600,\n    )\n\n    assert golden_types == config[\"input_features\"]\n"
  },
  {
    "path": "tests/regression_tests/automl/utils.py",
    "content": "from pathlib import Path\n\nimport ludwig.datasets\nfrom ludwig.datasets.loaders.dataset_loader import DatasetLoader\n\n# Subset of Ludwig Dataset Zoo used for AutoML type inference regression tests.\nTEST_DATASET_REGISTRY = {\"adult_census_income\", \"mnist\"}\n\n\ndef get_dataset_golden_types_path(dataset_name: str) -> str:\n    \"\"\"Returns the path to the golden types file for the given dataset.\"\"\"\n    return str(Path(__file__).resolve().parent / \"golden\" / f\"{dataset_name}.types.json\")\n\n\ndef get_dataset_object(dataset_name: str) -> DatasetLoader:\n    \"\"\"Returns a Ludwig dataset instance for the given dataset.\"\"\"\n    return ludwig.datasets.get_dataset(dataset_name)\n"
  },
  {
    "path": "tests/regression_tests/benchmark/configs/adult_census_income.ecd.yaml",
    "content": "combiner:\n  type: tabnet\ndefaults:\n  number:\n    preprocessing:\n      missing_value_strategy: fill_with_const\n      normalization: null\ninput_features:\n  - name: age\n    type: number\n  - name: workclass\n    type: category\n  - name: fnlwgt\n    type: number\n  - name: education\n    type: category\n  - name: education-num\n    type: number\n  - name: marital-status\n    type: category\n  - name: occupation\n    type: category\n  - name: relationship\n    type: category\n  - name: race\n    type: category\n  - name: sex\n    type: category\n  - name: capital-gain\n    type: number\n  - name: capital-loss\n    type: number\n  - name: hours-per-week\n    type: number\n  - name: native-country\n    type: category\noutput_features:\n  - name: income\n    type: category\ntrainer:\n  batch_size: 1345\n  eval_batch_size: 16384\n  evaluate_training_set: false\n  learning_rate: 0.02714507227517137\n"
  },
  {
    "path": "tests/regression_tests/benchmark/configs/ames_housing.ecd.yaml",
    "content": "combiner:\n  type: tabnet\ndefaults:\n  number:\n    preprocessing:\n      missing_value_strategy: fill_with_mean\n      normalization: null\ninput_features:\n  - name: MSSubClass\n    type: category\n  - name: MSZoning\n    type: category\n  - name: LotFrontage\n    type: number\n  - name: LotArea\n    type: number\n  - name: Street\n    type: category\n  - name: Alley\n    type: category\n  - name: LotShape\n    type: category\n  - name: LandContour\n    type: category\n  - name: Utilities\n    type: category\n  - name: LotConfig\n    type: category\n  - name: LandSlope\n    type: category\n  - name: Neighborhood\n    type: category\n  - name: Condition1\n    type: category\n  - name: Condition2\n    type: category\n  - name: BldgType\n    type: category\n  - name: HouseStyle\n    type: category\n  - name: OverallQual\n    type: category\n  - name: OverallCond\n    type: category\n  - name: YearBuilt\n    type: number\n  - name: YearRemodAdd\n    type: number\n  - name: RoofStyle\n    type: category\n  - name: RoofMatl\n    type: category\n  - name: Exterior1st\n    type: category\n  - name: Exterior2nd\n    type: category\n  - name: MasVnrType\n    type: category\n  - name: MasVnrArea\n    type: number\n  - name: ExterQual\n    type: category\n  - name: ExterCond\n    type: category\n  - name: Foundation\n    type: category\n  - name: BsmtQual\n    type: category\n  - name: BsmtCond\n    type: category\n  - name: BsmtExposure\n    type: category\n  - name: BsmtFinType1\n    type: category\n  - name: BsmtFinSF1\n    type: number\n  - name: BsmtFinType2\n    type: category\n  - name: BsmtFinSF2\n    type: number\n  - name: BsmtUnfSF\n    type: number\n  - name: TotalBsmtSF\n    type: number\n  - name: Heating\n    type: category\n  - name: HeatingQC\n    type: category\n  - name: CentralAir\n    type: binary\n  - name: Electrical\n    type: category\n  - name: 1stFlrSF\n    type: number\n  - name: 2ndFlrSF\n    type: number\n  - name: LowQualFinSF\n    type: number\n  - name: GrLivArea\n    type: number\n  - name: BsmtFullBath\n    type: number\n  - name: BsmtHalfBath\n    type: number\n  - name: FullBath\n    type: number\n  - name: HalfBath\n    type: number\n  - name: BedroomAbvGr\n    type: number\n  - name: KitchenAbvGr\n    type: number\n  - name: KitchenQual\n    type: category\n  - name: TotRmsAbvGrd\n    type: number\n  - name: Functional\n    type: category\n  - name: Fireplaces\n    type: number\n  - name: FireplaceQu\n    type: category\n  - name: GarageType\n    type: category\n  - name: GarageYrBlt\n    type: number\n  - name: GarageFinish\n    type: category\n  - name: GarageCars\n    type: number\n  - name: GarageArea\n    type: number\n  - name: GarageQual\n    type: category\n  - name: GarageCond\n    type: category\n  - name: PavedDrive\n    type: category\n  - name: WoodDeckSF\n    type: number\n  - name: OpenPorchSF\n    type: number\n  - name: EnclosedPorch\n    type: number\n  - name: 3SsnPorch\n    type: number\n  - name: ScreenPorch\n    type: number\n  - name: PoolArea\n    type: number\n  - name: PoolQC\n    type: category\n  - name: Fence\n    type: category\n  - name: MiscFeature\n    type: category\n  - name: MiscVal\n    type: number\n  - name: MoSold\n    type: category\n  - name: YrSold\n    type: number\n  - name: SaleType\n    type: category\n  - name: SaleCondition\n    type: category\noutput_features:\n  - name: SalePrice\n    type: number\ntrainer:\n  batch_size: 35\n  eval_batch_size: 16384\n  evaluate_training_set: false\n  learning_rate: 0.0858479746528337\n"
  },
  {
    "path": "tests/regression_tests/benchmark/configs/mercedes_benz_greener.ecd.yaml",
    "content": "output_features:\n  - name: y\n    type: number\ninput_features:\n  - name: X0\n    type: category\n  - name: X1\n    type: category\n  - name: X2\n    type: category\n  - name: X3\n    type: category\n  - name: X4\n    type: category\n  - name: X5\n    type: category\n  - name: X6\n    type: category\n  - name: X8\n    type: category\n  - name: X10\n    type: binary\n  - name: X11\n    type: binary\n  - name: X12\n    type: binary\n  - name: X13\n    type: binary\n  - name: X14\n    type: binary\n  - name: X15\n    type: binary\n  - name: X16\n    type: binary\n  - name: X17\n    type: binary\n  - name: X18\n    type: binary\n  - name: X19\n    type: binary\n  - name: X20\n    type: binary\n  - name: X21\n    type: binary\n  - name: X22\n    type: binary\n  - name: X23\n    type: binary\n  - name: X24\n    type: binary\n  - name: X26\n    type: binary\n  - name: X27\n    type: binary\n  - name: X28\n    type: binary\n  - name: X29\n    type: binary\n  - name: X30\n    type: binary\n  - name: X31\n    type: binary\n  - name: X32\n    type: binary\n  - name: X33\n    type: binary\n  - name: X34\n    type: binary\n  - name: X35\n    type: binary\n  - name: X36\n    type: binary\n  - name: X37\n    type: binary\n  - name: X38\n    type: binary\n  - name: X39\n    type: binary\n  - name: X40\n    type: binary\n  - name: X41\n    type: binary\n  - name: X42\n    type: binary\n  - name: X43\n    type: binary\n  - name: X44\n    type: binary\n  - name: X45\n    type: binary\n  - name: X46\n    type: binary\n  - name: X47\n    type: binary\n  - name: X48\n    type: binary\n  - name: X49\n    type: binary\n  - name: X50\n    type: binary\n  - name: X51\n    type: binary\n  - name: X52\n    type: binary\n  - name: X53\n    type: binary\n  - name: X54\n    type: binary\n  - name: X55\n    type: binary\n  - name: X56\n    type: binary\n  - name: X57\n    type: binary\n  - name: X58\n    type: binary\n  - name: X59\n    type: binary\n  - name: X60\n    type: binary\n  - name: X61\n    type: binary\n  - name: X62\n    type: binary\n  - name: X63\n    type: binary\n  - name: X64\n    type: binary\n  - name: X65\n    type: binary\n  - name: X66\n    type: binary\n  - name: X67\n    type: binary\n  - name: X68\n    type: binary\n  - name: X69\n    type: binary\n  - name: X70\n    type: binary\n  - name: X71\n    type: binary\n  - name: X73\n    type: binary\n  - name: X74\n    type: binary\n  - name: X75\n    type: binary\n  - name: X76\n    type: binary\n  - name: X77\n    type: binary\n  - name: X78\n    type: binary\n  - name: X79\n    type: binary\n  - name: X80\n    type: binary\n  - name: X81\n    type: binary\n  - name: X82\n    type: binary\n  - name: X83\n    type: binary\n  - name: X84\n    type: binary\n  - name: X85\n    type: binary\n  - name: X86\n    type: binary\n  - name: X87\n    type: binary\n  - name: X88\n    type: binary\n  - name: X89\n    type: binary\n  - name: X90\n    type: binary\n  - name: X91\n    type: binary\n  - name: X92\n    type: binary\n  - name: X93\n    type: binary\n  - name: X94\n    type: binary\n  - name: X95\n    type: binary\n  - name: X96\n    type: binary\n  - name: X97\n    type: binary\n  - name: X98\n    type: binary\n  - name: X99\n    type: binary\n  - name: X100\n    type: binary\n  - name: X101\n    type: binary\n  - name: X102\n    type: binary\n  - name: X103\n    type: binary\n  - name: X104\n    type: binary\n  - name: X105\n    type: binary\n  - name: X106\n    type: binary\n  - name: X107\n    type: binary\n  - name: X108\n    type: binary\n  - name: X109\n    type: binary\n  - name: X110\n    type: binary\n  - name: X111\n    type: binary\n  - name: X112\n    type: binary\n  - name: X113\n    type: binary\n  - name: X114\n    type: binary\n  - name: X115\n    type: binary\n  - name: X116\n    type: binary\n  - name: X117\n    type: binary\n  - name: X118\n    type: binary\n  - name: X119\n    type: binary\n  - name: X120\n    type: binary\n  - name: X122\n    type: binary\n  - name: X123\n    type: binary\n  - name: X124\n    type: binary\n  - name: X125\n    type: binary\n  - name: X126\n    type: binary\n  - name: X127\n    type: binary\n  - name: X128\n    type: binary\n  - name: X129\n    type: binary\n  - name: X130\n    type: binary\n  - name: X131\n    type: binary\n  - name: X132\n    type: binary\n  - name: X133\n    type: binary\n  - name: X134\n    type: binary\n  - name: X135\n    type: binary\n  - name: X136\n    type: binary\n  - name: X137\n    type: binary\n  - name: X138\n    type: binary\n  - name: X139\n    type: binary\n  - name: X140\n    type: binary\n  - name: X141\n    type: binary\n  - name: X142\n    type: binary\n  - name: X143\n    type: binary\n  - name: X144\n    type: binary\n  - name: X145\n    type: binary\n  - name: X146\n    type: binary\n  - name: X147\n    type: binary\n  - name: X148\n    type: binary\n  - name: X150\n    type: binary\n  - name: X151\n    type: binary\n  - name: X152\n    type: binary\n  - name: X153\n    type: binary\n  - name: X154\n    type: binary\n  - name: X155\n    type: binary\n  - name: X156\n    type: binary\n  - name: X157\n    type: binary\n  - name: X158\n    type: binary\n  - name: X159\n    type: binary\n  - name: X160\n    type: binary\n  - name: X161\n    type: binary\n  - name: X162\n    type: binary\n  - name: X163\n    type: binary\n  - name: X164\n    type: binary\n  - name: X165\n    type: binary\n  - name: X166\n    type: binary\n  - name: X167\n    type: binary\n  - name: X168\n    type: binary\n  - name: X169\n    type: binary\n  - name: X170\n    type: binary\n  - name: X171\n    type: binary\n  - name: X172\n    type: binary\n  - name: X173\n    type: binary\n  - name: X174\n    type: binary\n  - name: X175\n    type: binary\n  - name: X176\n    type: binary\n  - name: X177\n    type: binary\n  - name: X178\n    type: binary\n  - name: X179\n    type: binary\n  - name: X180\n    type: binary\n  - name: X181\n    type: binary\n  - name: X182\n    type: binary\n  - name: X183\n    type: binary\n  - name: X184\n    type: binary\n  - name: X185\n    type: binary\n  - name: X186\n    type: binary\n  - name: X187\n    type: binary\n  - name: X189\n    type: binary\n  - name: X190\n    type: binary\n  - name: X191\n    type: binary\n  - name: X192\n    type: binary\n  - name: X194\n    type: binary\n  - name: X195\n    type: binary\n  - name: X196\n    type: binary\n  - name: X197\n    type: binary\n  - name: X198\n    type: binary\n  - name: X199\n    type: binary\n  - name: X200\n    type: binary\n  - name: X201\n    type: binary\n  - name: X202\n    type: binary\n  - name: X203\n    type: binary\n  - name: X204\n    type: binary\n  - name: X205\n    type: binary\n  - name: X206\n    type: binary\n  - name: X207\n    type: binary\n  - name: X208\n    type: binary\n  - name: X209\n    type: binary\n  - name: X210\n    type: binary\n  - name: X211\n    type: binary\n  - name: X212\n    type: binary\n  - name: X213\n    type: binary\n  - name: X214\n    type: binary\n  - name: X215\n    type: binary\n  - name: X216\n    type: binary\n  - name: X217\n    type: binary\n  - name: X218\n    type: binary\n  - name: X219\n    type: binary\n  - name: X220\n    type: binary\n  - name: X221\n    type: binary\n  - name: X222\n    type: binary\n  - name: X223\n    type: binary\n  - name: X224\n    type: binary\n  - name: X225\n    type: binary\n  - name: X226\n    type: binary\n  - name: X227\n    type: binary\n  - name: X228\n    type: binary\n  - name: X229\n    type: binary\n  - name: X230\n    type: binary\n  - name: X231\n    type: binary\n  - name: X232\n    type: binary\n  - name: X233\n    type: binary\n  - name: X234\n    type: binary\n  - name: X235\n    type: binary\n  - name: X236\n    type: binary\n  - name: X237\n    type: binary\n  - name: X238\n    type: binary\n  - name: X239\n    type: binary\n  - name: X240\n    type: binary\n  - name: X241\n    type: binary\n  - name: X242\n    type: binary\n  - name: X243\n    type: binary\n  - name: X244\n    type: binary\n  - name: X245\n    type: binary\n  - name: X246\n    type: binary\n  - name: X247\n    type: binary\n  - name: X248\n    type: binary\n  - name: X249\n    type: binary\n  - name: X250\n    type: binary\n  - name: X251\n    type: binary\n  - name: X252\n    type: binary\n  - name: X253\n    type: binary\n  - name: X254\n    type: binary\n  - name: X255\n    type: binary\n  - name: X256\n    type: binary\n  - name: X257\n    type: binary\n  - name: X258\n    type: binary\n  - name: X259\n    type: binary\n  - name: X260\n    type: binary\n  - name: X261\n    type: binary\n  - name: X262\n    type: binary\n  - name: X263\n    type: binary\n  - name: X264\n    type: binary\n  - name: X265\n    type: binary\n  - name: X266\n    type: binary\n  - name: X267\n    type: binary\n  - name: X268\n    type: binary\n  - name: X269\n    type: binary\n  - name: X270\n    type: binary\n  - name: X271\n    type: binary\n  - name: X272\n    type: binary\n  - name: X273\n    type: binary\n  - name: X274\n    type: binary\n  - name: X275\n    type: binary\n  - name: X276\n    type: binary\n  - name: X277\n    type: binary\n  - name: X278\n    type: binary\n  - name: X279\n    type: binary\n  - name: X280\n    type: binary\n  - name: X281\n    type: binary\n  - name: X282\n    type: binary\n  - name: X283\n    type: binary\n  - name: X284\n    type: binary\n  - name: X285\n    type: binary\n  - name: X286\n    type: binary\n  - name: X287\n    type: binary\n  - name: X288\n    type: binary\n  - name: X289\n    type: binary\n  - name: X290\n    type: binary\n  - name: X291\n    type: binary\n  - name: X292\n    type: binary\n  - name: X293\n    type: binary\n  - name: X294\n    type: binary\n  - name: X295\n    type: binary\n  - name: X296\n    type: binary\n  - name: X297\n    type: binary\n  - name: X298\n    type: binary\n  - name: X299\n    type: binary\n  - name: X300\n    type: binary\n  - name: X301\n    type: binary\n  - name: X302\n    type: binary\n  - name: X304\n    type: binary\n  - name: X305\n    type: binary\n  - name: X306\n    type: binary\n  - name: X307\n    type: binary\n  - name: X308\n    type: binary\n  - name: X309\n    type: binary\n  - name: X310\n    type: binary\n  - name: X311\n    type: binary\n  - name: X312\n    type: binary\n  - name: X313\n    type: binary\n  - name: X314\n    type: binary\n  - name: X315\n    type: binary\n  - name: X316\n    type: binary\n  - name: X317\n    type: binary\n  - name: X318\n    type: binary\n  - name: X319\n    type: binary\n  - name: X320\n    type: binary\n  - name: X321\n    type: binary\n  - name: X322\n    type: binary\n  - name: X323\n    type: binary\n  - name: X324\n    type: binary\n  - name: X325\n    type: binary\n  - name: X326\n    type: binary\n  - name: X327\n    type: binary\n  - name: X328\n    type: binary\n  - name: X329\n    type: binary\n  - name: X330\n    type: binary\n  - name: X331\n    type: binary\n  - name: X332\n    type: binary\n  - name: X333\n    type: binary\n  - name: X334\n    type: binary\n  - name: X335\n    type: binary\n  - name: X336\n    type: binary\n  - name: X337\n    type: binary\n  - name: X338\n    type: binary\n  - name: X339\n    type: binary\n  - name: X340\n    type: binary\n  - name: X341\n    type: binary\n  - name: X342\n    type: binary\n  - name: X343\n    type: binary\n  - name: X344\n    type: binary\n  - name: X345\n    type: binary\n  - name: X346\n    type: binary\n  - name: X347\n    type: binary\n  - name: X348\n    type: binary\n  - name: X349\n    type: binary\n  - name: X350\n    type: binary\n  - name: X351\n    type: binary\n  - name: X352\n    type: binary\n  - name: X353\n    type: binary\n  - name: X354\n    type: binary\n  - name: X355\n    type: binary\n  - name: X356\n    type: binary\n  - name: X357\n    type: binary\n  - name: X358\n    type: binary\n  - name: X359\n    type: binary\n  - name: X360\n    type: binary\n  - name: X361\n    type: binary\n  - name: X362\n    type: binary\n  - name: X363\n    type: binary\n  - name: X364\n    type: binary\n  - name: X365\n    type: binary\n  - name: X366\n    type: binary\n  - name: X367\n    type: binary\n  - name: X368\n    type: binary\n  - name: X369\n    type: binary\n  - name: X370\n    type: binary\n  - name: X371\n    type: binary\n  - name: X372\n    type: binary\n  - name: X373\n    type: binary\n  - name: X374\n    type: binary\n  - name: X375\n    type: binary\n  - name: X376\n    type: binary\n  - name: X377\n    type: binary\n  - name: X378\n    type: binary\n  - name: X379\n    type: binary\n  - name: X380\n    type: binary\n  - name: X382\n    type: binary\n  - name: X383\n    type: binary\n  - name: X384\n    type: binary\n  - name: X385\n    type: binary\ncombiner:\n  type: tabnet\ntrainer:\n  eval_batch_size: 16384\n  evaluate_training_set: false\n  learning_rate: 0.02465493752015043\n  batch_size: 33\ndefaults:\n  number:\n    preprocessing:\n      normalization: null\n      missing_value_strategy: fill_with_const\n"
  },
  {
    "path": "tests/regression_tests/benchmark/configs/sarcos.ecd.yaml",
    "content": "combiner:\n  type: tabnet\ndefaults:\n  number:\n    preprocessing:\n      missing_value_strategy: fill_with_const\n      normalization: null\ninput_features:\n  - column: position_1\n    name: position_1\n    type: number\n  - column: position_2\n    name: position_2\n    type: number\n  - column: position_3\n    name: position_3\n    type: number\n  - column: position_4\n    name: position_4\n    type: number\n  - column: position_5\n    name: position_5\n    type: number\n  - column: position_6\n    name: position_6\n    type: number\n  - column: position_7\n    name: position_7\n    type: number\n  - column: velocity_1\n    name: velocity_1\n    type: number\n  - column: velocity_2\n    name: velocity_2\n    type: number\n  - column: velocity_3\n    name: velocity_3\n    type: number\n  - column: velocity_4\n    name: velocity_4\n    type: number\n  - column: velocity_5\n    name: velocity_5\n    type: number\n  - column: velocity_6\n    name: velocity_6\n    type: number\n  - column: velocity_7\n    name: velocity_7\n    type: number\n  - column: acceleration_1\n    name: acceleration_1\n    type: number\n  - column: acceleration_2\n    name: acceleration_2\n    type: number\n  - column: acceleration_3\n    name: acceleration_3\n    type: number\n  - column: acceleration_4\n    name: acceleration_4\n    type: number\n  - column: acceleration_5\n    name: acceleration_5\n    type: number\n  - column: acceleration_6\n    name: acceleration_6\n    type: number\n  - column: acceleration_7\n    name: acceleration_7\n    type: number\n  - column: torque_2\n    name: torque_2\n    type: number\n  - column: torque_3\n    name: torque_3\n    type: number\n  - column: torque_4\n    name: torque_4\n    type: number\n  - column: torque_5\n    name: torque_5\n    type: number\n  - column: torque_6\n    name: torque_6\n    type: number\n  - column: torque_7\n    name: torque_7\n    type: number\noutput_features:\n  - column: torque_1\n    name: torque_1\n    type: number\ntrainer:\n  batch_size: 118\n  learning_rate_scheduler:\n    decay: exponential\n    decay_rate: 0.5371397744663506\n  eval_batch_size: 16384\n  evaluate_training_set: false\n  learning_rate: 0.001004563044919135\n"
  },
  {
    "path": "tests/regression_tests/benchmark/expected_metric.py",
    "content": "from dataclasses import dataclass\n\nfrom dataclasses_json import dataclass_json\n\n\n@dataclass_json\n@dataclass\nclass ExpectedMetric:\n    # Output feature name.\n    output_feature_name: str\n\n    # Metric name.\n    metric_name: str\n\n    # Expected metric value.\n    expected_value: int | float\n\n    # The percentage change that would trigger a notification/failure.\n    tolerance_percentage: float\n"
  },
  {
    "path": "tests/regression_tests/benchmark/expected_metrics/adult_census_income.ecd.yaml",
    "content": "metrics:\n  - output_feature_name: income\n    metric_name: accuracy\n    expected_value: 0.8547970652580261\n    tolerance_percentage: 0.15\n"
  },
  {
    "path": "tests/regression_tests/benchmark/expected_metrics/ames_housing.ecd.yaml",
    "content": "metrics:\n  - output_feature_name: SalePrice\n    metric_name: r2\n    expected_value: 0.7343850135803223\n    tolerance_percentage: 0.15\n"
  },
  {
    "path": "tests/regression_tests/benchmark/expected_metrics/mercedes_benz_greener.ecd.yaml",
    "content": "metrics:\n  - output_feature_name: y\n    metric_name: r2\n    expected_value: 0.47405338287353516\n    tolerance_percentage: 0.15\n"
  },
  {
    "path": "tests/regression_tests/benchmark/expected_metrics/sarcos.ecd.yaml",
    "content": "metrics:\n  - output_feature_name: torque_1\n    metric_name: r2\n    expected_value: 0.9871084690093994\n    tolerance_percentage: 0.15\n"
  },
  {
    "path": "tests/regression_tests/benchmark/test_model_performance.py",
    "content": "import os\n\nimport pytest\nfrom expected_metric import ExpectedMetric\n\nfrom ludwig.benchmarking.benchmark import benchmark\nfrom ludwig.utils.data_utils import load_yaml\nfrom tests.integration_tests.utils import parse_flag_from_env\n\nSKIPPED_CONFIG_ISSUES = {\n    \"mercedes_benz_greener.ecd.yaml\": \"https://github.com/ludwig-ai/ludwig/issues/2978\",\n    \"sarcos.ecd.yaml\": \"Takes more than 300s\",\n    \"ames_housing.ecd.yaml\": \"https://github.com/ludwig-ai/ludwig/issues/3344\",\n}\nCONFIGS_REQUIRING_DATASET_CREDENTIALS = {\n    \"mercedes_benz_greener.ecd.yaml\",\n    \"ames_housing.ecd.yaml\",\n}\nRUN_PRIVATE = parse_flag_from_env(\"RUN_PRIVATE\", default=False)\n\n\ndef update_skipped_configs_issues(config_filename):\n    if not RUN_PRIVATE and config_filename in CONFIGS_REQUIRING_DATASET_CREDENTIALS:\n        SKIPPED_CONFIG_ISSUES[config_filename] = \"Requires credentials. Can't run from a forked repo.\"\n\n\ndef get_test_config_filenames() -> list[str]:\n    \"\"\"Return list of the config filenames used for benchmarking.\"\"\"\n    benchmark_directory = \"/\".join(__file__.split(\"/\")[:-1] + [\"configs\"])\n    return [config_fp for config_fp in os.listdir(benchmark_directory)]\n\n\ndef get_dataset_from_config_path(config_path: str) -> str:\n    \"\"\"path/to/config/<dataset>.<descriptors>.yaml -> dataset.\"\"\"\n    return os.path.basename(config_path).split(\".\")[0]\n\n\n@pytest.mark.benchmark\n@pytest.mark.parametrize(\"config_filename\", get_test_config_filenames())\ndef test_performance(config_filename, tmpdir):\n    update_skipped_configs_issues(config_filename)\n    if config_filename in SKIPPED_CONFIG_ISSUES:\n        pytest.skip(reason=SKIPPED_CONFIG_ISSUES[config_filename])\n        return\n\n    benchmark_directory = \"/\".join(__file__.split(\"/\")[:-1])\n    config_path = os.path.join(benchmark_directory, \"configs\", config_filename)\n    expected_test_statistics_fp = os.path.join(benchmark_directory, \"expected_metrics\", config_filename)\n    dataset_name = get_dataset_from_config_path(config_path)\n\n    if not os.path.exists(expected_test_statistics_fp):\n        raise FileNotFoundError(\"\"\"No corresponding expected metrics found for benchmarking config '{config_path}'.\n            Please add a new metrics YAML file '{expected_test_statistics_fp}'. Suggested content:\n\n            metrics:\n              - output_feature_name: <YOUR_OUTPUT_FEATURE e.g. SalePrice>\n                metric_name: <YOUR METRIC NAME e.g. accuracy>\n                expected_value: <A FLOAT VALUE>\n                tolerance_percent: 0.15\"\"\")\n    expected_metrics_dict = load_yaml(expected_test_statistics_fp)\n\n    benchmarking_config = {\n        \"experiment_name\": \"regression_test\",\n        \"export\": {\"export_artifacts\": True, \"export_base_path\": tmpdir},\n        \"experiments\": [{\"dataset_name\": dataset_name, \"config_path\": config_path}],\n    }\n    benchmarking_artifacts = benchmark(benchmarking_config)\n    experiment_artifact, err = benchmarking_artifacts[dataset_name]\n    if err is not None:\n        raise err\n\n    expected_metrics: list[ExpectedMetric] = [\n        ExpectedMetric.from_dict(expected_metric) for expected_metric in expected_metrics_dict[\"metrics\"]\n    ]\n    for expected_metric in expected_metrics:\n        tolerance = expected_metric.tolerance_percentage * expected_metric.expected_value\n        output_feature_name = expected_metric.output_feature_name\n        metric_name = expected_metric.metric_name\n        experiment_metric_value = experiment_artifact.test_statistics[output_feature_name][metric_name]\n        assert abs(expected_metric.expected_value - experiment_metric_value) <= tolerance, (\n            f\"The obtained {metric_name} value ({experiment_metric_value}) was not within\"\n            f\" {100 * expected_metric.tolerance_percentage}% of the expected value ({expected_metric.expected_value}).\"\n        )\n"
  },
  {
    "path": "tests/regression_tests/model/test_old_models.py",
    "content": "import os\nimport zipfile\n\nimport pandas as pd\nimport pytest\nimport wget\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.data.dataset_synthesizer import build_synthetic_dataset_df\nfrom ludwig.globals import MODEL_FILE_NAME\n\nNUM_EXAMPLES = 25\n\n\ndef test_model_loaded_from_old_config_prediction_works(tmpdir):\n    # Titanic model based on 0.5.3.\n    old_model_url = \"https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/old_model.zip\"\n    old_model_filename = wget.download(old_model_url, tmpdir)\n    with zipfile.ZipFile(old_model_filename, \"r\") as zip_ref:\n        zip_ref.extractall(tmpdir)\n    example_data = {\n        \"PassengerId\": 892,\n        \"Pclass\": 3,\n        \"Name\": \"Kelly, Mr. James\",\n        \"Sex\": \"male\",\n        \"Age\": 34.5,\n        \"SibSp\": 0,\n        \"Parch\": 0,\n        \"Ticket\": \"330911\",\n        \"Fare\": 7.8292,\n        \"Cabin\": None,\n        \"Embarked\": \"Q\",\n    }\n    test_set = pd.DataFrame(example_data, index=[0])\n\n    ludwig_model = LudwigModel.load(os.path.join(tmpdir, \"old_model/model\"))\n    predictions, _ = ludwig_model.predict(dataset=test_set)\n\n    assert predictions.to_dict()[\"Survived_predictions\"] == {0: False}\n\n\n@pytest.mark.parametrize(\n    \"model_url\",\n    [\n        \"https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/titanic_v07.zip\",\n        \"https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/twitter_bots_v05_1.zip\",\n        \"https://predibase-public-us-west-2.s3.us-west-2.amazonaws.com/ludwig_unit_tests/respiratory_v05.zip\",\n    ],\n    ids=[\"titanic\", \"twitter_bots\", \"respiratory\"],\n)\ndef test_predict_deprecated_model(model_url, tmpdir):\n    model_dir = os.path.join(tmpdir, MODEL_FILE_NAME)\n    os.makedirs(model_dir)\n\n    archive_path = wget.download(model_url, tmpdir)\n    with zipfile.ZipFile(archive_path, \"r\") as zip_ref:\n        zip_ref.extractall(model_dir)\n\n    ludwig_model = LudwigModel.load(model_dir)\n    df = build_synthetic_dataset_df(NUM_EXAMPLES, ludwig_model.config)\n\n    pred_df, _ = ludwig_model.predict(df)\n    assert len(pred_df) == 25\n"
  },
  {
    "path": "tests/training_success/__init__.py",
    "content": ""
  },
  {
    "path": "tests/training_success/configs.py",
    "content": "from ludwig.config_sampling.explore_schema import (\n    combine_configs,\n    combine_configs_for_comparator_combiner,\n    combine_configs_for_sequence_combiner,\n)\n\n# A generic tabular to text config used to generate synthetic data and train a model on it.\nTABULAR_TO_TEXT = \"\"\"\ninput_features:\n  - name: category_1\n    type: category\n  - name: number_1\n    type: number\n  - name: binary_1\n    type: binary\noutput_features:\n  - name: text_output_1\n    type: text\n\"\"\"\n\n# A generic tabular config used to generate synthetic data and train a model on it.\nTABULAR = \"\"\"\ninput_features:\n  - name: category_1\n    type: category\n  - name: number_1\n    type: number\n  - name: binary_1\n    type: binary\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a single text input feature used to generate synthetic data and train a model on it.\nTEXT_INPUT = \"\"\"\ninput_features:\n  - name: text_1\n    type: text\n    preprocessing:\n      max_sequence_length: 8\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a single number input feature used to generate synthetic data and train a model on it.\nNUMBER_INPUT = \"\"\"\ninput_features:\n  - name: number_1\n    type: number\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a single category input feature used to generate synthetic data and train a model on it.\nCATEGORY_INPUT = \"\"\"\ninput_features:\n  - name: category_1\n    type: category\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a single binary input feature used to generate synthetic data and train a model on it.\nBINARY_INPUT = \"\"\"\ninput_features:\n  - name: binary_1\n    type: binary\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a text output feature used to generate synthetic data and train a model on it.\nTEXT_OUTPUT = \"\"\"\ninput_features:\n  - name: text_1\n    type: text\n    preprocessing:\n      max_sequence_length: 8\noutput_features:\n  - name: text_output_1\n    type: text\n\"\"\"\n\n# A generic config with a number output feature used to generate synthetic data and train a model on it.\nNUMBER_OUTPUT = \"\"\"\ninput_features:\n  - name: number_1\n    type: number\noutput_features:\n  - name: number_output_1\n    type: number\n\"\"\"\n\n# A generic config with a category output feature used to generate synthetic data and train a model on it.\nCATEGORY_OUTPUT = \"\"\"\ninput_features:\n  - name: category_1\n    type: category\noutput_features:\n  - name: category_output_1\n    type: category\n\"\"\"\n\n# A generic config with a binary output feature used to generate synthetic data and train a model on it.\nBINARY_OUTPUT = \"\"\"\ninput_features:\n  - name: binary_1\n    type: binary\noutput_features:\n  - name: binary_output_1\n    type: binary\n\"\"\"\n\n# Dictionary that maps from feature type to base config used to test the encoder and preprocessing sections.\nFEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING = {\n    \"number\": NUMBER_INPUT,\n    \"category\": CATEGORY_INPUT,\n    \"binary\": BINARY_INPUT,\n    \"text\": TEXT_INPUT,\n}\n\n# Dictionary that maps from feature type to base config used to test the decoder and loss sections.\nFEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS = {\n    \"number\": NUMBER_OUTPUT,\n    \"category\": CATEGORY_OUTPUT,\n    \"binary\": BINARY_OUTPUT,\n    \"text\": TEXT_OUTPUT,\n}\n\n# Dictionary that maps from config section to base config used to test that section.\nECD_CONFIG_SECTION_TO_CONFIG = {\n    \"trainer\": TABULAR,\n    \"comparator\": TABULAR,\n    \"concat\": TABULAR,\n    \"project_aggregate\": TABULAR,\n    \"sequence\": TEXT_INPUT,\n    \"sequence_concat\": TEXT_INPUT,\n    \"tabnet\": TABULAR,\n    \"tabtransformer\": TABULAR,\n    \"transformer\": TABULAR,\n}\n\n# Dictionary that maps from the combiner type to base config used to test that combiner.\nCOMBINER_TYPE_TO_COMBINE_FN_MAP = {\n    \"comparator\": combine_configs_for_comparator_combiner,\n    \"concat\": combine_configs,\n    \"project_aggregate\": combine_configs,\n    \"sequence\": combine_configs_for_sequence_combiner,\n    \"sequence_concat\": combine_configs_for_sequence_combiner,\n    \"tabnet\": combine_configs,\n    \"tabtransformer\": combine_configs,\n    \"transformer\": combine_configs,\n}\n"
  },
  {
    "path": "tests/training_success/test_training_success.py",
    "content": "import logging\nfrom collections import deque\nfrom pprint import pprint\nfrom typing import Any\n\nimport pandas as pd\nimport pytest\nimport yaml\n\nfrom ludwig.api import LudwigModel\nfrom ludwig.config_sampling.explore_schema import combine_configs, ConfigOption, explore_properties\nfrom ludwig.config_validation.validation import get_schema\nfrom ludwig.types import ModelConfigDict\n\nfrom .configs import (\n    COMBINER_TYPE_TO_COMBINE_FN_MAP,\n    ECD_CONFIG_SECTION_TO_CONFIG,\n    FEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS,\n    FEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING,\n)\n\n\ndef defaults_config_generator(\n    feature_type: str, allow_list: str, static_schema: dict[str, Any] = None\n) -> tuple[ModelConfigDict, pd.DataFrame]:\n    \"\"\"Generate combinatorial configs for the defaults section of the Ludwig config.\n\n    Args:\n        feature_type: feature type to explore.\n        allow_list: top-level parameter of the defaults sections that should be included.\n    \"\"\"\n    assert isinstance(allow_list, str)\n    assert allow_list in {\"preprocessing\", \"encoder\", \"decoder\", \"loss\"}\n\n    schema = get_schema() if not static_schema else static_schema\n    properties = schema[\"properties\"][\"defaults\"][\"properties\"][feature_type][\"properties\"]\n    raw_entry = deque([ConfigOption(dict(), False)])\n    explored = explore_properties(\n        properties, parent_parameter_path=f\"defaults.{feature_type}\", dq=raw_entry, allow_list=[allow_list]\n    )\n\n    if allow_list in [\"preprocessing\", \"encoder\"]:\n        config = FEATURE_TYPE_TO_CONFIG_FOR_ENCODER_PREPROCESSING[feature_type]\n        config = yaml.safe_load(config)\n    else:  # decoder and loss\n        config = FEATURE_TYPE_TO_CONFIG_FOR_DECODER_LOSS[feature_type]\n        config = yaml.safe_load(config)\n\n    config[\"model_type\"] = \"ecd\"\n    config[\"trainer\"] = {\"train_steps\": 1}\n\n    combined_configs = combine_configs(explored, config)\n    logging.info(f\"Generated {len(combined_configs)} for {feature_type} {allow_list} combinatorial tests.\")\n\n    for config, dataset in combined_configs:\n        yield config, dataset\n\n\ndef ecd_trainer_config_generator(static_schema: dict[str, Any] = None) -> tuple[ModelConfigDict, pd.DataFrame]:\n    \"\"\"Generate combinatorial configs for the ECD trainer section of the Ludwig config.\"\"\"\n    schema = get_schema() if not static_schema else static_schema\n    properties = schema[\"properties\"]\n\n    raw_entry = deque([ConfigOption(dict(), False)])\n    explored = explore_properties(properties, parent_parameter_path=\"\", dq=raw_entry, allow_list=[\"trainer\"])\n    config = ECD_CONFIG_SECTION_TO_CONFIG[\"trainer\"]\n    config = yaml.safe_load(config)\n    config[\"model_type\"] = \"ecd\"\n    config[\"trainer\"] = {\"train_steps\": 1}\n\n    combined_configs = combine_configs(explored, config)\n\n    # HACK(Arnav): Remove configs that have LARS, LAMB or Lion optimizers, or Paged or 8-bit optimizers.\n    # This is because they require GPUs.\n    filtered_configs = []\n\n    for config, dataset in combined_configs:\n        optimizer_type = config.get(\"trainer\", {}).get(\"optimizer\", \"\").get(\"type\", \"\")\n\n        if optimizer_type not in {\"lars\", \"lamb\", \"lion\"} and not (\n            \"paged\" in optimizer_type or \"8bit\" in optimizer_type\n        ):\n            filtered_configs.append((config, dataset))\n\n    # Replace combined_configs with the filtered_configs\n    combined_configs = filtered_configs\n\n    logging.info(f\"Generated {len(combined_configs)} for ECD trainer combinatorial tests.\")\n\n    for config, dataset in combined_configs:\n        yield config, dataset\n\n\ndef combiner_config_generator(\n    combiner_type: str, static_schema: dict[str, Any] = None\n) -> tuple[ModelConfigDict, pd.DataFrame]:\n    \"\"\"Generate combinatorial configs for the combiner section of the Ludwig config.\n\n    Args:\n        combiner_type: combiner type to explore.\n    \"\"\"\n    schema = get_schema() if not static_schema else static_schema\n    properties = schema[\"properties\"]\n\n    raw_entry = deque([ConfigOption(dict(), False)])\n    explored = explore_properties(properties, parent_parameter_path=\"\", dq=raw_entry, allow_list=[\"combiner\"])\n    config = ECD_CONFIG_SECTION_TO_CONFIG[combiner_type]\n    config = yaml.safe_load(config)\n    config[\"model_type\"] = \"ecd\"\n    config[\"trainer\"] = {\"train_steps\": 1}\n\n    combine_configs_fn = COMBINER_TYPE_TO_COMBINE_FN_MAP[combiner_type]\n\n    combined_configs = combine_configs_fn(explored, config)\n    combined_configs = [\n        (config, dataset) for config, dataset in combined_configs if config[\"combiner\"][\"type\"] == combiner_type\n    ]\n    logging.info(f\"Generated {len(combined_configs)} for {combiner_type} combiner combinatorial tests.\")\n\n    for config, dataset in combined_configs:\n        yield config, dataset\n\n\ndef train_and_evaluate(config: ModelConfigDict, dataset: pd.DataFrame):\n    \"\"\"Trains and evaluates a model with the given config.\n\n    Args:\n        config: valid Ludwig config.\n        dataset: Ludwig dataset name to train on.\n    \"\"\"\n    # adding print statements to be captured in pytest stdout and help debug tests.\n    print(\"Config used (trained on synthetic data)\")\n    pprint(config)\n    model = LudwigModel(config=config, callbacks=None, logging_level=logging.ERROR)\n    model.train(dataset=dataset)\n    model.evaluate(dataset=dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"sequence_concat\"))\ndef test_ecd_sequence_concat_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"sequence\"))\ndef test_ecd_sequence_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"comparator\"))\ndef test_ecd_comparator_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"concat\"))\ndef test_ecd_concat_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"project_aggregate\"))\ndef test_ecd_project_aggregate_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"tabnet\"))\ndef test_ecd_tabnet_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"tabtransformer\"))\ndef test_ecd_tabtransformer_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", combiner_config_generator(\"transformer\"))\ndef test_ecd_transformer_combiner(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", ecd_trainer_config_generator())\ndef test_ecd_trainer(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"number\", \"encoder\"))\ndef test_number_encoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"number\", \"decoder\"))\ndef test_number_decoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"number\", \"loss\"))\ndef test_number_encoder_loss(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"number\", \"preprocessing\"))\ndef test_number_preprocessing_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"category\", \"encoder\"))\ndef test_category_encoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"category\", \"decoder\"))\ndef test_category_decoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"category\", \"loss\"))\ndef test_category_loss_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"category\", \"preprocessing\"))\ndef test_category_preprocessing_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"binary\", \"encoder\"))\ndef test_binary_encoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"binary\", \"decoder\"))\ndef test_binary_decoder_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"binary\", \"loss\"))\ndef test_binary_loss_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n@pytest.mark.combinatorial\n@pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"binary\", \"preprocessing\"))\ndef test_binary_preprocessing_defaults(config, dataset):\n    train_and_evaluate(config, dataset)\n\n\n# @pytest.mark.combinatorial\n# @pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"text\", \"preprocessing\"))\n# def test_text_preprocessing_defaults(config, dataset):\n#     train_and_evaluate(config, dataset)\n\n\n# @pytest.mark.combinatorial\n# @pytest.mark.parametrize(\"config,dataset\", defaults_config_generator(\"text\", \"encoder\"))\n# def test_text_encoder_defaults(config):\n#     train_and_evaluate(config, dataset)\n"
  }
]