mirror of
https://github.com/microsoft/FLAML.git
synced 2026-02-11 03:09:15 +08:00
Compare commits
108 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc1e4dc5ea | ||
|
|
158ff7d99e | ||
|
|
a5021152d2 | ||
|
|
fc4efe3510 | ||
|
|
cd0e9fb0d2 | ||
|
|
a9c0a9e30a | ||
|
|
a05b669de3 | ||
|
|
6e59103e86 | ||
|
|
d9e74031e0 | ||
|
|
7ec1414e9b | ||
|
|
9233a52736 | ||
|
|
7ac076d544 | ||
|
|
3d489f1aaa | ||
|
|
c64eeb5e8d | ||
|
|
bf35f98a24 | ||
|
|
1687ca9a94 | ||
|
|
7a597adcc9 | ||
|
|
4ea9650f99 | ||
|
|
fa1a32afb6 | ||
|
|
5eb7d623b0 | ||
|
|
22dcfcd3c0 | ||
|
|
d7208b32d0 | ||
|
|
5f1aa2dda8 | ||
|
|
67bdcde4d5 | ||
|
|
46a406edd4 | ||
|
|
f1817ea7b1 | ||
|
|
f6a5163e6a | ||
|
|
e64b486528 | ||
|
|
a74354f7a9 | ||
|
|
ced1d6f331 | ||
|
|
bb213e7ebd | ||
|
|
d241e8de90 | ||
|
|
0b138d9193 | ||
|
|
1c9835dc0a | ||
|
|
1285700d7a | ||
|
|
7f42bece89 | ||
|
|
e19107407b | ||
|
|
f5d6693253 | ||
|
|
d4e43c50a2 | ||
|
|
13aec414ea | ||
|
|
bb16dcde93 | ||
|
|
be81a76da9 | ||
|
|
2d16089529 | ||
|
|
01c3c83653 | ||
|
|
9b66103f7c | ||
|
|
48dfd72e64 | ||
|
|
dec92e5b02 | ||
|
|
22911ea1ef | ||
|
|
12183e5f73 | ||
|
|
c2b25310fc | ||
|
|
0f9420590d | ||
|
|
5107c506b4 | ||
|
|
9e219ef8dc | ||
|
|
6e4083743b | ||
|
|
17e95edd9e | ||
|
|
468bc62d27 | ||
|
|
437c239c11 | ||
|
|
8e753f1092 | ||
|
|
a3b57e11d4 | ||
|
|
a80dcf9925 | ||
|
|
7157af44e0 | ||
|
|
1798c4591e | ||
|
|
dd26263330 | ||
|
|
2ba5f8bed1 | ||
|
|
d0a11958a5 | ||
|
|
0ef9b00a75 | ||
|
|
840f76e5e5 | ||
|
|
d8b7d25b80 | ||
|
|
6d53929803 | ||
|
|
c038fbca07 | ||
|
|
6a99202492 | ||
|
|
42d1dcfa0e | ||
|
|
b83c8a7d3b | ||
|
|
b9194cdcf2 | ||
|
|
9a1f6b0291 | ||
|
|
07f4413aae | ||
|
|
5a74227bc3 | ||
|
|
7644958e21 | ||
|
|
a316f84fe1 | ||
|
|
72881d3a2b | ||
|
|
69da685d1e | ||
|
|
c01c3910eb | ||
|
|
98d3fd2f48 | ||
|
|
9724c626cc | ||
|
|
0d92400200 | ||
|
|
d224218ecf | ||
|
|
a2a5e1abb9 | ||
|
|
5c0f18b7bc | ||
|
|
e5d95f5674 | ||
|
|
49ba962d47 | ||
|
|
8e171bc402 | ||
|
|
c90946f303 | ||
|
|
64f30af603 | ||
|
|
f45582d3c7 | ||
|
|
bf4bca2195 | ||
|
|
efaba26d2e | ||
|
|
62194f321d | ||
|
|
5bfa0b1cd3 | ||
|
|
bd34b4e75a | ||
|
|
7670945298 | ||
|
|
43537cb539 | ||
|
|
f913b79225 | ||
|
|
a092a39b5e | ||
|
|
04bf1b8741 | ||
|
|
b348cb1136 | ||
|
|
cd0e88e383 | ||
|
|
a17c6e392e | ||
|
|
52627ff14b |
@@ -1,5 +1,7 @@
|
||||
[run]
|
||||
branch = True
|
||||
source = flaml
|
||||
source =
|
||||
flaml
|
||||
omit =
|
||||
*test*
|
||||
*/test/*
|
||||
*/flaml/autogen/*
|
||||
|
||||
73
.github/ISSUE_TEMPLATE.md
vendored
Normal file
73
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
### Description
|
||||
|
||||
<!-- A clear and concise description of the issue or feature request. -->
|
||||
|
||||
### Environment
|
||||
|
||||
- FLAML version: <!-- Specify the FLAML version (e.g., v0.2.0) -->
|
||||
- Python version: <!-- Specify the Python version (e.g., 3.8) -->
|
||||
- Operating System: <!-- Specify the OS (e.g., Windows 10, Ubuntu 20.04) -->
|
||||
|
||||
### Steps to Reproduce (for bugs)
|
||||
|
||||
<!-- Provide detailed steps to reproduce the issue. Include code snippets, configuration files, or any other relevant information. -->
|
||||
|
||||
1. Step 1
|
||||
1. Step 2
|
||||
1. ...
|
||||
|
||||
### Expected Behavior
|
||||
|
||||
<!-- Describe what you expected to happen. -->
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
<!-- Describe what actually happened. Include any error messages, stack traces, or unexpected behavior. -->
|
||||
|
||||
### Screenshots / Logs (if applicable)
|
||||
|
||||
<!-- If relevant, include screenshots or logs that help illustrate the issue. -->
|
||||
|
||||
### Additional Information
|
||||
|
||||
<!-- Include any additional information that might be helpful, such as specific configurations, data samples, or context about the environment. -->
|
||||
|
||||
### Possible Solution (if you have one)
|
||||
|
||||
<!-- If you have suggestions on how to address the issue, provide them here. -->
|
||||
|
||||
### Is this a Bug or Feature Request?
|
||||
|
||||
<!-- Choose one: Bug | Feature Request -->
|
||||
|
||||
### Priority
|
||||
|
||||
<!-- Choose one: High | Medium | Low -->
|
||||
|
||||
### Difficulty
|
||||
|
||||
<!-- Choose one: Easy | Moderate | Hard -->
|
||||
|
||||
### Any related issues?
|
||||
|
||||
<!-- If this is related to another issue, reference it here. -->
|
||||
|
||||
### Any relevant discussions?
|
||||
|
||||
<!-- If there are any discussions or forum threads related to this issue, provide links. -->
|
||||
|
||||
### Checklist
|
||||
|
||||
<!-- Please check the items that you have completed -->
|
||||
|
||||
- [ ] I have searched for similar issues and didn't find any duplicates.
|
||||
- [ ] I have provided a clear and concise description of the issue.
|
||||
- [ ] I have included the necessary environment details.
|
||||
- [ ] I have outlined the steps to reproduce the issue.
|
||||
- [ ] I have included any relevant logs or screenshots.
|
||||
- [ ] I have indicated whether this is a bug or a feature request.
|
||||
- [ ] I have set the priority and difficulty levels.
|
||||
|
||||
### Additional Comments
|
||||
|
||||
<!-- Any additional comments or context that you think would be helpful. -->
|
||||
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Bug Report
|
||||
description: File a bug report
|
||||
title: "[Bug]: "
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear and concise description of what the bug is.
|
||||
placeholder: What went wrong?
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. ...
|
||||
4. See error
|
||||
placeholder: How can we replicate the issue?
|
||||
- type: textarea
|
||||
id: modelused
|
||||
attributes:
|
||||
label: Model Used
|
||||
description: A description of the model that was used when the error was encountered
|
||||
placeholder: gpt-4, mistral-7B etc
|
||||
- type: textarea
|
||||
id: expected_behavior
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
placeholder: What should have happened?
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots and logs
|
||||
description: If applicable, add screenshots and logs to help explain your problem.
|
||||
placeholder: Add screenshots here
|
||||
- type: textarea
|
||||
id: additional_information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
- FLAML Version: <!-- Specify the FLAML version (e.g., v0.2.0) -->
|
||||
- Operating System: <!-- Specify the OS (e.g., Windows 10, Ubuntu 20.04) -->
|
||||
- Python Version: <!-- Specify the Python version (e.g., 3.8) -->
|
||||
- Related Issues: <!-- Link to any related issues here (e.g., #1) -->
|
||||
- Any other relevant information.
|
||||
placeholder: Any additional details
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: true
|
||||
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
26
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Feature Request
|
||||
description: File a feature request
|
||||
labels: ["enhancement"]
|
||||
title: "[Feature Request]: "
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem_description
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
placeholder: What problem are you trying to solve?
|
||||
|
||||
- type: textarea
|
||||
id: solution_description
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: How do you envision the solution?
|
||||
|
||||
- type: textarea
|
||||
id: additional_context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
placeholder: Any additional information
|
||||
41
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
Normal file
41
.github/ISSUE_TEMPLATE/general_issue.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: General Issue
|
||||
description: File a general issue
|
||||
title: "[Issue]: "
|
||||
labels: []
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the issue
|
||||
description: A clear and concise description of what the issue is.
|
||||
placeholder: What went wrong?
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Step 1
|
||||
2. Step 2
|
||||
3. ...
|
||||
4. See error
|
||||
placeholder: How can we replicate the issue?
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots and logs
|
||||
description: If applicable, add screenshots and logs to help explain your problem.
|
||||
placeholder: Add screenshots here
|
||||
- type: textarea
|
||||
id: additional_information
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: |
|
||||
- FLAML Version: <!-- Specify the FLAML version (e.g., v0.2.0) -->
|
||||
- Operating System: <!-- Specify the OS (e.g., Windows 10, Ubuntu 20.04) -->
|
||||
- Python Version: <!-- Specify the Python version (e.g., 3.8) -->
|
||||
- Related Issues: <!-- Link to any related issues here (e.g., #1) -->
|
||||
- Any other relevant information.
|
||||
placeholder: Any additional details
|
||||
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -12,8 +12,7 @@
|
||||
|
||||
## Checks
|
||||
|
||||
<!-- - I've used [pre-commit](https://microsoft.github.io/FLAML/docs/Contribute#pre-commit) to lint the changes in this PR (note the same in integrated in our CI checks). -->
|
||||
|
||||
- [ ] I've used [pre-commit](https://microsoft.github.io/FLAML/docs/Contribute#pre-commit) to lint the changes in this PR (note the same in integrated in our CI checks).
|
||||
- [ ] I've included any doc changes needed for https://microsoft.github.io/FLAML/. See https://microsoft.github.io/FLAML/docs/Contribute#documentation to build and test documentation locally.
|
||||
- [ ] I've added tests (if relevant) corresponding to the changes introduced in this PR.
|
||||
- [ ] I've made sure all auto checks have passed.
|
||||
|
||||
243
.github/copilot-instructions.md
vendored
Normal file
243
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,243 @@
|
||||
# GitHub Copilot Instructions for FLAML
|
||||
|
||||
## Project Overview
|
||||
|
||||
FLAML (Fast Library for Automated Machine Learning & Tuning) is a lightweight Python library for efficient automation of machine learning and AI operations. It automates workflow based on large language models, machine learning models, etc. and optimizes their performance.
|
||||
|
||||
**Key Components:**
|
||||
|
||||
- `flaml/automl/`: AutoML functionality for classification and regression
|
||||
- `flaml/tune/`: Generic hyperparameter tuning
|
||||
- `flaml/default/`: Zero-shot AutoML with default configurations
|
||||
- `flaml/autogen/`: Legacy autogen code (note: AutoGen has moved to a separate repository)
|
||||
- `flaml/fabric/`: Microsoft Fabric integration
|
||||
- `test/`: Comprehensive test suite
|
||||
|
||||
## Build and Test Commands
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Basic installation
|
||||
pip install -e .
|
||||
|
||||
# Install with test dependencies
|
||||
pip install -e .[test]
|
||||
|
||||
# Install with automl dependencies
|
||||
pip install -e .[automl]
|
||||
|
||||
# Install with forecast dependencies (Linux only)
|
||||
pip install -e .[forecast]
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests (excluding autogen)
|
||||
pytest test/ --ignore=test/autogen --reruns 2 --reruns-delay 10
|
||||
|
||||
# Run tests with coverage
|
||||
coverage run -a -m pytest test --ignore=test/autogen --reruns 2 --reruns-delay 10
|
||||
coverage xml
|
||||
|
||||
# Check dependencies
|
||||
python test/check_dependency.py
|
||||
```
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Run pre-commit hooks
|
||||
pre-commit run --all-files
|
||||
|
||||
# Format with black (line length: 120)
|
||||
black . --line-length 120
|
||||
|
||||
# Run ruff for linting and auto-fix
|
||||
ruff check . --fix
|
||||
```
|
||||
|
||||
## Code Style and Formatting
|
||||
|
||||
### Python Style
|
||||
|
||||
- **Line length:** 120 characters (configured in both Black and Ruff)
|
||||
- **Formatter:** Black (v23.3.0+)
|
||||
- **Linter:** Ruff with Pyflakes and pycodestyle rules
|
||||
- **Import sorting:** Use isort (via Ruff)
|
||||
- **Python version:** Supports Python >= 3.10 (full support for 3.10, 3.11, 3.12 and 3.13)
|
||||
|
||||
### Code Quality Rules
|
||||
|
||||
- Follow Black formatting conventions
|
||||
- Keep imports sorted and organized
|
||||
- Avoid unused imports (F401) - these are flagged but not auto-fixed
|
||||
- Avoid wildcard imports (F403) where possible
|
||||
- Complexity: Max McCabe complexity of 10
|
||||
- Use type hints where appropriate
|
||||
- Write clear docstrings for public APIs
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
The repository uses pre-commit hooks for:
|
||||
|
||||
- Checking for large files, AST syntax, YAML/TOML/JSON validity
|
||||
- Detecting merge conflicts and private keys
|
||||
- Trailing whitespace and end-of-file fixes
|
||||
- pyupgrade for Python 3.8+ syntax
|
||||
- Black formatting
|
||||
- Markdown formatting (mdformat with GFM and frontmatter support)
|
||||
- Ruff linting with auto-fix
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Tests are in the `test/` directory, organized by module
|
||||
- `test/automl/`: AutoML feature tests
|
||||
- `test/tune/`: Hyperparameter tuning tests
|
||||
- `test/default/`: Zero-shot AutoML tests
|
||||
- `test/nlp/`: NLP-related tests
|
||||
- `test/spark/`: Spark integration tests
|
||||
|
||||
### Test Requirements
|
||||
|
||||
- Write tests for new functionality
|
||||
- Ensure tests pass on multiple Python versions (3.10, 3.11, 3.12 and 3.13)
|
||||
- Tests should work on both Ubuntu and Windows
|
||||
- Use pytest markers for platform-specific tests (e.g., `@pytest.mark.spark`)
|
||||
- Tests should be idempotent and not depend on external state
|
||||
- Use `--reruns 2 --reruns-delay 10` for flaky tests
|
||||
|
||||
### Coverage
|
||||
|
||||
- Aim for good test coverage on new code
|
||||
- Coverage reports are generated for Python 3.11 builds
|
||||
- Coverage reports are uploaded to Codecov
|
||||
|
||||
## Git Workflow and Best Practices
|
||||
|
||||
### Branching
|
||||
|
||||
- Main branch: `main`
|
||||
- Create feature branches from `main`
|
||||
- PR reviews are required before merging
|
||||
|
||||
### Commit Messages
|
||||
|
||||
- Use clear, descriptive commit messages
|
||||
- Reference issue numbers when applicable
|
||||
- ALWAYS run `pre-commit run --all-files` before each commit to avoid formatting issues
|
||||
|
||||
### Pull Requests
|
||||
|
||||
- Ensure all tests pass before requesting review
|
||||
- Update documentation if adding new features
|
||||
- Follow the PR template in `.github/PULL_REQUEST_TEMPLATE.md`
|
||||
- ALWAYS run `pre-commit run --all-files` before each commit to avoid formatting issues
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
flaml/
|
||||
├── automl/ # AutoML functionality
|
||||
├── tune/ # Hyperparameter tuning
|
||||
├── default/ # Zero-shot AutoML
|
||||
├── autogen/ # Legacy autogen (deprecated, moved to separate repo)
|
||||
├── fabric/ # Microsoft Fabric integration
|
||||
├── onlineml/ # Online learning
|
||||
└── version.py # Version information
|
||||
|
||||
test/ # Test suite
|
||||
├── automl/
|
||||
├── tune/
|
||||
├── default/
|
||||
├── nlp/
|
||||
└── spark/
|
||||
|
||||
notebook/ # Example notebooks
|
||||
website/ # Documentation website
|
||||
```
|
||||
|
||||
## Dependencies and Package Management
|
||||
|
||||
### Core Dependencies
|
||||
|
||||
- NumPy >= 1.17
|
||||
- Python >= 3.10 (officially supported: 3.10, 3.11, 3.12 and 3.13)
|
||||
|
||||
### Optional Dependencies
|
||||
|
||||
- `[automl]`: lightgbm, xgboost, scipy, pandas, scikit-learn
|
||||
- `[test]`: Full test suite dependencies
|
||||
- `[spark]`: PySpark and joblib dependencies
|
||||
- `[forecast]`: holidays, prophet, statsmodels, hcrystalball, pytorch-forecasting, pytorch-lightning, tensorboardX
|
||||
- `[hf]`: Hugging Face transformers and datasets
|
||||
- See `setup.py` for complete list
|
||||
|
||||
### Version Constraints
|
||||
|
||||
- Be mindful of Python version-specific dependencies (check setup.py)
|
||||
- XGBoost versions differ based on Python version
|
||||
- NumPy 2.0+ only for Python >= 3.13
|
||||
- Some features (like vowpalwabbit) only work with older Python versions
|
||||
|
||||
## Boundaries and Restrictions
|
||||
|
||||
### Do NOT Modify
|
||||
|
||||
- `.git/` directory and Git configuration
|
||||
- `LICENSE` file
|
||||
- Version information in `flaml/version.py` (unless explicitly updating version)
|
||||
- GitHub Actions workflows without careful consideration
|
||||
- Existing test files unless fixing bugs or adding coverage
|
||||
|
||||
### Be Cautious With
|
||||
|
||||
- `setup.py`: Changes to dependencies should be carefully reviewed
|
||||
- `pyproject.toml`: Linting and testing configuration
|
||||
- `.pre-commit-config.yaml`: Pre-commit hook configuration
|
||||
- Backward compatibility: FLAML is a library with external users
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- Never commit secrets or API keys
|
||||
- Be careful with external data sources in tests
|
||||
- Validate user inputs in public APIs
|
||||
- Follow secure coding practices for ML operations
|
||||
|
||||
## Special Notes
|
||||
|
||||
### AutoGen Migration
|
||||
|
||||
- AutoGen has moved to a separate repository: https://github.com/microsoft/autogen
|
||||
- The `flaml/autogen/` directory contains legacy code
|
||||
- Tests in `test/autogen/` are ignored in the main test suite
|
||||
- Direct users to the new AutoGen repository for AutoGen-related issues
|
||||
|
||||
### Platform-Specific Considerations
|
||||
|
||||
- Some tests only run on Linux (e.g., forecast tests with prophet)
|
||||
- Windows and Ubuntu are the primary supported platforms
|
||||
- macOS support exists but requires special libomp setup for lgbm/xgboost
|
||||
|
||||
### Performance
|
||||
|
||||
- FLAML focuses on efficient automation and tuning
|
||||
- Consider computational cost when adding new features
|
||||
- Optimize for low resource usage where possible
|
||||
|
||||
## Documentation
|
||||
|
||||
- Main documentation: https://microsoft.github.io/FLAML/
|
||||
- Update documentation when adding new features
|
||||
- Provide clear examples in docstrings
|
||||
- Add notebook examples for significant new features
|
||||
|
||||
## Contributing
|
||||
|
||||
- Follow the contributing guide: https://microsoft.github.io/FLAML/docs/Contribute
|
||||
- Sign the Microsoft CLA when making your first contribution
|
||||
- Be respectful and follow the Microsoft Open Source Code of Conduct
|
||||
- Join the Discord community for discussions: https://discord.gg/Cppx2vSPVP
|
||||
21
.github/workflows/CD.yml
vendored
21
.github/workflows/CD.yml
vendored
@@ -12,26 +12,17 @@ jobs:
|
||||
deploy:
|
||||
strategy:
|
||||
matrix:
|
||||
os: ['ubuntu-latest']
|
||||
python-version: [3.8]
|
||||
os: ["ubuntu-latest"]
|
||||
python-version: ["3.12"]
|
||||
runs-on: ${{ matrix.os }}
|
||||
environment: package
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Cache conda
|
||||
uses: actions/cache@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
path: ~/conda_pkgs_dir
|
||||
key: conda-${{ matrix.os }}-python-${{ matrix.python-version }}-${{ hashFiles('environment.yml') }}
|
||||
- name: Setup Miniconda
|
||||
uses: conda-incubator/setup-miniconda@v2
|
||||
with:
|
||||
auto-update-conda: true
|
||||
auto-activate-base: false
|
||||
activate-environment: hcrystalball
|
||||
python-version: ${{ matrix.python-version }}
|
||||
use-only-tar-bz2: true
|
||||
- name: Install from source
|
||||
# This is required for the pre-commit tests
|
||||
shell: pwsh
|
||||
@@ -42,7 +33,7 @@ jobs:
|
||||
- name: Build
|
||||
shell: pwsh
|
||||
run: |
|
||||
pip install twine
|
||||
pip install twine wheel setuptools
|
||||
python setup.py sdist bdist_wheel
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
|
||||
8
.github/workflows/deploy-website.yml
vendored
8
.github/workflows/deploy-website.yml
vendored
@@ -37,11 +37,11 @@ jobs:
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
python-version: "3.12"
|
||||
- name: pydoc-markdown install
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pydoc-markdown==4.5.0
|
||||
pip install pydoc-markdown==4.7.0 setuptools
|
||||
- name: pydoc-markdown run
|
||||
run: |
|
||||
pydoc-markdown
|
||||
@@ -73,11 +73,11 @@ jobs:
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
python-version: "3.12"
|
||||
- name: pydoc-markdown install
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pydoc-markdown==4.5.0
|
||||
pip install pydoc-markdown==4.7.0 setuptools
|
||||
- name: pydoc-markdown run
|
||||
run: |
|
||||
pydoc-markdown
|
||||
|
||||
17
.github/workflows/openai.yml
vendored
17
.github/workflows/openai.yml
vendored
@@ -4,14 +4,15 @@
|
||||
name: OpenAI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- 'flaml/autogen/**'
|
||||
- 'test/autogen/**'
|
||||
- 'notebook/autogen_openai_completion.ipynb'
|
||||
- 'notebook/autogen_chatgpt_gpt4.ipynb'
|
||||
- '.github/workflows/openai.yml'
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# branches: ['main']
|
||||
# paths:
|
||||
# - 'flaml/autogen/**'
|
||||
# - 'test/autogen/**'
|
||||
# - 'notebook/autogen_openai_completion.ipynb'
|
||||
# - 'notebook/autogen_chatgpt_gpt4.ipynb'
|
||||
# - '.github/workflows/openai.yml'
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
||||
4
.github/workflows/pre-commit.yml
vendored
4
.github/workflows/pre-commit.yml
vendored
@@ -1,9 +1,7 @@
|
||||
name: Code formatting
|
||||
|
||||
# see: https://help.github.com/en/actions/reference/events-that-trigger-workflows
|
||||
on: # Trigger the workflow on push or pull request, but only for the main branch
|
||||
push:
|
||||
branches: [main]
|
||||
on:
|
||||
pull_request: {}
|
||||
|
||||
defaults:
|
||||
|
||||
120
.github/workflows/python-package.yml
vendored
120
.github/workflows/python-package.yml
vendored
@@ -14,10 +14,20 @@ on:
|
||||
- 'setup.py'
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
paths:
|
||||
- 'flaml/**'
|
||||
- 'test/**'
|
||||
- 'notebook/**'
|
||||
- '.github/workflows/python-package.yml'
|
||||
- 'setup.py'
|
||||
merge_group:
|
||||
types: [checks_requested]
|
||||
schedule:
|
||||
# Every other day at 02:00 UTC
|
||||
- cron: '0 2 */2 * *'
|
||||
|
||||
permissions: {}
|
||||
permissions:
|
||||
contents: write
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
@@ -29,8 +39,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-2019]
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -38,7 +48,7 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: On mac, install libomp to facilitate lgbm and xgboost install
|
||||
if: matrix.os == 'macOS-latest'
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew update
|
||||
brew install libomp
|
||||
@@ -50,76 +60,82 @@ jobs:
|
||||
export LDFLAGS="$LDFLAGS -Wl,-rpath,/usr/local/opt/libomp/lib -L/usr/local/opt/libomp/lib -lomp"
|
||||
- name: Install packages and dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip wheel
|
||||
python -m pip install --upgrade pip wheel setuptools
|
||||
pip install -e .
|
||||
python -c "import flaml"
|
||||
pip install -e .[test]
|
||||
- name: On Ubuntu python 3.8, install pyspark 3.2.3
|
||||
if: matrix.python-version == '3.8' && matrix.os == 'ubuntu-latest'
|
||||
- name: On Ubuntu python 3.11, install pyspark 3.5.1
|
||||
if: matrix.python-version == '3.11' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
pip install pyspark==3.2.3
|
||||
pip install pyspark==3.5.1
|
||||
pip list | grep "pyspark"
|
||||
- name: If linux and python<3.11, install ray 2
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version != '3.11'
|
||||
- name: On Ubuntu python 3.12, install pyspark 4.0.1
|
||||
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
pip install "ray[tune]<2.5.0"
|
||||
- name: If mac and python 3.10, install ray and xgboost 1
|
||||
if: matrix.os == 'macOS-latest' && matrix.python-version == '3.10'
|
||||
pip install pyspark==4.0.1
|
||||
pip list | grep "pyspark"
|
||||
- name: On Ubuntu python 3.13, install pyspark 4.1.0
|
||||
if: matrix.python-version == '3.13' && matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
pip install -e .[ray]
|
||||
# use macOS to test xgboost 1, but macOS also supports xgboost 2
|
||||
pip install "xgboost<2"
|
||||
- name: If linux, install prophet on python < 3.9
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.8'
|
||||
pip install pyspark==4.1.0
|
||||
pip list | grep "pyspark"
|
||||
# # TODO: support ray
|
||||
# - name: If linux and python<3.11, install ray 2
|
||||
# if: matrix.os == 'ubuntu-latest' && matrix.python-version < '3.11'
|
||||
# run: |
|
||||
# pip install "ray[tune]<2.5.0"
|
||||
- name: Install prophet when on linux
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
pip install -e .[forecast]
|
||||
- name: Install vw on python < 3.10
|
||||
if: matrix.python-version == '3.8' || matrix.python-version == '3.9'
|
||||
# TODO: support vw for python 3.10+
|
||||
- name: If linux and python<3.10, install vw
|
||||
if: matrix.os == 'ubuntu-latest' && matrix.python-version < '3.10'
|
||||
run: |
|
||||
pip install -e .[vw]
|
||||
- name: Uninstall pyspark on (python 3.9) or windows
|
||||
if: matrix.python-version == '3.9' || matrix.os == 'windows-2019'
|
||||
- name: Pip freeze
|
||||
run: |
|
||||
# Uninstall pyspark to test env without pyspark
|
||||
pip uninstall -y pyspark
|
||||
pip freeze
|
||||
- name: Check dependencies
|
||||
run: |
|
||||
python test/check_dependency.py
|
||||
- name: Clear pip cache
|
||||
run: |
|
||||
pip cache purge
|
||||
- name: Test with pytest
|
||||
if: matrix.python-version != '3.10'
|
||||
timeout-minutes: 120
|
||||
if: matrix.python-version != '3.11'
|
||||
run: |
|
||||
pytest test
|
||||
pytest test/ --ignore=test/autogen --reruns 2 --reruns-delay 10
|
||||
- name: Coverage
|
||||
if: matrix.python-version == '3.10'
|
||||
timeout-minutes: 120
|
||||
if: matrix.python-version == '3.11'
|
||||
run: |
|
||||
pip install coverage
|
||||
coverage run -a -m pytest test
|
||||
coverage run -a -m pytest test --ignore=test/autogen --reruns 2 --reruns-delay 10
|
||||
coverage xml
|
||||
- name: Upload coverage to Codecov
|
||||
if: matrix.python-version == '3.10'
|
||||
if: matrix.python-version == '3.11'
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
flags: unittests
|
||||
- name: Save dependencies
|
||||
if: github.ref == 'refs/heads/main'
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global user.name 'github-actions[bot]'
|
||||
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
|
||||
git config advice.addIgnoredFile false
|
||||
|
||||
# docs:
|
||||
BRANCH=unit-tests-installed-dependencies
|
||||
git fetch origin
|
||||
git checkout -B "$BRANCH" "origin/$BRANCH"
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - name: Setup Python
|
||||
# uses: actions/setup-python@v4
|
||||
# with:
|
||||
# python-version: '3.8'
|
||||
# - name: Compile documentation
|
||||
# run: |
|
||||
# pip install -e .
|
||||
# python -m pip install sphinx sphinx_rtd_theme
|
||||
# cd docs
|
||||
# make html
|
||||
# - name: Deploy to GitHub pages
|
||||
# if: ${{ github.ref == 'refs/heads/main' }}
|
||||
# uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
# with:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# BRANCH: gh-pages
|
||||
# FOLDER: docs/_build/html
|
||||
# CLEAN: true
|
||||
pip freeze > installed_all_dependencies_${{ matrix.python-version }}_${{ matrix.os }}.txt
|
||||
python test/check_dependency.py > installed_first_tier_dependencies_${{ matrix.python-version }}_${{ matrix.os }}.txt
|
||||
git add installed_*dependencies*.txt
|
||||
mv coverage.xml ./coverage_${{ matrix.python-version }}_${{ matrix.os }}.xml || true
|
||||
git add -f ./coverage_${{ matrix.python-version }}_${{ matrix.os }}.xml || true
|
||||
git commit -m "Update installed dependencies for Python ${{ matrix.python-version }} on ${{ matrix.os }}" || exit 0
|
||||
git push origin "$BRANCH" --force
|
||||
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
@@ -60,6 +60,7 @@ coverage.xml
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
junit
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@@ -163,6 +164,28 @@ output/
|
||||
flaml/tune/spark/mylearner.py
|
||||
*.pkl
|
||||
|
||||
data/
|
||||
benchmark/pmlb/csv_datasets
|
||||
benchmark/*.csv
|
||||
|
||||
checkpoints/
|
||||
test/default
|
||||
test/housing.json
|
||||
test/nlp/default/transformer_ms/seq-classification.json
|
||||
|
||||
flaml/fabric/fanova/*fanova.c
|
||||
# local config files
|
||||
*.config.local
|
||||
|
||||
local_debug/
|
||||
patch.diff
|
||||
|
||||
# Test things
|
||||
notebook/lightning_logs/
|
||||
lightning_logs/
|
||||
flaml/autogen/extensions/tmp/
|
||||
test/autogen/my_tmp/
|
||||
catboost_*
|
||||
|
||||
# Internal configs
|
||||
.pypirc
|
||||
|
||||
@@ -23,13 +23,20 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: no-commit-to-branch
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py38-plus]
|
||||
name: Upgrade code
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.17
|
||||
rev: 0.7.22
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# basic setup
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.8
|
||||
FROM mcr.microsoft.com/devcontainers/python:3.10
|
||||
RUN apt-get update && apt-get -y update
|
||||
RUN apt-get install -y sudo git npm
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ This repository incorporates material as listed below or described in the code.
|
||||
|
||||
## Component. Ray.
|
||||
|
||||
Code in tune/\[analysis.py, sample.py, trial.py, result.py\],
|
||||
searcher/\[suggestion.py, variant_generator.py\], and scheduler/trial_scheduler.py is adapted from
|
||||
Code in tune/[analysis.py, sample.py, trial.py, result.py],
|
||||
searcher/[suggestion.py, variant_generator.py], and scheduler/trial_scheduler.py is adapted from
|
||||
https://github.com/ray-project/ray/blob/master/python/ray/tune/
|
||||
|
||||
## Open Source License/Copyright Notice.
|
||||
|
||||
64
README.md
64
README.md
@@ -1,7 +1,7 @@
|
||||
[](https://badge.fury.io/py/FLAML)
|
||||

|
||||
[](https://github.com/microsoft/FLAML/actions/workflows/python-package.yml)
|
||||

|
||||
[](https://pypi.org/project/FLAML/)
|
||||
[](https://pepy.tech/project/flaml)
|
||||
[](https://discord.gg/Cppx2vSPVP)
|
||||
|
||||
@@ -14,15 +14,9 @@
|
||||
<br>
|
||||
</p>
|
||||
|
||||
:fire: Heads-up: We have migrated [AutoGen](https://microsoft.github.io/autogen/) into a dedicated [github repository](https://github.com/microsoft/autogen). Alongside this move, we have also launched a dedicated [Discord](https://discord.gg/pAbnFJrkgZ) server and a [website](https://microsoft.github.io/autogen/) for comprehensive documentation.
|
||||
:fire: FLAML supports AutoML and Hyperparameter Tuning in [Microsoft Fabric Data Science](https://learn.microsoft.com/en-us/fabric/data-science/automated-machine-learning-fabric). In addition, we've introduced Python 3.11 and 3.12 support, along with a range of new estimators, and comprehensive integration with MLflow—thanks to contributions from the Microsoft Fabric product team.
|
||||
|
||||
:fire: The automated multi-agent chat framework in [AutoGen](https://microsoft.github.io/autogen/) is in preview from v2.0.0.
|
||||
|
||||
:fire: FLAML is highlighted in OpenAI's [cookbook](https://github.com/openai/openai-cookbook#related-resources-from-around-the-web).
|
||||
|
||||
:fire: [autogen](https://microsoft.github.io/autogen/) is released with support for ChatGPT and GPT-4, based on [Cost-Effective Hyperparameter Optimization for Large Language Model Generation Inference](https://arxiv.org/abs/2303.04673).
|
||||
|
||||
:fire: FLAML supports Code-First AutoML & Tuning – Private Preview in [Microsoft Fabric Data Science](https://learn.microsoft.com/en-us/fabric/data-science/).
|
||||
:fire: Heads-up: [AutoGen](https://microsoft.github.io/autogen/) has moved to a dedicated [GitHub repository](https://github.com/microsoft/autogen). FLAML no longer includes the `autogen` module—please use AutoGen directly.
|
||||
|
||||
## What is FLAML
|
||||
|
||||
@@ -30,7 +24,7 @@ FLAML is a lightweight Python library for efficient automation of machine
|
||||
learning and AI operations. It automates workflow based on large language models, machine learning models, etc.
|
||||
and optimizes their performance.
|
||||
|
||||
- FLAML enables building next-gen GPT-X applications based on multi-agent conversations with minimal effort. It simplifies the orchestration, automation and optimization of a complex GPT-X workflow. It maximizes the performance of GPT-X models and augments their weakness.
|
||||
- FLAML enables economical automation and tuning for ML/AI workflows, including model selection and hyperparameter optimization under resource constraints.
|
||||
- For common machine learning tasks like classification and regression, it quickly finds quality models for user-provided data with low computational resources. It is easy to customize or extend. Users can find their desired customizability from a smooth range.
|
||||
- It supports fast and economical automatic tuning (e.g., inference hyperparameters for foundation models, configurations in MLOps/LMOps workflows, pipelines, mathematical/statistical models, algorithms, computing experiments, software configurations), capable of handling large search space with heterogeneous evaluation cost and complex constraints/guidance/early stopping.
|
||||
|
||||
@@ -40,16 +34,16 @@ FLAML has a .NET implementation in [ML.NET](http://dot.net/ml), an open-source,
|
||||
|
||||
## Installation
|
||||
|
||||
FLAML requires **Python version >= 3.8**. It can be installed from pip:
|
||||
The latest version of FLAML requires **Python >= 3.10 and < 3.14**. While other Python versions may work for core components, full model support is not guaranteed. FLAML can be installed via `pip`:
|
||||
|
||||
```bash
|
||||
pip install flaml
|
||||
```
|
||||
|
||||
Minimal dependencies are installed without extra options. You can install extra options based on the feature you need. For example, use the following to install the dependencies needed by the [`autogen`](https://microsoft.github.io/autogen/) package.
|
||||
Minimal dependencies are installed without extra options. You can install extra options based on the feature you need. For example, use the following to install the dependencies needed by the [`automl`](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML) module.
|
||||
|
||||
```bash
|
||||
pip install "flaml[autogen]"
|
||||
pip install "flaml[automl]"
|
||||
```
|
||||
|
||||
Find more options in [Installation](https://microsoft.github.io/FLAML/docs/Installation).
|
||||
@@ -57,39 +51,6 @@ Each of the [`notebook examples`](https://github.com/microsoft/FLAML/tree/main/n
|
||||
|
||||
## Quickstart
|
||||
|
||||
- (New) The [autogen](https://microsoft.github.io/autogen/) package enables the next-gen GPT-X applications with a generic multi-agent conversation framework.
|
||||
It offers customizable and conversable agents which integrate LLMs, tools and human.
|
||||
By automating chat among multiple capable agents, one can easily make them collectively perform tasks autonomously or with human feedback, including tasks that require using tools via code. For example,
|
||||
|
||||
```python
|
||||
from flaml import autogen
|
||||
|
||||
assistant = autogen.AssistantAgent("assistant")
|
||||
user_proxy = autogen.UserProxyAgent("user_proxy")
|
||||
user_proxy.initiate_chat(
|
||||
assistant,
|
||||
message="Show me the YTD gain of 10 largest technology companies as of today.",
|
||||
)
|
||||
# This initiates an automated chat between the two agents to solve the task
|
||||
```
|
||||
|
||||
Autogen also helps maximize the utility out of the expensive LLMs such as ChatGPT and GPT-4. It offers a drop-in replacement of `openai.Completion` or `openai.ChatCompletion` with powerful functionalites like tuning, caching, templating, filtering. For example, you can optimize generations by LLM with your own tuning data, success metrics and budgets.
|
||||
|
||||
```python
|
||||
# perform tuning
|
||||
config, analysis = autogen.Completion.tune(
|
||||
data=tune_data,
|
||||
metric="success",
|
||||
mode="max",
|
||||
eval_func=eval_func,
|
||||
inference_budget=0.05,
|
||||
optimization_budget=3,
|
||||
num_samples=-1,
|
||||
)
|
||||
# perform inference for a test instance
|
||||
response = autogen.Completion.create(context=test_instance, **config)
|
||||
```
|
||||
|
||||
- With three lines of code, you can start using this economical and fast
|
||||
AutoML engine as a [scikit-learn style estimator](https://microsoft.github.io/FLAML/docs/Use-Cases/Task-Oriented-AutoML).
|
||||
|
||||
@@ -111,7 +72,10 @@ automl.fit(X_train, y_train, task="classification", estimator_list=["lgbm"])
|
||||
|
||||
```python
|
||||
from flaml import tune
|
||||
tune.run(evaluation_function, config={…}, low_cost_partial_config={…}, time_budget_s=3600)
|
||||
|
||||
tune.run(
|
||||
evaluation_function, config={…}, low_cost_partial_config={…}, time_budget_s=3600
|
||||
)
|
||||
```
|
||||
|
||||
- [Zero-shot AutoML](https://microsoft.github.io/FLAML/docs/Use-Cases/Zero-Shot-AutoML) allows using the existing training API from lightgbm, xgboost etc. while getting the benefit of AutoML in choosing high-performance hyperparameter configurations per task.
|
||||
@@ -154,3 +118,9 @@ provided by the bot. You will only need to do this once across all repos using o
|
||||
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
|
||||
For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or
|
||||
contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.
|
||||
|
||||
## Contributors Wall
|
||||
|
||||
<a href="https://github.com/microsoft/flaml/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=microsoft/flaml&max=204" />
|
||||
</a>
|
||||
|
||||
@@ -12,7 +12,7 @@ If you believe you have found a security vulnerability in any Microsoft-owned re
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
from flaml.automl import AutoML, logger_formatter
|
||||
try:
|
||||
from flaml.automl import AutoML, logger_formatter
|
||||
|
||||
has_automl = True
|
||||
except ImportError:
|
||||
has_automl = False
|
||||
from flaml.onlineml.autovw import AutoVW
|
||||
from flaml.tune.searcher import CFO, FLOW2, BlendSearch, BlendSearchTuner, RandomSearch
|
||||
from flaml.version import __version__
|
||||
|
||||
# Set the root logger.
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
if logger.level == logging.NOTSET:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
if not has_automl:
|
||||
warnings.warn("flaml.automl is not available. Please install flaml[automl] to enable AutoML functionalities.")
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import warnings
|
||||
|
||||
from .agentchat import *
|
||||
from .code_utils import DEFAULT_MODEL, FAST_MODEL
|
||||
from .oai import *
|
||||
|
||||
warnings.warn(
|
||||
"The `flaml.autogen` module is deprecated and will be removed in a future release. "
|
||||
"Please refer to `https://github.com/microsoft/autogen` for latest usage.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
@@ -156,7 +156,7 @@ class MathUserProxyAgent(UserProxyAgent):
|
||||
when the number of auto reply reaches the max_consecutive_auto_reply or when is_termination_msg is True.
|
||||
default_auto_reply (str or dict or None): the default auto reply message when no code execution or llm based reply is generated.
|
||||
max_invalid_q_per_step (int): (ADDED) the maximum number of invalid queries per step.
|
||||
**kwargs (dict): other kwargs in [UserProxyAgent](user_proxy_agent#__init__).
|
||||
**kwargs (dict): other kwargs in [UserProxyAgent](../user_proxy_agent#__init__).
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
|
||||
@@ -123,7 +123,7 @@ class RetrieveUserProxyAgent(UserProxyAgent):
|
||||
can be found at `https://www.sbert.net/docs/pretrained_models.html`. The default model is a
|
||||
fast model. If you want to use a high performance model, `all-mpnet-base-v2` is recommended.
|
||||
- customized_prompt (Optional, str): the customized prompt for the retrieve chat. Default is None.
|
||||
**kwargs (dict): other kwargs in [UserProxyAgent](user_proxy_agent#__init__).
|
||||
**kwargs (dict): other kwargs in [UserProxyAgent](../user_proxy_agent#__init__).
|
||||
"""
|
||||
super().__init__(
|
||||
name=name,
|
||||
|
||||
@@ -125,7 +125,7 @@ def improve_function(file_name, func_name, objective, **config):
|
||||
"""(work in progress) Improve the function to achieve the objective."""
|
||||
params = {**_IMPROVE_FUNCTION_CONFIG, **config}
|
||||
# read the entire file into a str
|
||||
with open(file_name, "r") as f:
|
||||
with open(file_name) as f:
|
||||
file_string = f.read()
|
||||
response = oai.Completion.create(
|
||||
{"func_name": func_name, "objective": objective, "file_string": file_string}, **params
|
||||
@@ -158,7 +158,7 @@ def improve_code(files, objective, suggest_only=True, **config):
|
||||
code = ""
|
||||
for file_name in files:
|
||||
# read the entire file into a string
|
||||
with open(file_name, "r") as f:
|
||||
with open(file_name) as f:
|
||||
file_string = f.read()
|
||||
code += f"""{file_name}:
|
||||
{file_string}
|
||||
|
||||
@@ -130,7 +130,7 @@ def _fix_a_slash_b(string: str) -> str:
|
||||
try:
|
||||
a = int(a_str)
|
||||
b = int(b_str)
|
||||
assert string == "{}/{}".format(a, b)
|
||||
assert string == f"{a}/{b}"
|
||||
new_string = "\\frac{" + str(a) + "}{" + str(b) + "}"
|
||||
return new_string
|
||||
except Exception:
|
||||
|
||||
@@ -126,7 +126,7 @@ def split_files_to_chunks(
|
||||
"""Split a list of files into chunks of max_tokens."""
|
||||
chunks = []
|
||||
for file in files:
|
||||
with open(file, "r") as f:
|
||||
with open(file) as f:
|
||||
text = f.read()
|
||||
chunks += split_text_to_chunks(text, max_tokens, chunk_mode, must_break_at_empty_line)
|
||||
return chunks
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from flaml.automl.automl import AutoML, size
|
||||
from flaml.automl.logger import logger_formatter
|
||||
from flaml.automl.state import AutoMLState, SearchState
|
||||
|
||||
__all__ = ["AutoML", "AutoMLState", "SearchState", "logger_formatter", "size"]
|
||||
try:
|
||||
from flaml.automl.automl import AutoML, size
|
||||
from flaml.automl.state import AutoMLState, SearchState
|
||||
|
||||
__all__ = ["AutoML", "AutoMLState", "SearchState", "logger_formatter", "size"]
|
||||
except ImportError:
|
||||
__all__ = ["logger_formatter"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
try:
|
||||
from sklearn.ensemble import HistGradientBoostingClassifier, HistGradientBoostingRegressor
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError as e:
|
||||
print(f"scikit-learn is required for HistGradientBoostingEstimator. Please install it; error: {e}")
|
||||
|
||||
from flaml import tune
|
||||
from flaml.automl.model import SKLearnEstimator
|
||||
|
||||
@@ -2,13 +2,18 @@
|
||||
# * Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# * Licensed under the MIT License. See LICENSE file in the
|
||||
# * project root for license information.
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import random
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import numpy as np
|
||||
|
||||
from flaml.automl.spark import DataFrame, Series, pd, ps, psDataFrame, psSeries
|
||||
from flaml.automl.spark import DataFrame, F, Series, T, pd, ps, psDataFrame, psSeries
|
||||
from flaml.automl.training_log import training_log_reader
|
||||
|
||||
try:
|
||||
@@ -19,6 +24,7 @@ except ImportError:
|
||||
if TYPE_CHECKING:
|
||||
from flaml.automl.task import Task
|
||||
|
||||
|
||||
TS_TIMESTAMP_COL = "ds"
|
||||
TS_VALUE_COL = "y"
|
||||
|
||||
@@ -45,7 +51,10 @@ def load_openml_dataset(dataset_id, data_dir=None, random_state=0, dataset_forma
|
||||
"""
|
||||
import pickle
|
||||
|
||||
import openml
|
||||
try:
|
||||
import openml
|
||||
except ImportError:
|
||||
openml = None
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
filename = "openml_ds" + str(dataset_id) + ".pkl"
|
||||
@@ -56,15 +65,15 @@ def load_openml_dataset(dataset_id, data_dir=None, random_state=0, dataset_forma
|
||||
dataset = pickle.load(f)
|
||||
else:
|
||||
print("download dataset from openml")
|
||||
dataset = openml.datasets.get_dataset(dataset_id)
|
||||
dataset = openml.datasets.get_dataset(dataset_id) if openml else None
|
||||
if not os.path.exists(data_dir):
|
||||
os.makedirs(data_dir)
|
||||
with open(filepath, "wb") as f:
|
||||
pickle.dump(dataset, f, pickle.HIGHEST_PROTOCOL)
|
||||
print("Dataset name:", dataset.name)
|
||||
print("Dataset name:", dataset.name) if dataset else None
|
||||
try:
|
||||
X, y, *__ = dataset.get_data(target=dataset.default_target_attribute, dataset_format=dataset_format)
|
||||
except ValueError:
|
||||
except (ValueError, AttributeError, TypeError):
|
||||
from sklearn.datasets import fetch_openml
|
||||
|
||||
X, y = fetch_openml(data_id=dataset_id, return_X_y=True)
|
||||
@@ -293,7 +302,7 @@ class DataTransformer:
|
||||
y = y.rename(TS_VALUE_COL)
|
||||
for column in X.columns:
|
||||
# sklearn\utils\validation.py needs int/float values
|
||||
if X[column].dtype.name in ("object", "category"):
|
||||
if X[column].dtype.name in ("object", "category", "string"):
|
||||
if X[column].nunique() == 1 or X[column].nunique(dropna=True) == n - X[column].isnull().sum():
|
||||
X.drop(columns=column, inplace=True)
|
||||
drop = True
|
||||
@@ -445,3 +454,343 @@ class DataTransformer:
|
||||
def group_counts(groups):
|
||||
_, i, c = np.unique(groups, return_counts=True, return_index=True)
|
||||
return c[np.argsort(i)]
|
||||
|
||||
|
||||
def get_random_dataframe(n_rows: int = 200, ratio_none: float = 0.1, seed: int = 42) -> DataFrame:
|
||||
"""Generate a random pandas DataFrame with various data types for testing.
|
||||
This function creates a DataFrame with multiple column types including:
|
||||
- Timestamps
|
||||
- Integers
|
||||
- Floats
|
||||
- Categorical values
|
||||
- Booleans
|
||||
- Lists (tags)
|
||||
- Decimal strings
|
||||
- UUIDs
|
||||
- Binary data (as hex strings)
|
||||
- JSON blobs
|
||||
- Nullable text fields
|
||||
Parameters
|
||||
----------
|
||||
n_rows : int, default=200
|
||||
Number of rows in the generated DataFrame
|
||||
ratio_none : float, default=0.1
|
||||
Probability of generating None values in applicable columns
|
||||
seed : int, default=42
|
||||
Random seed for reproducibility
|
||||
Returns
|
||||
-------
|
||||
pd.DataFrame
|
||||
A DataFrame with 14 columns of various data types
|
||||
Examples
|
||||
--------
|
||||
>>> df = get_random_dataframe(100, 0.05, 123)
|
||||
>>> df.shape
|
||||
(100, 14)
|
||||
>>> df.dtypes
|
||||
timestamp datetime64[ns]
|
||||
id int64
|
||||
score float64
|
||||
status object
|
||||
flag object
|
||||
count object
|
||||
value object
|
||||
tags object
|
||||
rating object
|
||||
uuid object
|
||||
binary object
|
||||
json_blob object
|
||||
category category
|
||||
nullable_text object
|
||||
dtype: object
|
||||
"""
|
||||
|
||||
np.random.seed(seed)
|
||||
random.seed(seed)
|
||||
|
||||
def random_tags():
|
||||
tags = ["AI", "ML", "data", "robotics", "vision"]
|
||||
return random.sample(tags, k=random.randint(1, 3)) if random.random() > ratio_none else None
|
||||
|
||||
def random_decimal():
|
||||
return (
|
||||
str(Decimal(random.uniform(1, 5)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
|
||||
if random.random() > ratio_none
|
||||
else None
|
||||
)
|
||||
|
||||
def random_json_blob():
|
||||
blob = {"a": random.randint(1, 10), "b": random.random()}
|
||||
return json.dumps(blob) if random.random() > ratio_none else None
|
||||
|
||||
def random_binary():
|
||||
return bytes(random.randint(0, 255) for _ in range(4)).hex() if random.random() > ratio_none else None
|
||||
|
||||
data = {
|
||||
"timestamp": [
|
||||
datetime(2020, 1, 1) + timedelta(days=np.random.randint(0, 1000)) if np.random.rand() > ratio_none else None
|
||||
for _ in range(n_rows)
|
||||
],
|
||||
"id": range(1, n_rows + 1),
|
||||
"score": np.random.uniform(0, 100, n_rows),
|
||||
"status": np.random.choice(
|
||||
["active", "inactive", "pending", None],
|
||||
size=n_rows,
|
||||
p=[(1 - ratio_none) / 3, (1 - ratio_none) / 3, (1 - ratio_none) / 3, ratio_none],
|
||||
),
|
||||
"flag": np.random.choice(
|
||||
[True, False, None], size=n_rows, p=[(1 - ratio_none) / 2, (1 - ratio_none) / 2, ratio_none]
|
||||
),
|
||||
"count": [np.random.randint(0, 100) if np.random.rand() > ratio_none else None for _ in range(n_rows)],
|
||||
"value": [round(np.random.normal(50, 15), 2) if np.random.rand() > ratio_none else None for _ in range(n_rows)],
|
||||
"tags": [random_tags() for _ in range(n_rows)],
|
||||
"rating": [random_decimal() for _ in range(n_rows)],
|
||||
"uuid": [str(uuid.uuid4()) if np.random.rand() > ratio_none else None for _ in range(n_rows)],
|
||||
"binary": [random_binary() for _ in range(n_rows)],
|
||||
"json_blob": [random_json_blob() for _ in range(n_rows)],
|
||||
"category": pd.Categorical(
|
||||
np.random.choice(
|
||||
["A", "B", "C", None],
|
||||
size=n_rows,
|
||||
p=[(1 - ratio_none) / 3, (1 - ratio_none) / 3, (1 - ratio_none) / 3, ratio_none],
|
||||
)
|
||||
),
|
||||
"nullable_text": [random.choice(["Good", "Bad", "Average", None]) for _ in range(n_rows)],
|
||||
}
|
||||
|
||||
return pd.DataFrame(data)
|
||||
|
||||
|
||||
def auto_convert_dtypes_spark(
|
||||
df: psDataFrame,
|
||||
na_values: list = None,
|
||||
category_threshold: float = 0.3,
|
||||
convert_threshold: float = 0.6,
|
||||
sample_ratio: float = 0.1,
|
||||
) -> tuple[psDataFrame, dict]:
|
||||
"""Automatically convert data types in a PySpark DataFrame using heuristics.
|
||||
|
||||
This function analyzes a sample of the DataFrame to infer appropriate data types
|
||||
and applies the conversions. It handles timestamps, numeric values, booleans,
|
||||
and categorical fields.
|
||||
|
||||
Args:
|
||||
df: A PySpark DataFrame to convert.
|
||||
na_values: List of strings to be considered as NA/NaN. Defaults to
|
||||
['NA', 'na', 'NULL', 'null', ''].
|
||||
category_threshold: Maximum ratio of unique values to total values
|
||||
to consider a column categorical. Defaults to 0.3.
|
||||
convert_threshold: Minimum ratio of successfully converted values required
|
||||
to apply a type conversion. Defaults to 0.6.
|
||||
sample_ratio: Fraction of data to sample for type inference. Defaults to 0.1.
|
||||
|
||||
Returns:
|
||||
tuple: (The DataFrame with converted types, A dictionary mapping column names to
|
||||
their inferred types as strings)
|
||||
|
||||
Note:
|
||||
- 'category' in the schema dict is conceptual as PySpark doesn't have a true
|
||||
category type like pandas
|
||||
- The function uses sampling for efficiency with large datasets
|
||||
"""
|
||||
n_rows = df.count()
|
||||
if na_values is None:
|
||||
na_values = ["NA", "na", "NULL", "null", ""]
|
||||
|
||||
# Normalize NA-like values
|
||||
for colname, coltype in df.dtypes:
|
||||
if coltype == "string":
|
||||
df = df.withColumn(
|
||||
colname,
|
||||
F.when(F.trim(F.lower(F.col(colname))).isin([v.lower() for v in na_values]), None).otherwise(
|
||||
F.col(colname)
|
||||
),
|
||||
)
|
||||
|
||||
schema = {}
|
||||
for colname in df.columns:
|
||||
# Sample once at an appropriate ratio
|
||||
sample_ratio_to_use = min(1.0, sample_ratio if n_rows * sample_ratio > 100 else 100 / n_rows)
|
||||
col_sample = df.select(colname).sample(withReplacement=False, fraction=sample_ratio_to_use).dropna()
|
||||
sample_count = col_sample.count()
|
||||
|
||||
inferred_type = "string" # Default
|
||||
|
||||
if col_sample.dtypes[0][1] != "string":
|
||||
schema[colname] = col_sample.dtypes[0][1]
|
||||
continue
|
||||
|
||||
if sample_count == 0:
|
||||
schema[colname] = "string"
|
||||
continue
|
||||
|
||||
# Check if timestamp
|
||||
ts_col = col_sample.withColumn("parsed", F.to_timestamp(F.col(colname)))
|
||||
|
||||
# Check numeric
|
||||
if (
|
||||
col_sample.withColumn("n", F.col(colname).cast("double")).filter("n is not null").count()
|
||||
>= sample_count * convert_threshold
|
||||
):
|
||||
# All whole numbers?
|
||||
all_whole = (
|
||||
col_sample.withColumn("n", F.col(colname).cast("double"))
|
||||
.filter("n is not null")
|
||||
.withColumn("frac", F.abs(F.col("n") % 1))
|
||||
.filter("frac > 0.000001")
|
||||
.count()
|
||||
== 0
|
||||
)
|
||||
inferred_type = "int" if all_whole else "double"
|
||||
|
||||
# Check low-cardinality (category-like)
|
||||
elif (
|
||||
sample_count > 0
|
||||
and col_sample.select(F.countDistinct(F.col(colname))).collect()[0][0] / sample_count <= category_threshold
|
||||
):
|
||||
inferred_type = "category" # Will just be string, but marked as such
|
||||
|
||||
# Check if timestamp
|
||||
elif ts_col.filter(F.col("parsed").isNotNull()).count() >= sample_count * convert_threshold:
|
||||
inferred_type = "timestamp"
|
||||
|
||||
schema[colname] = inferred_type
|
||||
|
||||
# Apply inferred schema
|
||||
for colname, inferred_type in schema.items():
|
||||
if inferred_type == "int":
|
||||
df = df.withColumn(colname, F.col(colname).cast(T.IntegerType()))
|
||||
elif inferred_type == "double":
|
||||
df = df.withColumn(colname, F.col(colname).cast(T.DoubleType()))
|
||||
elif inferred_type == "boolean":
|
||||
df = df.withColumn(
|
||||
colname,
|
||||
F.when(F.lower(F.col(colname)).isin("true", "yes", "1"), True)
|
||||
.when(F.lower(F.col(colname)).isin("false", "no", "0"), False)
|
||||
.otherwise(None),
|
||||
)
|
||||
elif inferred_type == "timestamp":
|
||||
df = df.withColumn(colname, F.to_timestamp(F.col(colname)))
|
||||
elif inferred_type == "category":
|
||||
df = df.withColumn(colname, F.col(colname).cast(T.StringType())) # Marked conceptually
|
||||
|
||||
# otherwise keep as string (or original type)
|
||||
|
||||
return df, schema
|
||||
|
||||
|
||||
def auto_convert_dtypes_pandas(
|
||||
df: DataFrame,
|
||||
na_values: list = None,
|
||||
category_threshold: float = 0.3,
|
||||
convert_threshold: float = 0.6,
|
||||
sample_ratio: float = 1.0,
|
||||
) -> tuple[DataFrame, dict]:
|
||||
"""Automatically convert data types in a pandas DataFrame using heuristics.
|
||||
|
||||
This function analyzes the DataFrame to infer appropriate data types
|
||||
and applies the conversions. It handles timestamps, timedeltas, numeric values,
|
||||
and categorical fields.
|
||||
|
||||
Args:
|
||||
df: A pandas DataFrame to convert.
|
||||
na_values: List of strings to be considered as NA/NaN. Defaults to
|
||||
['NA', 'na', 'NULL', 'null', ''].
|
||||
category_threshold: Maximum ratio of unique values to total values
|
||||
to consider a column categorical. Defaults to 0.3.
|
||||
convert_threshold: Minimum ratio of successfully converted values required
|
||||
to apply a type conversion. Defaults to 0.6.
|
||||
sample_ratio: Fraction of data to sample for type inference. Not used in pandas version
|
||||
but included for API compatibility. Defaults to 1.0.
|
||||
|
||||
Returns:
|
||||
tuple: (The DataFrame with converted types, A dictionary mapping column names to
|
||||
their inferred types as strings)
|
||||
"""
|
||||
if na_values is None:
|
||||
na_values = {"NA", "na", "NULL", "null", ""}
|
||||
# Remove the empty string separately (handled by the regex `^\s*$`)
|
||||
vals = [re.escape(v) for v in na_values if v != ""]
|
||||
# Build inner alternation group
|
||||
inner = "|".join(vals) if vals else ""
|
||||
if inner:
|
||||
pattern = re.compile(rf"^\s*(?:{inner})?\s*$")
|
||||
else:
|
||||
pattern = re.compile(r"^\s*$")
|
||||
|
||||
df_converted = df.convert_dtypes()
|
||||
schema = {}
|
||||
|
||||
# Sample if needed (for API compatibility)
|
||||
if sample_ratio < 1.0:
|
||||
df = df.sample(frac=sample_ratio)
|
||||
|
||||
n_rows = len(df)
|
||||
|
||||
for col in df.columns:
|
||||
series = df[col]
|
||||
# Replace NA-like values if string
|
||||
if series.dtype == object:
|
||||
mask = series.astype(str).str.match(pattern)
|
||||
series_cleaned = series.where(~mask, np.nan)
|
||||
else:
|
||||
series_cleaned = series
|
||||
|
||||
# Skip conversion if already non-object data type, except bool which can potentially be categorical
|
||||
if (
|
||||
not isinstance(series_cleaned.dtype, pd.BooleanDtype)
|
||||
and not isinstance(series_cleaned.dtype, pd.StringDtype)
|
||||
and series_cleaned.dtype != "object"
|
||||
):
|
||||
# Keep the original data type for non-object dtypes
|
||||
df_converted[col] = series
|
||||
schema[col] = str(series_cleaned.dtype)
|
||||
continue
|
||||
|
||||
# print(f"type: {series_cleaned.dtype}, column: {series_cleaned.name}")
|
||||
|
||||
if not isinstance(series_cleaned.dtype, pd.BooleanDtype):
|
||||
# Try numeric (int or float)
|
||||
numeric = pd.to_numeric(series_cleaned, errors="coerce")
|
||||
if numeric.notna().sum() >= n_rows * convert_threshold:
|
||||
if (numeric.dropna() % 1 == 0).all():
|
||||
try:
|
||||
df_converted[col] = numeric.astype("int") # Nullable integer
|
||||
schema[col] = "int"
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
df_converted[col] = numeric.astype("double")
|
||||
schema[col] = "double"
|
||||
continue
|
||||
|
||||
# Try datetime
|
||||
datetime_converted = pd.to_datetime(series_cleaned, errors="coerce")
|
||||
if datetime_converted.notna().sum() >= n_rows * convert_threshold:
|
||||
df_converted[col] = datetime_converted
|
||||
schema[col] = "timestamp"
|
||||
continue
|
||||
|
||||
# Try timedelta
|
||||
try:
|
||||
timedelta_converted = pd.to_timedelta(series_cleaned, errors="coerce")
|
||||
if timedelta_converted.notna().sum() >= n_rows * convert_threshold:
|
||||
df_converted[col] = timedelta_converted
|
||||
schema[col] = "timedelta"
|
||||
continue
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
# Try category
|
||||
try:
|
||||
unique_ratio = series_cleaned.nunique(dropna=True) / n_rows if n_rows > 0 else 1.0
|
||||
if unique_ratio <= category_threshold:
|
||||
df_converted[col] = series_cleaned.astype("category")
|
||||
schema[col] = "category"
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
df_converted[col] = series_cleaned.astype("string")
|
||||
schema[col] = "string"
|
||||
|
||||
return df_converted, schema
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
# ANSI escape codes for colors
|
||||
COLORS = {
|
||||
# logging.DEBUG: "\033[36m", # Cyan
|
||||
# logging.INFO: "\033[32m", # Green
|
||||
logging.WARNING: "\033[33m", # Yellow
|
||||
logging.ERROR: "\033[31m", # Red
|
||||
logging.CRITICAL: "\033[1;31m", # Bright Red
|
||||
}
|
||||
RESET = "\033[0m" # Reset to default
|
||||
|
||||
def __init__(self, fmt, datefmt, use_color=True):
|
||||
super().__init__(fmt, datefmt)
|
||||
self.use_color = use_color
|
||||
|
||||
def format(self, record):
|
||||
formatted = super().format(record)
|
||||
if self.use_color:
|
||||
color = self.COLORS.get(record.levelno, "")
|
||||
if color:
|
||||
return f"{color}{formatted}{self.RESET}"
|
||||
return formatted
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger_formatter = logging.Formatter(
|
||||
"[%(name)s: %(asctime)s] {%(lineno)d} %(levelname)s - %(message)s", "%m-%d %H:%M:%S"
|
||||
use_color = True
|
||||
if os.getenv("FLAML_LOG_NO_COLOR"):
|
||||
use_color = False
|
||||
|
||||
logger_formatter = ColoredFormatter(
|
||||
"[%(name)s: %(asctime)s] {%(lineno)d} %(levelname)s - %(message)s", "%m-%d %H:%M:%S", use_color
|
||||
)
|
||||
logger.propagate = False
|
||||
|
||||
@@ -13,6 +13,7 @@ from flaml.automl.model import BaseEstimator, TransformersEstimator
|
||||
from flaml.automl.spark import ERROR as SPARK_ERROR
|
||||
from flaml.automl.spark import DataFrame, Series, psDataFrame, psSeries
|
||||
from flaml.automl.task.task import Task
|
||||
from flaml.automl.time_series import TimeSeriesDataset
|
||||
|
||||
try:
|
||||
from sklearn.metrics import (
|
||||
@@ -33,7 +34,6 @@ except ImportError:
|
||||
if SPARK_ERROR is None:
|
||||
from flaml.automl.spark.metrics import spark_metric_loss_score
|
||||
|
||||
from flaml.automl.time_series import TimeSeriesDataset
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -89,6 +89,11 @@ huggingface_metric_to_mode = {
|
||||
"wer": "min",
|
||||
}
|
||||
huggingface_submetric_to_metric = {"rouge1": "rouge", "rouge2": "rouge"}
|
||||
spark_metric_name_dict = {
|
||||
"Regression": ["r2", "rmse", "mse", "mae", "var"],
|
||||
"Binary Classification": ["pr_auc", "roc_auc"],
|
||||
"Multi-class Classification": ["accuracy", "log_loss", "f1", "micro_f1", "macro_f1"],
|
||||
}
|
||||
|
||||
|
||||
def metric_loss_score(
|
||||
@@ -122,9 +127,21 @@ def metric_loss_score(
|
||||
import datasets
|
||||
|
||||
datasets_metric_name = huggingface_submetric_to_metric.get(metric_name, metric_name.split(":")[0])
|
||||
metric = datasets.load_metric(datasets_metric_name)
|
||||
metric_mode = huggingface_metric_to_mode[datasets_metric_name]
|
||||
|
||||
# datasets>=3 removed load_metric; prefer evaluate if available
|
||||
try:
|
||||
import evaluate
|
||||
|
||||
metric = evaluate.load(datasets_metric_name, trust_remote_code=True)
|
||||
except Exception:
|
||||
if hasattr(datasets, "load_metric"):
|
||||
metric = datasets.load_metric(datasets_metric_name, trust_remote_code=True)
|
||||
else:
|
||||
from datasets import load_metric as _load_metric # older datasets
|
||||
|
||||
metric = _load_metric(datasets_metric_name, trust_remote_code=True)
|
||||
|
||||
if metric_name.startswith("seqeval"):
|
||||
y_processed_true = [[labels[tr] for tr in each_list] for each_list in y_processed_true]
|
||||
elif metric in ("pearsonr", "spearmanr"):
|
||||
@@ -294,14 +311,14 @@ def get_y_pred(estimator, X, eval_metric, task: Task):
|
||||
else:
|
||||
y_pred = estimator.predict(X)
|
||||
|
||||
if isinstance(y_pred, Series) or isinstance(y_pred, DataFrame):
|
||||
if isinstance(y_pred, (Series, DataFrame)):
|
||||
y_pred = y_pred.values
|
||||
|
||||
return y_pred
|
||||
|
||||
|
||||
def to_numpy(x):
|
||||
if isinstance(x, Series or isinstance(x, DataFrame)):
|
||||
if isinstance(x, (Series, DataFrame)):
|
||||
x = x.values
|
||||
else:
|
||||
x = np.ndarray(x)
|
||||
@@ -334,6 +351,14 @@ def compute_estimator(
|
||||
if fit_kwargs is None:
|
||||
fit_kwargs = {}
|
||||
|
||||
fe_params = {}
|
||||
for param, value in config_dic.items():
|
||||
if param.startswith("fe."):
|
||||
fe_params[param] = value
|
||||
|
||||
for param, value in fe_params.items():
|
||||
config_dic.pop(param)
|
||||
|
||||
estimator_class = estimator_class or task.estimator_class_from_str(estimator_name)
|
||||
estimator = estimator_class(
|
||||
**config_dic,
|
||||
@@ -401,12 +426,21 @@ def train_estimator(
|
||||
free_mem_ratio=0,
|
||||
) -> Tuple[EstimatorSubclass, float]:
|
||||
start_time = time.time()
|
||||
fe_params = {}
|
||||
for param, value in config_dic.items():
|
||||
if param.startswith("fe."):
|
||||
fe_params[param] = value
|
||||
|
||||
for param, value in fe_params.items():
|
||||
config_dic.pop(param)
|
||||
|
||||
estimator_class = estimator_class or task.estimator_class_from_str(estimator_name)
|
||||
estimator = estimator_class(
|
||||
**config_dic,
|
||||
task=task,
|
||||
n_jobs=n_jobs,
|
||||
)
|
||||
|
||||
if fit_kwargs is None:
|
||||
fit_kwargs = {}
|
||||
|
||||
@@ -552,7 +586,7 @@ def _eval_estimator(
|
||||
|
||||
# TODO: why are integer labels being cast to str in the first place?
|
||||
|
||||
if isinstance(val_pred_y, Series) or isinstance(val_pred_y, DataFrame) or isinstance(val_pred_y, np.ndarray):
|
||||
if isinstance(val_pred_y, (Series, DataFrame, np.ndarray)):
|
||||
test = val_pred_y if isinstance(val_pred_y, np.ndarray) else val_pred_y.values
|
||||
if not np.issubdtype(test.dtype, np.number):
|
||||
# some NLP models return a list
|
||||
@@ -582,7 +616,12 @@ def _eval_estimator(
|
||||
logger.warning(f"ValueError {e} happened in `metric_loss_score`, set `val_loss` to `np.inf`")
|
||||
metric_for_logging = {"pred_time": pred_time}
|
||||
if log_training_metric:
|
||||
train_pred_y = get_y_pred(estimator, X_train, eval_metric, task)
|
||||
# For time series forecasting, X_train may be a sampled dataset whose
|
||||
# test partition can be empty. Use the training partition from X_val
|
||||
# (which is the dataset used to define y_train above) to keep shapes
|
||||
# aligned and avoid empty prediction inputs.
|
||||
X_train_for_metric = X_val.X_train if isinstance(X_val, TimeSeriesDataset) else X_train
|
||||
train_pred_y = get_y_pred(estimator, X_train_for_metric, eval_metric, task)
|
||||
metric_for_logging["train_loss"] = metric_loss_score(
|
||||
eval_metric,
|
||||
train_pred_y,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ class DataCollatorForMultipleChoiceClassification(DataCollatorWithPadding):
|
||||
[{k: v[i] for k, v in feature.items()} for i in range(num_choices)] for feature in features
|
||||
]
|
||||
flattened_features = list(chain(*flattened_features))
|
||||
batch = super(DataCollatorForMultipleChoiceClassification, self).__call__(flattened_features)
|
||||
batch = super().__call__(flattened_features)
|
||||
# Un-flatten
|
||||
batch = {k: v.view(batch_size, num_choices, -1) for k, v in batch.items()}
|
||||
# Add back labels
|
||||
|
||||
@@ -5,7 +5,7 @@ from typing import List, Optional
|
||||
from flaml.automl.task.task import NLG_TASKS
|
||||
|
||||
try:
|
||||
from transformers import TrainingArguments
|
||||
from transformers import Seq2SeqTrainingArguments as TrainingArguments
|
||||
except ImportError:
|
||||
TrainingArguments = object
|
||||
|
||||
@@ -77,6 +77,14 @@ class TrainingArgumentsForAuto(TrainingArguments):
|
||||
|
||||
logging_steps: int = field(default=500, metadata={"help": "Log every X updates steps."})
|
||||
|
||||
# Newer versions of HuggingFace Transformers may access `TrainingArguments.generation_config`
|
||||
# (e.g., in generation-aware trainers/callbacks). Keep this attribute to remain compatible
|
||||
# while defaulting to None for non-generation tasks.
|
||||
generation_config: Optional[object] = field(
|
||||
default=None,
|
||||
metadata={"help": "Optional generation config (or path) used by generation-aware trainers."},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def load_args_from_console():
|
||||
from dataclasses import fields
|
||||
|
||||
@@ -211,29 +211,28 @@ def tokenize_onedataframe(
|
||||
hf_args=None,
|
||||
prefix_str=None,
|
||||
):
|
||||
with tokenizer.as_target_tokenizer():
|
||||
_, tokenized_column_names = tokenize_row(
|
||||
dict(X.iloc[0]),
|
||||
_, tokenized_column_names = tokenize_row(
|
||||
dict(X.iloc[0]),
|
||||
tokenizer,
|
||||
prefix=(prefix_str,) if task is SUMMARIZATION else None,
|
||||
task=task,
|
||||
hf_args=hf_args,
|
||||
return_column_name=True,
|
||||
)
|
||||
d = X.apply(
|
||||
lambda x: tokenize_row(
|
||||
x,
|
||||
tokenizer,
|
||||
prefix=(prefix_str,) if task is SUMMARIZATION else None,
|
||||
task=task,
|
||||
hf_args=hf_args,
|
||||
return_column_name=True,
|
||||
)
|
||||
d = X.apply(
|
||||
lambda x: tokenize_row(
|
||||
x,
|
||||
tokenizer,
|
||||
prefix=(prefix_str,) if task is SUMMARIZATION else None,
|
||||
task=task,
|
||||
hf_args=hf_args,
|
||||
),
|
||||
axis=1,
|
||||
result_type="expand",
|
||||
)
|
||||
X_tokenized = pd.DataFrame(columns=tokenized_column_names)
|
||||
X_tokenized[tokenized_column_names] = d
|
||||
return X_tokenized
|
||||
),
|
||||
axis=1,
|
||||
result_type="expand",
|
||||
)
|
||||
X_tokenized = pd.DataFrame(columns=tokenized_column_names)
|
||||
X_tokenized[tokenized_column_names] = d
|
||||
return X_tokenized
|
||||
|
||||
|
||||
def tokenize_row(
|
||||
@@ -245,7 +244,7 @@ def tokenize_row(
|
||||
return_column_name=False,
|
||||
):
|
||||
if prefix:
|
||||
this_row = tuple(["".join(x) for x in zip(prefix, this_row)])
|
||||
this_row = tuple("".join(x) for x in zip(prefix, this_row))
|
||||
|
||||
# tokenizer.pad_token = tokenizer.eos_token
|
||||
tokenized_example = tokenizer(
|
||||
@@ -396,7 +395,7 @@ def load_model(checkpoint_path, task, num_labels=None):
|
||||
|
||||
if task in (SEQCLASSIFICATION, SEQREGRESSION):
|
||||
return AutoModelForSequenceClassification.from_pretrained(
|
||||
checkpoint_path, config=model_config, ignore_mismatched_sizes=True
|
||||
checkpoint_path, config=model_config, ignore_mismatched_sizes=True, trust_remote_code=True
|
||||
)
|
||||
elif task == TOKENCLASSIFICATION:
|
||||
return AutoModelForTokenClassification.from_pretrained(checkpoint_path, config=model_config)
|
||||
|
||||
@@ -25,14 +25,12 @@ def load_default_huggingface_metric_for_task(task):
|
||||
|
||||
|
||||
def is_a_list_of_str(this_obj):
|
||||
return (isinstance(this_obj, list) or isinstance(this_obj, np.ndarray)) and all(
|
||||
isinstance(x, str) for x in this_obj
|
||||
)
|
||||
return isinstance(this_obj, (list, np.ndarray)) and all(isinstance(x, str) for x in this_obj)
|
||||
|
||||
|
||||
def _clean_value(value: Any) -> str:
|
||||
if isinstance(value, float):
|
||||
return "{:.5}".format(value)
|
||||
return f"{value:.5}"
|
||||
else:
|
||||
return str(value).replace("/", "_")
|
||||
|
||||
@@ -86,7 +84,7 @@ class Counter:
|
||||
@staticmethod
|
||||
def get_trial_fold_name(local_dir, trial_config, trial_id):
|
||||
Counter.counter += 1
|
||||
experiment_tag = "{0}_{1}".format(str(Counter.counter), format_vars(trial_config))
|
||||
experiment_tag = f"{str(Counter.counter)}_{format_vars(trial_config)}"
|
||||
logdir = get_logdir_name(_generate_dirname(experiment_tag, trial_id=trial_id), local_dir)
|
||||
return logdir
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
|
||||
os.environ["PYARROW_IGNORE_TIMEZONE"] = "1"
|
||||
@@ -10,13 +12,14 @@ try:
|
||||
from pyspark.pandas import Series as psSeries
|
||||
from pyspark.pandas import set_option
|
||||
from pyspark.sql import DataFrame as sparkDataFrame
|
||||
from pyspark.sql import SparkSession
|
||||
from pyspark.util import VersionUtils
|
||||
except ImportError:
|
||||
|
||||
class psDataFrame:
|
||||
pass
|
||||
|
||||
F = T = ps = sparkDataFrame = psSeries = psDataFrame
|
||||
F = T = ps = sparkDataFrame = SparkSession = psSeries = psDataFrame
|
||||
_spark_major_minor_version = set_option = None
|
||||
ERROR = ImportError(
|
||||
"""Please run pip install flaml[spark]
|
||||
@@ -32,3 +35,60 @@ try:
|
||||
from pandas import DataFrame, Series
|
||||
except ImportError:
|
||||
DataFrame = Series = pd = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def disable_spark_ansi_mode():
|
||||
"""Disable Spark ANSI mode if it is enabled."""
|
||||
spark = SparkSession.getActiveSession() if hasattr(SparkSession, "getActiveSession") else None
|
||||
adjusted = False
|
||||
try:
|
||||
ps_conf = ps.get_option("compute.fail_on_ansi_mode")
|
||||
except Exception:
|
||||
ps_conf = None
|
||||
ansi_conf = [None, ps_conf] # ansi_conf and ps_conf original values
|
||||
# Spark may store the config as string 'true'/'false' (or boolean in some contexts)
|
||||
if spark is not None:
|
||||
ansi_conf[0] = spark.conf.get("spark.sql.ansi.enabled")
|
||||
ansi_enabled = (
|
||||
(isinstance(ansi_conf[0], str) and ansi_conf[0].lower() == "true")
|
||||
or (isinstance(ansi_conf[0], bool) and ansi_conf[0] is True)
|
||||
or ansi_conf[0] is None
|
||||
)
|
||||
try:
|
||||
if ansi_enabled:
|
||||
logger.debug("Adjusting spark.sql.ansi.enabled to false")
|
||||
spark.conf.set("spark.sql.ansi.enabled", "false")
|
||||
adjusted = True
|
||||
except Exception:
|
||||
# If reading/setting options fail for some reason, keep going and let
|
||||
# pandas-on-Spark raise a meaningful error later.
|
||||
logger.exception("Failed to set spark.sql.ansi.enabled")
|
||||
|
||||
if ansi_conf[1]:
|
||||
logger.debug("Adjusting pandas-on-Spark compute.fail_on_ansi_mode to False")
|
||||
ps.set_option("compute.fail_on_ansi_mode", False)
|
||||
adjusted = True
|
||||
|
||||
return spark, ansi_conf, adjusted
|
||||
|
||||
|
||||
def restore_spark_ansi_mode(spark, ansi_conf, adjusted):
|
||||
"""Restore Spark ANSI mode to its original setting."""
|
||||
# Restore the original spark.sql.ansi.enabled to avoid persistent side-effects.
|
||||
if adjusted and spark and ansi_conf[0] is not None:
|
||||
try:
|
||||
logger.debug(f"Restoring spark.sql.ansi.enabled to {ansi_conf[0]}")
|
||||
spark.conf.set("spark.sql.ansi.enabled", ansi_conf[0])
|
||||
except Exception:
|
||||
logger.exception("Failed to restore spark.sql.ansi.enabled")
|
||||
|
||||
if adjusted and ansi_conf[1]:
|
||||
logger.debug(f"Restoring pandas-on-Spark compute.fail_on_ansi_mode to {ansi_conf[1]}")
|
||||
ps.set_option("compute.fail_on_ansi_mode", ansi_conf[1])
|
||||
|
||||
|
||||
spark, ansi_conf, adjusted = disable_spark_ansi_mode()
|
||||
atexit.register(restore_spark_ansi_mode, spark, ansi_conf, adjusted)
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
ParamList_LightGBM_Base = [
|
||||
"baggingFraction",
|
||||
"baggingFreq",
|
||||
"baggingSeed",
|
||||
"binSampleCount",
|
||||
"boostFromAverage",
|
||||
"boostingType",
|
||||
"catSmooth",
|
||||
"categoricalSlotIndexes",
|
||||
"categoricalSlotNames",
|
||||
"catl2",
|
||||
"chunkSize",
|
||||
"dataRandomSeed",
|
||||
"defaultListenPort",
|
||||
"deterministic",
|
||||
"driverListenPort",
|
||||
"dropRate",
|
||||
"dropSeed",
|
||||
"earlyStoppingRound",
|
||||
"executionMode",
|
||||
"extraSeed" "featureFraction",
|
||||
"featureFractionByNode",
|
||||
"featureFractionSeed",
|
||||
"featuresCol",
|
||||
"featuresShapCol",
|
||||
"fobj" "improvementTolerance",
|
||||
"initScoreCol",
|
||||
"isEnableSparse",
|
||||
"isProvideTrainingMetric",
|
||||
"labelCol",
|
||||
"lambdaL1",
|
||||
"lambdaL2",
|
||||
"leafPredictionCol",
|
||||
"learningRate",
|
||||
"matrixType",
|
||||
"maxBin",
|
||||
"maxBinByFeature",
|
||||
"maxCatThreshold",
|
||||
"maxCatToOnehot",
|
||||
"maxDeltaStep",
|
||||
"maxDepth",
|
||||
"maxDrop",
|
||||
"metric",
|
||||
"microBatchSize",
|
||||
"minDataInLeaf",
|
||||
"minDataPerBin",
|
||||
"minDataPerGroup",
|
||||
"minGainToSplit",
|
||||
"minSumHessianInLeaf",
|
||||
"modelString",
|
||||
"monotoneConstraints",
|
||||
"monotoneConstraintsMethod",
|
||||
"monotonePenalty",
|
||||
"negBaggingFraction",
|
||||
"numBatches",
|
||||
"numIterations",
|
||||
"numLeaves",
|
||||
"numTasks",
|
||||
"numThreads",
|
||||
"objectiveSeed",
|
||||
"otherRate",
|
||||
"parallelism",
|
||||
"passThroughArgs",
|
||||
"posBaggingFraction",
|
||||
"predictDisableShapeCheck",
|
||||
"predictionCol",
|
||||
"repartitionByGroupingColumn",
|
||||
"seed",
|
||||
"skipDrop",
|
||||
"slotNames",
|
||||
"timeout",
|
||||
"topK",
|
||||
"topRate",
|
||||
"uniformDrop",
|
||||
"useBarrierExecutionMode",
|
||||
"useMissing",
|
||||
"useSingleDatasetMode",
|
||||
"validationIndicatorCol",
|
||||
"verbosity",
|
||||
"weightCol",
|
||||
"xGBoostDartMode",
|
||||
"zeroAsMissing",
|
||||
"objective",
|
||||
]
|
||||
ParamList_LightGBM_Classifier = ParamList_LightGBM_Base + [
|
||||
"isUnbalance",
|
||||
"probabilityCol",
|
||||
"rawPredictionCol",
|
||||
"thresholds",
|
||||
]
|
||||
ParamList_LightGBM_Regressor = ParamList_LightGBM_Base + ["tweedieVariancePower"]
|
||||
ParamList_LightGBM_Ranker = ParamList_LightGBM_Base + [
|
||||
"groupCol",
|
||||
"evalAt",
|
||||
"labelGain",
|
||||
"maxPosition",
|
||||
]
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from typing import Union
|
||||
|
||||
import numpy as np
|
||||
@@ -9,7 +10,7 @@ from pyspark.ml.evaluation import (
|
||||
RegressionEvaluator,
|
||||
)
|
||||
|
||||
from flaml.automl.spark import F, psSeries
|
||||
from flaml.automl.spark import F, T, psDataFrame, psSeries, sparkDataFrame
|
||||
|
||||
|
||||
def ps_group_counts(groups: Union[psSeries, np.ndarray]) -> np.ndarray:
|
||||
@@ -36,6 +37,16 @@ def _compute_label_from_probability(df, probability_col, prediction_col):
|
||||
return df
|
||||
|
||||
|
||||
def string_to_array(s):
|
||||
try:
|
||||
return json.loads(s)
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
|
||||
string_to_array_udf = F.udf(string_to_array, T.ArrayType(T.DoubleType()))
|
||||
|
||||
|
||||
def spark_metric_loss_score(
|
||||
metric_name: str,
|
||||
y_predict: psSeries,
|
||||
@@ -135,6 +146,11 @@ def spark_metric_loss_score(
|
||||
)
|
||||
elif metric_name == "log_loss":
|
||||
# For log_loss, prediction_col should be probability, and we need to convert it to label
|
||||
# handle data like "{'type': '1', 'values': '[1, 2, 3]'}"
|
||||
# Fix cannot resolve "array_max(prediction)" due to data type mismatch: Parameter 1 requires the "ARRAY" type,
|
||||
# however "prediction" has the type "STRUCT<type: TINYINT, size: INT, indices: ARRAY<INT>, values: ARRAY<DOUBLE>>"
|
||||
df = df.withColumn(prediction_col, df[prediction_col].cast(T.StringType()))
|
||||
df = df.withColumn(prediction_col, string_to_array_udf(df[prediction_col]))
|
||||
df = _compute_label_from_probability(df, prediction_col, prediction_col + "_label")
|
||||
evaluator = MulticlassClassificationEvaluator(
|
||||
metricName="logLoss",
|
||||
|
||||
@@ -59,17 +59,29 @@ def to_pandas_on_spark(
|
||||
```
|
||||
"""
|
||||
set_option("compute.default_index_type", default_index_type)
|
||||
if isinstance(df, (DataFrame, Series)):
|
||||
return ps.from_pandas(df)
|
||||
elif isinstance(df, sparkDataFrame):
|
||||
if _spark_major_minor_version[0] == 3 and _spark_major_minor_version[1] < 3:
|
||||
return df.to_pandas_on_spark(index_col=index_col)
|
||||
try:
|
||||
orig_ps_conf = ps.get_option("compute.fail_on_ansi_mode")
|
||||
except Exception:
|
||||
orig_ps_conf = None
|
||||
if orig_ps_conf:
|
||||
ps.set_option("compute.fail_on_ansi_mode", False)
|
||||
|
||||
try:
|
||||
if isinstance(df, (DataFrame, Series)):
|
||||
return ps.from_pandas(df)
|
||||
elif isinstance(df, sparkDataFrame):
|
||||
if _spark_major_minor_version[0] == 3 and _spark_major_minor_version[1] < 3:
|
||||
return df.to_pandas_on_spark(index_col=index_col)
|
||||
else:
|
||||
return df.pandas_api(index_col=index_col)
|
||||
elif isinstance(df, (psDataFrame, psSeries)):
|
||||
return df
|
||||
else:
|
||||
return df.pandas_api(index_col=index_col)
|
||||
elif isinstance(df, (psDataFrame, psSeries)):
|
||||
return df
|
||||
else:
|
||||
raise TypeError(f"{type(df)} is not one of pandas.DataFrame, pandas.Series and pyspark.sql.DataFrame")
|
||||
raise TypeError(f"{type(df)} is not one of pandas.DataFrame, pandas.Series and pyspark.sql.DataFrame")
|
||||
finally:
|
||||
# Restore original config
|
||||
if orig_ps_conf:
|
||||
ps.set_option("compute.fail_on_ansi_mode", orig_ps_conf)
|
||||
|
||||
|
||||
def train_test_split_pyspark(
|
||||
|
||||
@@ -37,10 +37,9 @@ class SearchState:
|
||||
if isinstance(domain_one_dim, sample.Domain):
|
||||
renamed_type = list(inspect.signature(domain_one_dim.is_valid).parameters.values())[0].annotation
|
||||
type_match = (
|
||||
renamed_type == Any
|
||||
renamed_type is Any
|
||||
or isinstance(value_one_dim, renamed_type)
|
||||
or isinstance(value_one_dim, int)
|
||||
and renamed_type is float
|
||||
or (renamed_type is float and isinstance(value_one_dim, int))
|
||||
)
|
||||
if not (type_match and domain_one_dim.is_valid(value_one_dim)):
|
||||
return False
|
||||
@@ -65,6 +64,7 @@ class SearchState:
|
||||
custom_hp=None,
|
||||
max_iter=None,
|
||||
budget=None,
|
||||
featurization="auto",
|
||||
):
|
||||
self.init_eci = learner_class.cost_relative2lgbm() if budget >= 0 else 1
|
||||
self._search_space_domain = {}
|
||||
@@ -82,6 +82,7 @@ class SearchState:
|
||||
else:
|
||||
data_size = data.shape
|
||||
search_space = learner_class.search_space(data_size=data_size, task=task)
|
||||
|
||||
self.data_size = data_size
|
||||
|
||||
if custom_hp is not None:
|
||||
@@ -91,9 +92,7 @@ class SearchState:
|
||||
starting_point = AutoMLState.sanitize(starting_point)
|
||||
if max_iter > 1 and not self.valid_starting_point(starting_point, search_space):
|
||||
# If the number of iterations is larger than 1, remove invalid point
|
||||
logger.warning(
|
||||
"Starting point {} removed because it is outside of the search space".format(starting_point)
|
||||
)
|
||||
logger.warning(f"Starting point {starting_point} removed because it is outside of the search space")
|
||||
starting_point = None
|
||||
elif isinstance(starting_point, list):
|
||||
starting_point = [AutoMLState.sanitize(x) for x in starting_point]
|
||||
@@ -208,7 +207,7 @@ class SearchState:
|
||||
self.val_loss, self.config = obj, config
|
||||
|
||||
def get_hist_config_sig(self, sample_size, config):
|
||||
config_values = tuple([config[k] for k in self._hp_names if k in config])
|
||||
config_values = tuple(config[k] for k in self._hp_names if k in config)
|
||||
config_sig = str(sample_size) + "_" + str(config_values)
|
||||
return config_sig
|
||||
|
||||
@@ -290,9 +289,11 @@ class AutoMLState:
|
||||
budget = (
|
||||
None
|
||||
if state.time_budget < 0
|
||||
else state.time_budget - state.time_from_start
|
||||
if sample_size == state.data_size[0]
|
||||
else (state.time_budget - state.time_from_start) / 2 * sample_size / state.data_size[0]
|
||||
else (
|
||||
state.time_budget - state.time_from_start
|
||||
if sample_size == state.data_size[0]
|
||||
else (state.time_budget - state.time_from_start) / 2 * sample_size / state.data_size[0]
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
@@ -353,6 +354,7 @@ class AutoMLState:
|
||||
estimator: str,
|
||||
config_w_resource: dict,
|
||||
sample_size: Optional[int] = None,
|
||||
is_retrain: bool = False,
|
||||
):
|
||||
if not sample_size:
|
||||
sample_size = config_w_resource.get("FLAML_sample_size", len(self.y_train_all))
|
||||
@@ -378,9 +380,8 @@ class AutoMLState:
|
||||
this_estimator_kwargs[
|
||||
"groups"
|
||||
] = groups # NOTE: _train_with_config is after kwargs is updated to fit_kwargs_by_estimator
|
||||
|
||||
this_estimator_kwargs.update({"is_retrain": is_retrain})
|
||||
budget = None if self.time_budget < 0 else self.time_budget - self.time_from_start
|
||||
|
||||
estimator, train_time = train_estimator(
|
||||
X_train=sampled_X_train,
|
||||
y_train=sampled_y_train,
|
||||
|
||||
@@ -16,12 +16,7 @@ from flaml.automl.spark.utils import (
|
||||
unique_pandas_on_spark,
|
||||
unique_value_first_index,
|
||||
)
|
||||
from flaml.automl.task.task import (
|
||||
TS_FORECAST,
|
||||
TS_FORECASTPANEL,
|
||||
Task,
|
||||
get_classification_objective,
|
||||
)
|
||||
from flaml.automl.task.task import TS_FORECAST, TS_FORECASTPANEL, Task, get_classification_objective
|
||||
from flaml.config import RANDOM_SEED
|
||||
|
||||
try:
|
||||
@@ -53,13 +48,24 @@ class GenericTask(Task):
|
||||
from flaml.automl.contrib.histgb import HistGradientBoostingEstimator
|
||||
from flaml.automl.model import (
|
||||
CatBoostEstimator,
|
||||
ElasticNetEstimator,
|
||||
ExtraTreesEstimator,
|
||||
KNeighborsEstimator,
|
||||
LassoLarsEstimator,
|
||||
LGBMEstimator,
|
||||
LRL1Classifier,
|
||||
LRL2Classifier,
|
||||
RandomForestEstimator,
|
||||
SGDEstimator,
|
||||
SparkAFTSurvivalRegressionEstimator,
|
||||
SparkGBTEstimator,
|
||||
SparkGLREstimator,
|
||||
SparkLGBMEstimator,
|
||||
SparkLinearRegressionEstimator,
|
||||
SparkLinearSVCEstimator,
|
||||
SparkNaiveBayesEstimator,
|
||||
SparkRandomForestEstimator,
|
||||
SVCEstimator,
|
||||
TransformersEstimator,
|
||||
TransformersEstimatorModelSelection,
|
||||
XGBoostLimitDepthEstimator,
|
||||
@@ -72,6 +78,7 @@ class GenericTask(Task):
|
||||
"rf": RandomForestEstimator,
|
||||
"lgbm": LGBMEstimator,
|
||||
"lgbm_spark": SparkLGBMEstimator,
|
||||
"rf_spark": SparkRandomForestEstimator,
|
||||
"lrl1": LRL1Classifier,
|
||||
"lrl2": LRL2Classifier,
|
||||
"catboost": CatBoostEstimator,
|
||||
@@ -80,6 +87,16 @@ class GenericTask(Task):
|
||||
"transformer": TransformersEstimator,
|
||||
"transformer_ms": TransformersEstimatorModelSelection,
|
||||
"histgb": HistGradientBoostingEstimator,
|
||||
"svc": SVCEstimator,
|
||||
"sgd": SGDEstimator,
|
||||
"nb_spark": SparkNaiveBayesEstimator,
|
||||
"enet": ElasticNetEstimator,
|
||||
"lassolars": LassoLarsEstimator,
|
||||
"glr_spark": SparkGLREstimator,
|
||||
"lr_spark": SparkLinearRegressionEstimator,
|
||||
"svc_spark": SparkLinearSVCEstimator,
|
||||
"gbt_spark": SparkGBTEstimator,
|
||||
"aft_spark": SparkAFTSurvivalRegressionEstimator,
|
||||
}
|
||||
return self._estimators
|
||||
|
||||
@@ -271,8 +288,8 @@ class GenericTask(Task):
|
||||
seed=RANDOM_SEED,
|
||||
)
|
||||
columns_to_drop = [c for c in df_all_train.columns if c in [stratify_column, "sample_weight"]]
|
||||
X_train = df_all_train.drop(columns_to_drop)
|
||||
X_val = df_all_val.drop(columns_to_drop)
|
||||
X_train = df_all_train.drop(columns=columns_to_drop)
|
||||
X_val = df_all_val.drop(columns=columns_to_drop)
|
||||
y_train = df_all_train[stratify_column]
|
||||
y_val = df_all_val[stratify_column]
|
||||
|
||||
@@ -348,6 +365,465 @@ class GenericTask(Task):
|
||||
X_train, X_val, y_train, y_val = GenericTask._split_pyspark(state, X, y, split_ratio, stratify)
|
||||
return X_train, X_val, y_train, y_val
|
||||
|
||||
def _handle_missing_labels_fast(
|
||||
self,
|
||||
state,
|
||||
X_train,
|
||||
X_val,
|
||||
y_train,
|
||||
y_val,
|
||||
X_train_all,
|
||||
y_train_all,
|
||||
is_spark_dataframe,
|
||||
data_is_df,
|
||||
):
|
||||
"""Handle missing labels by adding first instance to the set with missing label.
|
||||
|
||||
This is the faster version that may create some overlap but ensures all labels
|
||||
are present in both sets. If a label is missing from train, it adds the first
|
||||
instance to train. If a label is missing from val, it adds the first instance to val.
|
||||
If no labels are missing, no instances are duplicated.
|
||||
|
||||
Args:
|
||||
state: The state object containing fit parameters
|
||||
X_train, X_val: Training and validation features
|
||||
y_train, y_val: Training and validation labels
|
||||
X_train_all, y_train_all: Complete dataset
|
||||
is_spark_dataframe: Whether data is pandas_on_spark
|
||||
data_is_df: Whether data is DataFrame/Series
|
||||
|
||||
Returns:
|
||||
Tuple of (X_train, X_val, y_train, y_val) with missing labels added
|
||||
"""
|
||||
# Check which labels are present in train and val sets
|
||||
if is_spark_dataframe:
|
||||
label_set_train, _ = unique_pandas_on_spark(y_train)
|
||||
label_set_val, _ = unique_pandas_on_spark(y_val)
|
||||
label_set_all, first = unique_value_first_index(y_train_all)
|
||||
else:
|
||||
label_set_all, first = unique_value_first_index(y_train_all)
|
||||
label_set_train = np.unique(y_train)
|
||||
label_set_val = np.unique(y_val)
|
||||
|
||||
# Find missing labels
|
||||
missing_in_train = np.setdiff1d(label_set_all, label_set_train)
|
||||
missing_in_val = np.setdiff1d(label_set_all, label_set_val)
|
||||
|
||||
# Add first instance of missing labels to train set
|
||||
if len(missing_in_train) > 0:
|
||||
missing_train_indices = []
|
||||
for label in missing_in_train:
|
||||
label_matches = np.where(label_set_all == label)[0]
|
||||
if len(label_matches) > 0 and label_matches[0] < len(first):
|
||||
missing_train_indices.append(first[label_matches[0]])
|
||||
|
||||
if len(missing_train_indices) > 0:
|
||||
X_missing_train = (
|
||||
iloc_pandas_on_spark(X_train_all, missing_train_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[missing_train_indices]
|
||||
if data_is_df
|
||||
else X_train_all[missing_train_indices]
|
||||
)
|
||||
y_missing_train = (
|
||||
iloc_pandas_on_spark(y_train_all, missing_train_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[missing_train_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[missing_train_indices]
|
||||
)
|
||||
X_train = concat(X_missing_train, X_train)
|
||||
y_train = concat(y_missing_train, y_train) if data_is_df else np.concatenate([y_missing_train, y_train])
|
||||
|
||||
# Handle sample_weight if present
|
||||
if "sample_weight" in state.fit_kwargs:
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and max(missing_train_indices) < len(sample_weight_source):
|
||||
missing_weights = (
|
||||
sample_weight_source[missing_train_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[missing_train_indices]
|
||||
)
|
||||
state.fit_kwargs["sample_weight"] = concat(missing_weights, state.fit_kwargs["sample_weight"])
|
||||
|
||||
# Add first instance of missing labels to val set
|
||||
if len(missing_in_val) > 0:
|
||||
missing_val_indices = []
|
||||
for label in missing_in_val:
|
||||
label_matches = np.where(label_set_all == label)[0]
|
||||
if len(label_matches) > 0 and label_matches[0] < len(first):
|
||||
missing_val_indices.append(first[label_matches[0]])
|
||||
|
||||
if len(missing_val_indices) > 0:
|
||||
X_missing_val = (
|
||||
iloc_pandas_on_spark(X_train_all, missing_val_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[missing_val_indices]
|
||||
if data_is_df
|
||||
else X_train_all[missing_val_indices]
|
||||
)
|
||||
y_missing_val = (
|
||||
iloc_pandas_on_spark(y_train_all, missing_val_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[missing_val_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[missing_val_indices]
|
||||
)
|
||||
X_val = concat(X_missing_val, X_val)
|
||||
y_val = concat(y_missing_val, y_val) if data_is_df else np.concatenate([y_missing_val, y_val])
|
||||
|
||||
# Handle sample_weight if present
|
||||
if (
|
||||
"sample_weight" in state.fit_kwargs
|
||||
and hasattr(state, "weight_val")
|
||||
and state.weight_val is not None
|
||||
):
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and max(missing_val_indices) < len(sample_weight_source):
|
||||
missing_weights = (
|
||||
sample_weight_source[missing_val_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[missing_val_indices]
|
||||
)
|
||||
state.weight_val = concat(missing_weights, state.weight_val)
|
||||
|
||||
return X_train, X_val, y_train, y_val
|
||||
|
||||
def _handle_missing_labels_no_overlap(
|
||||
self,
|
||||
state,
|
||||
X_train,
|
||||
X_val,
|
||||
y_train,
|
||||
y_val,
|
||||
X_train_all,
|
||||
y_train_all,
|
||||
is_spark_dataframe,
|
||||
data_is_df,
|
||||
split_ratio,
|
||||
):
|
||||
"""Handle missing labels intelligently to avoid overlap when possible.
|
||||
|
||||
This is the slower but more precise version that:
|
||||
- For single-instance classes: Adds to both sets (unavoidable overlap)
|
||||
- For multi-instance classes: Re-splits them properly to avoid overlap
|
||||
|
||||
Args:
|
||||
state: The state object containing fit parameters
|
||||
X_train, X_val: Training and validation features
|
||||
y_train, y_val: Training and validation labels
|
||||
X_train_all, y_train_all: Complete dataset
|
||||
is_spark_dataframe: Whether data is pandas_on_spark
|
||||
data_is_df: Whether data is DataFrame/Series
|
||||
split_ratio: The ratio for splitting
|
||||
|
||||
Returns:
|
||||
Tuple of (X_train, X_val, y_train, y_val) with missing labels handled
|
||||
"""
|
||||
# Check which labels are present in train and val sets
|
||||
if is_spark_dataframe:
|
||||
label_set_train, _ = unique_pandas_on_spark(y_train)
|
||||
label_set_val, _ = unique_pandas_on_spark(y_val)
|
||||
label_set_all, first = unique_value_first_index(y_train_all)
|
||||
else:
|
||||
label_set_all, first = unique_value_first_index(y_train_all)
|
||||
label_set_train = np.unique(y_train)
|
||||
label_set_val = np.unique(y_val)
|
||||
|
||||
# Find missing labels
|
||||
missing_in_train = np.setdiff1d(label_set_all, label_set_train)
|
||||
missing_in_val = np.setdiff1d(label_set_all, label_set_val)
|
||||
|
||||
# Handle missing labels intelligently
|
||||
# For classes with only 1 instance: add to both sets (unavoidable overlap)
|
||||
# For classes with multiple instances: move/split them properly to avoid overlap
|
||||
|
||||
if len(missing_in_train) > 0:
|
||||
# Process missing labels in training set
|
||||
for label in missing_in_train:
|
||||
# Find all indices for this label in the original data
|
||||
if is_spark_dataframe:
|
||||
label_indices = np.where(y_train_all.to_numpy() == label)[0].tolist()
|
||||
else:
|
||||
label_indices = np.where(np.asarray(y_train_all) == label)[0].tolist()
|
||||
|
||||
num_instances = len(label_indices)
|
||||
|
||||
if num_instances == 1:
|
||||
# Single instance: must add to both train and val (unavoidable overlap)
|
||||
X_single = (
|
||||
iloc_pandas_on_spark(X_train_all, label_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[label_indices]
|
||||
if data_is_df
|
||||
else X_train_all[label_indices]
|
||||
)
|
||||
y_single = (
|
||||
iloc_pandas_on_spark(y_train_all, label_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[label_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[label_indices]
|
||||
)
|
||||
X_train = concat(X_single, X_train)
|
||||
y_train = concat(y_single, y_train) if data_is_df else np.concatenate([y_single, y_train])
|
||||
|
||||
# Handle sample_weight
|
||||
if "sample_weight" in state.fit_kwargs:
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and label_indices[0] < len(sample_weight_source):
|
||||
single_weight = (
|
||||
sample_weight_source[label_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[label_indices]
|
||||
)
|
||||
state.fit_kwargs["sample_weight"] = concat(single_weight, state.fit_kwargs["sample_weight"])
|
||||
else:
|
||||
# Multiple instances: move some from val to train (no overlap needed)
|
||||
# Calculate how many to move to train (leave at least 1 in val)
|
||||
num_to_train = max(1, min(num_instances - 1, int(num_instances * (1 - split_ratio))))
|
||||
indices_to_move = label_indices[:num_to_train]
|
||||
|
||||
X_to_move = (
|
||||
iloc_pandas_on_spark(X_train_all, indices_to_move)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[indices_to_move]
|
||||
if data_is_df
|
||||
else X_train_all[indices_to_move]
|
||||
)
|
||||
y_to_move = (
|
||||
iloc_pandas_on_spark(y_train_all, indices_to_move)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[indices_to_move]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[indices_to_move]
|
||||
)
|
||||
|
||||
# Add to train
|
||||
X_train = concat(X_to_move, X_train)
|
||||
y_train = concat(y_to_move, y_train) if data_is_df else np.concatenate([y_to_move, y_train])
|
||||
|
||||
# Remove from val (they are currently all in val)
|
||||
if is_spark_dataframe:
|
||||
val_mask = ~y_val.isin([label])
|
||||
X_val = X_val[val_mask]
|
||||
y_val = y_val[val_mask]
|
||||
else:
|
||||
val_mask = np.asarray(y_val) != label
|
||||
if data_is_df:
|
||||
X_val = X_val[val_mask]
|
||||
y_val = y_val[val_mask]
|
||||
else:
|
||||
X_val = X_val[val_mask]
|
||||
y_val = y_val[val_mask]
|
||||
|
||||
# Add remaining instances back to val
|
||||
remaining_indices = label_indices[num_to_train:]
|
||||
if len(remaining_indices) > 0:
|
||||
X_remaining = (
|
||||
iloc_pandas_on_spark(X_train_all, remaining_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[remaining_indices]
|
||||
if data_is_df
|
||||
else X_train_all[remaining_indices]
|
||||
)
|
||||
y_remaining = (
|
||||
iloc_pandas_on_spark(y_train_all, remaining_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[remaining_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[remaining_indices]
|
||||
)
|
||||
X_val = concat(X_remaining, X_val)
|
||||
y_val = concat(y_remaining, y_val) if data_is_df else np.concatenate([y_remaining, y_val])
|
||||
|
||||
# Handle sample_weight
|
||||
if "sample_weight" in state.fit_kwargs:
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and max(indices_to_move) < len(sample_weight_source):
|
||||
weights_to_move = (
|
||||
sample_weight_source[indices_to_move]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[indices_to_move]
|
||||
)
|
||||
state.fit_kwargs["sample_weight"] = concat(
|
||||
weights_to_move, state.fit_kwargs["sample_weight"]
|
||||
)
|
||||
|
||||
if (
|
||||
len(remaining_indices) > 0
|
||||
and hasattr(state, "weight_val")
|
||||
and state.weight_val is not None
|
||||
):
|
||||
# Remove and re-add weights for val
|
||||
if isinstance(state.weight_val, np.ndarray):
|
||||
state.weight_val = state.weight_val[val_mask]
|
||||
else:
|
||||
state.weight_val = state.weight_val[val_mask]
|
||||
|
||||
if max(remaining_indices) < len(sample_weight_source):
|
||||
remaining_weights = (
|
||||
sample_weight_source[remaining_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[remaining_indices]
|
||||
)
|
||||
state.weight_val = concat(remaining_weights, state.weight_val)
|
||||
|
||||
if len(missing_in_val) > 0:
|
||||
# Process missing labels in validation set
|
||||
for label in missing_in_val:
|
||||
# Find all indices for this label in the original data
|
||||
if is_spark_dataframe:
|
||||
label_indices = np.where(y_train_all.to_numpy() == label)[0].tolist()
|
||||
else:
|
||||
label_indices = np.where(np.asarray(y_train_all) == label)[0].tolist()
|
||||
|
||||
num_instances = len(label_indices)
|
||||
|
||||
if num_instances == 1:
|
||||
# Single instance: must add to both train and val (unavoidable overlap)
|
||||
X_single = (
|
||||
iloc_pandas_on_spark(X_train_all, label_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[label_indices]
|
||||
if data_is_df
|
||||
else X_train_all[label_indices]
|
||||
)
|
||||
y_single = (
|
||||
iloc_pandas_on_spark(y_train_all, label_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[label_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[label_indices]
|
||||
)
|
||||
X_val = concat(X_single, X_val)
|
||||
y_val = concat(y_single, y_val) if data_is_df else np.concatenate([y_single, y_val])
|
||||
|
||||
# Handle sample_weight
|
||||
if "sample_weight" in state.fit_kwargs and hasattr(state, "weight_val"):
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and label_indices[0] < len(sample_weight_source):
|
||||
single_weight = (
|
||||
sample_weight_source[label_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[label_indices]
|
||||
)
|
||||
if state.weight_val is not None:
|
||||
state.weight_val = concat(single_weight, state.weight_val)
|
||||
else:
|
||||
# Multiple instances: move some from train to val (no overlap needed)
|
||||
# Calculate how many to move to val (leave at least 1 in train)
|
||||
num_to_val = max(1, min(num_instances - 1, int(num_instances * split_ratio)))
|
||||
indices_to_move = label_indices[:num_to_val]
|
||||
|
||||
X_to_move = (
|
||||
iloc_pandas_on_spark(X_train_all, indices_to_move)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[indices_to_move]
|
||||
if data_is_df
|
||||
else X_train_all[indices_to_move]
|
||||
)
|
||||
y_to_move = (
|
||||
iloc_pandas_on_spark(y_train_all, indices_to_move)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[indices_to_move]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[indices_to_move]
|
||||
)
|
||||
|
||||
# Add to val
|
||||
X_val = concat(X_to_move, X_val)
|
||||
y_val = concat(y_to_move, y_val) if data_is_df else np.concatenate([y_to_move, y_val])
|
||||
|
||||
# Remove from train (they are currently all in train)
|
||||
if is_spark_dataframe:
|
||||
train_mask = ~y_train.isin([label])
|
||||
X_train = X_train[train_mask]
|
||||
y_train = y_train[train_mask]
|
||||
else:
|
||||
train_mask = np.asarray(y_train) != label
|
||||
if data_is_df:
|
||||
X_train = X_train[train_mask]
|
||||
y_train = y_train[train_mask]
|
||||
else:
|
||||
X_train = X_train[train_mask]
|
||||
y_train = y_train[train_mask]
|
||||
|
||||
# Add remaining instances back to train
|
||||
remaining_indices = label_indices[num_to_val:]
|
||||
if len(remaining_indices) > 0:
|
||||
X_remaining = (
|
||||
iloc_pandas_on_spark(X_train_all, remaining_indices)
|
||||
if is_spark_dataframe
|
||||
else X_train_all.iloc[remaining_indices]
|
||||
if data_is_df
|
||||
else X_train_all[remaining_indices]
|
||||
)
|
||||
y_remaining = (
|
||||
iloc_pandas_on_spark(y_train_all, remaining_indices)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[remaining_indices]
|
||||
if isinstance(y_train_all, (pd.Series, psSeries))
|
||||
else y_train_all[remaining_indices]
|
||||
)
|
||||
X_train = concat(X_remaining, X_train)
|
||||
y_train = concat(y_remaining, y_train) if data_is_df else np.concatenate([y_remaining, y_train])
|
||||
|
||||
# Handle sample_weight
|
||||
if "sample_weight" in state.fit_kwargs:
|
||||
sample_weight_source = (
|
||||
state.sample_weight_all
|
||||
if hasattr(state, "sample_weight_all")
|
||||
else state.fit_kwargs.get("sample_weight")
|
||||
)
|
||||
if sample_weight_source is not None and max(indices_to_move) < len(sample_weight_source):
|
||||
weights_to_move = (
|
||||
sample_weight_source[indices_to_move]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[indices_to_move]
|
||||
)
|
||||
if hasattr(state, "weight_val") and state.weight_val is not None:
|
||||
state.weight_val = concat(weights_to_move, state.weight_val)
|
||||
|
||||
if len(remaining_indices) > 0:
|
||||
# Remove and re-add weights for train
|
||||
if isinstance(state.fit_kwargs["sample_weight"], np.ndarray):
|
||||
state.fit_kwargs["sample_weight"] = state.fit_kwargs["sample_weight"][train_mask]
|
||||
else:
|
||||
state.fit_kwargs["sample_weight"] = state.fit_kwargs["sample_weight"][train_mask]
|
||||
|
||||
if max(remaining_indices) < len(sample_weight_source):
|
||||
remaining_weights = (
|
||||
sample_weight_source[remaining_indices]
|
||||
if isinstance(sample_weight_source, np.ndarray)
|
||||
else sample_weight_source.iloc[remaining_indices]
|
||||
)
|
||||
state.fit_kwargs["sample_weight"] = concat(
|
||||
remaining_weights, state.fit_kwargs["sample_weight"]
|
||||
)
|
||||
|
||||
return X_train, X_val, y_train, y_val
|
||||
|
||||
def prepare_data(
|
||||
self,
|
||||
state,
|
||||
@@ -360,6 +836,7 @@ class GenericTask(Task):
|
||||
n_splits,
|
||||
data_is_df,
|
||||
sample_weight_full,
|
||||
allow_label_overlap=True,
|
||||
) -> int:
|
||||
X_val, y_val = state.X_val, state.y_val
|
||||
if issparse(X_val):
|
||||
@@ -425,8 +902,8 @@ class GenericTask(Task):
|
||||
X_train_all, y_train_all = shuffle(X_train_all, y_train_all, random_state=RANDOM_SEED)
|
||||
if data_is_df:
|
||||
X_train_all.reset_index(drop=True, inplace=True)
|
||||
if isinstance(y_train_all, pd.Series):
|
||||
y_train_all.reset_index(drop=True, inplace=True)
|
||||
if isinstance(y_train_all, pd.Series):
|
||||
y_train_all.reset_index(drop=True, inplace=True)
|
||||
|
||||
X_train, y_train = X_train_all, y_train_all
|
||||
state.groups_all = state.groups
|
||||
@@ -488,31 +965,47 @@ class GenericTask(Task):
|
||||
elif self.is_classification():
|
||||
# for classification, make sure the labels are complete in both
|
||||
# training and validation data
|
||||
label_set, first = unique_value_first_index(y_train_all)
|
||||
rest = []
|
||||
last = 0
|
||||
first.sort()
|
||||
for i in range(len(first)):
|
||||
rest.extend(range(last, first[i]))
|
||||
last = first[i] + 1
|
||||
rest.extend(range(last, len(y_train_all)))
|
||||
X_first = X_train_all.iloc[first] if data_is_df else X_train_all[first]
|
||||
X_rest = X_train_all.iloc[rest] if data_is_df else X_train_all[rest]
|
||||
y_rest = (
|
||||
y_train_all[rest]
|
||||
if isinstance(y_train_all, np.ndarray)
|
||||
else iloc_pandas_on_spark(y_train_all, rest)
|
||||
if is_spark_dataframe
|
||||
else y_train_all.iloc[rest]
|
||||
)
|
||||
stratify = y_rest if split_type == "stratified" else None
|
||||
stratify = y_train_all if split_type == "stratified" else None
|
||||
X_train, X_val, y_train, y_val = self._train_test_split(
|
||||
state, X_rest, y_rest, first, rest, split_ratio, stratify
|
||||
state, X_train_all, y_train_all, split_ratio=split_ratio, stratify=stratify
|
||||
)
|
||||
X_train = concat(X_first, X_train)
|
||||
y_train = concat(label_set, y_train) if data_is_df else np.concatenate([label_set, y_train])
|
||||
X_val = concat(X_first, X_val)
|
||||
y_val = concat(label_set, y_val) if data_is_df else np.concatenate([label_set, y_val])
|
||||
|
||||
# Handle missing labels using the appropriate strategy
|
||||
if allow_label_overlap:
|
||||
# Fast version: adds first instance to set with missing label (may create overlap)
|
||||
X_train, X_val, y_train, y_val = self._handle_missing_labels_fast(
|
||||
state,
|
||||
X_train,
|
||||
X_val,
|
||||
y_train,
|
||||
y_val,
|
||||
X_train_all,
|
||||
y_train_all,
|
||||
is_spark_dataframe,
|
||||
data_is_df,
|
||||
)
|
||||
else:
|
||||
# Precise version: avoids overlap when possible (slower)
|
||||
X_train, X_val, y_train, y_val = self._handle_missing_labels_no_overlap(
|
||||
state,
|
||||
X_train,
|
||||
X_val,
|
||||
y_train,
|
||||
y_val,
|
||||
X_train_all,
|
||||
y_train_all,
|
||||
is_spark_dataframe,
|
||||
data_is_df,
|
||||
split_ratio,
|
||||
)
|
||||
|
||||
if isinstance(y_train, (psDataFrame, pd.DataFrame)) and y_train.shape[1] == 1:
|
||||
y_train = y_train[y_train.columns[0]]
|
||||
y_val = y_val[y_val.columns[0]]
|
||||
# Only set name if y_train_all is a Series (not a DataFrame)
|
||||
if isinstance(y_train_all, (pd.Series, psSeries)):
|
||||
y_train.name = y_val.name = y_train_all.name
|
||||
|
||||
elif self.is_regression():
|
||||
X_train, X_val, y_train, y_val = self._train_test_split(
|
||||
state, X_train_all, y_train_all, split_ratio=split_ratio
|
||||
@@ -659,7 +1152,6 @@ class GenericTask(Task):
|
||||
fit_kwargs = {}
|
||||
if cv_score_agg_func is None:
|
||||
cv_score_agg_func = default_cv_score_agg_func
|
||||
start_time = time.time()
|
||||
val_loss_folds = []
|
||||
log_metric_folds = []
|
||||
metric = None
|
||||
@@ -701,7 +1193,10 @@ class GenericTask(Task):
|
||||
elif isinstance(kf, TimeSeriesSplit):
|
||||
kf = kf.split(X_train_split, y_train_split)
|
||||
else:
|
||||
kf = kf.split(X_train_split)
|
||||
try:
|
||||
kf = kf.split(X_train_split)
|
||||
except TypeError:
|
||||
kf = kf.split(X_train_split, y_train_split)
|
||||
|
||||
for train_index, val_index in kf:
|
||||
if shuffle:
|
||||
@@ -724,10 +1219,10 @@ class GenericTask(Task):
|
||||
if not is_spark_dataframe:
|
||||
y_train, y_val = y_train_split[train_index], y_train_split[val_index]
|
||||
if weight is not None:
|
||||
fit_kwargs["sample_weight"], weight_val = (
|
||||
weight[train_index],
|
||||
weight[val_index],
|
||||
fit_kwargs["sample_weight"] = (
|
||||
weight[train_index] if isinstance(weight, np.ndarray) else weight.iloc[train_index]
|
||||
)
|
||||
weight_val = weight[val_index] if isinstance(weight, np.ndarray) else weight.iloc[val_index]
|
||||
if groups is not None:
|
||||
fit_kwargs["groups"] = (
|
||||
groups[train_index] if isinstance(groups, np.ndarray) else groups.iloc[train_index]
|
||||
@@ -766,8 +1261,6 @@ class GenericTask(Task):
|
||||
if is_spark_dataframe:
|
||||
X_train.spark.unpersist() # uncache data to free memory
|
||||
X_val.spark.unpersist() # uncache data to free memory
|
||||
if budget and time.time() - start_time >= budget:
|
||||
break
|
||||
val_loss, metric = cv_score_agg_func(val_loss_folds, log_metric_folds)
|
||||
n = total_fold_num
|
||||
pred_time /= n
|
||||
@@ -810,27 +1303,23 @@ class GenericTask(Task):
|
||||
elif self.is_ts_forecastpanel():
|
||||
estimator_list = ["tft"]
|
||||
else:
|
||||
estimator_list = [
|
||||
"lgbm",
|
||||
"rf",
|
||||
"xgboost",
|
||||
"extra_tree",
|
||||
"xgb_limitdepth",
|
||||
"lgbm_spark",
|
||||
"rf_spark",
|
||||
"sgd",
|
||||
]
|
||||
try:
|
||||
import catboost
|
||||
|
||||
estimator_list = [
|
||||
"lgbm",
|
||||
"rf",
|
||||
"catboost",
|
||||
"xgboost",
|
||||
"extra_tree",
|
||||
"xgb_limitdepth",
|
||||
"lgbm_spark",
|
||||
]
|
||||
estimator_list += ["catboost"]
|
||||
except ImportError:
|
||||
estimator_list = [
|
||||
"lgbm",
|
||||
"rf",
|
||||
"xgboost",
|
||||
"extra_tree",
|
||||
"xgb_limitdepth",
|
||||
"lgbm_spark",
|
||||
]
|
||||
pass
|
||||
|
||||
# if self.is_ts_forecast():
|
||||
# # catboost is removed because it has a `name` parameter, making it incompatible with hcrystalball
|
||||
# if "catboost" in estimator_list:
|
||||
@@ -862,9 +1351,7 @@ class GenericTask(Task):
|
||||
return metric
|
||||
|
||||
if self.is_nlp():
|
||||
from flaml.automl.nlp.utils import (
|
||||
load_default_huggingface_metric_for_task,
|
||||
)
|
||||
from flaml.automl.nlp.utils import load_default_huggingface_metric_for_task
|
||||
|
||||
return load_default_huggingface_metric_for_task(self.name)
|
||||
elif self.is_binary():
|
||||
|
||||
@@ -192,7 +192,7 @@ class Task(ABC):
|
||||
* Valid str options depend on different tasks.
|
||||
For classification tasks, valid choices are
|
||||
["auto", 'stratified', 'uniform', 'time', 'group']. "auto" -> stratified.
|
||||
For regression tasks, valid choices are ["auto", 'uniform', 'time'].
|
||||
For regression tasks, valid choices are ["auto", 'uniform', 'time', 'group'].
|
||||
"auto" -> uniform.
|
||||
For time series forecast tasks, must be "auto" or 'time'.
|
||||
For ranking task, must be "auto" or 'group'.
|
||||
|
||||
@@ -36,11 +36,17 @@ class TimeSeriesTask(Task):
|
||||
LGBM_TS,
|
||||
RF_TS,
|
||||
SARIMAX,
|
||||
Average,
|
||||
CatBoost_TS,
|
||||
ExtraTrees_TS,
|
||||
HoltWinters,
|
||||
LassoLars_TS,
|
||||
Naive,
|
||||
Orbit,
|
||||
Prophet,
|
||||
SeasonalAverage,
|
||||
SeasonalNaive,
|
||||
TCNEstimator,
|
||||
TemporalFusionTransformerEstimator,
|
||||
XGBoost_TS,
|
||||
XGBoostLimitDepth_TS,
|
||||
@@ -57,8 +63,19 @@ class TimeSeriesTask(Task):
|
||||
"holt-winters": HoltWinters,
|
||||
"catboost": CatBoost_TS,
|
||||
"tft": TemporalFusionTransformerEstimator,
|
||||
"lassolars": LassoLars_TS,
|
||||
"tcn": TCNEstimator,
|
||||
"snaive": SeasonalNaive,
|
||||
"naive": Naive,
|
||||
"savg": SeasonalAverage,
|
||||
"avg": Average,
|
||||
}
|
||||
|
||||
if self._estimators["tcn"] is None:
|
||||
# remove TCN if import failed
|
||||
del self._estimators["tcn"]
|
||||
logger.info("Couldn't import pytorch_lightning, skipping TCN estimator")
|
||||
|
||||
try:
|
||||
from prophet import Prophet as foo
|
||||
|
||||
@@ -71,7 +88,7 @@ class TimeSeriesTask(Task):
|
||||
|
||||
self._estimators["orbit"] = Orbit
|
||||
except ImportError:
|
||||
logger.info("Couldn't import Prophet, skipping")
|
||||
logger.info("Couldn't import orbit, skipping")
|
||||
|
||||
return self._estimators
|
||||
|
||||
@@ -134,7 +151,7 @@ class TimeSeriesTask(Task):
|
||||
raise ValueError("Must supply either X_train_all and y_train_all, or dataframe and label")
|
||||
|
||||
try:
|
||||
dataframe[self.time_col] = pd.to_datetime(dataframe[self.time_col])
|
||||
dataframe.loc[:, self.time_col] = pd.to_datetime(dataframe[self.time_col])
|
||||
except Exception:
|
||||
raise ValueError(
|
||||
f"For '{TS_FORECAST}' task, time column {self.time_col} must contain timestamp values."
|
||||
@@ -369,9 +386,8 @@ class TimeSeriesTask(Task):
|
||||
return X
|
||||
|
||||
def preprocess(self, X, transformer=None):
|
||||
if isinstance(X, pd.DataFrame) or isinstance(X, np.ndarray) or isinstance(X, pd.Series):
|
||||
X = X.copy()
|
||||
X = normalize_ts_data(X, self.target_names, self.time_col)
|
||||
if isinstance(X, (pd.DataFrame, np.ndarray, pd.Series)):
|
||||
X = normalize_ts_data(X.copy(), self.target_names, self.time_col)
|
||||
return self._preprocess(X, transformer)
|
||||
elif isinstance(X, int):
|
||||
return X
|
||||
@@ -512,7 +528,7 @@ def remove_ts_duplicates(
|
||||
duplicates = X.duplicated()
|
||||
|
||||
if any(duplicates):
|
||||
logger.warning("Duplicate timestamp values found in timestamp column. " f"\n{X.loc[duplicates, X][time_col]}")
|
||||
logger.warning("Duplicate timestamp values found in timestamp column. " f"\n{X.loc[duplicates, time_col]}")
|
||||
X = X.drop_duplicates()
|
||||
logger.warning("Removed duplicate rows based on all columns")
|
||||
assert (
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
from .tft import TemporalFusionTransformerEstimator
|
||||
from .ts_data import TimeSeriesDataset
|
||||
from .ts_model import (
|
||||
ARIMA,
|
||||
LGBM_TS,
|
||||
RF_TS,
|
||||
SARIMAX,
|
||||
Average,
|
||||
CatBoost_TS,
|
||||
ExtraTrees_TS,
|
||||
HoltWinters,
|
||||
LassoLars_TS,
|
||||
Naive,
|
||||
Orbit,
|
||||
Prophet,
|
||||
SeasonalAverage,
|
||||
SeasonalNaive,
|
||||
TimeSeriesEstimator,
|
||||
XGBoost_TS,
|
||||
XGBoostLimitDepth_TS,
|
||||
)
|
||||
|
||||
try:
|
||||
from .tcn import TCNEstimator
|
||||
except ImportError:
|
||||
TCNEstimator = None
|
||||
|
||||
from .ts_data import TimeSeriesDataset
|
||||
|
||||
@@ -17,24 +17,30 @@ from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def make_lag_features(X: pd.DataFrame, y: pd.Series, lags: int):
|
||||
"""Transform input data X, y into autoregressive form - shift
|
||||
them appropriately based on horizon and create `lags` columns.
|
||||
"""Transform input data X, y into autoregressive form by creating `lags` columns.
|
||||
|
||||
This function is called automatically by FLAML during the training process
|
||||
to convert time series data into a format suitable for sklearn-based regression
|
||||
models (e.g., lgbm, rf, xgboost). Users do NOT need to manually call this function
|
||||
or create lagged features themselves.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
X : pandas.DataFrame
|
||||
Input features.
|
||||
Input feature DataFrame, which may contain temporal features and/or exogenous variables.
|
||||
|
||||
y : array_like, (1d)
|
||||
Target vector.
|
||||
Target vector (time series values to forecast).
|
||||
|
||||
horizon : int
|
||||
length of X for `predict` method
|
||||
lags : int
|
||||
Number of lagged time steps to use as features.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pandas.DataFrame
|
||||
shifted dataframe with `lags` columns
|
||||
Shifted dataframe with `lags` columns for each original feature.
|
||||
The target variable y is also lagged to prevent data leakage
|
||||
(i.e., we use y(t-1), y(t-2), ..., y(t-lags) to predict y(t)).
|
||||
"""
|
||||
lag_features = []
|
||||
|
||||
@@ -55,6 +61,17 @@ def make_lag_features(X: pd.DataFrame, y: pd.Series, lags: int):
|
||||
|
||||
|
||||
class SklearnWrapper:
|
||||
"""Wrapper class for using sklearn-based models for time series forecasting.
|
||||
|
||||
This wrapper automatically handles the transformation of time series data into
|
||||
a supervised learning format by creating lagged features. It trains separate
|
||||
models for each step in the forecast horizon.
|
||||
|
||||
Users typically don't interact with this class directly - it's used internally
|
||||
by FLAML when sklearn-based estimators (lgbm, rf, xgboost, etc.) are selected
|
||||
for time series forecasting tasks.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model_class: type,
|
||||
@@ -76,6 +93,8 @@ class SklearnWrapper:
|
||||
self.pca = None
|
||||
|
||||
def fit(self, X: pd.DataFrame, y: pd.Series, **kwargs):
|
||||
if "is_retrain" in kwargs:
|
||||
kwargs.pop("is_retrain")
|
||||
self._X = X
|
||||
self._y = y
|
||||
|
||||
@@ -92,7 +111,14 @@ class SklearnWrapper:
|
||||
|
||||
for i, model in enumerate(self.models):
|
||||
offset = i + self.lags
|
||||
model.fit(X_trans[: len(X) - offset], y[offset:], **fit_params)
|
||||
if len(X) - offset > 2:
|
||||
# series with length 2 will meet All features are either constant or ignored.
|
||||
# TODO: see why the non-constant features are ignored. Selector?
|
||||
model.fit(X_trans[: len(X) - offset], y[offset:], **fit_params)
|
||||
elif len(X) > offset and "catboost" not in str(model).lower():
|
||||
model.fit(X_trans[: len(X) - offset], y[offset:], **fit_params)
|
||||
else:
|
||||
print("[INFO]: Length of data should longer than period + lags.")
|
||||
return self
|
||||
|
||||
def predict(self, X, X_train=None, y_train=None):
|
||||
|
||||
286
flaml/automl/time_series/tcn.py
Normal file
286
flaml/automl/time_series/tcn.py
Normal file
@@ -0,0 +1,286 @@
|
||||
# This file is adapted from
|
||||
# https://github.com/locuslab/TCN/blob/master/TCN/tcn.py
|
||||
# https://github.com/locuslab/TCN/blob/master/TCN/adding_problem/add_test.py
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
import pytorch_lightning as pl
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from pytorch_lightning.callbacks import EarlyStopping, LearningRateMonitor
|
||||
from pytorch_lightning.loggers import TensorBoardLogger
|
||||
from torch.nn.utils import weight_norm
|
||||
from torch.utils.data import DataLoader, TensorDataset
|
||||
|
||||
from flaml import tune
|
||||
from flaml.automl.data import add_time_idx_col
|
||||
from flaml.automl.logger import logger, logger_formatter
|
||||
from flaml.automl.time_series.ts_data import TimeSeriesDataset
|
||||
from flaml.automl.time_series.ts_model import TimeSeriesEstimator
|
||||
|
||||
|
||||
class Chomp1d(nn.Module):
|
||||
def __init__(self, chomp_size):
|
||||
super().__init__()
|
||||
self.chomp_size = chomp_size
|
||||
|
||||
def forward(self, x):
|
||||
return x[:, :, : -self.chomp_size].contiguous()
|
||||
|
||||
|
||||
class TemporalBlock(nn.Module):
|
||||
def __init__(self, n_inputs, n_outputs, kernel_size, stride, dilation, padding, dropout=0.2):
|
||||
super().__init__()
|
||||
self.conv1 = weight_norm(
|
||||
nn.Conv1d(n_inputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation)
|
||||
)
|
||||
self.chomp1 = Chomp1d(padding)
|
||||
self.relu1 = nn.ReLU()
|
||||
self.dropout1 = nn.Dropout(dropout)
|
||||
|
||||
self.conv2 = weight_norm(
|
||||
nn.Conv1d(n_outputs, n_outputs, kernel_size, stride=stride, padding=padding, dilation=dilation)
|
||||
)
|
||||
self.chomp2 = Chomp1d(padding)
|
||||
self.relu2 = nn.ReLU()
|
||||
self.dropout2 = nn.Dropout(dropout)
|
||||
|
||||
self.net = nn.Sequential(
|
||||
self.conv1, self.chomp1, self.relu1, self.dropout1, self.conv2, self.chomp2, self.relu2, self.dropout2
|
||||
)
|
||||
self.downsample = nn.Conv1d(n_inputs, n_outputs, 1) if n_inputs != n_outputs else None
|
||||
self.relu = nn.ReLU()
|
||||
self.init_weights()
|
||||
|
||||
def init_weights(self):
|
||||
self.conv1.weight.data.normal_(0, 0.01)
|
||||
self.conv2.weight.data.normal_(0, 0.01)
|
||||
if self.downsample is not None:
|
||||
self.downsample.weight.data.normal_(0, 0.01)
|
||||
|
||||
def forward(self, x):
|
||||
out = self.net(x)
|
||||
res = x if self.downsample is None else self.downsample(x)
|
||||
return self.relu(out + res)
|
||||
|
||||
|
||||
class TCNForecaster(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_feature_num,
|
||||
num_outputs,
|
||||
num_channels,
|
||||
kernel_size=2,
|
||||
dropout=0.2,
|
||||
):
|
||||
super().__init__()
|
||||
layers = []
|
||||
num_levels = len(num_channels)
|
||||
for i in range(num_levels):
|
||||
dilation_size = 2**i
|
||||
in_channels = input_feature_num if i == 0 else num_channels[i - 1]
|
||||
out_channels = num_channels[i]
|
||||
layers += [
|
||||
TemporalBlock(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=dilation_size,
|
||||
padding=(kernel_size - 1) * dilation_size,
|
||||
dropout=dropout,
|
||||
)
|
||||
]
|
||||
|
||||
self.network = nn.Sequential(*layers)
|
||||
self.linear = nn.Linear(num_channels[-1], num_outputs)
|
||||
|
||||
def forward(self, x):
|
||||
y1 = self.network(x)
|
||||
return self.linear(y1[:, :, -1])
|
||||
|
||||
|
||||
class TCNForecasterLightningModule(pl.LightningModule):
|
||||
def __init__(self, model: TCNForecaster, learning_rate: float = 1e-3):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.learning_rate = learning_rate
|
||||
self.loss_fn = nn.MSELoss()
|
||||
|
||||
def forward(self, x):
|
||||
return self.model(x)
|
||||
|
||||
def step(self, batch, batch_idx):
|
||||
x, y = batch
|
||||
y_hat = self.model(x)
|
||||
loss = self.loss_fn(y_hat, y)
|
||||
return loss
|
||||
|
||||
def training_step(self, batch, batch_idx):
|
||||
loss = self.step(batch, batch_idx)
|
||||
self.log("train_loss", loss)
|
||||
return loss
|
||||
|
||||
def validation_step(self, batch, batch_idx):
|
||||
loss = self.step(batch, batch_idx)
|
||||
self.log("val_loss", loss)
|
||||
return loss
|
||||
|
||||
def configure_optimizers(self):
|
||||
return torch.optim.Adam(self.parameters(), lr=self.learning_rate)
|
||||
|
||||
|
||||
class DataframeDataset(torch.utils.data.Dataset):
|
||||
def __init__(self, dataframe, target_column, features_columns, sequence_length, train=True):
|
||||
self.data = torch.tensor(dataframe[features_columns].to_numpy(), dtype=torch.float)
|
||||
self.sequence_length = sequence_length
|
||||
if train:
|
||||
self.labels = torch.tensor(dataframe[target_column].to_numpy(), dtype=torch.float)
|
||||
self.is_train = train
|
||||
|
||||
def __len__(self):
|
||||
return len(self.data) - self.sequence_length + 1
|
||||
|
||||
def __getitem__(self, idx):
|
||||
data = self.data[idx : idx + self.sequence_length]
|
||||
data = data.permute(1, 0)
|
||||
if self.is_train:
|
||||
label = self.labels[idx : idx + self.sequence_length]
|
||||
return data, label
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
class TCNEstimator(TimeSeriesEstimator):
|
||||
"""The class for tuning TCN Forecaster"""
|
||||
|
||||
@classmethod
|
||||
def search_space(cls, data, task, pred_horizon, **params):
|
||||
space = {
|
||||
"num_levels": {
|
||||
"domain": tune.randint(lower=4, upper=20), # hidden = 2^num_hidden
|
||||
"init_value": 4,
|
||||
},
|
||||
"num_hidden": {
|
||||
"domain": tune.randint(lower=4, upper=8), # hidden = 2^num_hidden
|
||||
"init_value": 5,
|
||||
},
|
||||
"kernel_size": {
|
||||
"domain": tune.choice([2, 3, 5, 7]), # common choices for kernel size
|
||||
"init_value": 3,
|
||||
},
|
||||
"dropout": {
|
||||
"domain": tune.uniform(lower=0.0, upper=0.5), # standard range for dropout
|
||||
"init_value": 0.1,
|
||||
},
|
||||
"learning_rate": {
|
||||
"domain": tune.loguniform(lower=1e-4, upper=1e-1), # typical range for learning rate
|
||||
"init_value": 1e-3,
|
||||
},
|
||||
}
|
||||
return space
|
||||
|
||||
def __init__(self, task="ts_forecast", n_jobs=1, **params):
|
||||
super().__init__(task, **params)
|
||||
logging.getLogger("pytorch_lightning").setLevel(logging.WARNING)
|
||||
|
||||
def fit(self, X_train: TimeSeriesDataset, y_train=None, budget=None, **kwargs):
|
||||
start_time = time.time()
|
||||
if budget is not None:
|
||||
deltabudget = datetime.timedelta(seconds=budget)
|
||||
else:
|
||||
deltabudget = None
|
||||
X_train = self.enrich(X_train)
|
||||
super().fit(X_train, y_train, budget, **kwargs)
|
||||
|
||||
self.batch_size = kwargs.get("batch_size", 64)
|
||||
self.horizon = kwargs.get("period", 1)
|
||||
self.feature_cols = X_train.time_varying_known_reals
|
||||
self.target_col = X_train.target_names[0]
|
||||
|
||||
train_dataset = DataframeDataset(
|
||||
X_train.train_data,
|
||||
self.target_col,
|
||||
self.feature_cols,
|
||||
self.horizon,
|
||||
)
|
||||
train_loader = DataLoader(train_dataset, batch_size=self.batch_size, shuffle=False)
|
||||
if not X_train.test_data.empty:
|
||||
val_dataset = DataframeDataset(
|
||||
X_train.test_data,
|
||||
self.target_col,
|
||||
self.feature_cols,
|
||||
self.horizon,
|
||||
)
|
||||
else:
|
||||
val_dataset = DataframeDataset(
|
||||
X_train.train_data.sample(frac=0.2, random_state=kwargs.get("random_state", 0)),
|
||||
self.target_col,
|
||||
self.feature_cols,
|
||||
self.horizon,
|
||||
)
|
||||
|
||||
val_loader = DataLoader(val_dataset, batch_size=self.batch_size, shuffle=False)
|
||||
|
||||
model = TCNForecaster(
|
||||
len(self.feature_cols),
|
||||
self.horizon,
|
||||
[2 ** self.params["num_hidden"]] * self.params["num_levels"],
|
||||
self.params["kernel_size"],
|
||||
self.params["dropout"],
|
||||
)
|
||||
|
||||
pl_module = TCNForecasterLightningModule(model, self.params["learning_rate"])
|
||||
|
||||
# Training loop
|
||||
# gpus is deprecated in v1.7 and removed in v2.0
|
||||
# accelerator="auto" can cast all condition.
|
||||
trainer = pl.Trainer(
|
||||
max_epochs=kwargs.get("max_epochs", 10),
|
||||
accelerator="auto",
|
||||
callbacks=[
|
||||
EarlyStopping(monitor="val_loss", min_delta=1e-4, patience=10, verbose=False, mode="min"),
|
||||
LearningRateMonitor(),
|
||||
],
|
||||
logger=TensorBoardLogger(kwargs.get("log_dir", "logs/lightning_logs")), # logging results to a tensorboard
|
||||
max_time=deltabudget,
|
||||
enable_model_summary=False,
|
||||
enable_progress_bar=False,
|
||||
)
|
||||
trainer.fit(
|
||||
pl_module,
|
||||
train_dataloaders=train_loader,
|
||||
val_dataloaders=val_loader,
|
||||
)
|
||||
best_model = trainer.model
|
||||
self._model = best_model
|
||||
train_time = time.time() - start_time
|
||||
return train_time
|
||||
|
||||
def predict(self, X):
|
||||
X = self.enrich(X)
|
||||
if isinstance(X, TimeSeriesDataset):
|
||||
# Use X_train if X_val is empty (e.g., when computing training metrics)
|
||||
df = X.X_val if len(X.test_data) > 0 else X.X_train
|
||||
else:
|
||||
df = X
|
||||
dataset = DataframeDataset(
|
||||
df,
|
||||
self.target_col,
|
||||
self.feature_cols,
|
||||
self.horizon,
|
||||
train=False,
|
||||
)
|
||||
data_loader = DataLoader(dataset, batch_size=self.batch_size, shuffle=False)
|
||||
self._model.eval()
|
||||
raw_preds = []
|
||||
for batch_x in data_loader:
|
||||
raw_pred = self._model(batch_x)
|
||||
raw_preds.append(raw_pred)
|
||||
raw_preds = torch.cat(raw_preds, dim=0)
|
||||
preds = pd.Series(raw_preds.detach().numpy().ravel())
|
||||
return preds
|
||||
@@ -1,3 +1,4 @@
|
||||
import inspect
|
||||
import time
|
||||
|
||||
try:
|
||||
@@ -106,12 +107,17 @@ class TemporalFusionTransformerEstimator(TimeSeriesEstimator):
|
||||
def fit(self, X_train, y_train, budget=None, **kwargs):
|
||||
import warnings
|
||||
|
||||
import pytorch_lightning as pl
|
||||
try:
|
||||
import lightning.pytorch as pl
|
||||
from lightning.pytorch.callbacks import EarlyStopping, LearningRateMonitor
|
||||
from lightning.pytorch.loggers import TensorBoardLogger
|
||||
except ImportError:
|
||||
import pytorch_lightning as pl
|
||||
from pytorch_lightning.callbacks import EarlyStopping, LearningRateMonitor
|
||||
from pytorch_lightning.loggers import TensorBoardLogger
|
||||
import torch
|
||||
from pytorch_forecasting import TemporalFusionTransformer
|
||||
from pytorch_forecasting.metrics import QuantileLoss
|
||||
from pytorch_lightning.callbacks import EarlyStopping, LearningRateMonitor
|
||||
from pytorch_lightning.loggers import TensorBoardLogger
|
||||
|
||||
# a bit of monkey patching to fix the MacOS test
|
||||
# all the log_prediction method appears to do is plot stuff, which ?breaks github tests
|
||||
@@ -132,12 +138,26 @@ class TemporalFusionTransformerEstimator(TimeSeriesEstimator):
|
||||
lr_logger = LearningRateMonitor() # log the learning rate
|
||||
logger = TensorBoardLogger(kwargs.get("log_dir", "lightning_logs")) # logging results to a tensorboard
|
||||
default_trainer_kwargs = dict(
|
||||
gpus=self._kwargs.get("gpu_per_trial", [0]) if torch.cuda.is_available() else None,
|
||||
max_epochs=max_epochs,
|
||||
gradient_clip_val=gradient_clip_val,
|
||||
callbacks=[lr_logger, early_stop_callback],
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
# PyTorch Lightning >=2.0 replaced `gpus` with `accelerator`/`devices`.
|
||||
# Also, passing `gpus=None` is not accepted on newer versions.
|
||||
trainer_sig_params = inspect.signature(pl.Trainer.__init__).parameters
|
||||
if torch.cuda.is_available() and "gpus" in trainer_sig_params:
|
||||
gpus = self._kwargs.get("gpu_per_trial", None)
|
||||
if gpus is not None:
|
||||
default_trainer_kwargs["gpus"] = gpus
|
||||
elif torch.cuda.is_available() and "devices" in trainer_sig_params:
|
||||
devices = self._kwargs.get("gpu_per_trial", None)
|
||||
if devices == -1:
|
||||
devices = "auto"
|
||||
if devices is not None:
|
||||
default_trainer_kwargs["accelerator"] = "gpu"
|
||||
default_trainer_kwargs["devices"] = devices
|
||||
trainer = pl.Trainer(
|
||||
**default_trainer_kwargs,
|
||||
)
|
||||
@@ -157,7 +177,14 @@ class TemporalFusionTransformerEstimator(TimeSeriesEstimator):
|
||||
val_dataloaders=val_dataloader,
|
||||
)
|
||||
best_model_path = trainer.checkpoint_callback.best_model_path
|
||||
best_tft = TemporalFusionTransformer.load_from_checkpoint(best_model_path)
|
||||
# PyTorch 2.6 changed `torch.load` default `weights_only` from False -> True.
|
||||
# Some Lightning checkpoints (including those produced here) can require full unpickling.
|
||||
# This path is generated locally during training, so it's trusted.
|
||||
load_sig_params = inspect.signature(TemporalFusionTransformer.load_from_checkpoint).parameters
|
||||
if "weights_only" in load_sig_params:
|
||||
best_tft = TemporalFusionTransformer.load_from_checkpoint(best_model_path, weights_only=False)
|
||||
else:
|
||||
best_tft = TemporalFusionTransformer.load_from_checkpoint(best_model_path)
|
||||
train_time = time.time() - current_time
|
||||
self._model = best_tft
|
||||
return train_time
|
||||
@@ -170,7 +197,11 @@ class TemporalFusionTransformerEstimator(TimeSeriesEstimator):
|
||||
last_data_cols = self.group_ids.copy()
|
||||
last_data_cols.append(self.target_names[0])
|
||||
last_data = self.data[lambda x: x.time_idx == x.time_idx.max()][last_data_cols]
|
||||
decoder_data = X.X_val if isinstance(X, TimeSeriesDataset) else X
|
||||
# Use X_train if test_data is empty (e.g., when computing training metrics)
|
||||
if isinstance(X, TimeSeriesDataset):
|
||||
decoder_data = X.X_val if len(X.test_data) > 0 else X.X_train
|
||||
else:
|
||||
decoder_data = X
|
||||
if "time_idx" not in decoder_data:
|
||||
decoder_data = add_time_idx_col(decoder_data)
|
||||
decoder_data["time_idx"] += encoder_data["time_idx"].max() + 1 - decoder_data["time_idx"].min()
|
||||
|
||||
@@ -9,6 +9,7 @@ import numpy as np
|
||||
try:
|
||||
import pandas as pd
|
||||
from pandas import DataFrame, Series, to_datetime
|
||||
from pandas.api.types import is_datetime64_any_dtype
|
||||
from scipy.sparse import issparse
|
||||
from sklearn.compose import ColumnTransformer
|
||||
from sklearn.impute import SimpleImputer
|
||||
@@ -120,7 +121,12 @@ class TimeSeriesDataset:
|
||||
|
||||
@property
|
||||
def X_all(self) -> pd.DataFrame:
|
||||
return pd.concat([self.X_train, self.X_val], axis=0)
|
||||
# Remove empty or all-NA columns before concatenation
|
||||
X_train_filtered = self.X_train.dropna(axis=1, how="all")
|
||||
X_val_filtered = self.X_val.dropna(axis=1, how="all")
|
||||
|
||||
# Concatenate the filtered DataFrames
|
||||
return pd.concat([X_train_filtered, X_val_filtered], axis=0)
|
||||
|
||||
@property
|
||||
def y_train(self) -> pd.DataFrame:
|
||||
@@ -392,8 +398,17 @@ class DataTransformerTS:
|
||||
assert len(self.num_columns) == 0, "Trying to call fit() twice, something is wrong"
|
||||
|
||||
for column in X.columns:
|
||||
# Never treat the time column as a feature for sklearn preprocessing
|
||||
if column == self.time_col:
|
||||
continue
|
||||
|
||||
# Robust datetime detection (covers datetime64[ms/us/ns], tz-aware, etc.)
|
||||
if is_datetime64_any_dtype(X[column]):
|
||||
self.datetime_columns.append(column)
|
||||
continue
|
||||
|
||||
# sklearn/utils/validation.py needs int/float values
|
||||
if X[column].dtype.name in ("object", "category"):
|
||||
if X[column].dtype.name in ("object", "category", "string"):
|
||||
if (
|
||||
# drop columns where all values are the same
|
||||
X[column].nunique() == 1
|
||||
@@ -462,7 +477,7 @@ class DataTransformerTS:
|
||||
if "__NAN__" not in X[col].cat.categories:
|
||||
X[col] = X[col].cat.add_categories("__NAN__").fillna("__NAN__")
|
||||
else:
|
||||
X[col] = X[col].fillna("__NAN__")
|
||||
X[col] = X[col].fillna("__NAN__").infer_objects(copy=False)
|
||||
X[col] = X[col].astype("category")
|
||||
|
||||
for column in self.num_columns:
|
||||
@@ -531,14 +546,12 @@ def normalize_ts_data(X_train_all, target_names, time_col, y_train_all=None):
|
||||
|
||||
|
||||
def validate_data_basic(X_train_all, y_train_all):
|
||||
assert isinstance(X_train_all, np.ndarray) or issparse(X_train_all) or isinstance(X_train_all, pd.DataFrame), (
|
||||
"X_train_all must be a numpy array, a pandas dataframe, " "or Scipy sparse matrix."
|
||||
)
|
||||
assert isinstance(X_train_all, (np.ndarray, DataFrame)) or issparse(
|
||||
X_train_all
|
||||
), "X_train_all must be a numpy array, a pandas dataframe, or Scipy sparse matrix."
|
||||
|
||||
assert (
|
||||
isinstance(y_train_all, np.ndarray)
|
||||
or isinstance(y_train_all, pd.Series)
|
||||
or isinstance(y_train_all, pd.DataFrame)
|
||||
assert isinstance(
|
||||
y_train_all, (np.ndarray, pd.Series, pd.DataFrame)
|
||||
), "y_train_all must be a numpy array or a pandas series or DataFrame."
|
||||
|
||||
assert X_train_all.size != 0 and y_train_all.size != 0, "Input data must not be empty, use None if no data"
|
||||
|
||||
@@ -26,6 +26,7 @@ from flaml.automl.data import TS_TIMESTAMP_COL, TS_VALUE_COL
|
||||
from flaml.automl.model import (
|
||||
CatBoostEstimator,
|
||||
ExtraTreesEstimator,
|
||||
LassoLarsEstimator,
|
||||
LGBMEstimator,
|
||||
RandomForestEstimator,
|
||||
SKLearnEstimator,
|
||||
@@ -193,7 +194,13 @@ class Orbit(TimeSeriesEstimator):
|
||||
|
||||
elif isinstance(X, TimeSeriesDataset):
|
||||
data = X
|
||||
X = data.test_data[[self.time_col] + X.regressors]
|
||||
# By default we predict on the dataset's test partition.
|
||||
# Some internal call paths (e.g., training-metric logging) may pass a
|
||||
# dataset whose test partition is empty; fall back to train partition.
|
||||
if data.test_data is not None and len(data.test_data):
|
||||
X = data.test_data[data.regressors + [data.time_col]]
|
||||
else:
|
||||
X = data.train_data[data.regressors + [data.time_col]]
|
||||
|
||||
if self._model is not None:
|
||||
forecast = self._model.predict(X, **kwargs)
|
||||
@@ -300,7 +307,13 @@ class Prophet(TimeSeriesEstimator):
|
||||
|
||||
if isinstance(X, TimeSeriesDataset):
|
||||
data = X
|
||||
X = data.test_data[data.regressors + [data.time_col]]
|
||||
# By default we predict on the dataset's test partition.
|
||||
# Some internal call paths (e.g., training-metric logging) may pass a
|
||||
# dataset whose test partition is empty; fall back to train partition.
|
||||
if data.test_data is not None and len(data.test_data):
|
||||
X = data.test_data[data.regressors + [data.time_col]]
|
||||
else:
|
||||
X = data.train_data[data.regressors + [data.time_col]]
|
||||
|
||||
X = X.rename(columns={self.time_col: "ds"})
|
||||
if self._model is not None:
|
||||
@@ -326,11 +339,19 @@ class StatsModelsEstimator(TimeSeriesEstimator):
|
||||
|
||||
if isinstance(X, TimeSeriesDataset):
|
||||
data = X
|
||||
X = data.test_data[data.regressors + [data.time_col]]
|
||||
# By default we predict on the dataset's test partition.
|
||||
# Some internal call paths (e.g., training-metric logging) may pass a
|
||||
# dataset whose test partition is empty; fall back to train partition.
|
||||
if data.test_data is not None and len(data.test_data):
|
||||
X = data.test_data[data.regressors + [data.time_col]]
|
||||
else:
|
||||
X = data.train_data[data.regressors + [data.time_col]]
|
||||
else:
|
||||
X = X[self.regressors + [self.time_col]]
|
||||
|
||||
if isinstance(X, DataFrame):
|
||||
if X.shape[0] == 0:
|
||||
return pd.Series([], name=self.target_names[0], dtype=float)
|
||||
start = X[self.time_col].iloc[0]
|
||||
end = X[self.time_col].iloc[-1]
|
||||
if len(self.regressors):
|
||||
@@ -631,6 +652,125 @@ class HoltWinters(StatsModelsEstimator):
|
||||
return train_time
|
||||
|
||||
|
||||
class SimpleForecaster(StatsModelsEstimator):
|
||||
"""Base class for Naive Forecaster like Seasonal Naive, Naive, Seasonal Average, Average"""
|
||||
|
||||
@classmethod
|
||||
def _search_space(cls, data: TimeSeriesDataset, task: Task, pred_horizon: int, **params):
|
||||
return {
|
||||
"season": {
|
||||
"domain": tune.randint(1, pred_horizon),
|
||||
"init_value": pred_horizon,
|
||||
}
|
||||
}
|
||||
|
||||
def joint_preprocess(self, X_train, y_train=None):
|
||||
X_train = self.enrich(X_train)
|
||||
|
||||
self.regressors = []
|
||||
|
||||
if isinstance(X_train, TimeSeriesDataset):
|
||||
data = X_train
|
||||
target_col = data.target_names[0]
|
||||
# this class only supports univariate regression
|
||||
train_df = data.train_data[self.regressors + [target_col]]
|
||||
train_df.index = to_datetime(data.train_data[data.time_col])
|
||||
else:
|
||||
target_col = TS_VALUE_COL
|
||||
train_df = self._join(X_train, y_train)
|
||||
|
||||
self.time_col = data.time_col
|
||||
self.target_names = data.target_names
|
||||
|
||||
train_df = self._preprocess(train_df)
|
||||
return train_df, target_col
|
||||
|
||||
def fit(self, X_train, y_train=None, budget=None, **kwargs):
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
from statsmodels.tsa.holtwinters import SimpleExpSmoothing
|
||||
|
||||
self.season = self.params.get("season", 1)
|
||||
current_time = time.time()
|
||||
super().fit(X_train, y_train, budget=budget, **kwargs)
|
||||
|
||||
train_df, target_col = self.joint_preprocess(X_train, y_train)
|
||||
|
||||
model = SimpleExpSmoothing(
|
||||
train_df[[target_col]],
|
||||
)
|
||||
with suppress_stdout_stderr():
|
||||
model = model.fit(smoothing_level=self.smoothing_level)
|
||||
train_time = time.time() - current_time
|
||||
self._model = model
|
||||
return train_time
|
||||
|
||||
|
||||
class SeasonalNaive(SimpleForecaster):
|
||||
smoothing_level = 1.0
|
||||
|
||||
def predict(self, X, **kwargs):
|
||||
if isinstance(X, int):
|
||||
forecasts = []
|
||||
for i in range(X):
|
||||
forecast = self._model.forecast(steps=self.season)[0]
|
||||
forecasts.append(forecast)
|
||||
return pd.Series(forecasts)
|
||||
else:
|
||||
return super().predict(X, **kwargs)
|
||||
|
||||
|
||||
class Naive(SimpleForecaster):
|
||||
smoothing_level = 0.0
|
||||
|
||||
@classmethod
|
||||
def _search_space(cls, data: TimeSeriesDataset, task: Task, pred_horizon: int, **params):
|
||||
return {}
|
||||
|
||||
def predict(self, X, **kwargs):
|
||||
if isinstance(X, int):
|
||||
last_observation = self._model.params["initial_level"]
|
||||
return pd.Series([last_observation] * X)
|
||||
else:
|
||||
return super().predict(X, **kwargs)
|
||||
|
||||
|
||||
class SeasonalAverage(SimpleForecaster):
|
||||
def fit(self, X_train, y_train=None, budget=None, **kwargs):
|
||||
from statsmodels.tsa.ar_model import AutoReg, ar_select_order
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
self.season = kwargs.get("season", 1) # seasonality period
|
||||
train_df, target_col = self.joint_preprocess(X_train, y_train)
|
||||
selection_res = ar_select_order(train_df[target_col], maxlag=self.season)
|
||||
|
||||
# Fit autoregressive model with optimal order
|
||||
model = AutoReg(train_df[target_col], lags=selection_res.ar_lags)
|
||||
self._model = model.fit()
|
||||
end_time = time.time()
|
||||
|
||||
return end_time - start_time
|
||||
|
||||
|
||||
class Average(SimpleForecaster):
|
||||
@classmethod
|
||||
def _search_space(cls, data: TimeSeriesDataset, task: Task, pred_horizon: int, **params):
|
||||
return {}
|
||||
|
||||
def fit(self, X_train, y_train=None, budget=None, **kwargs):
|
||||
from statsmodels.tsa.ar_model import AutoReg
|
||||
|
||||
start_time = time.time()
|
||||
train_df, target_col = self.joint_preprocess(X_train, y_train)
|
||||
model = AutoReg(train_df[target_col], lags=0)
|
||||
self._model = model.fit()
|
||||
end_time = time.time()
|
||||
|
||||
return end_time - start_time
|
||||
|
||||
|
||||
class TS_SKLearn(TimeSeriesEstimator):
|
||||
"""The class for tuning SKLearn Regressors for time-series forecasting"""
|
||||
|
||||
@@ -709,6 +849,13 @@ class TS_SKLearn(TimeSeriesEstimator):
|
||||
if isinstance(X, TimeSeriesDataset):
|
||||
data = X
|
||||
X = data.test_data
|
||||
# By default we predict on the dataset's test partition.
|
||||
# Some internal call paths (e.g., training-metric logging) may pass a
|
||||
# dataset whose test partition is empty; fall back to train partition.
|
||||
if data.test_data is not None and len(data.test_data):
|
||||
X = data.test_data
|
||||
else:
|
||||
X = data.train_data
|
||||
|
||||
if self._model is not None:
|
||||
X = X[self.regressors]
|
||||
@@ -757,3 +904,7 @@ class XGBoostLimitDepth_TS(TS_SKLearn):
|
||||
# catboost regressor is invalid because it has a `name` parameter, making it incompatible with hcrystalball
|
||||
class CatBoost_TS(TS_SKLearn):
|
||||
base_class = CatBoostEstimator
|
||||
|
||||
|
||||
class LassoLars_TS(TS_SKLearn):
|
||||
base_class = LassoLarsEstimator
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import IO
|
||||
logger = logging.getLogger("flaml.automl")
|
||||
|
||||
|
||||
class TrainingLogRecord(object):
|
||||
class TrainingLogRecord:
|
||||
def __init__(
|
||||
self,
|
||||
record_id: int,
|
||||
@@ -52,7 +52,7 @@ class TrainingLogCheckPoint(TrainingLogRecord):
|
||||
self.curr_best_record_id = curr_best_record_id
|
||||
|
||||
|
||||
class TrainingLogWriter(object):
|
||||
class TrainingLogWriter:
|
||||
def __init__(self, output_filename: str):
|
||||
self.output_filename = output_filename
|
||||
self.file = None
|
||||
@@ -79,7 +79,7 @@ class TrainingLogWriter(object):
|
||||
sample_size,
|
||||
):
|
||||
if self.file is None:
|
||||
raise IOError("Call open() to open the output file first.")
|
||||
raise OSError("Call open() to open the output file first.")
|
||||
if validation_loss is None:
|
||||
raise ValueError("TEST LOSS NONE ERROR!!!")
|
||||
record = TrainingLogRecord(
|
||||
@@ -109,7 +109,7 @@ class TrainingLogWriter(object):
|
||||
|
||||
def checkpoint(self):
|
||||
if self.file is None:
|
||||
raise IOError("Call open() to open the output file first.")
|
||||
raise OSError("Call open() to open the output file first.")
|
||||
if self.current_best_loss_record_id is None:
|
||||
logger.warning("flaml.training_log: checkpoint() called before any record is written, skipped.")
|
||||
return
|
||||
@@ -124,7 +124,7 @@ class TrainingLogWriter(object):
|
||||
self.file = None # for pickle
|
||||
|
||||
|
||||
class TrainingLogReader(object):
|
||||
class TrainingLogReader:
|
||||
def __init__(self, filename: str):
|
||||
self.filename = filename
|
||||
self.file = None
|
||||
@@ -134,7 +134,7 @@ class TrainingLogReader(object):
|
||||
|
||||
def records(self):
|
||||
if self.file is None:
|
||||
raise IOError("Call open() before reading log file.")
|
||||
raise OSError("Call open() before reading log file.")
|
||||
for line in self.file:
|
||||
data = json.loads(line)
|
||||
if len(data) == 1:
|
||||
@@ -149,7 +149,7 @@ class TrainingLogReader(object):
|
||||
|
||||
def get_record(self, record_id) -> TrainingLogRecord:
|
||||
if self.file is None:
|
||||
raise IOError("Call open() before reading log file.")
|
||||
raise OSError("Call open() before reading log file.")
|
||||
for rec in self.records():
|
||||
if rec.record_id == record_id:
|
||||
return rec
|
||||
|
||||
@@ -95,6 +95,27 @@ def flamlize_estimator(super_class, name: str, task: str, alternatives=None):
|
||||
def fit(self, X, y, *args, **params):
|
||||
hyperparams, estimator_name, X, y_transformed = self.suggest_hyperparams(X, y)
|
||||
self.set_params(**hyperparams)
|
||||
|
||||
# Transform eval_set if present
|
||||
if "eval_set" in params and params["eval_set"] is not None:
|
||||
transformed_eval_set = []
|
||||
for eval_X, eval_y in params["eval_set"]:
|
||||
# Transform features
|
||||
eval_X_transformed = self._feature_transformer.transform(eval_X)
|
||||
# Transform labels if applicable
|
||||
if self._label_transformer and estimator_name in [
|
||||
"rf",
|
||||
"extra_tree",
|
||||
"xgboost",
|
||||
"xgb_limitdepth",
|
||||
"choose_xgb",
|
||||
]:
|
||||
eval_y_transformed = self._label_transformer.transform(eval_y)
|
||||
transformed_eval_set.append((eval_X_transformed, eval_y_transformed))
|
||||
else:
|
||||
transformed_eval_set.append((eval_X_transformed, eval_y))
|
||||
params["eval_set"] = transformed_eval_set
|
||||
|
||||
if self._label_transformer and estimator_name in [
|
||||
"rf",
|
||||
"extra_tree",
|
||||
|
||||
@@ -32,6 +32,7 @@ def construct_portfolio(regret_matrix, meta_features, regret_bound):
|
||||
if meta_features is not None:
|
||||
scaler = RobustScaler()
|
||||
meta_features = meta_features.loc[tasks]
|
||||
meta_features = meta_features.astype(float)
|
||||
meta_features.loc[:, :] = scaler.fit_transform(meta_features)
|
||||
nearest_task = {}
|
||||
for t in tasks:
|
||||
|
||||
@@ -26,6 +26,7 @@ def config_predictor_tuple(tasks, configs, meta_features, regret_matrix):
|
||||
# pre-processing
|
||||
scaler = RobustScaler()
|
||||
meta_features_norm = meta_features.loc[tasks] # this makes a copy
|
||||
meta_features_norm = meta_features_norm.astype(float)
|
||||
meta_features_norm.loc[:, :] = scaler.fit_transform(meta_features_norm)
|
||||
|
||||
proc = {
|
||||
@@ -69,7 +70,7 @@ def build_portfolio(meta_features, regret, strategy):
|
||||
|
||||
def load_json(filename):
|
||||
"""Returns the contents of json file filename."""
|
||||
with open(filename, "r") as f:
|
||||
with open(filename) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ def meta_feature(task, X_train, y_train, meta_feature_names):
|
||||
# 'numpy.ndarray' object has no attribute 'select_dtypes'
|
||||
this_feature.append(1) # all features are numeric
|
||||
else:
|
||||
raise ValueError("Feature {} not implemented. ".format(each_feature_name))
|
||||
raise ValueError(f"Feature {each_feature_name} not implemented. ")
|
||||
|
||||
return this_feature
|
||||
|
||||
@@ -57,7 +57,7 @@ def load_config_predictor(estimator_name, task, location=None):
|
||||
task = "multiclass" if task == "multi" else task # TODO: multi -> multiclass?
|
||||
try:
|
||||
location = location or LOCATION
|
||||
with open(f"{location}/{estimator_name}/{task}.json", "r") as f:
|
||||
with open(f"{location}/{estimator_name}/{task}.json") as f:
|
||||
CONFIG_PREDICTORS[key] = predictor = json.load(f)
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Portfolio has not been built for {estimator_name} on {task} task.")
|
||||
|
||||
0
flaml/fabric/__init__.py
Normal file
0
flaml/fabric/__init__.py
Normal file
1039
flaml/fabric/mlflow.py
Normal file
1039
flaml/fabric/mlflow.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
# ChaCha for Online AutoML
|
||||
|
||||
FLAML includes *ChaCha* which is an automatic hyperparameter tuning solution for online machine learning. Online machine learning has the following properties: (1) data comes in sequential order; and (2) the performance of the machine learning model is evaluated online, i.e., at every iteration. *ChaCha* performs online AutoML respecting the aforementioned properties of online learning, and at the same time respecting the following constraints: (1) only a small constant number of 'live' models are allowed to perform online learning at the same time; and (2) no model persistence or offline training is allowed, which means that once we decide to replace a 'live' model with a new one, the replaced model can no longer be retrieved.
|
||||
FLAML includes *ChaCha* which is an automatic hyperparameter tuning solution for online machine learning. Online machine learning has the following properties: (1) data comes in sequential order; and (2) the performance of the machine learning model is evaluated online, i.e., at every iteration. *ChaCha* performs online AutoML respecting the aforementioned properties of online learning, and at the same time respecting the following constraints: (1) only a small constant number of 'live' models are allowed to perform online learning at the same time; and (2) no model persistence or offline training is allowed, which means that once we decide to replace a 'live' model with a new one, the replaced model can no longer be retrieved.
|
||||
|
||||
For more technical details about *ChaCha*, please check our paper.
|
||||
|
||||
|
||||
37
flaml/tune/logger.py
Normal file
37
flaml/tune/logger.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
class ColoredFormatter(logging.Formatter):
|
||||
# ANSI escape codes for colors
|
||||
COLORS = {
|
||||
# logging.DEBUG: "\033[36m", # Cyan
|
||||
# logging.INFO: "\033[32m", # Green
|
||||
logging.WARNING: "\033[33m", # Yellow
|
||||
logging.ERROR: "\033[31m", # Red
|
||||
logging.CRITICAL: "\033[1;31m", # Bright Red
|
||||
}
|
||||
RESET = "\033[0m" # Reset to default
|
||||
|
||||
def __init__(self, fmt, datefmt, use_color=True):
|
||||
super().__init__(fmt, datefmt)
|
||||
self.use_color = use_color
|
||||
|
||||
def format(self, record):
|
||||
formatted = super().format(record)
|
||||
if self.use_color:
|
||||
color = self.COLORS.get(record.levelno, "")
|
||||
if color:
|
||||
return f"{color}{formatted}{self.RESET}"
|
||||
return formatted
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
use_color = True
|
||||
if os.getenv("FLAML_LOG_NO_COLOR"):
|
||||
use_color = False
|
||||
|
||||
logger_formatter = ColoredFormatter(
|
||||
"[%(name)s: %(asctime)s] {%(lineno)d} %(levelname)s - %(message)s", "%m-%d %H:%M:%S", use_color
|
||||
)
|
||||
logger.propagate = False
|
||||
@@ -217,7 +217,24 @@ class BlendSearch(Searcher):
|
||||
if global_search_alg is not None:
|
||||
self._gs = global_search_alg
|
||||
elif getattr(self, "__name__", None) != "CFO":
|
||||
if space and self._ls.hierarchical:
|
||||
# Use define-by-run for OptunaSearch when needed:
|
||||
# - Hierarchical/conditional spaces are best supported via define-by-run.
|
||||
# - Ray Tune domain/grid specs can trigger an "unresolved search space" warning
|
||||
# unless we switch to define-by-run.
|
||||
use_define_by_run = bool(getattr(self._ls, "hierarchical", False))
|
||||
if (not use_define_by_run) and isinstance(space, dict) and space:
|
||||
try:
|
||||
from .variant_generator import parse_spec_vars
|
||||
|
||||
_, domain_vars, grid_vars = parse_spec_vars(space)
|
||||
use_define_by_run = bool(domain_vars or grid_vars)
|
||||
except Exception:
|
||||
# Be conservative: if we can't determine whether the space is
|
||||
# unresolved, fall back to the original behavior.
|
||||
use_define_by_run = False
|
||||
|
||||
self._use_define_by_run = use_define_by_run
|
||||
if use_define_by_run:
|
||||
from functools import partial
|
||||
|
||||
gs_space = partial(define_by_run_func, space=space)
|
||||
@@ -244,13 +261,32 @@ class BlendSearch(Searcher):
|
||||
evaluated_rewards=evaluated_rewards,
|
||||
)
|
||||
except (AssertionError, ValueError):
|
||||
self._gs = GlobalSearch(
|
||||
space=gs_space,
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
seed=gs_seed,
|
||||
sampler=sampler,
|
||||
)
|
||||
try:
|
||||
self._gs = GlobalSearch(
|
||||
space=gs_space,
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
seed=gs_seed,
|
||||
sampler=sampler,
|
||||
)
|
||||
except ValueError:
|
||||
# Ray Tune's OptunaSearch converts Tune domains into Optuna
|
||||
# distributions. Optuna disallows integer log distributions
|
||||
# with step != 1 (e.g., qlograndint with q>1), which can
|
||||
# raise here. Fall back to FLAML's OptunaSearch wrapper,
|
||||
# which handles these spaces more permissively.
|
||||
if getattr(GlobalSearch, "__module__", "").startswith("ray.tune"):
|
||||
from .suggestion import OptunaSearch as _FallbackOptunaSearch
|
||||
|
||||
self._gs = _FallbackOptunaSearch(
|
||||
space=gs_space,
|
||||
metric=metric,
|
||||
mode=mode,
|
||||
seed=gs_seed,
|
||||
sampler=sampler,
|
||||
)
|
||||
else:
|
||||
raise
|
||||
self._gs.space = space
|
||||
else:
|
||||
self._gs = None
|
||||
@@ -468,7 +504,7 @@ class BlendSearch(Searcher):
|
||||
self._ls_bound_max,
|
||||
self._subspace.get(trial_id, self._ls.space),
|
||||
)
|
||||
if self._gs is not None and self._experimental and (not self._ls.hierarchical):
|
||||
if self._gs is not None and self._experimental and (not getattr(self, "_use_define_by_run", False)):
|
||||
self._gs.add_evaluated_point(flatten_dict(config), objective)
|
||||
# TODO: recover when supported
|
||||
# converted = convert_key(config, self._gs.space)
|
||||
|
||||
@@ -109,7 +109,7 @@ class FLOW2(Searcher):
|
||||
else:
|
||||
mode = "min"
|
||||
|
||||
super(FLOW2, self).__init__(metric=metric, mode=mode)
|
||||
super().__init__(metric=metric, mode=mode)
|
||||
# internally minimizes, so "max" => -1
|
||||
if mode == "max":
|
||||
self.metric_op = -1.0
|
||||
@@ -350,7 +350,7 @@ class FLOW2(Searcher):
|
||||
else:
|
||||
assert (
|
||||
self.lexico_objectives["tolerances"][k_metric][-1] == "%"
|
||||
), "String tolerance of {} should use %% as the suffix".format(k_metric)
|
||||
), f"String tolerance of {k_metric} should use %% as the suffix"
|
||||
tolerance_bound = self._f_best[k_metric] * (
|
||||
1 + 0.01 * float(self.lexico_objectives["tolerances"][k_metric].replace("%", ""))
|
||||
)
|
||||
@@ -385,7 +385,7 @@ class FLOW2(Searcher):
|
||||
else:
|
||||
assert (
|
||||
self.lexico_objectives["tolerances"][k_metric][-1] == "%"
|
||||
), "String tolerance of {} should use %% as the suffix".format(k_metric)
|
||||
), f"String tolerance of {k_metric} should use %% as the suffix"
|
||||
tolerance_bound = self._f_best[k_metric] * (
|
||||
1 + 0.01 * float(self.lexico_objectives["tolerances"][k_metric].replace("%", ""))
|
||||
)
|
||||
@@ -641,8 +641,10 @@ class FLOW2(Searcher):
|
||||
else:
|
||||
# key must be in space
|
||||
domain = space[key]
|
||||
if self.hierarchical and not (
|
||||
domain is None or type(domain) in (str, int, float) or isinstance(domain, sample.Domain)
|
||||
if (
|
||||
self.hierarchical
|
||||
and domain is not None
|
||||
and not isinstance(domain, (str, int, float, sample.Domain))
|
||||
):
|
||||
# not domain or hashable
|
||||
# get rid of list type for hierarchical search space.
|
||||
|
||||
@@ -207,7 +207,7 @@ class ChampionFrontierSearcher(BaseSearcher):
|
||||
hyperparameter_config_groups.append(partial_new_configs)
|
||||
# does not have searcher_trial_ids
|
||||
searcher_trial_ids_groups.append([])
|
||||
elif isinstance(config_domain, Float) or isinstance(config_domain, Categorical):
|
||||
elif isinstance(config_domain, (Float, Categorical)):
|
||||
# otherwise we need to deal with them in group
|
||||
nonpoly_config[k] = v
|
||||
if k not in self._space_of_nonpoly_hp:
|
||||
@@ -319,7 +319,7 @@ class ChampionFrontierSearcher(BaseSearcher):
|
||||
candidate_configs = [set(seed_interactions) | set(item) for item in space]
|
||||
final_candidate_configs = []
|
||||
for c in candidate_configs:
|
||||
new_c = set([e for e in c if len(e) > 1])
|
||||
new_c = {e for e in c if len(e) > 1}
|
||||
final_candidate_configs.append(new_c)
|
||||
return final_candidate_configs
|
||||
|
||||
|
||||
@@ -25,6 +25,31 @@ from .flow2 import FLOW2
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _recursive_dict_update(target: Dict, source: Dict) -> None:
|
||||
"""Recursively update target dictionary with source dictionary.
|
||||
|
||||
Unlike dict.update(), this function merges nested dictionaries instead of
|
||||
replacing them entirely. This is crucial for configurations with nested
|
||||
structures (e.g., XGBoost params).
|
||||
|
||||
Args:
|
||||
target: The dictionary to be updated (modified in place).
|
||||
source: The dictionary containing values to merge into target.
|
||||
|
||||
Example:
|
||||
>>> target = {'params': {'eta': 0.1, 'max_depth': 3}}
|
||||
>>> source = {'params': {'verbosity': 0}}
|
||||
>>> _recursive_dict_update(target, source)
|
||||
>>> target
|
||||
{'params': {'eta': 0.1, 'max_depth': 3, 'verbosity': 0}}
|
||||
"""
|
||||
for key, value in source.items():
|
||||
if isinstance(value, dict) and key in target and isinstance(target[key], dict):
|
||||
_recursive_dict_update(target[key], value)
|
||||
else:
|
||||
target[key] = value
|
||||
|
||||
|
||||
class SearchThread:
|
||||
"""Class of global or local search thread."""
|
||||
|
||||
@@ -65,7 +90,7 @@ class SearchThread:
|
||||
try:
|
||||
config = self._search_alg.suggest(trial_id)
|
||||
if isinstance(self._search_alg._space, dict):
|
||||
config.update(self._const)
|
||||
_recursive_dict_update(config, self._const)
|
||||
else:
|
||||
# define by run
|
||||
config, self.space = unflatten_hierarchical(config, self._space)
|
||||
|
||||
@@ -35,6 +35,73 @@ from ..sample import (
|
||||
Quantized,
|
||||
Uniform,
|
||||
)
|
||||
|
||||
# If Ray is installed, flaml.tune may re-export Ray Tune sampling functions.
|
||||
# In that case, the search space contains Ray Tune Domain/Sampler objects,
|
||||
# which should be accepted by our Optuna search-space conversion.
|
||||
try:
|
||||
from ray import __version__ as _ray_version # type: ignore
|
||||
|
||||
if str(_ray_version).startswith("1."):
|
||||
from ray.tune.sample import ( # type: ignore
|
||||
Categorical as _RayCategorical,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
Domain as _RayDomain,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
Float as _RayFloat,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
Integer as _RayInteger,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
LogUniform as _RayLogUniform,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
Quantized as _RayQuantized,
|
||||
)
|
||||
from ray.tune.sample import (
|
||||
Uniform as _RayUniform,
|
||||
)
|
||||
else:
|
||||
from ray.tune.search.sample import ( # type: ignore
|
||||
Categorical as _RayCategorical,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
Domain as _RayDomain,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
Float as _RayFloat,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
Integer as _RayInteger,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
LogUniform as _RayLogUniform,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
Quantized as _RayQuantized,
|
||||
)
|
||||
from ray.tune.search.sample import (
|
||||
Uniform as _RayUniform,
|
||||
)
|
||||
|
||||
_FLOAT_TYPES = (Float, _RayFloat)
|
||||
_INTEGER_TYPES = (Integer, _RayInteger)
|
||||
_CATEGORICAL_TYPES = (Categorical, _RayCategorical)
|
||||
_DOMAIN_TYPES = (Domain, _RayDomain)
|
||||
_QUANTIZED_TYPES = (Quantized, _RayQuantized)
|
||||
_UNIFORM_TYPES = (Uniform, _RayUniform)
|
||||
_LOGUNIFORM_TYPES = (LogUniform, _RayLogUniform)
|
||||
except Exception: # pragma: no cover
|
||||
_FLOAT_TYPES = (Float,)
|
||||
_INTEGER_TYPES = (Integer,)
|
||||
_CATEGORICAL_TYPES = (Categorical,)
|
||||
_DOMAIN_TYPES = (Domain,)
|
||||
_QUANTIZED_TYPES = (Quantized,)
|
||||
_UNIFORM_TYPES = (Uniform,)
|
||||
_LOGUNIFORM_TYPES = (LogUniform,)
|
||||
from ..trial import flatten_dict, unflatten_dict
|
||||
from .variant_generator import parse_spec_vars
|
||||
|
||||
@@ -191,7 +258,7 @@ class ConcurrencyLimiter(Searcher):
|
||||
self.batch = batch
|
||||
self.live_trials = set()
|
||||
self.cached_results = {}
|
||||
super(ConcurrencyLimiter, self).__init__(metric=self.searcher.metric, mode=self.searcher.mode)
|
||||
super().__init__(metric=self.searcher.metric, mode=self.searcher.mode)
|
||||
|
||||
def suggest(self, trial_id: str) -> Optional[Dict]:
|
||||
assert trial_id not in self.live_trials, f"Trial ID {trial_id} must be unique: already found in set."
|
||||
@@ -285,25 +352,21 @@ def validate_warmstart(
|
||||
"""
|
||||
if points_to_evaluate:
|
||||
if not isinstance(points_to_evaluate, list):
|
||||
raise TypeError("points_to_evaluate expected to be a list, got {}.".format(type(points_to_evaluate)))
|
||||
raise TypeError(f"points_to_evaluate expected to be a list, got {type(points_to_evaluate)}.")
|
||||
for point in points_to_evaluate:
|
||||
if not isinstance(point, (dict, list)):
|
||||
raise TypeError(f"points_to_evaluate expected to include list or dict, " f"got {point}.")
|
||||
|
||||
if validate_point_name_lengths and (not len(point) == len(parameter_names)):
|
||||
raise ValueError(
|
||||
"Dim of point {}".format(point)
|
||||
+ " and parameter_names {}".format(parameter_names)
|
||||
+ " do not match."
|
||||
)
|
||||
raise ValueError(f"Dim of point {point}" + f" and parameter_names {parameter_names}" + " do not match.")
|
||||
|
||||
if points_to_evaluate and evaluated_rewards:
|
||||
if not isinstance(evaluated_rewards, list):
|
||||
raise TypeError("evaluated_rewards expected to be a list, got {}.".format(type(evaluated_rewards)))
|
||||
raise TypeError(f"evaluated_rewards expected to be a list, got {type(evaluated_rewards)}.")
|
||||
if not len(evaluated_rewards) == len(points_to_evaluate):
|
||||
raise ValueError(
|
||||
"Dim of evaluated_rewards {}".format(evaluated_rewards)
|
||||
+ " and points_to_evaluate {}".format(points_to_evaluate)
|
||||
f"Dim of evaluated_rewards {evaluated_rewards}"
|
||||
+ f" and points_to_evaluate {points_to_evaluate}"
|
||||
+ " do not match."
|
||||
)
|
||||
|
||||
@@ -547,7 +610,7 @@ class OptunaSearch(Searcher):
|
||||
evaluated_rewards: Optional[List] = None,
|
||||
):
|
||||
assert ot is not None, "Optuna must be installed! Run `pip install optuna`."
|
||||
super(OptunaSearch, self).__init__(metric=metric, mode=mode)
|
||||
super().__init__(metric=metric, mode=mode)
|
||||
|
||||
if isinstance(space, dict) and space:
|
||||
resolved_vars, domain_vars, grid_vars = parse_spec_vars(space)
|
||||
@@ -854,19 +917,22 @@ class OptunaSearch(Searcher):
|
||||
def resolve_value(domain: Domain) -> ot.distributions.BaseDistribution:
|
||||
quantize = None
|
||||
|
||||
sampler = domain.get_sampler()
|
||||
if isinstance(sampler, Quantized):
|
||||
# Ray Tune Domains and FLAML Domains both provide get_sampler(), but
|
||||
# fall back to the .sampler attribute for robustness.
|
||||
sampler = domain.get_sampler() if hasattr(domain, "get_sampler") else getattr(domain, "sampler", None)
|
||||
|
||||
if isinstance(sampler, _QUANTIZED_TYPES) or type(sampler).__name__ == "Quantized":
|
||||
quantize = sampler.q
|
||||
sampler = sampler.sampler
|
||||
if isinstance(sampler, LogUniform):
|
||||
sampler = getattr(sampler, "sampler", None) or sampler.get_sampler()
|
||||
if isinstance(sampler, _LOGUNIFORM_TYPES) or type(sampler).__name__ == "LogUniform":
|
||||
logger.warning(
|
||||
"Optuna does not handle quantization in loguniform "
|
||||
"sampling. The parameter will be passed but it will "
|
||||
"probably be ignored."
|
||||
)
|
||||
|
||||
if isinstance(domain, Float):
|
||||
if isinstance(sampler, LogUniform):
|
||||
if isinstance(domain, _FLOAT_TYPES) or type(domain).__name__ == "Float":
|
||||
if isinstance(sampler, _LOGUNIFORM_TYPES) or type(sampler).__name__ == "LogUniform":
|
||||
if quantize:
|
||||
logger.warning(
|
||||
"Optuna does not support both quantization and "
|
||||
@@ -874,17 +940,17 @@ class OptunaSearch(Searcher):
|
||||
)
|
||||
return ot.distributions.LogUniformDistribution(domain.lower, domain.upper)
|
||||
|
||||
elif isinstance(sampler, Uniform):
|
||||
elif isinstance(sampler, _UNIFORM_TYPES) or type(sampler).__name__ == "Uniform":
|
||||
if quantize:
|
||||
return ot.distributions.DiscreteUniformDistribution(domain.lower, domain.upper, quantize)
|
||||
return ot.distributions.UniformDistribution(domain.lower, domain.upper)
|
||||
|
||||
elif isinstance(domain, Integer):
|
||||
if isinstance(sampler, LogUniform):
|
||||
elif isinstance(domain, _INTEGER_TYPES) or type(domain).__name__ == "Integer":
|
||||
if isinstance(sampler, _LOGUNIFORM_TYPES) or type(sampler).__name__ == "LogUniform":
|
||||
# ``step`` argument Deprecated in v2.0.0. ``step`` argument should be 1 in Log Distribution
|
||||
# The removal of this feature is currently scheduled for v4.0.0,
|
||||
return ot.distributions.IntLogUniformDistribution(domain.lower, domain.upper - 1, step=1)
|
||||
elif isinstance(sampler, Uniform):
|
||||
elif isinstance(sampler, _UNIFORM_TYPES) or type(sampler).__name__ == "Uniform":
|
||||
# Upper bound should be inclusive for quantization and
|
||||
# exclusive otherwise
|
||||
return ot.distributions.IntUniformDistribution(
|
||||
@@ -892,16 +958,16 @@ class OptunaSearch(Searcher):
|
||||
domain.upper - int(bool(not quantize)),
|
||||
step=quantize or 1,
|
||||
)
|
||||
elif isinstance(domain, Categorical):
|
||||
if isinstance(sampler, Uniform):
|
||||
elif isinstance(domain, _CATEGORICAL_TYPES) or type(domain).__name__ == "Categorical":
|
||||
if isinstance(sampler, _UNIFORM_TYPES) or type(sampler).__name__ == "Uniform":
|
||||
return ot.distributions.CategoricalDistribution(domain.categories)
|
||||
|
||||
raise ValueError(
|
||||
"Optuna search does not support parameters of type "
|
||||
"`{}` with samplers of type `{}`".format(type(domain).__name__, type(domain.sampler).__name__)
|
||||
"`{}` with samplers of type `{}`".format(type(domain).__name__, type(sampler).__name__)
|
||||
)
|
||||
|
||||
# Parameter name is e.g. "a/b/c" for nested dicts
|
||||
values = {"/".join(path): resolve_value(domain) for path, domain in domain_vars}
|
||||
|
||||
return values
|
||||
return values
|
||||
|
||||
@@ -252,7 +252,7 @@ def _try_resolve(v) -> Tuple[bool, Any]:
|
||||
# Grid search values
|
||||
grid_values = v["grid_search"]
|
||||
if not isinstance(grid_values, list):
|
||||
raise TuneError("Grid search expected list of values, got: {}".format(grid_values))
|
||||
raise TuneError(f"Grid search expected list of values, got: {grid_values}")
|
||||
return False, Categorical(grid_values).grid()
|
||||
return True, v
|
||||
|
||||
@@ -302,13 +302,13 @@ def has_unresolved_values(spec: Dict) -> bool:
|
||||
|
||||
class _UnresolvedAccessGuard(dict):
|
||||
def __init__(self, *args, **kwds):
|
||||
super(_UnresolvedAccessGuard, self).__init__(*args, **kwds)
|
||||
super().__init__(*args, **kwds)
|
||||
self.__dict__ = self
|
||||
|
||||
def __getattribute__(self, item):
|
||||
value = dict.__getattribute__(self, item)
|
||||
if not _is_resolved(value):
|
||||
raise RecursiveDependencyError("`{}` recursively depends on {}".format(item, value))
|
||||
raise RecursiveDependencyError(f"`{item}` recursively depends on {value}")
|
||||
elif isinstance(value, dict):
|
||||
return _UnresolvedAccessGuard(value)
|
||||
else:
|
||||
|
||||
@@ -261,7 +261,7 @@ def add_cost_to_space(space: Dict, low_cost_point: Dict, choice_cost: Dict):
|
||||
low_cost[i] = point
|
||||
if len(low_cost) > len(domain.categories):
|
||||
if domain.ordered:
|
||||
low_cost[-1] = int(np.where(ind == low_cost[-1])[0])
|
||||
low_cost[-1] = int(np.where(ind == low_cost[-1])[0].item())
|
||||
domain.low_cost_point = low_cost[-1]
|
||||
return
|
||||
if low_cost:
|
||||
|
||||
@@ -162,6 +162,10 @@ def broadcast_code(custom_code="", file_name="mylearner"):
|
||||
assert isinstance(MyLargeLGBM(), LGBMEstimator)
|
||||
```
|
||||
"""
|
||||
# Check if Spark is available
|
||||
spark_available, _ = check_spark()
|
||||
|
||||
# Write to local driver file system
|
||||
flaml_path = os.path.dirname(os.path.abspath(__file__))
|
||||
custom_code = textwrap.dedent(custom_code)
|
||||
custom_path = os.path.join(flaml_path, file_name + ".py")
|
||||
@@ -169,6 +173,24 @@ def broadcast_code(custom_code="", file_name="mylearner"):
|
||||
with open(custom_path, "w") as f:
|
||||
f.write(custom_code)
|
||||
|
||||
# If using Spark, broadcast the code content to executors
|
||||
if spark_available:
|
||||
spark = SparkSession.builder.getOrCreate()
|
||||
bc_code = spark.sparkContext.broadcast(custom_code)
|
||||
|
||||
# Execute a job to ensure the code is distributed to all executors
|
||||
def _write_code(bc):
|
||||
code = bc.value
|
||||
import os
|
||||
|
||||
module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_name + ".py")
|
||||
os.makedirs(os.path.dirname(module_path), exist_ok=True)
|
||||
with open(module_path, "w") as f:
|
||||
f.write(code)
|
||||
return True
|
||||
|
||||
spark.sparkContext.parallelize(range(1)).map(lambda _: _write_code(bc_code)).collect()
|
||||
|
||||
return custom_path
|
||||
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class Trial:
|
||||
}
|
||||
self.metric_n_steps[metric] = {}
|
||||
for n in self.n_steps:
|
||||
key = "last-{:d}-avg".format(n)
|
||||
key = f"last-{n:d}-avg"
|
||||
self.metric_analysis[metric][key] = value
|
||||
# Store n as string for correct restore.
|
||||
self.metric_n_steps[metric][str(n)] = deque([value], maxlen=n)
|
||||
@@ -124,7 +124,7 @@ class Trial:
|
||||
self.metric_analysis[metric]["last"] = value
|
||||
|
||||
for n in self.n_steps:
|
||||
key = "last-{:d}-avg".format(n)
|
||||
key = f"last-{n:d}-avg"
|
||||
self.metric_n_steps[metric][str(n)].append(value)
|
||||
self.metric_analysis[metric][key] = sum(self.metric_n_steps[metric][str(n)]) / len(
|
||||
self.metric_n_steps[metric][str(n)]
|
||||
|
||||
@@ -21,16 +21,26 @@ except (ImportError, AssertionError):
|
||||
from .analysis import ExperimentAnalysis as EA
|
||||
else:
|
||||
ray_available = True
|
||||
|
||||
import logging
|
||||
|
||||
from flaml.tune.spark.utils import PySparkOvertimeMonitor, check_spark
|
||||
|
||||
from .logger import logger, logger_formatter
|
||||
from .result import DEFAULT_METRIC
|
||||
from .trial import Trial
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.propagate = False
|
||||
try:
|
||||
import mlflow
|
||||
except ImportError:
|
||||
mlflow = None
|
||||
try:
|
||||
from flaml.fabric.mlflow import MLflowIntegration, is_autolog_enabled
|
||||
|
||||
internal_mlflow = True
|
||||
except ImportError:
|
||||
internal_mlflow = False
|
||||
|
||||
|
||||
_use_ray = True
|
||||
_runner = None
|
||||
_verbose = 0
|
||||
@@ -44,6 +54,7 @@ class ExperimentAnalysis(EA):
|
||||
"""Class for storing the experiment results."""
|
||||
|
||||
def __init__(self, trials, metric, mode, lexico_objectives=None):
|
||||
self.best_run_id = None
|
||||
try:
|
||||
super().__init__(self, None, trials, metric, mode)
|
||||
self.lexico_objectives = lexico_objectives
|
||||
@@ -128,6 +139,16 @@ class ExperimentAnalysis(EA):
|
||||
else:
|
||||
return self.best_trial.last_result
|
||||
|
||||
@property
|
||||
def best_iteration(self) -> List[str]:
|
||||
"""Help better navigate"""
|
||||
best_trial = self.best_trial
|
||||
best_trial_id = best_trial.trial_id
|
||||
for i, trial in enumerate(self.trials):
|
||||
if trial.trial_id == best_trial_id:
|
||||
return i
|
||||
return None
|
||||
|
||||
|
||||
def report(_metric=None, **kwargs):
|
||||
"""A function called by the HPO application to report final or intermediate
|
||||
@@ -174,9 +195,16 @@ def report(_metric=None, **kwargs):
|
||||
global _training_iteration
|
||||
if _use_ray:
|
||||
try:
|
||||
from ray import tune
|
||||
from ray import __version__ as ray_version
|
||||
|
||||
return tune.report(_metric, **kwargs)
|
||||
if ray_version.startswith("1."):
|
||||
from ray import tune
|
||||
|
||||
return tune.report(_metric, **kwargs)
|
||||
else: # ray>=2
|
||||
from ray.air import session
|
||||
|
||||
return session.report(metrics={"metric": _metric, **kwargs})
|
||||
except ImportError:
|
||||
# calling tune.report() outside tune.run()
|
||||
return
|
||||
@@ -234,6 +262,11 @@ def run(
|
||||
lexico_objectives: Optional[dict] = None,
|
||||
force_cancel: Optional[bool] = False,
|
||||
n_concurrent_trials: Optional[int] = 0,
|
||||
mlflow_exp_name: Optional[str] = None,
|
||||
automl_info: Optional[Tuple[float]] = None,
|
||||
extra_tag: Optional[dict] = None,
|
||||
cost_attr: Optional[str] = "auto",
|
||||
cost_budget: Optional[float] = None,
|
||||
**ray_args,
|
||||
):
|
||||
"""The function-based way of performing HPO.
|
||||
@@ -424,6 +457,10 @@ def run(
|
||||
}
|
||||
```
|
||||
force_cancel: boolean, default=False | Whether to forcely cancel the PySpark job if overtime.
|
||||
mlflow_exp_name: str, default=None | The name of the mlflow experiment. This should be specified if
|
||||
enable mlflow autologging on Spark. Otherwise it will log all the results into the experiment of the
|
||||
same name as the basename of main entry file.
|
||||
automl_info: tuple, default=None | The information of the automl run. It should be a tuple of (mlflow_log_latency,).
|
||||
n_concurrent_trials: int, default=0 | The number of concurrent trials when perform hyperparameter
|
||||
tuning with Spark. Only valid when use_spark=True and spark is required:
|
||||
`pip install flaml[spark]`. Please check
|
||||
@@ -431,6 +468,13 @@ def run(
|
||||
for more details about installing Spark. When tune.run() is called from AutoML, it will be
|
||||
overwritten by the value of `n_concurrent_trials` in AutoML. When <= 0, the concurrent trials
|
||||
will be set to the number of executors.
|
||||
extra_tag: dict, default=None | Extra tags to be added to the mlflow runs created by autologging.
|
||||
cost_attr: None or str to specify the attribute to evaluate the cost of different trials.
|
||||
Default is "auto", which means that we will automatically choose the cost attribute to use (depending
|
||||
on the nature of the resource budget). When cost_attr is set to None, cost differences between different trials will be omitted
|
||||
in our search algorithm. When cost_attr is set to a str different from "auto" and "time_total_s",
|
||||
this cost_attr must be available in the result dict of the trial.
|
||||
cost_budget: A float of the cost budget. Only valid when cost_attr is a str different from "auto" and "time_total_s".
|
||||
**ray_args: keyword arguments to pass to ray.tune.run().
|
||||
Only valid when use_ray=True.
|
||||
"""
|
||||
@@ -438,10 +482,12 @@ def run(
|
||||
global _verbose
|
||||
global _running_trial
|
||||
global _training_iteration
|
||||
global internal_mlflow
|
||||
old_use_ray = _use_ray
|
||||
old_verbose = _verbose
|
||||
old_running_trial = _running_trial
|
||||
old_training_iteration = _training_iteration
|
||||
|
||||
if log_file_name:
|
||||
dir_name = os.path.dirname(log_file_name)
|
||||
if dir_name:
|
||||
@@ -473,10 +519,6 @@ def run(
|
||||
elif not logger.hasHandlers():
|
||||
# Add the console handler.
|
||||
_ch = logging.StreamHandler(stream=sys.stdout)
|
||||
logger_formatter = logging.Formatter(
|
||||
"[%(name)s: %(asctime)s] {%(lineno)d} %(levelname)s - %(message)s",
|
||||
"%m-%d %H:%M:%S",
|
||||
)
|
||||
_ch.setFormatter(logger_formatter)
|
||||
logger.addHandler(_ch)
|
||||
if verbose <= 2:
|
||||
@@ -486,6 +528,13 @@ def run(
|
||||
else:
|
||||
logger.setLevel(logging.CRITICAL)
|
||||
|
||||
if internal_mlflow and not automl_info and (mlflow.active_run() or is_autolog_enabled()):
|
||||
mlflow_integration = MLflowIntegration("tune", mlflow_exp_name, extra_tag)
|
||||
evaluation_function = mlflow_integration.wrap_evaluation_function(evaluation_function)
|
||||
_internal_mlflow = not automl_info # True if mlflow_integration will be used for logging
|
||||
else:
|
||||
_internal_mlflow = False
|
||||
|
||||
from .searcher.blendsearch import CFO, BlendSearch, RandomSearch
|
||||
|
||||
if lexico_objectives is not None:
|
||||
@@ -531,7 +580,7 @@ def run(
|
||||
import optuna as _
|
||||
|
||||
SearchAlgorithm = BlendSearch
|
||||
logger.info("Using search algorithm {}.".format(SearchAlgorithm.__name__))
|
||||
logger.info(f"Using search algorithm {SearchAlgorithm.__name__}.")
|
||||
except ImportError:
|
||||
if search_alg == "BlendSearch":
|
||||
raise ValueError("To use BlendSearch, run: pip install flaml[blendsearch]")
|
||||
@@ -540,7 +589,7 @@ def run(
|
||||
logger.warning("Using CFO for search. To use BlendSearch, run: pip install flaml[blendsearch]")
|
||||
else:
|
||||
SearchAlgorithm = locals()[search_alg]
|
||||
logger.info("Using search algorithm {}.".format(SearchAlgorithm.__name__))
|
||||
logger.info(f"Using search algorithm {SearchAlgorithm.__name__}.")
|
||||
metric = metric or DEFAULT_METRIC
|
||||
search_alg = SearchAlgorithm(
|
||||
metric=metric,
|
||||
@@ -560,6 +609,8 @@ def run(
|
||||
metric_constraints=metric_constraints,
|
||||
use_incumbent_result_in_evaluation=use_incumbent_result_in_evaluation,
|
||||
lexico_objectives=lexico_objectives,
|
||||
cost_attr=cost_attr,
|
||||
cost_budget=cost_budget,
|
||||
)
|
||||
else:
|
||||
if metric is None or mode is None:
|
||||
@@ -695,10 +746,16 @@ def run(
|
||||
max_concurrent = max(1, search_alg.max_concurrent)
|
||||
else:
|
||||
max_concurrent = max(1, max_spark_parallelism)
|
||||
passed_in_n_concurrent_trials = max(n_concurrent_trials, max_concurrent)
|
||||
n_concurrent_trials = min(
|
||||
n_concurrent_trials if n_concurrent_trials > 0 else num_executors,
|
||||
max_concurrent,
|
||||
)
|
||||
if n_concurrent_trials < passed_in_n_concurrent_trials:
|
||||
logger.warning(
|
||||
f"The actual concurrent trials is {n_concurrent_trials}. You can set the environment "
|
||||
f"variable `FLAML_MAX_CONCURRENT` to '{passed_in_n_concurrent_trials}' to override the detected num of executors."
|
||||
)
|
||||
with parallel_backend("spark"):
|
||||
with Parallel(n_jobs=n_concurrent_trials, verbose=max(0, (verbose - 1) * 50)) as parallel:
|
||||
try:
|
||||
@@ -713,11 +770,15 @@ def run(
|
||||
time_budget_s = np.inf
|
||||
num_failures = 0
|
||||
upperbound_num_failures = (len(evaluated_rewards) if evaluated_rewards else 0) + max_failure
|
||||
logger.debug(f"automl_info: {automl_info}")
|
||||
while (
|
||||
time.time() - time_start < time_budget_s
|
||||
and (num_samples < 0 or num_trials < num_samples)
|
||||
and num_failures < upperbound_num_failures
|
||||
):
|
||||
if automl_info and automl_info[1] == "all" and automl_info[0] > 0 and time_budget_s < np.inf:
|
||||
time_budget_s -= automl_info[0] * n_concurrent_trials
|
||||
logger.debug(f"Remaining time budget with mlflow log latency: {time_budget_s} seconds.")
|
||||
while len(_runner.running_trials) < n_concurrent_trials:
|
||||
# suggest trials for spark
|
||||
trial_next = _runner.step()
|
||||
@@ -741,15 +802,26 @@ def run(
|
||||
)
|
||||
results = None
|
||||
with PySparkOvertimeMonitor(time_start, time_budget_s, force_cancel, parallel=parallel):
|
||||
results = parallel(
|
||||
delayed(evaluation_function)(trial_to_run.config) for trial_to_run in trials_to_run
|
||||
)
|
||||
try:
|
||||
results = parallel(
|
||||
delayed(evaluation_function)(trial_to_run.config) for trial_to_run in trials_to_run
|
||||
)
|
||||
except RuntimeError as e:
|
||||
logger.warning(f"RuntimeError: {e}")
|
||||
results = None
|
||||
logger.info(
|
||||
"Encountered RuntimeError. Waiting 10 seconds for Spark cluster to recover before retrying."
|
||||
)
|
||||
time.sleep(10)
|
||||
# results = [evaluation_function(trial_to_run.config) for trial_to_run in trials_to_run]
|
||||
while results:
|
||||
result = results.pop(0)
|
||||
trial_to_run = trials_to_run[0]
|
||||
_runner.running_trial = trial_to_run
|
||||
if result is not None:
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.record_trial(result, trial_to_run, metric)
|
||||
|
||||
if isinstance(result, dict):
|
||||
if result:
|
||||
logger.info(f"Brief result: {result}")
|
||||
@@ -758,7 +830,7 @@ def run(
|
||||
# When the result returned is an empty dict, set the trial status to error
|
||||
trial_to_run.set_status(Trial.ERROR)
|
||||
else:
|
||||
logger.info("Brief result: {}".format({metric: result}))
|
||||
logger.info("Brief result: {metric: result}")
|
||||
report(_metric=result)
|
||||
_runner.stop_trial(trial_to_run)
|
||||
num_failures = 0
|
||||
@@ -768,6 +840,20 @@ def run(
|
||||
mode=mode,
|
||||
lexico_objectives=lexico_objectives,
|
||||
)
|
||||
analysis.search_space = config
|
||||
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.log_tune(analysis, metric)
|
||||
# try:
|
||||
# _best_config = analysis.best_config
|
||||
# except Exception:
|
||||
# _best_config = None
|
||||
# if _best_config:
|
||||
# parallel(
|
||||
# delayed(mlflow_integration.retrain)(evaluation_function, analysis.best_config)
|
||||
# for dummy in [0]
|
||||
# )
|
||||
|
||||
return analysis
|
||||
finally:
|
||||
# recover the global variables in case of nested run
|
||||
@@ -779,6 +865,8 @@ def run(
|
||||
_runner = old_runner
|
||||
logger.handlers = old_handlers
|
||||
logger.setLevel(old_level)
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.adopt_children()
|
||||
|
||||
# simple sequential run without using tune.run() from ray
|
||||
time_start = time.time()
|
||||
@@ -812,7 +900,11 @@ def run(
|
||||
result = None
|
||||
with PySparkOvertimeMonitor(time_start, time_budget_s, force_cancel):
|
||||
result = evaluation_function(trial_to_run.config)
|
||||
logger.debug(f"result in tune: {trial_to_run}, {result}")
|
||||
if result is not None:
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.record_trial(result, trial_to_run, metric)
|
||||
|
||||
if isinstance(result, dict):
|
||||
if result:
|
||||
report(**result)
|
||||
@@ -838,6 +930,19 @@ def run(
|
||||
mode=mode,
|
||||
lexico_objectives=lexico_objectives,
|
||||
)
|
||||
analysis.search_space = config
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.log_tune(analysis, metric)
|
||||
if analysis.best_run_id is not None:
|
||||
logger.info(f"Best MLflow run name: {analysis.best_run_name}")
|
||||
logger.info(f"Best MLflow run id: {analysis.best_run_id}")
|
||||
# try:
|
||||
# _best_config = analysis.best_config
|
||||
# except Exception:
|
||||
# _best_config = None
|
||||
# if _best_config:
|
||||
# mlflow_integration.retrain(evaluation_function, analysis.best_config)
|
||||
|
||||
return analysis
|
||||
finally:
|
||||
# recover the global variables in case of nested run
|
||||
@@ -849,6 +954,8 @@ def run(
|
||||
_runner = old_runner
|
||||
logger.handlers = old_handlers
|
||||
logger.setLevel(old_level)
|
||||
if _internal_mlflow:
|
||||
mlflow_integration.adopt_children()
|
||||
|
||||
|
||||
class Tuner:
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.2.0"
|
||||
__version__ = "2.5.0"
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
"import datasets\n",
|
||||
"\n",
|
||||
"seed = 41\n",
|
||||
"data = datasets.load_dataset(\"competition_math\")\n",
|
||||
"data = datasets.load_dataset(\"competition_math\", trust_remote_code=True)\n",
|
||||
"train_data = data[\"train\"].shuffle(seed=seed)\n",
|
||||
"test_data = data[\"test\"].shuffle(seed=seed)\n",
|
||||
"n_tune_data = 20\n",
|
||||
@@ -390,7 +390,7 @@
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[32m[I 2023-08-01 22:38:01,549]\u001b[0m A new study created in memory with name: optuna\u001b[0m\n"
|
||||
"\u001B[32m[I 2023-08-01 22:38:01,549]\u001B[0m A new study created in memory with name: optuna\u001B[0m\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
"import datasets\n",
|
||||
"\n",
|
||||
"seed = 41\n",
|
||||
"data = datasets.load_dataset(\"openai_humaneval\")[\"test\"].shuffle(seed=seed)\n",
|
||||
"data = datasets.load_dataset(\"openai_humaneval\", trust_remote_code=True)[\"test\"].shuffle(seed=seed)\n",
|
||||
"n_tune_data = 20\n",
|
||||
"tune_data = [\n",
|
||||
" {\n",
|
||||
@@ -444,8 +444,8 @@
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[32m[I 2023-07-30 04:19:08,150]\u001b[0m A new study created in memory with name: optuna\u001b[0m\n",
|
||||
"\u001b[32m[I 2023-07-30 04:19:08,153]\u001b[0m A new study created in memory with name: optuna\u001b[0m\n"
|
||||
"\u001B[32m[I 2023-07-30 04:19:08,150]\u001B[0m A new study created in memory with name: optuna\u001B[0m\n",
|
||||
"\u001B[32m[I 2023-07-30 04:19:08,153]\u001B[0m A new study created in memory with name: optuna\u001B[0m\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
"import datasets\n",
|
||||
"\n",
|
||||
"seed = 41\n",
|
||||
"data = datasets.load_dataset(\"openai_humaneval\")[\"test\"].shuffle(seed=seed)\n",
|
||||
"data = datasets.load_dataset(\"openai_humaneval\", trust_remote_code=True)[\"test\"].shuffle(seed=seed)\n",
|
||||
"data = data.select(range(len(data))).rename_column(\"prompt\", \"definition\").remove_columns([\"task_id\", \"canonical_solution\"])"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"import datasets\n",
|
||||
"\n",
|
||||
"seed = 41\n",
|
||||
"data = datasets.load_dataset(\"competition_math\")\n",
|
||||
"data = datasets.load_dataset(\"competition_math\", trust_remote_code=True)\n",
|
||||
"train_data = data[\"train\"].shuffle(seed=seed)\n",
|
||||
"test_data = data[\"test\"].shuffle(seed=seed)\n",
|
||||
"n_tune_data = 20\n",
|
||||
|
||||
@@ -112,9 +112,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"raw_dataset = datasets.load_dataset(\"glue\", TASK)"
|
||||
]
|
||||
"source": "raw_dataset = datasets.load_dataset(\"glue\", TASK, trust_remote_code=True)"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -425,9 +423,7 @@
|
||||
"execution_count": 14,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"metric = datasets.load_metric(\"glue\", TASK)"
|
||||
]
|
||||
"source": "metric = datasets.load_metric(\"glue\", TASK, trust_remote_code=True)"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@@ -646,7 +642,7 @@
|
||||
"def train_distilbert(config: dict):\n",
|
||||
"\n",
|
||||
" # Load CoLA dataset and apply tokenizer\n",
|
||||
" cola_raw = datasets.load_dataset(\"glue\", TASK)\n",
|
||||
" cola_raw = datasets.load_dataset(\"glue\", TASK, trust_remote_code=True)\n",
|
||||
" cola_encoded = cola_raw.map(tokenize, batched=True)\n",
|
||||
" train_dataset, eval_dataset = cola_encoded[\"train\"], cola_encoded[\"validation\"]\n",
|
||||
"\n",
|
||||
@@ -654,7 +650,7 @@
|
||||
" MODEL_CHECKPOINT, num_labels=NUM_LABELS\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" metric = datasets.load_metric(\"glue\", TASK)\n",
|
||||
" metric = datasets.load_metric(\"glue\", TASK, trust_remote_code=True)\n",
|
||||
" def compute_metrics(eval_pred):\n",
|
||||
" predictions, labels = eval_pred\n",
|
||||
" predictions = np.argmax(predictions, axis=1)\n",
|
||||
@@ -847,7 +843,7 @@
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m Reusing dataset glue (/home/ec2-user/.cache/huggingface/datasets/glue/cola/1.0.0/7c99657241149a24692c402a5c3f34d4c9f1df5ac2e4c3759fadea38f6cb29c4)\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m Reusing dataset glue (/home/ec2-user/.cache/huggingface/datasets/glue/cola/1.0.0/7c99657241149a24692c402a5c3f34d4c9f1df5ac2e4c3759fadea38f6cb29c4)\n",
|
||||
" 0%| | 0/9 [00:00<?, ?ba/s]\n",
|
||||
" 22%|██▏ | 2/9 [00:00<00:00, 19.41ba/s]\n",
|
||||
" 56%|█████▌ | 5/9 [00:00<00:00, 20.98ba/s]\n",
|
||||
@@ -856,25 +852,25 @@
|
||||
"100%|██████████| 2/2 [00:00<00:00, 42.79ba/s]\n",
|
||||
" 0%| | 0/2 [00:00<?, ?ba/s]\n",
|
||||
"100%|██████████| 2/2 [00:00<00:00, 41.48ba/s]\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m Some weights of the model checkpoint at distilbert-base-uncased were not used when initializing DistilBertForSequenceClassification: ['vocab_transform.weight', 'vocab_transform.bias', 'vocab_layer_norm.weight', 'vocab_layer_norm.bias', 'vocab_projector.weight', 'vocab_projector.bias']\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m - This IS expected if you are initializing DistilBertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m - This IS NOT expected if you are initializing DistilBertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m Some weights of DistilBertForSequenceClassification were not initialized from the model checkpoint at distilbert-base-uncased and are newly initialized: ['pre_classifier.weight', 'pre_classifier.bias', 'classifier.weight', 'classifier.bias']\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m Some weights of the model checkpoint at distilbert-base-uncased were not used when initializing DistilBertForSequenceClassification: ['vocab_transform.weight', 'vocab_transform.bias', 'vocab_layer_norm.weight', 'vocab_layer_norm.bias', 'vocab_projector.weight', 'vocab_projector.bias']\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m - This IS expected if you are initializing DistilBertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m - This IS NOT expected if you are initializing DistilBertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m Some weights of DistilBertForSequenceClassification were not initialized from the model checkpoint at distilbert-base-uncased and are newly initialized: ['pre_classifier.weight', 'pre_classifier.bias', 'classifier.weight', 'classifier.bias']\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m To disable this warning, you can either:\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m \t- Avoid using `tokenizers` before the fork if possible\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m \t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m To disable this warning, you can either:\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m \t- Avoid using `tokenizers` before the fork if possible\n",
|
||||
"\u001b[2m\u001b[36m(pid=11344)\u001b[0m \t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n"
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m To disable this warning, you can either:\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m \t- Avoid using `tokenizers` before the fork if possible\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m \t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m huggingface/tokenizers: The current process just got forked, after parallelism has already been used. Disabling parallelism to avoid deadlocks...\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m To disable this warning, you can either:\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m \t- Avoid using `tokenizers` before the fork if possible\n",
|
||||
"\u001B[2m\u001B[36m(pid=11344)\u001B[0m \t- Explicitly set the environment variable TOKENIZERS_PARALLELISM=(true | false)\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
license_file = "LICENSE"
|
||||
description-file = "README.md"
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = '-m "not conda"'
|
||||
markers = [
|
||||
|
||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
markers =
|
||||
spark: mark a test as requiring Spark
|
||||
90
setup.py
90
setup.py
@@ -4,7 +4,7 @@ import setuptools
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
with open("README.md", "r", encoding="UTF-8") as fh:
|
||||
with open("README.md", encoding="UTF-8") as fh:
|
||||
long_description = fh.read()
|
||||
|
||||
|
||||
@@ -51,58 +51,59 @@ setuptools.setup(
|
||||
"joblib<=1.3.2",
|
||||
],
|
||||
"test": [
|
||||
"numpy>=1.17,<2.0.0; python_version<'3.13'",
|
||||
"numpy>=1.17; python_version>='3.13'",
|
||||
"jupyter",
|
||||
"lightgbm>=2.3.1",
|
||||
"xgboost>=0.90,<2.0.0",
|
||||
"xgboost>=0.90,<2.0.0; python_version<'3.11'",
|
||||
"xgboost>=2.0.0; python_version>='3.11'",
|
||||
"scipy>=1.4.1",
|
||||
"pandas>=1.1.4",
|
||||
"scikit-learn>=1.0.0",
|
||||
"pandas>=1.1.4,<2.0.0; python_version<'3.10'",
|
||||
"pandas>=1.1.4; python_version>='3.10'",
|
||||
"scikit-learn>=1.2.0",
|
||||
"thop",
|
||||
"pytest>=6.1.1",
|
||||
"pytest-rerunfailures>=13.0",
|
||||
"coverage>=5.3",
|
||||
"pre-commit",
|
||||
"torch",
|
||||
"torchvision",
|
||||
"catboost>=0.26,<1.2; python_version<'3.11'",
|
||||
"catboost>=0.26; python_version>='3.11'",
|
||||
"catboost>=0.26",
|
||||
"rgf-python",
|
||||
"optuna>=2.8.0,<=3.6.1",
|
||||
"openml",
|
||||
"statsmodels>=0.12.2",
|
||||
"psutil==5.8.0",
|
||||
"psutil",
|
||||
"dataclasses",
|
||||
"transformers[torch]==4.26",
|
||||
"transformers[torch]",
|
||||
"datasets",
|
||||
"nltk",
|
||||
"evaluate",
|
||||
"nltk!=3.8.2", # 3.8.2 doesn't work with mlflow
|
||||
"rouge_score",
|
||||
"hcrystalball==0.1.10",
|
||||
"hcrystalball",
|
||||
"seqeval",
|
||||
"pytorch-forecasting>=0.9.0,<=0.10.1; python_version<'3.11'",
|
||||
"mlflow",
|
||||
"pyspark>=3.2.0",
|
||||
"pytorch-forecasting",
|
||||
"mlflow-skinny<=2.22.1", # Refer to https://mvnrepository.com/artifact/org.mlflow/mlflow-spark
|
||||
"joblibspark>=0.5.0",
|
||||
"joblib<=1.3.2",
|
||||
"nbconvert",
|
||||
"nbformat",
|
||||
"ipykernel",
|
||||
"pytorch-lightning<1.9.1", # test_forecast_panel
|
||||
"tensorboardX==2.6", # test_forecast_panel
|
||||
"requests<2.29.0", # https://github.com/docker/docker-py/issues/3113
|
||||
"pytorch-lightning", # test_forecast_panel
|
||||
"tensorboardX", # test_forecast_panel
|
||||
"requests", # https://github.com/docker/docker-py/issues/3113
|
||||
"packaging",
|
||||
"pydantic==1.10.9",
|
||||
"sympy",
|
||||
"wolframalpha",
|
||||
"dill", # a drop in replacement of pickle
|
||||
],
|
||||
"catboost": [
|
||||
"catboost>=0.26,<1.2; python_version<'3.11'",
|
||||
"catboost>=0.26,<=1.2.5; python_version>='3.11'",
|
||||
"catboost>=0.26",
|
||||
],
|
||||
"blendsearch": [
|
||||
"optuna>=2.8.0,<=3.6.1",
|
||||
"packaging",
|
||||
],
|
||||
"ray": [
|
||||
"ray[tune]~=1.13",
|
||||
"ray[tune]>=1.13,<2.5.0",
|
||||
],
|
||||
"azureml": [
|
||||
"azureml-mlflow",
|
||||
@@ -115,46 +116,35 @@ setuptools.setup(
|
||||
"scikit-learn",
|
||||
],
|
||||
"hf": [
|
||||
"transformers[torch]==4.26",
|
||||
"transformers[torch]>=4.26",
|
||||
"datasets",
|
||||
"nltk",
|
||||
"nltk<=3.8.1",
|
||||
"rouge_score",
|
||||
"seqeval",
|
||||
],
|
||||
"nlp": [ # for backward compatibility; hf is the new option name
|
||||
"transformers[torch]==4.26",
|
||||
"transformers[torch]>=4.26",
|
||||
"datasets",
|
||||
"nltk",
|
||||
"nltk<=3.8.1",
|
||||
"rouge_score",
|
||||
"seqeval",
|
||||
],
|
||||
"ts_forecast": [
|
||||
"holidays<0.14", # to prevent installation error for prophet
|
||||
"prophet>=1.0.1",
|
||||
"holidays",
|
||||
"prophet>=1.1.5",
|
||||
"statsmodels>=0.12.2",
|
||||
"hcrystalball==0.1.10",
|
||||
"hcrystalball>=0.1.10",
|
||||
],
|
||||
"forecast": [
|
||||
"holidays<0.14", # to prevent installation error for prophet
|
||||
"prophet>=1.0.1",
|
||||
"holidays",
|
||||
"prophet>=1.1.5",
|
||||
"statsmodels>=0.12.2",
|
||||
"hcrystalball==0.1.10",
|
||||
"pytorch-forecasting>=0.9.0",
|
||||
"pytorch-lightning==1.9.0",
|
||||
"tensorboardX==2.6",
|
||||
"hcrystalball>=0.1.10",
|
||||
"pytorch-forecasting>=0.10.4",
|
||||
"pytorch-lightning>=1.9.0",
|
||||
"tensorboardX>=2.6",
|
||||
],
|
||||
"benchmark": ["catboost>=0.26", "psutil==5.8.0", "xgboost==1.3.3", "pandas==1.1.4"],
|
||||
"openai": ["openai==0.27.8", "diskcache"],
|
||||
"autogen": ["openai==0.27.8", "diskcache", "termcolor"],
|
||||
"mathchat": ["openai==0.27.8", "diskcache", "termcolor", "sympy", "pydantic==1.10.9", "wolframalpha"],
|
||||
"retrievechat": [
|
||||
"openai==0.27.8",
|
||||
"diskcache",
|
||||
"termcolor",
|
||||
"chromadb",
|
||||
"tiktoken",
|
||||
"sentence_transformers",
|
||||
],
|
||||
"synapse": [
|
||||
"joblibspark>=0.5.0",
|
||||
"optuna>=2.8.0,<=3.6.1",
|
||||
@@ -163,9 +153,13 @@ setuptools.setup(
|
||||
"autozero": ["scikit-learn", "pandas", "packaging"],
|
||||
},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
# Specify the Python versions you support here.
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
python_requires=">=3.6",
|
||||
python_requires=">=3.10",
|
||||
)
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_tsp(human_input_mode="NEVER", max_consecutive_auto_reply=10):
|
||||
class TSPUserProxyAgent(UserProxyAgent):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
with open(f"{here}/tsp_prompt.txt", "r") as f:
|
||||
with open(f"{here}/tsp_prompt.txt") as f:
|
||||
self._prompt = f.read()
|
||||
|
||||
def generate_init_message(self, question) -> str:
|
||||
|
||||
@@ -187,7 +187,7 @@ def test_humaneval(num_samples=1):
|
||||
)
|
||||
|
||||
seed = 41
|
||||
data = datasets.load_dataset("openai_humaneval")["test"].shuffle(seed=seed)
|
||||
data = datasets.load_dataset("openai_humaneval", trust_remote_code=True)["test"].shuffle(seed=seed)
|
||||
n_tune_data = 20
|
||||
tune_data = [
|
||||
{
|
||||
@@ -334,7 +334,7 @@ def test_math(num_samples=-1):
|
||||
return
|
||||
|
||||
seed = 41
|
||||
data = datasets.load_dataset("competition_math")
|
||||
data = datasets.load_dataset("competition_math", trust_remote_code=True)
|
||||
train_data = data["train"].shuffle(seed=seed)
|
||||
test_data = data["test"].shuffle(seed=seed)
|
||||
n_tune_data = 20
|
||||
@@ -356,7 +356,7 @@ def test_math(num_samples=-1):
|
||||
]
|
||||
print(
|
||||
"max tokens in tuning data's canonical solutions",
|
||||
max([len(x["solution"].split()) for x in tune_data]),
|
||||
max(len(x["solution"].split()) for x in tune_data),
|
||||
)
|
||||
print(len(tune_data), len(test_data))
|
||||
# prompt template
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
from test.conftest import evaluate_cv_folds_with_underlying_model
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
import scipy.sparse
|
||||
from sklearn.datasets import load_breast_cancer
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.model_selection import (
|
||||
train_test_split,
|
||||
)
|
||||
|
||||
from flaml import AutoML, tune
|
||||
from flaml.automl.model import LGBMEstimator
|
||||
@@ -420,6 +424,122 @@ class TestClassification(unittest.TestCase):
|
||||
print(automl_experiment.best_estimator)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"estimator",
|
||||
[
|
||||
"catboost",
|
||||
"extra_tree",
|
||||
"histgb",
|
||||
"kneighbor",
|
||||
"lgbm",
|
||||
# "lrl1",
|
||||
"lrl2",
|
||||
"rf",
|
||||
"svc",
|
||||
"xgboost",
|
||||
"xgb_limitdepth",
|
||||
],
|
||||
)
|
||||
def test_reproducibility_of_classification_models(estimator: str):
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues where FLAML was providing an incorrect model - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1317
|
||||
In this test we take the best model which FLAML provided us, and then retrain and test it on the
|
||||
same folds, to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"time_budget": -1,
|
||||
"task": "classification",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": [estimator],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 10,
|
||||
"metric": "f1",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
}
|
||||
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
config = best_model.get_params()
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
|
||||
# Take the best model, and see if we can reproduce the best result
|
||||
reproduced_val_loss, metric_for_logging, train_time, pred_time = automl._state.task.evaluate_model_CV(
|
||||
config=config,
|
||||
estimator=best_model,
|
||||
X_train_all=automl._state.X_train_all,
|
||||
y_train_all=automl._state.y_train_all,
|
||||
budget=None,
|
||||
kf=automl._state.kf,
|
||||
eval_metric="f1",
|
||||
best_val_loss=None,
|
||||
cv_score_agg_func=None,
|
||||
log_training_metric=False,
|
||||
fit_kwargs=None,
|
||||
free_mem_ratio=0,
|
||||
)
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"estimator",
|
||||
[
|
||||
"catboost",
|
||||
"extra_tree",
|
||||
"histgb",
|
||||
"kneighbor",
|
||||
"lgbm",
|
||||
# "lrl1",
|
||||
"lrl2",
|
||||
"svc",
|
||||
"rf",
|
||||
"xgboost",
|
||||
"xgb_limitdepth",
|
||||
],
|
||||
)
|
||||
def test_reproducibility_of_underlying_classification_models(estimator: str):
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues where FLAML was providing an incorrect model - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1317
|
||||
FLAML defines FLAMLised models, which wrap around the underlying (SKLearn/XGBoost/CatBoost) model.
|
||||
Ideally, FLAMLised models should perform identically to the underlying model, when fitted
|
||||
to the same data, with no budget. This verifies that this is the case for classification models.
|
||||
In this test we take the best model which FLAML provided us, extract the underlying model,
|
||||
before retraining and testing it on the same folds - to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"time_budget": -1,
|
||||
"task": "classification",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": [estimator],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 10,
|
||||
"metric": "f1",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
}
|
||||
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
reproduced_val_loss_underlying_model = np.mean(
|
||||
evaluate_cv_folds_with_underlying_model(
|
||||
automl._state.X_train_all, automl._state.y_train_all, automl._state.kf, best_model.model, "classification"
|
||||
)
|
||||
)
|
||||
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss_underlying_model
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test = TestClassification()
|
||||
test.test_preprocess()
|
||||
|
||||
@@ -125,14 +125,12 @@ def test_metric_constraints_custom():
|
||||
print(automl.estimator_list)
|
||||
print(automl.search_space)
|
||||
print(automl.points_to_evaluate)
|
||||
print("Best minimization objective on validation data: {0:.4g}".format(automl.best_loss))
|
||||
print(f"Best minimization objective on validation data: {automl.best_loss:.4g}")
|
||||
print(
|
||||
"pred_time of the best config on validation data: {0:.4g}".format(
|
||||
automl.metrics_for_best_config[1]["pred_time"]
|
||||
)
|
||||
"pred_time of the best config on validation data: {:.4g}".format(automl.metrics_for_best_config[1]["pred_time"])
|
||||
)
|
||||
print(
|
||||
"val_train_loss_gap of the best config on validation data: {0:.4g}".format(
|
||||
"val_train_loss_gap of the best config on validation data: {:.4g}".format(
|
||||
automl.metrics_for_best_config[1]["val_train_loss_gap"]
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,8 +4,17 @@ import pytest
|
||||
|
||||
from flaml import AutoML, tune
|
||||
|
||||
try:
|
||||
import transformers
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "darwin", reason="do not run on mac os")
|
||||
_transformers_installed = True
|
||||
except ImportError:
|
||||
_transformers_installed = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform == "darwin" or not _transformers_installed, reason="do not run on mac os or transformers not installed"
|
||||
)
|
||||
def test_custom_hp_nlp():
|
||||
from test.nlp.utils import get_automl_settings, get_toy_data_seqclassification
|
||||
|
||||
@@ -63,5 +72,39 @@ def test_custom_hp():
|
||||
print(automl.best_config_per_estimator)
|
||||
|
||||
|
||||
def test_lgbm_objective():
|
||||
"""Test that objective parameter can be set via custom_hp for LGBMEstimator"""
|
||||
import numpy as np
|
||||
|
||||
# Create a simple regression dataset
|
||||
np.random.seed(42)
|
||||
X_train = np.random.rand(100, 5)
|
||||
y_train = np.random.rand(100) * 100 # Scale to avoid division issues with MAPE
|
||||
|
||||
automl = AutoML()
|
||||
settings = {
|
||||
"time_budget": 3,
|
||||
"metric": "mape",
|
||||
"task": "regression",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
"custom_hp": {"lgbm": {"objective": {"domain": "mape"}}}, # Fixed value, not tuned
|
||||
}
|
||||
|
||||
automl.fit(X_train, y_train, **settings)
|
||||
|
||||
# Verify that objective was set correctly
|
||||
assert "objective" in automl.best_config, "objective should be in best_config"
|
||||
assert automl.best_config["objective"] == "mape", "objective should be 'mape'"
|
||||
|
||||
# Verify the model has the correct objective
|
||||
if hasattr(automl.model, "estimator") and hasattr(automl.model.estimator, "get_params"):
|
||||
model_params = automl.model.estimator.get_params()
|
||||
assert model_params.get("objective") == "mape", "Model should use 'mape' objective"
|
||||
|
||||
print("Test passed: objective parameter works correctly with LGBMEstimator")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_custom_hp()
|
||||
test_lgbm_objective()
|
||||
|
||||
332
test/automl/test_extra_models.py
Normal file
332
test/automl/test_extra_models.py
Normal file
@@ -0,0 +1,332 @@
|
||||
import atexit
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
|
||||
import mlflow
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
import scipy
|
||||
from packaging.version import Version
|
||||
from sklearn.datasets import load_breast_cancer, load_diabetes, load_iris
|
||||
from sklearn.model_selection import train_test_split
|
||||
|
||||
from flaml import AutoML
|
||||
from flaml.automl.ml import sklearn_metric_loss_score
|
||||
from flaml.automl.spark import disable_spark_ansi_mode, restore_spark_ansi_mode
|
||||
from flaml.tune.spark.utils import check_spark
|
||||
|
||||
try:
|
||||
import pytorch_lightning
|
||||
|
||||
_pl_installed = True
|
||||
except ImportError:
|
||||
_pl_installed = False
|
||||
|
||||
pytestmark = pytest.mark.spark
|
||||
|
||||
leaderboard = defaultdict(dict)
|
||||
|
||||
warnings.simplefilter(action="ignore")
|
||||
if sys.platform == "darwin" or "nt" in os.name:
|
||||
# skip this test if the platform is not linux
|
||||
skip_spark = True
|
||||
else:
|
||||
try:
|
||||
import pyspark
|
||||
from pyspark.ml.evaluation import MulticlassClassificationEvaluator, RegressionEvaluator
|
||||
from pyspark.ml.feature import VectorAssembler
|
||||
|
||||
from flaml.automl.spark.utils import to_pandas_on_spark
|
||||
|
||||
spark = (
|
||||
pyspark.sql.SparkSession.builder.appName("MyApp")
|
||||
.master("local[2]")
|
||||
.config(
|
||||
"spark.jars.packages",
|
||||
(
|
||||
"com.microsoft.azure:synapseml_2.12:1.1.0,"
|
||||
"org.apache.hadoop:hadoop-azure:3.3.5,"
|
||||
"com.microsoft.azure:azure-storage:8.6.6,"
|
||||
f"org.mlflow:mlflow-spark_2.12:{mlflow.__version__}"
|
||||
if Version(mlflow.__version__) >= Version("2.9.0")
|
||||
else f"org.mlflow:mlflow-spark:{mlflow.__version__}"
|
||||
),
|
||||
)
|
||||
.config("spark.jars.repositories", "https://mmlspark.azureedge.net/maven")
|
||||
.config("spark.sql.debug.maxToStringFields", "100")
|
||||
.config("spark.driver.extraJavaOptions", "-Xss1m")
|
||||
.config("spark.executor.extraJavaOptions", "-Xss1m")
|
||||
.getOrCreate()
|
||||
)
|
||||
spark.sparkContext._conf.set(
|
||||
"spark.mlflow.pysparkml.autolog.logModelAllowlistFile",
|
||||
"https://mmlspark.blob.core.windows.net/publicwasb/log_model_allowlist.txt",
|
||||
)
|
||||
# spark.sparkContext.setLogLevel("ERROR")
|
||||
spark_available, _ = check_spark()
|
||||
skip_spark = not spark_available
|
||||
except ImportError:
|
||||
skip_spark = True
|
||||
|
||||
spark, ansi_conf, adjusted = disable_spark_ansi_mode()
|
||||
atexit.register(restore_spark_ansi_mode, spark, ansi_conf, adjusted)
|
||||
|
||||
|
||||
def _test_regular_models(estimator_list, task):
|
||||
if isinstance(estimator_list, str):
|
||||
estimator_list = [estimator_list]
|
||||
if task == "classification":
|
||||
load_dataset_func = load_iris
|
||||
metric = "accuracy"
|
||||
else:
|
||||
load_dataset_func = load_diabetes
|
||||
metric = "r2"
|
||||
|
||||
x, y = load_dataset_func(return_X_y=True, as_frame=True)
|
||||
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=7654321)
|
||||
|
||||
automl_experiment = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": task,
|
||||
"estimator_list": estimator_list,
|
||||
"metric": metric,
|
||||
}
|
||||
automl_experiment.fit(X_train=x_train, y_train=y_train, **automl_settings)
|
||||
predictions = automl_experiment.predict(x_test)
|
||||
score = sklearn_metric_loss_score(metric, predictions, y_test)
|
||||
for estimator_name in estimator_list:
|
||||
leaderboard[task][estimator_name] = score
|
||||
|
||||
|
||||
def _test_spark_models(estimator_list, task):
|
||||
if isinstance(estimator_list, str):
|
||||
estimator_list = [estimator_list]
|
||||
if task == "classification":
|
||||
load_dataset_func = load_iris
|
||||
evaluator = MulticlassClassificationEvaluator(
|
||||
labelCol="target", predictionCol="prediction", metricName="accuracy"
|
||||
)
|
||||
metric = "accuracy"
|
||||
|
||||
elif task == "regression":
|
||||
load_dataset_func = load_diabetes
|
||||
evaluator = RegressionEvaluator(labelCol="target", predictionCol="prediction", metricName="r2")
|
||||
metric = "r2"
|
||||
|
||||
elif task == "binary":
|
||||
load_dataset_func = load_breast_cancer
|
||||
evaluator = MulticlassClassificationEvaluator(
|
||||
labelCol="target", predictionCol="prediction", metricName="accuracy"
|
||||
)
|
||||
metric = "accuracy"
|
||||
|
||||
final_cols = ["target", "features"]
|
||||
extra_args = {}
|
||||
|
||||
if estimator_list is not None and "aft_spark" in estimator_list:
|
||||
# survival analysis task
|
||||
pd_df = pd.read_csv(
|
||||
"https://raw.githubusercontent.com/CamDavidsonPilon/lifelines/master/lifelines/datasets/rossi.csv"
|
||||
)
|
||||
pd_df.rename(columns={"week": "target"}, inplace=True)
|
||||
final_cols += ["arrest"]
|
||||
extra_args["censorCol"] = "arrest"
|
||||
else:
|
||||
pd_df = load_dataset_func(as_frame=True).frame
|
||||
|
||||
rename = {}
|
||||
for attr in pd_df.columns:
|
||||
rename[attr] = attr.replace(" ", "_")
|
||||
pd_df = pd_df.rename(columns=rename)
|
||||
df = spark.createDataFrame(pd_df)
|
||||
df = df.repartition(4)
|
||||
train, test = df.randomSplit([0.8, 0.2], seed=7654321)
|
||||
feature_cols = [col for col in df.columns if col not in ["target", "arrest"]]
|
||||
featurizer = VectorAssembler(inputCols=feature_cols, outputCol="features")
|
||||
train_data = featurizer.transform(train)[final_cols]
|
||||
test_data = featurizer.transform(test)[final_cols]
|
||||
automl = AutoML()
|
||||
settings = {
|
||||
"max_iter": 1,
|
||||
"estimator_list": estimator_list, # ML learner we intend to test
|
||||
"task": task, # task type
|
||||
"metric": metric, # metric to optimize
|
||||
}
|
||||
settings.update(extra_args)
|
||||
df = to_pandas_on_spark(to_pandas_on_spark(train_data).to_spark(index_col="index"))
|
||||
|
||||
automl.fit(
|
||||
dataframe=df,
|
||||
label="target",
|
||||
**settings,
|
||||
)
|
||||
|
||||
model = automl.model.estimator
|
||||
predictions = model.transform(test_data)
|
||||
predictions.show(5)
|
||||
|
||||
score = evaluator.evaluate(predictions)
|
||||
if estimator_list is not None:
|
||||
for estimator_name in estimator_list:
|
||||
leaderboard[task][estimator_name] = score
|
||||
|
||||
|
||||
def _test_sparse_matrix_classification(estimator):
|
||||
automl_experiment = AutoML()
|
||||
automl_settings = {
|
||||
"estimator_list": [estimator],
|
||||
"time_budget": 2,
|
||||
"metric": "auto",
|
||||
"task": "classification",
|
||||
"log_file_name": "test/sparse_classification.log",
|
||||
"split_type": "uniform",
|
||||
"n_jobs": 1,
|
||||
"model_history": True,
|
||||
}
|
||||
# NOTE: Avoid `dtype=int` here. On some NumPy/SciPy combinations (notably
|
||||
# Windows + Python 3.13), `scipy.sparse.random(..., dtype=int)` may trigger
|
||||
# integer sampling paths which raise "low is out of bounds for int32".
|
||||
# A float sparse matrix is sufficient to validate sparse-input support.
|
||||
X_train = scipy.sparse.random(1554, 21, dtype=np.float32)
|
||||
y_train = np.random.randint(3, size=1554)
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
|
||||
|
||||
def load_multi_dataset():
|
||||
"""multivariate time series forecasting dataset"""
|
||||
import pandas as pd
|
||||
|
||||
# pd.set_option("display.max_rows", None, "display.max_columns", None)
|
||||
df = pd.read_csv(
|
||||
"https://raw.githubusercontent.com/srivatsan88/YouTubeLI/master/dataset/nyc_energy_consumption.csv"
|
||||
)
|
||||
# preprocessing data
|
||||
df["timeStamp"] = pd.to_datetime(df["timeStamp"])
|
||||
df = df.set_index("timeStamp")
|
||||
df = df.resample("D").mean()
|
||||
df["temp"] = df["temp"].fillna(method="ffill")
|
||||
df["precip"] = df["precip"].fillna(method="ffill")
|
||||
df = df[:-2] # last two rows are NaN for 'demand' column so remove them
|
||||
df = df.reset_index()
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def _test_forecast(estimator_list, budget=10):
|
||||
if isinstance(estimator_list, str):
|
||||
estimator_list = [estimator_list]
|
||||
df = load_multi_dataset()
|
||||
# split data into train and test
|
||||
time_horizon = 180
|
||||
num_samples = df.shape[0]
|
||||
split_idx = num_samples - time_horizon
|
||||
train_df = df[:split_idx]
|
||||
test_df = df[split_idx:]
|
||||
# test dataframe must contain values for the regressors / multivariate variables
|
||||
X_test = test_df[["timeStamp", "precip", "temp"]]
|
||||
y_test = test_df["demand"]
|
||||
# return
|
||||
automl = AutoML()
|
||||
settings = {
|
||||
"time_budget": budget, # total running time in seconds
|
||||
"metric": "mape", # primary metric
|
||||
"task": "ts_forecast", # task type
|
||||
"log_file_name": "test/energy_forecast_numerical.log", # flaml log file
|
||||
"log_dir": "logs/forecast_logs", # tcn/tft log folder
|
||||
"eval_method": "holdout",
|
||||
"log_type": "all",
|
||||
"label": "demand",
|
||||
"estimator_list": estimator_list,
|
||||
}
|
||||
"""The main flaml automl API"""
|
||||
automl.fit(dataframe=train_df, **settings, period=time_horizon)
|
||||
print(automl.best_config)
|
||||
pred_y = automl.predict(X_test)
|
||||
mape = sklearn_metric_loss_score("mape", pred_y, y_test)
|
||||
for estimator_name in estimator_list:
|
||||
leaderboard["forecast"][estimator_name] = mape
|
||||
|
||||
|
||||
class TestExtraModel(unittest.TestCase):
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_rf_spark(self):
|
||||
tasks = ["classification", "regression"]
|
||||
for task in tasks:
|
||||
_test_spark_models("rf_spark", task)
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_nb_spark(self):
|
||||
_test_spark_models("nb_spark", "classification")
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_glr(self):
|
||||
_test_spark_models("glr_spark", "regression")
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_lr(self):
|
||||
_test_spark_models("lr_spark", "regression")
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_svc_spark(self):
|
||||
_test_spark_models("svc_spark", "binary")
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_gbt_spark(self):
|
||||
tasks = ["binary", "regression"]
|
||||
for task in tasks:
|
||||
_test_spark_models("gbt_spark", task)
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_aft(self):
|
||||
_test_spark_models("aft_spark", "regression")
|
||||
|
||||
@unittest.skipIf(skip_spark, reason="Spark is not installed. Skip all spark tests.")
|
||||
def test_default_spark(self):
|
||||
# TODO: remove the estimator assignment once SynapseML supports spark 4+.
|
||||
from flaml.automl.spark.utils import _spark_major_minor_version
|
||||
|
||||
estimator_list = ["rf_spark"] if _spark_major_minor_version[0] >= 4 else None
|
||||
_test_spark_models(estimator_list, "classification")
|
||||
|
||||
def test_svc(self):
|
||||
_test_regular_models("svc", "classification")
|
||||
_test_sparse_matrix_classification("svc")
|
||||
|
||||
def test_sgd(self):
|
||||
tasks = ["classification", "regression"]
|
||||
for task in tasks:
|
||||
_test_regular_models("sgd", task)
|
||||
_test_sparse_matrix_classification("sgd")
|
||||
|
||||
def test_enet(self):
|
||||
_test_regular_models("enet", "regression")
|
||||
|
||||
def test_lassolars(self):
|
||||
_test_regular_models("lassolars", "regression")
|
||||
_test_forecast("lassolars")
|
||||
|
||||
def test_seasonal_naive(self):
|
||||
_test_forecast("snaive")
|
||||
|
||||
def test_naive(self):
|
||||
_test_forecast("naive")
|
||||
|
||||
def test_seasonal_avg(self):
|
||||
_test_forecast("savg")
|
||||
|
||||
def test_avg(self):
|
||||
_test_forecast("avg")
|
||||
|
||||
@unittest.skipIf(skip_spark or not _pl_installed, reason="Skip on Mac or Windows or no pytorch_lightning.")
|
||||
def test_tcn(self):
|
||||
_test_forecast("tcn")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
print(leaderboard)
|
||||
@@ -1,4 +1,5 @@
|
||||
import datetime
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
@@ -9,7 +10,7 @@ from flaml import AutoML
|
||||
from flaml.automl.task.time_series_task import TimeSeriesTask
|
||||
|
||||
|
||||
def test_forecast_automl(budget=10, estimators_when_no_prophet=["arima", "sarimax", "holt-winters"]):
|
||||
def test_forecast_automl(budget=20, estimators_when_no_prophet=["arima", "sarimax", "holt-winters"]):
|
||||
# using dataframe
|
||||
import statsmodels.api as sm
|
||||
|
||||
@@ -95,6 +96,7 @@ def test_forecast_automl(budget=10, estimators_when_no_prophet=["arima", "sarima
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "darwin" or "nt" in os.name, reason="skip on mac or windows")
|
||||
def test_models(budget=3):
|
||||
n = 200
|
||||
X = pd.DataFrame(
|
||||
@@ -151,6 +153,10 @@ def test_numpy():
|
||||
print(automl.predict(12))
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform in ["darwin"],
|
||||
reason="do not run on mac os",
|
||||
)
|
||||
def test_numpy_large():
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -471,7 +477,10 @@ def test_forecast_classification(budget=5):
|
||||
def get_stalliion_data():
|
||||
from pytorch_forecasting.data.examples import get_stallion_data
|
||||
|
||||
data = get_stallion_data()
|
||||
# data = get_stallion_data()
|
||||
data = pd.read_parquet(
|
||||
"https://raw.githubusercontent.com/sktime/pytorch-forecasting/refs/heads/main/examples/data/stallion.parquet"
|
||||
)
|
||||
# add time index - For datasets with no missing values, FLAML will automate this process
|
||||
data["time_idx"] = data["date"].dt.year * 12 + data["date"].dt.month
|
||||
data["time_idx"] -= data["time_idx"].min()
|
||||
@@ -501,8 +510,12 @@ def get_stalliion_data():
|
||||
"3.11" in sys.version,
|
||||
reason="do not run on py 3.11",
|
||||
)
|
||||
def test_forecast_panel(budget=5):
|
||||
data, special_days = get_stalliion_data()
|
||||
def test_forecast_panel(budget=30):
|
||||
try:
|
||||
data, special_days = get_stalliion_data()
|
||||
except ImportError:
|
||||
print("pytorch_forecasting not installed")
|
||||
return
|
||||
time_horizon = 6 # predict six months
|
||||
training_cutoff = data["time_idx"].max() - time_horizon
|
||||
data["time_idx"] = data["time_idx"].astype("int")
|
||||
@@ -567,7 +580,7 @@ def test_forecast_panel(budget=5):
|
||||
print(f"Training duration of best run: {automl.best_config_train_time}s")
|
||||
print(automl.model.estimator)
|
||||
""" pickle and save the automl object """
|
||||
import pickle
|
||||
import dill as pickle
|
||||
|
||||
with open("automl.pkl", "wb") as f:
|
||||
pickle.dump(automl, f, pickle.HIGHEST_PROTOCOL)
|
||||
@@ -668,11 +681,55 @@ def test_cv_step():
|
||||
print("yahoo!")
|
||||
|
||||
|
||||
def test_log_training_metric_ts_models():
|
||||
"""Test that log_training_metric=True works with time series models (arima, sarimax, holt-winters)."""
|
||||
import statsmodels.api as sm
|
||||
|
||||
from flaml.automl.task.time_series_task import TimeSeriesTask
|
||||
|
||||
estimators_all = TimeSeriesTask("forecast").estimators.keys()
|
||||
estimators_to_test = ["xgboost", "arima", "lassolars", "tcn", "snaive", "prophet", "orbit"]
|
||||
estimators = [
|
||||
est for est in estimators_to_test if est in estimators_all
|
||||
] # not all estimators available in current python env
|
||||
print(f"Testing estimators: {estimators}")
|
||||
|
||||
# Prepare data
|
||||
data = sm.datasets.co2.load_pandas().data["co2"]
|
||||
data = data.resample("MS").mean()
|
||||
data = data.bfill().ffill()
|
||||
data = data.to_frame().reset_index()
|
||||
data = data.rename(columns={"index": "ds", "co2": "y"})
|
||||
num_samples = data.shape[0]
|
||||
time_horizon = 12
|
||||
split_idx = num_samples - time_horizon
|
||||
df = data[:split_idx]
|
||||
|
||||
# Test each time series model with log_training_metric=True
|
||||
for estimator in estimators:
|
||||
print(f"\nTesting {estimator} with log_training_metric=True")
|
||||
automl = AutoML()
|
||||
settings = {
|
||||
"time_budget": 3,
|
||||
"metric": "mape",
|
||||
"task": "forecast",
|
||||
"eval_method": "holdout",
|
||||
"label": "y",
|
||||
"log_training_metric": True, # This should not cause errors
|
||||
"estimator_list": [estimator],
|
||||
}
|
||||
automl.fit(dataframe=df, **settings, period=time_horizon, force_cancel=True)
|
||||
print(f" ✅ {estimator} SUCCESS with log_training_metric=True")
|
||||
if automl.best_estimator:
|
||||
assert automl.best_estimator == estimator
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test_forecast_automl(60)
|
||||
# test_multivariate_forecast_num(5)
|
||||
# test_multivariate_forecast_cat(5)
|
||||
test_numpy()
|
||||
# test_numpy()
|
||||
# test_forecast_classification(5)
|
||||
# test_forecast_panel(5)
|
||||
# test_cv_step()
|
||||
test_log_training_metric_ts_models()
|
||||
|
||||
51
test/automl/test_max_iter_1.py
Normal file
51
test/automl/test_max_iter_1.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import mlflow
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from flaml import AutoML
|
||||
|
||||
|
||||
def test_max_iter_1():
|
||||
date_rng = pd.date_range(start="2024-01-01", periods=100, freq="H")
|
||||
X = pd.DataFrame({"ds": date_rng})
|
||||
y_train_24h = np.random.rand(len(X)) * 100
|
||||
|
||||
# AutoML
|
||||
settings = {
|
||||
"max_iter": 1,
|
||||
"estimator_list": ["xgboost", "lgbm"],
|
||||
"starting_points": {"xgboost": {}, "lgbm": {}},
|
||||
"task": "ts_forecast",
|
||||
"log_file_name": "test_max_iter_1.log",
|
||||
"seed": 41,
|
||||
"mlflow_exp_name": "TestExp-max_iter-1",
|
||||
"use_spark": False,
|
||||
"n_concurrent_trials": 1,
|
||||
"verbose": 1,
|
||||
"featurization": "off",
|
||||
"metric": "rmse",
|
||||
"mlflow_logging": True,
|
||||
}
|
||||
|
||||
automl = AutoML(**settings)
|
||||
|
||||
with mlflow.start_run(run_name="AutoMLModel-XGBoost-and-LGBM-max_iter_1"):
|
||||
automl.fit(
|
||||
X_train=X,
|
||||
y_train=y_train_24h,
|
||||
period=24,
|
||||
X_val=X,
|
||||
y_val=y_train_24h,
|
||||
split_ratio=0,
|
||||
force_cancel=False,
|
||||
)
|
||||
|
||||
assert automl.model is not None, "AutoML failed to return a model"
|
||||
assert automl.best_run_id is not None, "Best run ID should not be None with mlflow logging"
|
||||
|
||||
print("Best model:", automl.model)
|
||||
print("Best run ID:", automl.best_run_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_max_iter_1()
|
||||
@@ -1,3 +1,5 @@
|
||||
import pickle
|
||||
|
||||
import mlflow
|
||||
import mlflow.entities
|
||||
import pytest
|
||||
@@ -8,58 +10,113 @@ from flaml import AutoML
|
||||
|
||||
|
||||
class TestMLFlowLoggingParam:
|
||||
def test_update_and_install_requirements(self):
|
||||
import mlflow
|
||||
from sklearn import tree
|
||||
|
||||
from flaml.fabric.mlflow import update_and_install_requirements
|
||||
|
||||
with mlflow.start_run(run_name="test") as run:
|
||||
sk_model = tree.DecisionTreeClassifier()
|
||||
mlflow.sklearn.log_model(sk_model, "model", registered_model_name="test")
|
||||
|
||||
update_and_install_requirements(run_id=run.info.run_id)
|
||||
|
||||
def test_should_start_new_run_by_default(self, automl_settings):
|
||||
with mlflow.start_run():
|
||||
parent = mlflow.last_active_run()
|
||||
with mlflow.start_run() as parent_run:
|
||||
automl = AutoML()
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
automl.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
try:
|
||||
self._check_mlflow_parameters(automl, parent_run.info)
|
||||
except FileNotFoundError:
|
||||
print("[WARNING]: No file found")
|
||||
|
||||
children = self._get_child_runs(parent)
|
||||
assert len(children) >= 1, "Expected at least 1 child run, got {}".format(len(children))
|
||||
children = self._get_child_runs(parent_run)
|
||||
assert len(children) >= 1, f"Expected at least 1 child run, got {len(children)}"
|
||||
|
||||
def test_should_not_start_new_run_when_mlflow_logging_set_to_false_in_init(self, automl_settings):
|
||||
with mlflow.start_run():
|
||||
parent = mlflow.last_active_run()
|
||||
with mlflow.start_run() as parent_run:
|
||||
automl = AutoML(mlflow_logging=False)
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
automl.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
try:
|
||||
self._check_mlflow_parameters(automl, parent_run.info)
|
||||
except FileNotFoundError:
|
||||
print("[WARNING]: No file found")
|
||||
|
||||
children = self._get_child_runs(parent)
|
||||
assert len(children) == 0, "Expected 0 child runs, got {}".format(len(children))
|
||||
children = self._get_child_runs(parent_run)
|
||||
assert len(children) == 0, f"Expected 0 child runs, got {len(children)}"
|
||||
|
||||
def test_should_not_start_new_run_when_mlflow_logging_set_to_false_in_fit(self, automl_settings):
|
||||
with mlflow.start_run():
|
||||
parent = mlflow.last_active_run()
|
||||
with mlflow.start_run() as parent_run:
|
||||
automl = AutoML()
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
automl.fit(X_train=X_train, y_train=y_train, mlflow_logging=False, **automl_settings)
|
||||
try:
|
||||
self._check_mlflow_parameters(automl, parent_run.info)
|
||||
except FileNotFoundError:
|
||||
print("[WARNING]: No file found")
|
||||
|
||||
children = self._get_child_runs(parent)
|
||||
assert len(children) == 0, "Expected 0 child runs, got {}".format(len(children))
|
||||
children = self._get_child_runs(parent_run)
|
||||
assert len(children) == 0, f"Expected 0 child runs, got {len(children)}"
|
||||
|
||||
def test_should_start_new_run_when_mlflow_logging_set_to_true_in_fit(self, automl_settings):
|
||||
with mlflow.start_run():
|
||||
parent = mlflow.last_active_run()
|
||||
with mlflow.start_run() as parent_run:
|
||||
automl = AutoML(mlflow_logging=False)
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
automl.fit(X_train=X_train, y_train=y_train, mlflow_logging=True, **automl_settings)
|
||||
try:
|
||||
self._check_mlflow_parameters(automl, parent_run.info)
|
||||
except FileNotFoundError:
|
||||
print("[WARNING]: No file found")
|
||||
|
||||
children = self._get_child_runs(parent)
|
||||
assert len(children) >= 1, "Expected at least 1 child run, got {}".format(len(children))
|
||||
children = self._get_child_runs(parent_run)
|
||||
assert len(children) >= 1, f"Expected at least 1 child run, got {len(children)}"
|
||||
|
||||
@staticmethod
|
||||
def _get_child_runs(parent_run: mlflow.entities.Run) -> DataFrame:
|
||||
experiment_id = parent_run.info.experiment_id
|
||||
return mlflow.search_runs(
|
||||
[experiment_id], filter_string="tags.mlflow.parentRunId = '{}'".format(parent_run.info.run_id)
|
||||
[experiment_id], filter_string=f"tags.mlflow.parentRunId = '{parent_run.info.run_id}'"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_mlflow_parameters(automl: AutoML, run_info: mlflow.entities.RunInfo):
|
||||
with open(
|
||||
f"./mlruns/{run_info.experiment_id}/{run_info.run_id}/artifacts/automl_pipeline/model.pkl", "rb"
|
||||
) as f:
|
||||
t = pickle.load(f)
|
||||
if __name__ == "__main__":
|
||||
print(t)
|
||||
if not hasattr(automl.model._model, "_get_param_names"):
|
||||
return
|
||||
for param in automl.model._model._get_param_names():
|
||||
assert eval("t._final_estimator._model" + f".{param}") == eval(
|
||||
"automl.model._model" + f".{param}"
|
||||
), "The mlflow logging not consistent with automl model"
|
||||
if __name__ == "__main__":
|
||||
print(param, "\t", eval("automl.model._model" + f".{param}"))
|
||||
print("[INFO]: Successfully Logged")
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def automl_settings(self):
|
||||
mlflow.end_run()
|
||||
return {
|
||||
"time_budget": 2, # in seconds
|
||||
"time_budget": 5, # in seconds
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"log_file_name": "iris.log",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
s = TestMLFlowLoggingParam()
|
||||
automl_settings = {
|
||||
"time_budget": 5, # in seconds
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"log_file_name": "iris.log",
|
||||
}
|
||||
s.test_should_start_new_run_by_default(automl_settings)
|
||||
s.test_should_start_new_run_when_mlflow_logging_set_to_true_in_fit(automl_settings)
|
||||
|
||||
@@ -143,4 +143,5 @@ def test_prep():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_lrl2()
|
||||
test_prep()
|
||||
@@ -181,13 +181,55 @@ class TestMultiClass(unittest.TestCase):
|
||||
}
|
||||
automl.fit(X_train=X_train, y_train=y_train, **settings)
|
||||
|
||||
def test_ensemble_final_estimator_params_not_tuned(self):
|
||||
"""Test that final_estimator parameters in ensemble are not automatically tuned.
|
||||
|
||||
This test verifies that when a custom final_estimator is provided with specific
|
||||
parameters, those parameters are used as-is without any hyperparameter tuning.
|
||||
"""
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
|
||||
automl = AutoML()
|
||||
X_train, y_train = load_wine(return_X_y=True)
|
||||
|
||||
# Create a LogisticRegression with specific non-default parameters
|
||||
custom_params = {
|
||||
"C": 0.5, # Non-default value
|
||||
"max_iter": 50, # Non-default value
|
||||
"random_state": 42,
|
||||
}
|
||||
final_est = LogisticRegression(**custom_params)
|
||||
|
||||
settings = {
|
||||
"time_budget": 5,
|
||||
"estimator_list": ["rf", "lgbm"],
|
||||
"task": "classification",
|
||||
"ensemble": {
|
||||
"final_estimator": final_est,
|
||||
"passthrough": False,
|
||||
},
|
||||
"n_jobs": 1,
|
||||
}
|
||||
automl.fit(X_train=X_train, y_train=y_train, **settings)
|
||||
|
||||
# Verify that the final estimator in the stacker uses the exact parameters we specified
|
||||
if hasattr(automl.model, "final_estimator_"):
|
||||
# The model is a StackingClassifier
|
||||
fitted_final_estimator = automl.model.final_estimator_
|
||||
assert (
|
||||
abs(fitted_final_estimator.C - custom_params["C"]) < 1e-9
|
||||
), f"Expected C={custom_params['C']}, but got {fitted_final_estimator.C}"
|
||||
assert (
|
||||
fitted_final_estimator.max_iter == custom_params["max_iter"]
|
||||
), f"Expected max_iter={custom_params['max_iter']}, but got {fitted_final_estimator.max_iter}"
|
||||
print("✓ Final estimator parameters were preserved (not tuned)")
|
||||
|
||||
def test_dataframe(self):
|
||||
self.test_classification(True)
|
||||
|
||||
def test_custom_metric(self):
|
||||
df, y = load_iris(return_X_y=True, as_frame=True)
|
||||
df["label"] = y
|
||||
automl = AutoML()
|
||||
settings = {
|
||||
"dataframe": df,
|
||||
"label": "label",
|
||||
@@ -204,7 +246,8 @@ class TestMultiClass(unittest.TestCase):
|
||||
"pred_time_limit": 1e-5,
|
||||
"ensemble": True,
|
||||
}
|
||||
automl.fit(**settings)
|
||||
automl = AutoML(**settings) # test safe_json_dumps
|
||||
automl.fit(dataframe=df, label="label")
|
||||
print(automl.classes_)
|
||||
print(automl.model)
|
||||
print(automl.config_history)
|
||||
@@ -235,6 +278,34 @@ class TestMultiClass(unittest.TestCase):
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def test_invalid_custom_metric(self):
|
||||
"""Test that proper error is raised when custom_metric is called instead of passed."""
|
||||
from sklearn.datasets import load_iris
|
||||
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
|
||||
# Test with non-callable metric in __init__
|
||||
with self.assertRaises(ValueError) as context:
|
||||
automl = AutoML(metric=123) # passing an int instead of function
|
||||
self.assertIn("must be either a string or a callable function", str(context.exception))
|
||||
self.assertIn("but got int", str(context.exception))
|
||||
|
||||
# Test with non-callable metric in fit
|
||||
automl = AutoML()
|
||||
with self.assertRaises(ValueError) as context:
|
||||
automl.fit(X_train=X_train, y_train=y_train, metric=[], task="classification", time_budget=1)
|
||||
self.assertIn("must be either a string or a callable function", str(context.exception))
|
||||
self.assertIn("but got list", str(context.exception))
|
||||
|
||||
# Test with tuple (simulating result of calling a function that returns tuple)
|
||||
with self.assertRaises(ValueError) as context:
|
||||
automl = AutoML()
|
||||
automl.fit(
|
||||
X_train=X_train, y_train=y_train, metric=(0.5, {"loss": 0.5}), task="classification", time_budget=1
|
||||
)
|
||||
self.assertIn("must be either a string or a callable function", str(context.exception))
|
||||
self.assertIn("but got tuple", str(context.exception))
|
||||
|
||||
def test_classification(self, as_frame=False):
|
||||
automl_experiment = AutoML()
|
||||
automl_settings = {
|
||||
@@ -368,7 +439,11 @@ class TestMultiClass(unittest.TestCase):
|
||||
"n_jobs": 1,
|
||||
"model_history": True,
|
||||
}
|
||||
X_train = scipy.sparse.random(1554, 21, dtype=int)
|
||||
# NOTE: Avoid `dtype=int` here. On some NumPy/SciPy combinations (notably
|
||||
# Windows + Python 3.13), `scipy.sparse.random(..., dtype=int)` may trigger
|
||||
# integer sampling paths which raise "low is out of bounds for int32".
|
||||
# A float sparse matrix is sufficient to validate sparse-input support.
|
||||
X_train = scipy.sparse.random(1554, 21, dtype=np.float32)
|
||||
y_train = np.random.randint(3, size=1554)
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
print(automl_experiment.classes_)
|
||||
@@ -438,8 +513,8 @@ class TestMultiClass(unittest.TestCase):
|
||||
automl_val_accuracy = 1.0 - automl.best_loss
|
||||
print("Best ML leaner:", automl.best_estimator)
|
||||
print("Best hyperparmeter config:", automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(automl_val_accuracy))
|
||||
print("Training duration of best run: {0:.4g} s".format(automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {automl_val_accuracy:.4g}")
|
||||
print(f"Training duration of best run: {automl.best_config_train_time:.4g} s")
|
||||
|
||||
starting_points = automl.best_config_per_estimator
|
||||
print("starting_points", starting_points)
|
||||
@@ -461,8 +536,8 @@ class TestMultiClass(unittest.TestCase):
|
||||
new_automl_val_accuracy = 1.0 - new_automl.best_loss
|
||||
print("Best ML leaner:", new_automl.best_estimator)
|
||||
print("Best hyperparmeter config:", new_automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(new_automl_val_accuracy))
|
||||
print("Training duration of best run: {0:.4g} s".format(new_automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {new_automl_val_accuracy:.4g}")
|
||||
print(f"Training duration of best run: {new_automl.best_config_train_time:.4g} s")
|
||||
|
||||
def test_fit_w_starting_point_2(self, as_frame=True):
|
||||
try:
|
||||
@@ -493,8 +568,8 @@ class TestMultiClass(unittest.TestCase):
|
||||
automl_val_accuracy = 1.0 - automl.best_loss
|
||||
print("Best ML leaner:", automl.best_estimator)
|
||||
print("Best hyperparmeter config:", automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(automl_val_accuracy))
|
||||
print("Training duration of best run: {0:.4g} s".format(automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {automl_val_accuracy:.4g}")
|
||||
print(f"Training duration of best run: {automl.best_config_train_time:.4g} s")
|
||||
|
||||
starting_points = {}
|
||||
log_file_name = settings["log_file_name"]
|
||||
@@ -508,7 +583,7 @@ class TestMultiClass(unittest.TestCase):
|
||||
if learner not in starting_points:
|
||||
starting_points[learner] = []
|
||||
starting_points[learner].append(config)
|
||||
max_iter = sum([len(s) for k, s in starting_points.items()])
|
||||
max_iter = sum(len(s) for k, s in starting_points.items())
|
||||
settings_resume = {
|
||||
"time_budget": 2,
|
||||
"metric": "accuracy",
|
||||
@@ -528,9 +603,35 @@ class TestMultiClass(unittest.TestCase):
|
||||
new_automl_val_accuracy = 1.0 - new_automl.best_loss
|
||||
# print('Best ML leaner:', new_automl.best_estimator)
|
||||
# print('Best hyperparmeter config:', new_automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(new_automl_val_accuracy))
|
||||
print(f"Best accuracy on validation data: {new_automl_val_accuracy:.4g}")
|
||||
# print('Training duration of best run: {0:.4g} s'.format(new_automl_experiment.best_config_train_time))
|
||||
|
||||
def test_starting_points_should_improve_performance(self):
|
||||
N = 10000 # a large N is needed to see the improvement
|
||||
X_train, y_train = load_iris(return_X_y=True)
|
||||
X_train = np.concatenate([X_train + 0.1 * i for i in range(N)], axis=0)
|
||||
y_train = np.concatenate([y_train] * N, axis=0)
|
||||
|
||||
am1 = AutoML()
|
||||
am1.fit(X_train, y_train, estimator_list=["lgbm"], time_budget=3, seed=11)
|
||||
|
||||
am2 = AutoML()
|
||||
am2.fit(
|
||||
X_train,
|
||||
y_train,
|
||||
estimator_list=["lgbm"],
|
||||
time_budget=2,
|
||||
seed=11,
|
||||
starting_points=am1.best_config_per_estimator,
|
||||
)
|
||||
|
||||
print(f"am1.best_loss: {am1.best_loss:.4f}")
|
||||
print(f"am2.best_loss: {am2.best_loss:.4f}")
|
||||
|
||||
assert np.round(am2.best_loss, 4) <= np.round(
|
||||
am1.best_loss, 4
|
||||
), "Starting points should help improve the performance!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
272
test/automl/test_no_overlap.py
Normal file
272
test/automl/test_no_overlap.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""Test to ensure correct label overlap handling for classification tasks"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.datasets import load_iris, make_classification
|
||||
|
||||
from flaml import AutoML
|
||||
|
||||
|
||||
def test_allow_label_overlap_true():
|
||||
"""Test with allow_label_overlap=True (fast mode, default)"""
|
||||
# Load iris dataset
|
||||
dic_data = load_iris(as_frame=True)
|
||||
iris_data = dic_data["frame"]
|
||||
|
||||
# Prepare data
|
||||
x_train = iris_data[["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"]].to_numpy()
|
||||
y_train = iris_data["target"]
|
||||
|
||||
# Train with fast mode (default)
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"estimator_list": ["lgbm"],
|
||||
"eval_method": "holdout",
|
||||
"split_type": "stratified",
|
||||
"keep_search_state": True,
|
||||
"retrain_full": False,
|
||||
"auto_augment": False,
|
||||
"verbose": 0,
|
||||
"allow_label_overlap": True, # Fast mode
|
||||
}
|
||||
automl.fit(x_train, y_train, **automl_settings)
|
||||
|
||||
# Check results
|
||||
input_size = len(x_train)
|
||||
train_size = len(automl._state.X_train)
|
||||
val_size = len(automl._state.X_val)
|
||||
|
||||
# With stratified split on balanced data, fast mode may have no overlap
|
||||
assert (
|
||||
train_size + val_size >= input_size
|
||||
), f"Inconsistent sizes. Input: {input_size}, Train: {train_size}, Val: {val_size}"
|
||||
|
||||
# Verify all classes are represented in both sets
|
||||
train_labels = set(np.unique(automl._state.y_train))
|
||||
val_labels = set(np.unique(automl._state.y_val))
|
||||
all_labels = set(np.unique(y_train))
|
||||
|
||||
assert train_labels == all_labels, f"Not all labels in train. All: {all_labels}, Train: {train_labels}"
|
||||
assert val_labels == all_labels, f"Not all labels in val. All: {all_labels}, Val: {val_labels}"
|
||||
|
||||
print(
|
||||
f"✓ Test passed (fast mode): Input: {input_size}, Train: {train_size}, Val: {val_size}, "
|
||||
f"Overlap: {train_size + val_size - input_size}"
|
||||
)
|
||||
|
||||
|
||||
def test_allow_label_overlap_false():
|
||||
"""Test with allow_label_overlap=False (precise mode)"""
|
||||
# Load iris dataset
|
||||
dic_data = load_iris(as_frame=True)
|
||||
iris_data = dic_data["frame"]
|
||||
|
||||
# Prepare data
|
||||
x_train = iris_data[["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"]].to_numpy()
|
||||
y_train = iris_data["target"]
|
||||
|
||||
# Train with precise mode
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"estimator_list": ["lgbm"],
|
||||
"eval_method": "holdout",
|
||||
"split_type": "stratified",
|
||||
"keep_search_state": True,
|
||||
"retrain_full": False,
|
||||
"auto_augment": False,
|
||||
"verbose": 0,
|
||||
"allow_label_overlap": False, # Precise mode
|
||||
}
|
||||
automl.fit(x_train, y_train, **automl_settings)
|
||||
|
||||
# Check that there's no overlap (or minimal overlap for single-instance classes)
|
||||
input_size = len(x_train)
|
||||
train_size = len(automl._state.X_train)
|
||||
val_size = len(automl._state.X_val)
|
||||
|
||||
# Verify all classes are represented
|
||||
all_labels = set(np.unique(y_train))
|
||||
|
||||
# Should have no overlap or minimal overlap
|
||||
overlap = train_size + val_size - input_size
|
||||
assert overlap <= len(all_labels), f"Excessive overlap: {overlap}"
|
||||
|
||||
# Verify all classes are represented
|
||||
train_labels = set(np.unique(automl._state.y_train))
|
||||
val_labels = set(np.unique(automl._state.y_val))
|
||||
|
||||
combined_labels = train_labels.union(val_labels)
|
||||
assert combined_labels == all_labels, f"Not all labels present. All: {all_labels}, Combined: {combined_labels}"
|
||||
|
||||
print(
|
||||
f"✓ Test passed (precise mode): Input: {input_size}, Train: {train_size}, Val: {val_size}, "
|
||||
f"Overlap: {overlap}"
|
||||
)
|
||||
|
||||
|
||||
def test_uniform_split_with_overlap_control():
|
||||
"""Test with uniform split and both overlap modes"""
|
||||
# Load iris dataset
|
||||
dic_data = load_iris(as_frame=True)
|
||||
iris_data = dic_data["frame"]
|
||||
|
||||
# Prepare data
|
||||
x_train = iris_data[["sepal length (cm)", "sepal width (cm)", "petal length (cm)", "petal width (cm)"]].to_numpy()
|
||||
y_train = iris_data["target"]
|
||||
|
||||
# Test precise mode with uniform split
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"metric": "accuracy",
|
||||
"task": "classification",
|
||||
"estimator_list": ["lgbm"],
|
||||
"eval_method": "holdout",
|
||||
"split_type": "uniform",
|
||||
"keep_search_state": True,
|
||||
"retrain_full": False,
|
||||
"auto_augment": False,
|
||||
"verbose": 0,
|
||||
"allow_label_overlap": False, # Precise mode
|
||||
}
|
||||
automl.fit(x_train, y_train, **automl_settings)
|
||||
|
||||
input_size = len(x_train)
|
||||
train_size = len(automl._state.X_train)
|
||||
val_size = len(automl._state.X_val)
|
||||
|
||||
# Verify all classes are represented
|
||||
train_labels = set(np.unique(automl._state.y_train))
|
||||
val_labels = set(np.unique(automl._state.y_val))
|
||||
all_labels = set(np.unique(y_train))
|
||||
|
||||
combined_labels = train_labels.union(val_labels)
|
||||
assert combined_labels == all_labels, "Not all labels present with uniform split"
|
||||
|
||||
print(f"✓ Test passed (uniform split): Input: {input_size}, Train: {train_size}, Val: {val_size}")
|
||||
|
||||
|
||||
def test_with_sample_weights():
|
||||
"""Test label overlap handling with sample weights"""
|
||||
# Create a simple dataset
|
||||
X, y = make_classification(
|
||||
n_samples=200,
|
||||
n_features=10,
|
||||
n_informative=5,
|
||||
n_redundant=2,
|
||||
n_classes=3,
|
||||
n_clusters_per_class=1,
|
||||
random_state=42,
|
||||
)
|
||||
|
||||
# Create sample weights (giving more weight to some samples)
|
||||
sample_weight = np.random.uniform(0.5, 2.0, size=len(y))
|
||||
|
||||
# Test fast mode with sample weights
|
||||
automl_fast = AutoML()
|
||||
automl_fast.fit(
|
||||
X,
|
||||
y,
|
||||
task="classification",
|
||||
metric="accuracy",
|
||||
estimator_list=["lgbm"],
|
||||
eval_method="holdout",
|
||||
split_type="stratified",
|
||||
max_iter=3,
|
||||
keep_search_state=True,
|
||||
retrain_full=False,
|
||||
auto_augment=False,
|
||||
verbose=0,
|
||||
allow_label_overlap=True, # Fast mode
|
||||
sample_weight=sample_weight,
|
||||
)
|
||||
|
||||
# Verify all labels present
|
||||
train_labels_fast = set(np.unique(automl_fast._state.y_train))
|
||||
val_labels_fast = set(np.unique(automl_fast._state.y_val))
|
||||
all_labels = set(np.unique(y))
|
||||
|
||||
assert train_labels_fast == all_labels, "Not all labels in train (fast mode with weights)"
|
||||
assert val_labels_fast == all_labels, "Not all labels in val (fast mode with weights)"
|
||||
|
||||
# Test precise mode with sample weights
|
||||
automl_precise = AutoML()
|
||||
automl_precise.fit(
|
||||
X,
|
||||
y,
|
||||
task="classification",
|
||||
metric="accuracy",
|
||||
estimator_list=["lgbm"],
|
||||
eval_method="holdout",
|
||||
split_type="stratified",
|
||||
max_iter=3,
|
||||
keep_search_state=True,
|
||||
retrain_full=False,
|
||||
auto_augment=False,
|
||||
verbose=0,
|
||||
allow_label_overlap=False, # Precise mode
|
||||
sample_weight=sample_weight,
|
||||
)
|
||||
|
||||
# Verify all labels present
|
||||
train_labels_precise = set(np.unique(automl_precise._state.y_train))
|
||||
val_labels_precise = set(np.unique(automl_precise._state.y_val))
|
||||
|
||||
combined_labels = train_labels_precise.union(val_labels_precise)
|
||||
assert combined_labels == all_labels, "Not all labels present (precise mode with weights)"
|
||||
|
||||
print("✓ Test passed with sample weights (fast and precise modes)")
|
||||
|
||||
|
||||
def test_single_instance_class():
|
||||
"""Test handling of single-instance classes"""
|
||||
# Create imbalanced dataset where one class has only 1 instance
|
||||
X = np.random.randn(50, 4)
|
||||
y = np.array([0] * 40 + [1] * 9 + [2] * 1) # Class 2 has only 1 instance
|
||||
|
||||
# Test precise mode - should add single instance to both sets
|
||||
automl = AutoML()
|
||||
automl.fit(
|
||||
X,
|
||||
y,
|
||||
task="classification",
|
||||
metric="accuracy",
|
||||
estimator_list=["lgbm"],
|
||||
eval_method="holdout",
|
||||
split_type="uniform",
|
||||
max_iter=3,
|
||||
keep_search_state=True,
|
||||
retrain_full=False,
|
||||
auto_augment=False,
|
||||
verbose=0,
|
||||
allow_label_overlap=False, # Precise mode
|
||||
)
|
||||
|
||||
# Verify all labels present
|
||||
train_labels = set(np.unique(automl._state.y_train))
|
||||
val_labels = set(np.unique(automl._state.y_val))
|
||||
all_labels = set(np.unique(y))
|
||||
|
||||
# Single-instance class should be in both sets
|
||||
combined_labels = train_labels.union(val_labels)
|
||||
assert combined_labels == all_labels, "Not all labels present with single-instance class"
|
||||
|
||||
# Check that single-instance class (label 2) is in both sets
|
||||
assert 2 in train_labels, "Single-instance class not in train"
|
||||
assert 2 in val_labels, "Single-instance class not in val"
|
||||
|
||||
print("✓ Test passed with single-instance class")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_allow_label_overlap_true()
|
||||
test_allow_label_overlap_false()
|
||||
test_uniform_split_with_overlap_control()
|
||||
test_with_sample_weights()
|
||||
test_single_instance_class()
|
||||
print("\n✓ All tests passed!")
|
||||
@@ -1,8 +1,23 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from minio.error import ServerError
|
||||
from openml.exceptions import OpenMLServerException
|
||||
|
||||
try:
|
||||
from minio.error import ServerError
|
||||
except ImportError:
|
||||
|
||||
class ServerError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
from openml.exceptions import OpenMLServerException
|
||||
except ImportError:
|
||||
|
||||
class OpenMLServerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
from requests.exceptions import ChunkedEncodingError, SSLError
|
||||
|
||||
|
||||
@@ -64,9 +79,12 @@ def test_automl(budget=5, dataset_format="dataframe", hpo_method=None):
|
||||
automl.fit(X_train=X_train, y_train=y_train, **settings)
|
||||
""" retrieve best config and best learner """
|
||||
print("Best ML leaner:", automl.best_estimator)
|
||||
if not automl.best_estimator:
|
||||
print("Training budget is not sufficient")
|
||||
return
|
||||
print("Best hyperparmeter config:", automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(1 - automl.best_loss))
|
||||
print("Training duration of best run: {0:.4g} s".format(automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {1 - automl.best_loss:.4g}")
|
||||
print(f"Training duration of best run: {automl.best_config_train_time:.4g} s")
|
||||
print(automl.model.estimator)
|
||||
print(automl.best_config_per_estimator)
|
||||
print("time taken to find best model:", automl.time_to_find_best_model)
|
||||
|
||||
236
test/automl/test_preprocess_api.py
Normal file
236
test/automl/test_preprocess_api.py
Normal file
@@ -0,0 +1,236 @@
|
||||
"""Tests for the public preprocessor APIs."""
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.datasets import load_breast_cancer, load_diabetes
|
||||
|
||||
from flaml import AutoML
|
||||
|
||||
|
||||
class TestPreprocessAPI(unittest.TestCase):
|
||||
"""Test cases for the public preprocess() API methods."""
|
||||
|
||||
def test_automl_preprocess_before_fit(self):
|
||||
"""Test that calling preprocess before fit raises an error."""
|
||||
automl = AutoML()
|
||||
X_test = np.array([[1, 2, 3], [4, 5, 6]])
|
||||
|
||||
with self.assertRaises(AttributeError) as context:
|
||||
automl.preprocess(X_test)
|
||||
# Check that an error is raised about not being fitted
|
||||
self.assertIn("fit()", str(context.exception))
|
||||
|
||||
def test_automl_preprocess_classification(self):
|
||||
"""Test task-level preprocessing for classification."""
|
||||
# Load dataset
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
X_train, y_train = X[:400], y[:400]
|
||||
X_test = X[400:450]
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "classification",
|
||||
"metric": "accuracy",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Test task-level preprocessing
|
||||
X_preprocessed = automl.preprocess(X_test)
|
||||
|
||||
# Verify the output is not None and has the right shape
|
||||
self.assertIsNotNone(X_preprocessed)
|
||||
self.assertEqual(X_preprocessed.shape[0], X_test.shape[0])
|
||||
|
||||
def test_automl_preprocess_regression(self):
|
||||
"""Test task-level preprocessing for regression."""
|
||||
# Load dataset
|
||||
X, y = load_diabetes(return_X_y=True)
|
||||
X_train, y_train = X[:300], y[:300]
|
||||
X_test = X[300:350]
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "regression",
|
||||
"metric": "r2",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Test task-level preprocessing
|
||||
X_preprocessed = automl.preprocess(X_test)
|
||||
|
||||
# Verify the output
|
||||
self.assertIsNotNone(X_preprocessed)
|
||||
self.assertEqual(X_preprocessed.shape[0], X_test.shape[0])
|
||||
|
||||
def test_automl_preprocess_with_dataframe(self):
|
||||
"""Test task-level preprocessing with pandas DataFrame."""
|
||||
# Create a simple dataset
|
||||
X_train = pd.DataFrame(
|
||||
{
|
||||
"feature1": [1, 2, 3, 4, 5] * 20,
|
||||
"feature2": [5, 4, 3, 2, 1] * 20,
|
||||
"category": ["a", "b", "a", "b", "a"] * 20,
|
||||
}
|
||||
)
|
||||
y_train = pd.Series([0, 1, 0, 1, 0] * 20)
|
||||
|
||||
X_test = pd.DataFrame(
|
||||
{
|
||||
"feature1": [6, 7, 8],
|
||||
"feature2": [1, 2, 3],
|
||||
"category": ["a", "b", "a"],
|
||||
}
|
||||
)
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "classification",
|
||||
"metric": "accuracy",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Test preprocessing
|
||||
X_preprocessed = automl.preprocess(X_test)
|
||||
|
||||
# Verify the output - check the number of rows matches
|
||||
self.assertIsNotNone(X_preprocessed)
|
||||
preprocessed_len = len(X_preprocessed) if hasattr(X_preprocessed, "__len__") else X_preprocessed.shape[0]
|
||||
self.assertEqual(preprocessed_len, len(X_test))
|
||||
|
||||
def test_estimator_preprocess(self):
|
||||
"""Test estimator-level preprocessing."""
|
||||
# Load dataset
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
X_train, y_train = X[:400], y[:400]
|
||||
X_test = X[400:450]
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "classification",
|
||||
"metric": "accuracy",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Get the trained estimator
|
||||
estimator = automl.model
|
||||
self.assertIsNotNone(estimator)
|
||||
|
||||
# First apply task-level preprocessing
|
||||
X_task_preprocessed = automl.preprocess(X_test)
|
||||
|
||||
# Then apply estimator-level preprocessing
|
||||
X_estimator_preprocessed = estimator.preprocess(X_task_preprocessed)
|
||||
|
||||
# Verify the output
|
||||
self.assertIsNotNone(X_estimator_preprocessed)
|
||||
self.assertEqual(X_estimator_preprocessed.shape[0], X_test.shape[0])
|
||||
|
||||
def test_preprocess_pipeline(self):
|
||||
"""Test the complete preprocessing pipeline (task-level then estimator-level)."""
|
||||
# Load dataset
|
||||
X, y = load_breast_cancer(return_X_y=True)
|
||||
X_train, y_train = X[:400], y[:400]
|
||||
X_test = X[400:450]
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "classification",
|
||||
"metric": "accuracy",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Apply the complete preprocessing pipeline
|
||||
X_task_preprocessed = automl.preprocess(X_test)
|
||||
X_final = automl.model.preprocess(X_task_preprocessed)
|
||||
|
||||
# Verify predictions work with preprocessed data
|
||||
# The internal predict already does this preprocessing,
|
||||
# but we verify our manual preprocessing gives consistent results
|
||||
y_pred_manual = automl.model._model.predict(X_final)
|
||||
y_pred_auto = automl.predict(X_test)
|
||||
|
||||
# Both should give the same predictions
|
||||
np.testing.assert_array_equal(y_pred_manual, y_pred_auto)
|
||||
|
||||
def test_preprocess_with_mixed_types(self):
|
||||
"""Test preprocessing with mixed data types."""
|
||||
# Create dataset with mixed types
|
||||
X_train = pd.DataFrame(
|
||||
{
|
||||
"numeric1": np.random.rand(100),
|
||||
"numeric2": np.random.randint(0, 100, 100),
|
||||
"categorical": np.random.choice(["cat", "dog", "bird"], 100),
|
||||
"boolean": np.random.choice([True, False], 100),
|
||||
}
|
||||
)
|
||||
y_train = pd.Series(np.random.randint(0, 2, 100))
|
||||
|
||||
X_test = pd.DataFrame(
|
||||
{
|
||||
"numeric1": np.random.rand(10),
|
||||
"numeric2": np.random.randint(0, 100, 10),
|
||||
"categorical": np.random.choice(["cat", "dog", "bird"], 10),
|
||||
"boolean": np.random.choice([True, False], 10),
|
||||
}
|
||||
)
|
||||
|
||||
# Train AutoML
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"task": "classification",
|
||||
"metric": "accuracy",
|
||||
"estimator_list": ["lgbm"],
|
||||
"verbose": 0,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
# Test preprocessing
|
||||
X_preprocessed = automl.preprocess(X_test)
|
||||
|
||||
# Verify the output
|
||||
self.assertIsNotNone(X_preprocessed)
|
||||
|
||||
def test_estimator_preprocess_without_automl(self):
|
||||
"""Test that estimator.preprocess() can be used independently."""
|
||||
from flaml.automl.model import LGBMEstimator
|
||||
|
||||
# Create a simple estimator
|
||||
X_train = np.random.rand(100, 5)
|
||||
y_train = np.random.randint(0, 2, 100)
|
||||
|
||||
estimator = LGBMEstimator(task="classification")
|
||||
estimator.fit(X_train, y_train)
|
||||
|
||||
# Test preprocessing
|
||||
X_test = np.random.rand(10, 5)
|
||||
X_preprocessed = estimator.preprocess(X_test)
|
||||
|
||||
# Verify the output
|
||||
self.assertIsNotNone(X_preprocessed)
|
||||
self.assertEqual(X_preprocessed.shape, X_test.shape)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -38,7 +38,7 @@ class TestLogging(unittest.TestCase):
|
||||
"keep_search_state": True,
|
||||
"learner_selector": "roundrobin",
|
||||
}
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True, data_home="test")
|
||||
n = len(y_train) >> 1
|
||||
print(automl.model, automl.classes_, automl.predict(X_train))
|
||||
automl.fit(
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import unittest
|
||||
from test.conftest import evaluate_cv_folds_with_underlying_model
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import scipy.sparse
|
||||
from sklearn.datasets import (
|
||||
fetch_california_housing,
|
||||
make_regression,
|
||||
)
|
||||
|
||||
from flaml import AutoML
|
||||
@@ -44,7 +47,7 @@ class TestRegression(unittest.TestCase):
|
||||
"n_jobs": 1,
|
||||
"model_history": True,
|
||||
}
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True, data_home="test")
|
||||
n = int(len(y_train) * 9 // 10)
|
||||
automl.fit(X_train=X_train[:n], y_train=y_train[:n], X_val=X_train[n:], y_val=y_train[n:], **automl_settings)
|
||||
assert automl._state.eval_method == "holdout"
|
||||
@@ -127,7 +130,7 @@ class TestRegression(unittest.TestCase):
|
||||
)
|
||||
automl.fit(X_train=X_train, y_train=y_train, X_val=X_val, y_val=y_val, **settings)
|
||||
|
||||
def test_parallel(self, hpo_method=None):
|
||||
def test_parallel_and_pickle(self, hpo_method=None):
|
||||
automl_experiment = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 10,
|
||||
@@ -138,7 +141,7 @@ class TestRegression(unittest.TestCase):
|
||||
"n_concurrent_trials": 10,
|
||||
"hpo_method": hpo_method,
|
||||
}
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True, data_home="test")
|
||||
try:
|
||||
automl_experiment.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
print(automl_experiment.predict(X_train))
|
||||
@@ -150,6 +153,18 @@ class TestRegression(unittest.TestCase):
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
# test pickle and load_pickle, should work for prediction
|
||||
automl_experiment.pickle("automl_xgboost_spark.pkl")
|
||||
automl_loaded = AutoML().load_pickle("automl_xgboost_spark.pkl")
|
||||
assert automl_loaded.best_estimator == automl_experiment.best_estimator
|
||||
assert automl_loaded.best_loss == automl_experiment.best_loss
|
||||
automl_loaded.predict(X_train)
|
||||
|
||||
import shutil
|
||||
|
||||
shutil.rmtree("automl_xgboost_spark.pkl", ignore_errors=True)
|
||||
shutil.rmtree("automl_xgboost_spark.pkl.flaml_artifacts", ignore_errors=True)
|
||||
|
||||
def test_sparse_matrix_regression_holdout(self):
|
||||
X_train = scipy.sparse.random(8, 100)
|
||||
y_train = np.random.uniform(size=8)
|
||||
@@ -205,7 +220,6 @@ class TestRegression(unittest.TestCase):
|
||||
|
||||
|
||||
def test_multioutput():
|
||||
from sklearn.datasets import make_regression
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.multioutput import MultiOutputRegressor, RegressorChain
|
||||
|
||||
@@ -230,5 +244,210 @@ def test_multioutput():
|
||||
print(model.predict(X_test))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"estimator",
|
||||
[
|
||||
"catboost",
|
||||
"enet",
|
||||
"extra_tree",
|
||||
"histgb",
|
||||
"kneighbor",
|
||||
"lgbm",
|
||||
"rf",
|
||||
"xgboost",
|
||||
"xgb_limitdepth",
|
||||
],
|
||||
)
|
||||
def test_reproducibility_of_regression_models(estimator: str):
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues where FLAML was providing an incorrect model - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1317
|
||||
In this test we take the best regression model which FLAML provided us, and then retrain and test it on the
|
||||
same folds, to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 2,
|
||||
"time_budget": -1,
|
||||
"task": "regression",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": [estimator],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 3,
|
||||
"metric": "r2",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
"retrain_full": True,
|
||||
}
|
||||
X, y = fetch_california_housing(return_X_y=True, as_frame=True, data_home="test")
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
config = best_model.get_params()
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
|
||||
# Take the best model, and see if we can reproduce the best result
|
||||
reproduced_val_loss, metric_for_logging, train_time, pred_time = automl._state.task.evaluate_model_CV(
|
||||
config=config,
|
||||
estimator=best_model,
|
||||
X_train_all=automl._state.X_train_all,
|
||||
y_train_all=automl._state.y_train_all,
|
||||
budget=None,
|
||||
kf=automl._state.kf,
|
||||
eval_metric="r2",
|
||||
best_val_loss=None,
|
||||
cv_score_agg_func=None,
|
||||
log_training_metric=False,
|
||||
fit_kwargs=None,
|
||||
free_mem_ratio=0,
|
||||
)
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss
|
||||
|
||||
|
||||
def test_reproducibility_of_catboost_regression_model():
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues around the catboost model - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1317
|
||||
In this test we take the best catboost regression model which FLAML provided us, and then retrain and test it on the
|
||||
same folds, to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 7,
|
||||
"task": "regression",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": ["catboost"],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 10,
|
||||
"metric": "r2",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
"retrain_full": True,
|
||||
}
|
||||
X, y = fetch_california_housing(return_X_y=True, as_frame=True, data_home="test")
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
config = best_model.get_params()
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
|
||||
# Take the best model, and see if we can reproduce the best result
|
||||
reproduced_val_loss, metric_for_logging, train_time, pred_time = automl._state.task.evaluate_model_CV(
|
||||
config=config,
|
||||
estimator=best_model,
|
||||
X_train_all=automl._state.X_train_all,
|
||||
y_train_all=automl._state.y_train_all,
|
||||
budget=None,
|
||||
kf=automl._state.kf,
|
||||
eval_metric="r2",
|
||||
best_val_loss=None,
|
||||
cv_score_agg_func=None,
|
||||
log_training_metric=False,
|
||||
fit_kwargs=None,
|
||||
free_mem_ratio=0,
|
||||
)
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss
|
||||
|
||||
|
||||
def test_reproducibility_of_lgbm_regression_model():
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues around LGBMs - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1368
|
||||
In this test we take the best LGBM regression model which FLAML provided us, and then retrain and test it on the
|
||||
same folds, to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 3,
|
||||
"task": "regression",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": ["lgbm"],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 9,
|
||||
"metric": "r2",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
"retrain_full": True,
|
||||
}
|
||||
X, y = fetch_california_housing(return_X_y=True, as_frame=True, data_home="test")
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
config = best_model.get_params()
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
|
||||
# Take the best model, and see if we can reproduce the best result
|
||||
reproduced_val_loss, metric_for_logging, train_time, pred_time = automl._state.task.evaluate_model_CV(
|
||||
config=config,
|
||||
estimator=best_model,
|
||||
X_train_all=automl._state.X_train_all,
|
||||
y_train_all=automl._state.y_train_all,
|
||||
budget=None,
|
||||
kf=automl._state.kf,
|
||||
eval_metric="r2",
|
||||
best_val_loss=None,
|
||||
cv_score_agg_func=None,
|
||||
log_training_metric=False,
|
||||
fit_kwargs=None,
|
||||
free_mem_ratio=0,
|
||||
)
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss or val_loss_flaml > reproduced_val_loss
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"estimator",
|
||||
[
|
||||
"catboost",
|
||||
"enet",
|
||||
"extra_tree",
|
||||
"histgb",
|
||||
"kneighbor",
|
||||
"lgbm",
|
||||
"rf",
|
||||
"xgboost",
|
||||
"xgb_limitdepth",
|
||||
],
|
||||
)
|
||||
def test_reproducibility_of_underlying_regression_models(estimator: str):
|
||||
"""FLAML finds the best model for a given dataset, which it then provides to users.
|
||||
|
||||
However, there are reported issues where FLAML was providing an incorrect model - see here:
|
||||
https://github.com/microsoft/FLAML/issues/1317
|
||||
FLAML defines FLAMLised models, which wrap around the underlying (SKLearn/XGBoost/CatBoost) model.
|
||||
Ideally, FLAMLised models should perform identically to the underlying model, when fitted
|
||||
to the same data, with no budget. This verifies that this is the case for regression models.
|
||||
In this test we take the best model which FLAML provided us, extract the underlying model,
|
||||
before retraining and testing it on the same folds - to verify that the result is reproducible.
|
||||
"""
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"time_budget": -1,
|
||||
"task": "regression",
|
||||
"n_jobs": 1,
|
||||
"estimator_list": [estimator],
|
||||
"eval_method": "cv",
|
||||
"n_splits": 10,
|
||||
"metric": "r2",
|
||||
"keep_search_state": True,
|
||||
"skip_transform": True,
|
||||
"retrain_full": False,
|
||||
}
|
||||
X, y = fetch_california_housing(return_X_y=True, as_frame=True, data_home="test")
|
||||
automl.fit(X_train=X, y_train=y, **automl_settings)
|
||||
best_model = automl.model
|
||||
assert best_model is not None
|
||||
val_loss_flaml = automl.best_result["val_loss"]
|
||||
reproduced_val_loss_underlying_model = np.mean(
|
||||
evaluate_cv_folds_with_underlying_model(
|
||||
automl._state.X_train_all, automl._state.y_train_all, automl._state.kf, best_model.model, "regression"
|
||||
)
|
||||
)
|
||||
assert pytest.approx(val_loss_flaml) == reproduced_val_loss_underlying_model
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -142,7 +142,7 @@ class TestScore:
|
||||
def test_regression(self):
|
||||
automl_experiment = AutoML()
|
||||
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True, data_home="test")
|
||||
n = int(len(y_train) * 9 // 10)
|
||||
|
||||
for each_estimator in [
|
||||
@@ -195,7 +195,7 @@ class TestScore:
|
||||
automl_settings = {
|
||||
"time_budget": 2,
|
||||
"task": "rank",
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"groups": np.array([0] * 200 + [1] * 200 + [2] * 100), # group labels
|
||||
"learner_selector": "roundrobin",
|
||||
|
||||
89
test/automl/test_sklearn_17_compat.py
Normal file
89
test/automl/test_sklearn_17_compat.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Test sklearn 1.7+ compatibility for estimator type detection.
|
||||
|
||||
This test ensures that FLAML estimators are properly recognized as
|
||||
regressors or classifiers by sklearn's is_regressor() and is_classifier()
|
||||
functions, which is required for sklearn 1.7+ ensemble methods.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from sklearn.base import is_classifier, is_regressor
|
||||
|
||||
from flaml.automl.model import (
|
||||
ExtraTreesEstimator,
|
||||
LGBMEstimator,
|
||||
RandomForestEstimator,
|
||||
XGBoostSklearnEstimator,
|
||||
)
|
||||
|
||||
|
||||
def test_extra_trees_regressor_type():
|
||||
"""Test that ExtraTreesEstimator with regression task is recognized as regressor."""
|
||||
est = ExtraTreesEstimator(task="regression")
|
||||
assert is_regressor(est), "ExtraTreesEstimator(task='regression') should be recognized as a regressor"
|
||||
assert not is_classifier(est), "ExtraTreesEstimator(task='regression') should not be recognized as a classifier"
|
||||
|
||||
|
||||
def test_extra_trees_classifier_type():
|
||||
"""Test that ExtraTreesEstimator with classification task is recognized as classifier."""
|
||||
est = ExtraTreesEstimator(task="binary")
|
||||
assert is_classifier(est), "ExtraTreesEstimator(task='binary') should be recognized as a classifier"
|
||||
assert not is_regressor(est), "ExtraTreesEstimator(task='binary') should not be recognized as a regressor"
|
||||
|
||||
est = ExtraTreesEstimator(task="multiclass")
|
||||
assert is_classifier(est), "ExtraTreesEstimator(task='multiclass') should be recognized as a classifier"
|
||||
assert not is_regressor(est), "ExtraTreesEstimator(task='multiclass') should not be recognized as a regressor"
|
||||
|
||||
|
||||
def test_random_forest_regressor_type():
|
||||
"""Test that RandomForestEstimator with regression task is recognized as regressor."""
|
||||
est = RandomForestEstimator(task="regression")
|
||||
assert is_regressor(est), "RandomForestEstimator(task='regression') should be recognized as a regressor"
|
||||
assert not is_classifier(est), "RandomForestEstimator(task='regression') should not be recognized as a classifier"
|
||||
|
||||
|
||||
def test_random_forest_classifier_type():
|
||||
"""Test that RandomForestEstimator with classification task is recognized as classifier."""
|
||||
est = RandomForestEstimator(task="binary")
|
||||
assert is_classifier(est), "RandomForestEstimator(task='binary') should be recognized as a classifier"
|
||||
assert not is_regressor(est), "RandomForestEstimator(task='binary') should not be recognized as a regressor"
|
||||
|
||||
|
||||
def test_lgbm_regressor_type():
|
||||
"""Test that LGBMEstimator with regression task is recognized as regressor."""
|
||||
est = LGBMEstimator(task="regression")
|
||||
assert is_regressor(est), "LGBMEstimator(task='regression') should be recognized as a regressor"
|
||||
assert not is_classifier(est), "LGBMEstimator(task='regression') should not be recognized as a classifier"
|
||||
|
||||
|
||||
def test_lgbm_classifier_type():
|
||||
"""Test that LGBMEstimator with classification task is recognized as classifier."""
|
||||
est = LGBMEstimator(task="binary")
|
||||
assert is_classifier(est), "LGBMEstimator(task='binary') should be recognized as a classifier"
|
||||
assert not is_regressor(est), "LGBMEstimator(task='binary') should not be recognized as a regressor"
|
||||
|
||||
|
||||
def test_xgboost_regressor_type():
|
||||
"""Test that XGBoostSklearnEstimator with regression task is recognized as regressor."""
|
||||
est = XGBoostSklearnEstimator(task="regression")
|
||||
assert is_regressor(est), "XGBoostSklearnEstimator(task='regression') should be recognized as a regressor"
|
||||
assert not is_classifier(est), "XGBoostSklearnEstimator(task='regression') should not be recognized as a classifier"
|
||||
|
||||
|
||||
def test_xgboost_classifier_type():
|
||||
"""Test that XGBoostSklearnEstimator with classification task is recognized as classifier."""
|
||||
est = XGBoostSklearnEstimator(task="binary")
|
||||
assert is_classifier(est), "XGBoostSklearnEstimator(task='binary') should be recognized as a classifier"
|
||||
assert not is_regressor(est), "XGBoostSklearnEstimator(task='binary') should not be recognized as a regressor"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run all tests
|
||||
test_extra_trees_regressor_type()
|
||||
test_extra_trees_classifier_type()
|
||||
test_random_forest_regressor_type()
|
||||
test_random_forest_classifier_type()
|
||||
test_lgbm_regressor_type()
|
||||
test_lgbm_classifier_type()
|
||||
test_xgboost_regressor_type()
|
||||
test_xgboost_classifier_type()
|
||||
print("All sklearn 1.7+ compatibility tests passed!")
|
||||
@@ -1,4 +1,6 @@
|
||||
from sklearn.datasets import fetch_openml
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.datasets import fetch_openml, load_iris
|
||||
from sklearn.metrics import accuracy_score
|
||||
from sklearn.model_selection import GroupKFold, KFold, train_test_split
|
||||
|
||||
@@ -16,7 +18,7 @@ def _test(split_type):
|
||||
"time_budget": 2,
|
||||
# "metric": 'accuracy',
|
||||
"task": "classification",
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"log_training_metric": True,
|
||||
"split_type": split_type,
|
||||
@@ -48,7 +50,7 @@ def test_time():
|
||||
_test(split_type="time")
|
||||
|
||||
|
||||
def test_groups():
|
||||
def test_groups_for_classification_task():
|
||||
from sklearn.externals._arff import ArffException
|
||||
|
||||
try:
|
||||
@@ -58,17 +60,15 @@ def test_groups():
|
||||
|
||||
X, y = load_wine(return_X_y=True)
|
||||
|
||||
import numpy as np
|
||||
|
||||
automl = AutoML()
|
||||
automl_settings = {
|
||||
"time_budget": 2,
|
||||
"task": "classification",
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"eval_method": "cv",
|
||||
"groups": np.random.randint(low=0, high=10, size=len(y)),
|
||||
"estimator_list": ["lgbm", "rf", "xgboost", "kneighbor"],
|
||||
"estimator_list": ["catboost", "lgbm", "rf", "xgboost", "kneighbor"],
|
||||
"learner_selector": "roundrobin",
|
||||
}
|
||||
automl.fit(X, y, **automl_settings)
|
||||
@@ -88,6 +88,72 @@ def test_groups():
|
||||
automl.fit(X, y, **automl_settings)
|
||||
|
||||
|
||||
def test_groups_for_regression_task():
|
||||
"""Append nonsensical groups to iris dataset and use it to test that GroupKFold works for regression tasks"""
|
||||
iris_dict_data = load_iris(as_frame=True) # numpy arrays
|
||||
iris_data = iris_dict_data["frame"] # pandas dataframe data + target
|
||||
|
||||
rng = np.random.default_rng(42)
|
||||
iris_data["cluster"] = rng.integers(
|
||||
low=0, high=5, size=iris_data.shape[0]
|
||||
) # np.random.randint(0, 5, iris_data.shape[0])
|
||||
|
||||
automl = AutoML()
|
||||
X = iris_data[["sepal length (cm)", "sepal width (cm)", "petal length (cm)"]].to_numpy()
|
||||
y = iris_data["petal width (cm)"]
|
||||
X_train, X_test, y_train, y_test, groups_train, groups_test = train_test_split(
|
||||
X, y, iris_data["cluster"], random_state=42
|
||||
)
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"time_budget": -1,
|
||||
"metric": "r2",
|
||||
"task": "regression",
|
||||
"estimator_list": ["lgbm", "rf", "xgboost", "kneighbor"],
|
||||
"eval_method": "cv",
|
||||
"split_type": "uniform",
|
||||
"groups": groups_train,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
|
||||
|
||||
def test_groups_with_sample_weights():
|
||||
"""Verifies that sample weights can be used with group splits i.e. that https://github.com/microsoft/FLAML/issues/1396 remains fixed"""
|
||||
iris_dict_data = load_iris(as_frame=True) # numpy arrays
|
||||
iris_data = iris_dict_data["frame"] # pandas dataframe data + target
|
||||
iris_data["cluster"] = np.random.randint(0, 5, iris_data.shape[0])
|
||||
automl = AutoML()
|
||||
|
||||
X = iris_data[["sepal length (cm)", "sepal width (cm)", "petal length (cm)"]].to_numpy()
|
||||
y = iris_data["petal width (cm)"]
|
||||
sample_weight = pd.Series(np.random.rand(X.shape[0]))
|
||||
(
|
||||
X_train,
|
||||
X_test,
|
||||
y_train,
|
||||
y_test,
|
||||
groups_train,
|
||||
groups_test,
|
||||
sample_weight_train,
|
||||
sample_weight_test,
|
||||
) = train_test_split(X, y, iris_data["cluster"], sample_weight, random_state=42)
|
||||
automl_settings = {
|
||||
"max_iter": 5,
|
||||
"time_budget": -1,
|
||||
"metric": "r2",
|
||||
"task": "regression",
|
||||
"log_file_name": "error.log",
|
||||
"log_type": "all",
|
||||
"estimator_list": ["lgbm"],
|
||||
"eval_method": "cv",
|
||||
"split_type": "group",
|
||||
"groups": groups_train,
|
||||
"sample_weight": sample_weight_train,
|
||||
}
|
||||
automl.fit(X_train, y_train, **automl_settings)
|
||||
assert automl.model is not None
|
||||
|
||||
|
||||
def test_stratified_groupkfold():
|
||||
from minio.error import ServerError
|
||||
from sklearn.model_selection import StratifiedGroupKFold
|
||||
@@ -108,6 +174,7 @@ def test_stratified_groupkfold():
|
||||
"split_type": splitter,
|
||||
"groups": X_train["Airline"],
|
||||
"estimator_list": [
|
||||
"catboost",
|
||||
"lgbm",
|
||||
"rf",
|
||||
"xgboost",
|
||||
@@ -136,7 +203,7 @@ def test_rank():
|
||||
automl_settings = {
|
||||
"time_budget": 2,
|
||||
"task": "rank",
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"eval_method": "cv",
|
||||
"groups": np.array([0] * 200 + [1] * 200 + [2] * 200 + [3] * 200 + [4] * 100 + [5] * 100), # group labels
|
||||
@@ -149,7 +216,7 @@ def test_rank():
|
||||
"time_budget": 2,
|
||||
"task": "rank",
|
||||
"metric": "ndcg@5", # 5 can be replaced by any number
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"groups": [200] * 4 + [100] * 2, # alternative way: group counts
|
||||
# "estimator_list": ['lgbm', 'xgboost'], # list of ML learners
|
||||
@@ -188,7 +255,7 @@ def test_object():
|
||||
automl_settings = {
|
||||
"time_budget": 2,
|
||||
"task": "classification",
|
||||
"log_file_name": "test/{}.log".format(dataset),
|
||||
"log_file_name": f"test/{dataset}.log",
|
||||
"model_history": True,
|
||||
"log_training_metric": True,
|
||||
"split_type": TestKFold(5),
|
||||
@@ -203,4 +270,4 @@ def test_object():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_groups()
|
||||
test_groups_for_classification_task()
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestTrainingLog(unittest.TestCase):
|
||||
"keep_search_state": True,
|
||||
"estimator_list": estimator_list,
|
||||
}
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True)
|
||||
X_train, y_train = fetch_california_housing(return_X_y=True, data_home="test")
|
||||
automl.fit(X_train=X_train, y_train=y_train, **automl_settings)
|
||||
# Check if the training log file is populated.
|
||||
self.assertTrue(os.path.exists(filename))
|
||||
|
||||
@@ -29,8 +29,8 @@ class TestWarmStart(unittest.TestCase):
|
||||
automl_val_accuracy = 1.0 - automl.best_loss
|
||||
print("Best ML leaner:", automl.best_estimator)
|
||||
print("Best hyperparmeter config:", automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(automl_val_accuracy))
|
||||
print("Training duration of best run: {0:.4g} s".format(automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {automl_val_accuracy:.4g}")
|
||||
print(f"Training duration of best run: {automl.best_config_train_time:.4g} s")
|
||||
# 1. Get starting points from previous experiments.
|
||||
starting_points = automl.best_config_per_estimator
|
||||
print("starting_points", starting_points)
|
||||
@@ -97,8 +97,8 @@ class TestWarmStart(unittest.TestCase):
|
||||
new_automl_val_accuracy = 1.0 - new_automl.best_loss
|
||||
print("Best ML leaner:", new_automl.best_estimator)
|
||||
print("Best hyperparmeter config:", new_automl.best_config)
|
||||
print("Best accuracy on validation data: {0:.4g}".format(new_automl_val_accuracy))
|
||||
print("Training duration of best run: {0:.4g} s".format(new_automl.best_config_train_time))
|
||||
print(f"Best accuracy on validation data: {new_automl_val_accuracy:.4g}")
|
||||
print(f"Training duration of best run: {new_automl.best_config_train_time:.4g} s")
|
||||
|
||||
def test_nobudget(self):
|
||||
automl = AutoML()
|
||||
@@ -108,7 +108,14 @@ class TestWarmStart(unittest.TestCase):
|
||||
|
||||
def test_FLAML_sample_size_in_starting_points(self):
|
||||
from minio.error import ServerError
|
||||
from openml.exceptions import OpenMLServerException
|
||||
|
||||
try:
|
||||
from openml.exceptions import OpenMLServerException
|
||||
except ImportError:
|
||||
|
||||
class OpenMLServerException(Exception):
|
||||
pass
|
||||
|
||||
from requests.exceptions import ChunkedEncodingError, SSLError
|
||||
|
||||
from flaml import AutoML
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user