Sync from GitHub via hub-sync
Browse files- CODE_OF_CONDUCT.md +136 -0
- CONTRIBUTING.md +22 -0
- LICENSE +201 -0
- MANIFEST.in +7 -0
- NOTICE +15 -0
- README.md +14 -53
- SECURITY.md +91 -0
- config.json +0 -20
- finetune_bert_mini_arabic_mltitasks_and_generation.pt +0 -3
- pyproject.toml +95 -0
- requirements.txt +11 -0
- setup.py +116 -0
- src/dytr/__init__.py +102 -0
- src/dytr/cli/__init__.py +28 -0
- src/dytr/cli/export.py +90 -0
- src/dytr/cli/train.py +173 -0
- src/dytr/core/__init__.py +52 -0
- src/dytr/core/config.py +196 -0
- src/dytr/core/decoder.py +300 -0
- src/dytr/core/encoder.py +143 -0
- src/dytr/core/exporter.py +356 -0
- src/dytr/core/layers.py +247 -0
- src/dytr/core/model.py +619 -0
- src/dytr/memory/__init__.py +29 -0
- src/dytr/memory/ewc.py +112 -0
- src/dytr/memory/replay.py +83 -0
- src/dytr/pretrained/__init__.py +13 -0
- src/dytr/pretrained/loader.py +867 -0
- src/dytr/tokenization/__init__.py +41 -0
- src/dytr/tokenization/download_manager.py +280 -0
- src/dytr/tokenization/simple_tokenizer.py +839 -0
- src/dytr/training/__init__.py +41 -0
- src/dytr/training/data.py +217 -0
- src/dytr/training/dataset.py +413 -0
- src/dytr/training/trainer.py +572 -0
- src/dytr/training/utils.py +196 -0
- src/dytr/utils/__init__.py +31 -0
- src/dytr/utils/caching.py +101 -0
- src/dytr/utils/logging.py +64 -0
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
| 6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
| 7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
| 8 |
+
identity and expression, level of experience, education, socio-economic status,
|
| 9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
| 10 |
+
and orientation.
|
| 11 |
+
|
| 12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
| 13 |
+
diverse, inclusive, and healthy community.
|
| 14 |
+
|
| 15 |
+
## Our Standards
|
| 16 |
+
|
| 17 |
+
Examples of behavior that contributes to a positive environment for our
|
| 18 |
+
community include:
|
| 19 |
+
|
| 20 |
+
* Demonstrating empathy and kindness toward other people
|
| 21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
| 22 |
+
* Giving and gracefully accepting constructive feedback
|
| 23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
| 24 |
+
and learning from the experience
|
| 25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
| 26 |
+
overall community
|
| 27 |
+
|
| 28 |
+
Examples of unacceptable behavior include:
|
| 29 |
+
|
| 30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
| 31 |
+
advances of any kind
|
| 32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
| 33 |
+
* Public or private harassment
|
| 34 |
+
* Publishing others' private information, such as a physical or email
|
| 35 |
+
address, without their explicit permission
|
| 36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
| 37 |
+
professional setting
|
| 38 |
+
|
| 39 |
+
## Enforcement Responsibilities
|
| 40 |
+
|
| 41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
| 42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
| 43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
| 44 |
+
or harmful.
|
| 45 |
+
|
| 46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
| 47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
| 48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
| 49 |
+
decisions when appropriate.
|
| 50 |
+
|
| 51 |
+
## Scope
|
| 52 |
+
|
| 53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
| 54 |
+
an individual is officially representing the community in public spaces.
|
| 55 |
+
Examples of representing our community include using an official e-mail address,
|
| 56 |
+
posting via an official social media account, or acting as an appointed
|
| 57 |
+
representative at an online or offline event.
|
| 58 |
+
|
| 59 |
+
## Enforcement
|
| 60 |
+
|
| 61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
| 62 |
+
reported to the community leaders responsible for enforcement at:
|
| 63 |
+
**akram.alsubari87@gmail.com** or **akram.alsubari@outlook.com**
|
| 64 |
+
|
| 65 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
| 66 |
+
|
| 67 |
+
All community leaders are obligated to respect the privacy and security of the
|
| 68 |
+
reporter of any incident.
|
| 69 |
+
|
| 70 |
+
## Enforcement Guidelines
|
| 71 |
+
|
| 72 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
| 73 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
| 74 |
+
|
| 75 |
+
### 1. Correction
|
| 76 |
+
|
| 77 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
| 78 |
+
unprofessional or unwelcome in the community.
|
| 79 |
+
|
| 80 |
+
**Consequence**: A private, written warning from community leaders, providing
|
| 81 |
+
clarity around the nature of the violation and an explanation of why the
|
| 82 |
+
behavior was inappropriate. A public apology may be requested.
|
| 83 |
+
|
| 84 |
+
### 2. Warning
|
| 85 |
+
|
| 86 |
+
**Community Impact**: A violation through a single incident or series
|
| 87 |
+
of actions.
|
| 88 |
+
|
| 89 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
| 90 |
+
interaction with the people involved, including unsolicited interaction with
|
| 91 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
| 92 |
+
includes avoiding interactions in community spaces as well as external channels
|
| 93 |
+
like social media. Violating these terms may lead to a temporary or
|
| 94 |
+
permanent ban.
|
| 95 |
+
|
| 96 |
+
### 3. Temporary Ban
|
| 97 |
+
|
| 98 |
+
**Community Impact**: A serious violation of community standards, including
|
| 99 |
+
sustained inappropriate behavior.
|
| 100 |
+
|
| 101 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
| 102 |
+
communication with the community for a specified period of time. No public or
|
| 103 |
+
private interaction with the people involved, including unsolicited interaction
|
| 104 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
| 105 |
+
Violating these terms may lead to a permanent ban.
|
| 106 |
+
|
| 107 |
+
### 4. Permanent Ban
|
| 108 |
+
|
| 109 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
| 110 |
+
standards, including sustained inappropriate behavior, harassment of an
|
| 111 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
| 112 |
+
|
| 113 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
| 114 |
+
the community.
|
| 115 |
+
|
| 116 |
+
## Attribution
|
| 117 |
+
|
| 118 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
| 119 |
+
version 2.0, available at
|
| 120 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
| 121 |
+
|
| 122 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
| 123 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
| 124 |
+
|
| 125 |
+
[homepage]: https://www.contributor-covenant.org
|
| 126 |
+
|
| 127 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
| 128 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
| 129 |
+
https://www.contributor-covenant.org/translations.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## Contact
|
| 134 |
+
|
| 135 |
+
For any Code of Conduct concerns or questions, please contact me through emails and linked:
|
| 136 |
+
**www.linkedin.com/in/akram-alsubari**
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to dytr
|
| 2 |
+
|
| 3 |
+
## How to Contribute
|
| 4 |
+
|
| 5 |
+
1. Fork the repository
|
| 6 |
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
| 7 |
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
| 8 |
+
4. Push to branch (`git push origin feature/amazing-feature`)
|
| 9 |
+
5. Open a Pull Request
|
| 10 |
+
|
| 11 |
+
## Pull Request Process
|
| 12 |
+
|
| 13 |
+
1. Ensure your code passes all tests
|
| 14 |
+
2. Update documentation if needed
|
| 15 |
+
3. PR will be merged after approval
|
| 16 |
+
|
| 17 |
+
## Development Setup
|
| 18 |
+
|
| 19 |
+
```bash
|
| 20 |
+
git clone https://github.com/AAlsubari/dytr.git
|
| 21 |
+
cd dytr
|
| 22 |
+
pip install -e .[dev]
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
MANIFEST.in
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
include LICENSE
|
| 2 |
+
include NOTICE
|
| 3 |
+
include README.md
|
| 4 |
+
include pyproject.toml
|
| 5 |
+
recursive-include src/dytr *.py
|
| 6 |
+
recursive-include src/dytr *.json
|
| 7 |
+
recursive-include src/dytr *.txt
|
NOTICE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dytr - Dynamic Transformer Library
|
| 2 |
+
Copyright 2025 Akram Alsubari
|
| 3 |
+
|
| 4 |
+
This product includes software developed by:
|
| 5 |
+
Akram Alsubari (http://www.linkedin.com/in/akram-alsubari)
|
| 6 |
+
|
| 7 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 8 |
+
you may not use this file except in compliance with the License.
|
| 9 |
+
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
| 10 |
+
|
| 11 |
+
Unless required by applicable law or agreed to in writing, software
|
| 12 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 13 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 14 |
+
See the License for the specific language governing permissions and
|
| 15 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,48 +1,15 @@
|
|
| 1 |
-
---
|
| 2 |
-
license: apache-2.0
|
| 3 |
-
language:
|
| 4 |
-
- ar
|
| 5 |
-
- en
|
| 6 |
-
- af
|
| 7 |
-
- fr
|
| 8 |
-
- fa
|
| 9 |
-
- ff
|
| 10 |
-
- fi
|
| 11 |
-
- fj
|
| 12 |
-
- fo
|
| 13 |
-
- fy
|
| 14 |
-
- qu
|
| 15 |
-
- zh
|
| 16 |
-
- za
|
| 17 |
-
- zu
|
| 18 |
-
- he
|
| 19 |
-
- es
|
| 20 |
-
- el
|
| 21 |
-
- ro
|
| 22 |
-
- ta
|
| 23 |
-
- pa
|
| 24 |
-
- hi
|
| 25 |
-
- de
|
| 26 |
-
metrics:
|
| 27 |
-
- perplexity
|
| 28 |
-
- f1
|
| 29 |
-
- accuracy
|
| 30 |
-
base_model:
|
| 31 |
-
- google-bert/bert-base-uncased
|
| 32 |
-
datasets:
|
| 33 |
-
- alsubari/arabic-grammar-errors
|
| 34 |
-
---
|
| 35 |
# dytr - Dynamic Transformer Library
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
[](https://www.python.org/downloads/)
|
| 38 |
[](https://opensource.org/licenses/Apache-2.0)
|
| 39 |
[](https://badge.fury.io/py/dytr)
|
| 40 |
[](https://github.com/AAlsubari/dytr/blob/main/dytr_bert_finetune_demo.ipynb)
|
| 41 |
|
| 42 |
-
**Build dynamic transformers that learn multiple tasks
|
| 43 |
-
|
| 44 |
-
dytr is a flexible PyTorch library for multi-task learning with transformer architectures. Train multiple tasks sequentially or simultaneously while preserving performance on previous tasks through built-in continual learning techniques.
|
| 45 |
-
|
| 46 |
## Why dytr?
|
| 47 |
|
| 48 |
- 🎯 **Multi-Task Ready** - Train classification, generation, and sequence tasks in one model
|
|
@@ -50,14 +17,12 @@ dytr is a flexible PyTorch library for multi-task learning with transformer arch
|
|
| 50 |
- 🔧 **No Black Box** - Full control over architecture, understand every component
|
| 51 |
- ⚡ **Lightweight** - Pure PyTorch, minimal dependencies
|
| 52 |
- 📦 **Pretrained Support** - Load BERT, RoBERTa, and more as your encoder backbone and fine tune it on multiple tasks.
|
| 53 |
-
|
| 54 |
|
| 55 |
## Architecture Overview
|
| 56 |
|
| 57 |
-

|
| 58 |
-
|
| 59 |
-
|
| 60 |
|
|
|
|
| 61 |
|
| 62 |
## Installation
|
| 63 |
|
|
@@ -162,12 +127,12 @@ Load powerful encoders as your backbone and extend them with tasks:
|
|
| 162 |
|
| 163 |
```python
|
| 164 |
from dytr import PretrainedModelLoader
|
| 165 |
-
|
| 166 |
loader = PretrainedModelLoader()
|
| 167 |
-
config = ModelConfig(tokenizer_name=
|
| 168 |
|
| 169 |
# Load pretrained BERT as your encoder
|
| 170 |
-
model = loader.load_pretrained(
|
| 171 |
|
| 172 |
# Now add your own tasks - the model is fully dytr compatible
|
| 173 |
class_train = pd.DataFrame(
|
|
@@ -229,7 +194,6 @@ train_datasets = {
|
|
| 229 |
classification_task.task_name: (class_dataset, classification_task.training_strategy),
|
| 230 |
lm_task.task_name: (lm_dataset, lm_task.training_strategy),
|
| 231 |
}
|
| 232 |
-
|
| 233 |
val_datasets = {
|
| 234 |
#classification_task.task_name: (class_val_dataset, classification_task.training_strategy)
|
| 235 |
}
|
|
@@ -278,6 +242,8 @@ config = ModelConfig(
|
|
| 278 |
|
| 279 |
|
| 280 |
|
|
|
|
|
|
|
| 281 |
## Who Should Use dytr?
|
| 282 |
|
| 283 |
| Audience | Why It Matters |
|
|
@@ -315,15 +281,10 @@ config = ModelConfig(
|
|
| 315 |
|
| 316 |
Apache License 2.0
|
| 317 |
|
| 318 |
-
## project links
|
| 319 |
-
- [PyPI Page](https://pypi.org/project/dytr/)
|
| 320 |
-
- [GitHub Repository](https://github.com/AAlsubari/dytr)
|
| 321 |
-
|
| 322 |
## Author
|
| 323 |
|
| 324 |
**Dr. Akram Alsubari**
|
| 325 |
-
|
| 326 |
-
- akram.alsubari87@gmail.com
|
| 327 |
|
| 328 |
## Contributing
|
| 329 |
|
|
@@ -338,4 +299,4 @@ For questions, issues, or suggestions:
|
|
| 338 |
- 🎓 **Research Interests**: Natural Language Processing, Deep Learning, Transformers, Continual Learning, Multi-Task Learning, Large Language Models
|
| 339 |
---
|
| 340 |
|
| 341 |
-
**Build once. Learn multiple tasks. Never forget.**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# dytr - Dynamic Transformer Library
|
| 2 |
+
dytr is a flexible PyTorch library for multi-task learning with dynamic transformer architectures. Train multiple tasks sequentially or simultaneously while preserving performance on previous tasks through built-in continual learning techniques. it also supports to finetune and modify pretrained model such as bert.
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
|
| 6 |
|
| 7 |
[](https://www.python.org/downloads/)
|
| 8 |
[](https://opensource.org/licenses/Apache-2.0)
|
| 9 |
[](https://badge.fury.io/py/dytr)
|
| 10 |
[](https://github.com/AAlsubari/dytr/blob/main/dytr_bert_finetune_demo.ipynb)
|
| 11 |
|
| 12 |
+
**Build dynamic transformers that learn multiple tasks.**
|
|
|
|
|
|
|
|
|
|
| 13 |
## Why dytr?
|
| 14 |
|
| 15 |
- 🎯 **Multi-Task Ready** - Train classification, generation, and sequence tasks in one model
|
|
|
|
| 17 |
- 🔧 **No Black Box** - Full control over architecture, understand every component
|
| 18 |
- ⚡ **Lightweight** - Pure PyTorch, minimal dependencies
|
| 19 |
- 📦 **Pretrained Support** - Load BERT, RoBERTa, and more as your encoder backbone and fine tune it on multiple tasks.
|
| 20 |
+
-
|
| 21 |
|
| 22 |
## Architecture Overview
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+

|
| 26 |
|
| 27 |
## Installation
|
| 28 |
|
|
|
|
| 127 |
|
| 128 |
```python
|
| 129 |
from dytr import PretrainedModelLoader
|
| 130 |
+
model_name='prajjwal1/bert-tiny'
|
| 131 |
loader = PretrainedModelLoader()
|
| 132 |
+
config = ModelConfig(tokenizer_name=model_name, per_device_train_batch_size=32, num_train_epochs=3, per_device_eval_batch_size=8, special_tokens={}, use_task_adapters=False, use_ewc=True, use_replay=True, use_rotary_embedding=False, training_from_scratch=False)
|
| 133 |
|
| 134 |
# Load pretrained BERT as your encoder
|
| 135 |
+
model = loader.load_pretrained(model_name, config)
|
| 136 |
|
| 137 |
# Now add your own tasks - the model is fully dytr compatible
|
| 138 |
class_train = pd.DataFrame(
|
|
|
|
| 194 |
classification_task.task_name: (class_dataset, classification_task.training_strategy),
|
| 195 |
lm_task.task_name: (lm_dataset, lm_task.training_strategy),
|
| 196 |
}
|
|
|
|
| 197 |
val_datasets = {
|
| 198 |
#classification_task.task_name: (class_val_dataset, classification_task.training_strategy)
|
| 199 |
}
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
+
|
| 246 |
+
|
| 247 |
## Who Should Use dytr?
|
| 248 |
|
| 249 |
| Audience | Why It Matters |
|
|
|
|
| 281 |
|
| 282 |
Apache License 2.0
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
## Author
|
| 285 |
|
| 286 |
**Dr. Akram Alsubari**
|
| 287 |
+
|
|
|
|
| 288 |
|
| 289 |
## Contributing
|
| 290 |
|
|
|
|
| 299 |
- 🎓 **Research Interests**: Natural Language Processing, Deep Learning, Transformers, Continual Learning, Multi-Task Learning, Large Language Models
|
| 300 |
---
|
| 301 |
|
| 302 |
+
**Build once. Learn multiple tasks. Never forget.**
|
SECURITY.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Security Policy
|
| 2 |
+
|
| 3 |
+
## Supported Versions
|
| 4 |
+
|
| 5 |
+
Currently supported versions with security updates:
|
| 6 |
+
|
| 7 |
+
| Version | Supported |
|
| 8 |
+
|---------|-----------|
|
| 9 |
+
| 0.1.x | ✅ Yes |
|
| 10 |
+
| < 0.1 | ❌ No |
|
| 11 |
+
|
| 12 |
+
## Reporting a Vulnerability
|
| 13 |
+
|
| 14 |
+
### How to Report
|
| 15 |
+
|
| 16 |
+
**DO NOT** create public GitHub issues for security vulnerabilities.
|
| 17 |
+
|
| 18 |
+
Please report security vulnerabilities by email to:
|
| 19 |
+
|
| 20 |
+
📧 **akram.alsubari@outlook.com**
|
| 21 |
+
|
| 22 |
+
📧 **akram.alsubari87@gmail.com** (alternative)
|
| 23 |
+
|
| 24 |
+
### What to Include
|
| 25 |
+
|
| 26 |
+
When reporting a vulnerability, please include:
|
| 27 |
+
|
| 28 |
+
- Description of the vulnerability
|
| 29 |
+
- Steps to reproduce (code examples if possible)
|
| 30 |
+
- Affected versions
|
| 31 |
+
- Potential impact
|
| 32 |
+
- Any suggested fixes (optional)
|
| 33 |
+
|
| 34 |
+
### What to Expect
|
| 35 |
+
|
| 36 |
+
| Step | Timeline | Description |
|
| 37 |
+
|------|----------|-------------|
|
| 38 |
+
| 1 | Within 48 hours | Acknowledgment of receipt |
|
| 39 |
+
| 2 | Within 5-7 days | Initial assessment and confirmation |
|
| 40 |
+
| 3 | Within 14 days | Fix development or mitigation plan |
|
| 41 |
+
| 4 | Within 30 days | Release of security update |
|
| 42 |
+
|
| 43 |
+
### Vulnerability Acceptance
|
| 44 |
+
|
| 45 |
+
If the vulnerability is accepted:
|
| 46 |
+
- You will receive credit in the release notes (unless you prefer to remain anonymous)
|
| 47 |
+
- A security advisory will be published
|
| 48 |
+
- A patch will be released for supported versions
|
| 49 |
+
|
| 50 |
+
### Vulnerability Decline
|
| 51 |
+
|
| 52 |
+
If the vulnerability is declined:
|
| 53 |
+
- You will receive a detailed explanation why
|
| 54 |
+
- The issue may be moved to a public discussion if appropriate
|
| 55 |
+
- No further action will be taken
|
| 56 |
+
|
| 57 |
+
## Security Best Practices for Users
|
| 58 |
+
|
| 59 |
+
When using dytr:
|
| 60 |
+
|
| 61 |
+
1. **Keep updated**: Always use the latest version
|
| 62 |
+
2. **Validate inputs**: Sanitize any untrusted input before passing to the model
|
| 63 |
+
3. **Use virtual environments**: Isolate dytr installations
|
| 64 |
+
4. **Review code changes**: Check pull requests before merging
|
| 65 |
+
|
| 66 |
+
## Security Updates
|
| 67 |
+
|
| 68 |
+
Security updates will be released as:
|
| 69 |
+
|
| 70 |
+
- **Patch versions** (0.1.x → 0.1.y) for critical fixes
|
| 71 |
+
- **Minor versions** (0.x → 0.y) for non-critical fixes
|
| 72 |
+
|
| 73 |
+
Watch the repository on GitHub to receive security update notifications.
|
| 74 |
+
|
| 75 |
+
## Responsible Disclosure
|
| 76 |
+
|
| 77 |
+
We follow responsible disclosure practices:
|
| 78 |
+
- Reporters will be acknowledged (unless anonymous)
|
| 79 |
+
- Fixes will be released before public disclosure
|
| 80 |
+
- Coordinated disclosure with reporters
|
| 81 |
+
|
| 82 |
+
## Contact
|
| 83 |
+
|
| 84 |
+
**Security Contact:** akram.alsubari@outlook.com
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
**Alternative Contact:** akram.alsubari87@gmail.com
|
| 88 |
+
|
| 89 |
+
## Acknowledgment
|
| 90 |
+
|
| 91 |
+
We thank the security research community for helping keep dytr secure.
|
config.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"embed_dim": 256,
|
| 3 |
-
"num_layers": 4,
|
| 4 |
-
"num_heads": 4,
|
| 5 |
-
"head_dim": 64,
|
| 6 |
-
"ff_mult": 4,
|
| 7 |
-
"max_seq_len": 512,
|
| 8 |
-
"dropout": 0.1,
|
| 9 |
-
"vocab_size": 32000,
|
| 10 |
-
"tokenizer_name": "asafaya/bert-mini-arabic",
|
| 11 |
-
"use_rotary_embedding": false,
|
| 12 |
-
"use_task_adapters": false,
|
| 13 |
-
"adapter_bottleneck": 64,
|
| 14 |
-
"tasks": [
|
| 15 |
-
"sentiment",
|
| 16 |
-
"error_detection",
|
| 17 |
-
"ner_detection",
|
| 18 |
-
"generation"
|
| 19 |
-
]
|
| 20 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
finetune_bert_mini_arabic_mltitasks_and_generation.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:814785ecd561cc5d5c82bba2a63a5ed0e8ef571af9e012a4a06667e7c7a453f0
|
| 3 |
-
size 100112722
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61.0", "wheel>=0.37.0"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "dytr"
|
| 7 |
+
version = "0.1.0"
|
| 8 |
+
description = "dytr: Dynamic Transformer for Multi-Task Learning with Continual Learning Support"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
requires-python = ">=3.8"
|
| 11 |
+
license = {text = "Apache-2.0"}
|
| 12 |
+
authors = [
|
| 13 |
+
{name = "Akram Alsubari", email = "akram.alsubari@outlook.com"}
|
| 14 |
+
]
|
| 15 |
+
maintainers = [
|
| 16 |
+
{name = "Akram Alsubari", email = "akram.alsubari87@gmail.com"}
|
| 17 |
+
]
|
| 18 |
+
keywords = [
|
| 19 |
+
"transformer",
|
| 20 |
+
"multi-task-learning",
|
| 21 |
+
"continual-learning",
|
| 22 |
+
"pytorch",
|
| 23 |
+
"nlp",
|
| 24 |
+
"deep-learning",
|
| 25 |
+
"attention",
|
| 26 |
+
"neural-networks",
|
| 27 |
+
"machine-learning",
|
| 28 |
+
"dytr"
|
| 29 |
+
]
|
| 30 |
+
classifiers = [
|
| 31 |
+
"Development Status :: 3 - Alpha",
|
| 32 |
+
"Intended Audience :: Science/Research",
|
| 33 |
+
"Intended Audience :: Developers",
|
| 34 |
+
"Intended Audience :: Education",
|
| 35 |
+
"Intended Audience :: Information Technology",
|
| 36 |
+
"License :: OSI Approved :: Apache Software License",
|
| 37 |
+
"Programming Language :: Python :: 3",
|
| 38 |
+
"Programming Language :: Python :: 3.8",
|
| 39 |
+
"Programming Language :: Python :: 3.9",
|
| 40 |
+
"Programming Language :: Python :: 3.10",
|
| 41 |
+
"Programming Language :: Python :: 3.11",
|
| 42 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 43 |
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
| 44 |
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 45 |
+
"Topic :: Text Processing :: Linguistic",
|
| 46 |
+
"Operating System :: OS Independent"
|
| 47 |
+
]
|
| 48 |
+
|
| 49 |
+
dependencies = [
|
| 50 |
+
"torch>=1.10.0",
|
| 51 |
+
"numpy>=1.19.0",
|
| 52 |
+
"pandas>=1.3.0",
|
| 53 |
+
"scikit-learn>=0.24.0",
|
| 54 |
+
"tqdm>=4.62.0",
|
| 55 |
+
"requests>=2.25.0"
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
[project.optional-dependencies]
|
| 59 |
+
transformers = [
|
| 60 |
+
"transformers>=4.0.0"
|
| 61 |
+
]
|
| 62 |
+
dev = [
|
| 63 |
+
"pytest>=6.0",
|
| 64 |
+
"pytest-cov>=2.0",
|
| 65 |
+
"black>=21.0",
|
| 66 |
+
"flake8>=3.9",
|
| 67 |
+
"mypy>=0.910",
|
| 68 |
+
"build>=0.7",
|
| 69 |
+
"twine>=3.4",
|
| 70 |
+
"isort>=5.9.0"
|
| 71 |
+
]
|
| 72 |
+
docs = ["sphinx>=4.0", "sphinx-rtd-theme>=0.5"]
|
| 73 |
+
all = [
|
| 74 |
+
"transformers>=4.0.0",
|
| 75 |
+
"pytest>=6.0",
|
| 76 |
+
"sphinx>=4.0"
|
| 77 |
+
]
|
| 78 |
+
|
| 79 |
+
[project.urls]
|
| 80 |
+
"Bug Reports" = "https://github.com/AAlsubari/dytr"
|
| 81 |
+
|
| 82 |
+
[tool.setuptools]
|
| 83 |
+
package-dir = {"" = "src"}
|
| 84 |
+
packages = {find = {where = ["src"], include = ["dytr*"]}}
|
| 85 |
+
include-package-data = true
|
| 86 |
+
|
| 87 |
+
[tool.setuptools.package-data]
|
| 88 |
+
dytr = [
|
| 89 |
+
"py.typed",
|
| 90 |
+
"**/*.json",
|
| 91 |
+
"**/*.txt",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
[tool.setuptools.dynamic]
|
| 95 |
+
version = {attr = "dytr.__version__"}
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies for Dynamic Transformers
|
| 2 |
+
|
| 3 |
+
torch>=1.10.0
|
| 4 |
+
numpy>=1.19.0
|
| 5 |
+
pandas>=1.3.0
|
| 6 |
+
scikit-learn>=0.24.0
|
| 7 |
+
tqdm>=4.62.0
|
| 8 |
+
requests>=2.25.0
|
| 9 |
+
|
| 10 |
+
# Optional dependencies (commented out)
|
| 11 |
+
# transformers>=4.0.0
|
setup.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Setup script for Dynamic Transformers library.
|
| 17 |
+
|
| 18 |
+
This file provides backward compatibility with older pip versions
|
| 19 |
+
and can be used as an alternative to pyproject.toml.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from setuptools import setup, find_packages
|
| 26 |
+
import os
|
| 27 |
+
import sys
|
| 28 |
+
|
| 29 |
+
# Read the contents of README file
|
| 30 |
+
this_directory = os.path.abspath(os.path.dirname(__file__))
|
| 31 |
+
with open(os.path.join(this_directory, 'README.md'), encoding='utf-8') as f:
|
| 32 |
+
long_description = f.read()
|
| 33 |
+
|
| 34 |
+
# Read version from __init__.py
|
| 35 |
+
version = {}
|
| 36 |
+
with open(os.path.join(this_directory, 'src', 'dytr', '__init__.py'), encoding='utf-8') as f:
|
| 37 |
+
for line in f:
|
| 38 |
+
if line.startswith('__version__'):
|
| 39 |
+
exec(line, version)
|
| 40 |
+
break
|
| 41 |
+
|
| 42 |
+
setup(
|
| 43 |
+
name="dytr",
|
| 44 |
+
version=version.get('__version__', '0.1.0'),
|
| 45 |
+
author="Akram Alsubari",
|
| 46 |
+
author_email="akram.alsubari@outlook.com",
|
| 47 |
+
maintainer="Akram Alsubari",
|
| 48 |
+
maintainer_email="akram.alsubari87@gmail.com",
|
| 49 |
+
description="Dynamic Transformer for Multi-Task Learning with Continual Learning Support",
|
| 50 |
+
long_description=long_description,
|
| 51 |
+
long_description_content_type="text/markdown",
|
| 52 |
+
url="", # No GitHub URL
|
| 53 |
+
project_urls={
|
| 54 |
+
"Bug Reports": "http://www.linkedin.com/in/akram-alsubari",
|
| 55 |
+
"Source": "",
|
| 56 |
+
"Documentation": "",
|
| 57 |
+
},
|
| 58 |
+
package_dir={"": "src"},
|
| 59 |
+
packages=find_packages(where="src"),
|
| 60 |
+
classifiers=[
|
| 61 |
+
"Development Status :: 3 - Alpha",
|
| 62 |
+
"Intended Audience :: Science/Research",
|
| 63 |
+
"Intended Audience :: Developers",
|
| 64 |
+
"Intended Audience :: Education",
|
| 65 |
+
"Intended Audience :: Information Technology",
|
| 66 |
+
"License :: OSI Approved :: Apache Software License",
|
| 67 |
+
"Programming Language :: Python :: 3",
|
| 68 |
+
"Programming Language :: Python :: 3.8",
|
| 69 |
+
"Programming Language :: Python :: 3.9",
|
| 70 |
+
"Programming Language :: Python :: 3.10",
|
| 71 |
+
"Programming Language :: Python :: 3.11",
|
| 72 |
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
| 73 |
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
| 74 |
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
| 75 |
+
"Topic :: Text Processing :: Linguistic",
|
| 76 |
+
"Operating System :: OS Independent",
|
| 77 |
+
],
|
| 78 |
+
python_requires=">=3.8",
|
| 79 |
+
install_requires=[
|
| 80 |
+
"torch>=1.10.0",
|
| 81 |
+
"numpy>=1.19.0",
|
| 82 |
+
"pandas>=1.3.0",
|
| 83 |
+
"scikit-learn>=0.24.0",
|
| 84 |
+
"tqdm>=4.62.0",
|
| 85 |
+
"requests>=2.25.0",
|
| 86 |
+
],
|
| 87 |
+
extras_require={
|
| 88 |
+
"transformers": ["transformers>=4.0.0"],
|
| 89 |
+
"dev": [
|
| 90 |
+
"pytest>=6.0",
|
| 91 |
+
"pytest-cov>=2.0",
|
| 92 |
+
"black>=21.0",
|
| 93 |
+
"flake8>=3.9",
|
| 94 |
+
"mypy>=0.910",
|
| 95 |
+
"build>=0.7",
|
| 96 |
+
"twine>=3.4",
|
| 97 |
+
],
|
| 98 |
+
"docs": [
|
| 99 |
+
"sphinx>=4.0",
|
| 100 |
+
"sphinx-rtd-theme>=0.5",
|
| 101 |
+
],
|
| 102 |
+
"all": [
|
| 103 |
+
"transformers>=4.0.0",
|
| 104 |
+
"pytest>=6.0",
|
| 105 |
+
"sphinx>=4.0",
|
| 106 |
+
],
|
| 107 |
+
},
|
| 108 |
+
include_package_data=True,
|
| 109 |
+
zip_safe=False,
|
| 110 |
+
entry_points={
|
| 111 |
+
"console_scripts": [
|
| 112 |
+
"dytr-train=dytr.cli.train:main",
|
| 113 |
+
"dytr-export=dytr.cli.export:main",
|
| 114 |
+
],
|
| 115 |
+
},
|
| 116 |
+
)
|
src/dytr/__init__.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
dytr - Dynamic Transformer for Multi-Task Learning with Continual Learning Support
|
| 17 |
+
|
| 18 |
+
A flexible PyTorch library for multi-task learning with transformer architectures.
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
__version__ = "0.1.0"
|
| 24 |
+
__author__ = "Akram Alsubari"
|
| 25 |
+
__email__ = "akram.alsubari@outlook.com"
|
| 26 |
+
__license__ = "Apache 2.0"
|
| 27 |
+
|
| 28 |
+
from dytr.core.config import ModelConfig, TaskConfig, TrainingStrategy
|
| 29 |
+
from dytr.core.exporter import ModelExporter
|
| 30 |
+
|
| 31 |
+
# Core model components
|
| 32 |
+
from dytr.core.model import DynamicTransformer
|
| 33 |
+
|
| 34 |
+
# Memory/Continual learning components
|
| 35 |
+
from dytr.memory.ewc import EWC
|
| 36 |
+
from dytr.memory.replay import ReplayBuffer
|
| 37 |
+
from dytr.tokenization.download_manager import (
|
| 38 |
+
DownloadManager,
|
| 39 |
+
download_file,
|
| 40 |
+
download_tokenizer_files,
|
| 41 |
+
get_url_from_HF,
|
| 42 |
+
process_vocab_text,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
# Tokenizer
|
| 46 |
+
from dytr.tokenization.simple_tokenizer import SimpleTokenizer
|
| 47 |
+
# Pretrained model loading
|
| 48 |
+
#from dytr.pretrained import PretrainedModelLoader
|
| 49 |
+
from dytr.pretrained.loader import PretrainedModelLoader
|
| 50 |
+
from dytr.training.data import MultiTaskDataset, TaskAwareBatchSampler, collate_fn
|
| 51 |
+
from dytr.training.dataset import SingleDatasetProcessing
|
| 52 |
+
|
| 53 |
+
# Training components
|
| 54 |
+
from dytr.training.trainer import Trainer
|
| 55 |
+
|
| 56 |
+
# Utility functions
|
| 57 |
+
from dytr.training.utils import process_qa_dataset, set_seed
|
| 58 |
+
from dytr.utils.caching import CacheManager, get_cache_manager
|
| 59 |
+
from dytr.utils.logging import get_logger, set_log_level, disable_logging
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# Define what gets imported with "from dynamic_transformers import *"
|
| 63 |
+
__all__ = [
|
| 64 |
+
# Core classes
|
| 65 |
+
"DynamicTransformer",
|
| 66 |
+
"ModelConfig",
|
| 67 |
+
"TaskConfig",
|
| 68 |
+
"TrainingStrategy",
|
| 69 |
+
"Trainer",
|
| 70 |
+
"ModelExporter",
|
| 71 |
+
# Dataset classes
|
| 72 |
+
"SingleDatasetProcessing",
|
| 73 |
+
"MultiTaskDataset",
|
| 74 |
+
"TaskAwareBatchSampler",
|
| 75 |
+
"collate_fn",
|
| 76 |
+
# Continual learning
|
| 77 |
+
"EWC",
|
| 78 |
+
"ReplayBuffer",
|
| 79 |
+
# Pretrained Models
|
| 80 |
+
"PretrainedModelLoader",
|
| 81 |
+
# Tokenizer
|
| 82 |
+
"SimpleTokenizer",
|
| 83 |
+
"DownloadManager",
|
| 84 |
+
"download_file",
|
| 85 |
+
"download_tokenizer_files",
|
| 86 |
+
"get_url_from_HF",
|
| 87 |
+
"process_vocab_text",
|
| 88 |
+
# Utilities
|
| 89 |
+
"set_seed",
|
| 90 |
+
"process_qa_dataset",
|
| 91 |
+
"CacheManager",
|
| 92 |
+
"get_cache_manager",
|
| 93 |
+
"get_logger",
|
| 94 |
+
"set_log_level",
|
| 95 |
+
"disable_logging",
|
| 96 |
+
# Version info
|
| 97 |
+
"__version__",
|
| 98 |
+
"__author__",
|
| 99 |
+
"__email__",
|
| 100 |
+
"__license__",
|
| 101 |
+
]
|
| 102 |
+
|
src/dytr/cli/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Command-line interface for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides CLI tools for training and exporting models.
|
| 19 |
+
|
| 20 |
+
Author: Akram Alsubari
|
| 21 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
from dytr.cli.export import main as export_main
|
| 25 |
+
from dytr.cli.train import main as train_main
|
| 26 |
+
|
| 27 |
+
__all__ = ["train_main", "export_main"]
|
| 28 |
+
|
src/dytr/cli/export.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Export CLI for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides a command-line interface for exporting single-task models.
|
| 19 |
+
|
| 20 |
+
Author: Akram Alsubari
|
| 21 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import sys
|
| 26 |
+
from pathlib import Path
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def parse_args():
|
| 30 |
+
"""Parse command-line arguments."""
|
| 31 |
+
parser = argparse.ArgumentParser(
|
| 32 |
+
description="Export a single-task model from a multi-task model"
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
parser.add_argument(
|
| 36 |
+
"--model_path", type=str, required=True, help="Path to the saved multi-task model"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
parser.add_argument("--task_name", type=str, required=True, help="Name of the task to export")
|
| 40 |
+
|
| 41 |
+
parser.add_argument(
|
| 42 |
+
"--output_path", type=str, required=True, help="Path to save the exported model"
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--device",
|
| 47 |
+
type=str,
|
| 48 |
+
choices=["cuda", "cpu"],
|
| 49 |
+
default=None,
|
| 50 |
+
help="Device to use for loading the model",
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
return parser.parse_args()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main():
|
| 57 |
+
"""Main export function."""
|
| 58 |
+
args = parse_args()
|
| 59 |
+
|
| 60 |
+
print("=" * 60)
|
| 61 |
+
print("Dynamic Transformers - Export CLI")
|
| 62 |
+
print("=" * 60)
|
| 63 |
+
|
| 64 |
+
print(f"\nLoading model from: {args.model_path}")
|
| 65 |
+
print(f"Exporting task: {args.task_name}")
|
| 66 |
+
print(f"Saving to: {args.output_path}")
|
| 67 |
+
|
| 68 |
+
# Import library components
|
| 69 |
+
from dytr import DynamicTransformer
|
| 70 |
+
|
| 71 |
+
# Load model
|
| 72 |
+
model = DynamicTransformer.load_model(args.model_path, device=args.device)
|
| 73 |
+
|
| 74 |
+
if model is None:
|
| 75 |
+
print("Error: Failed to load model")
|
| 76 |
+
sys.exit(1)
|
| 77 |
+
|
| 78 |
+
# Export task
|
| 79 |
+
exporter = model.get_exporter()
|
| 80 |
+
exported_model = exporter.export_single_task(args.task_name, args.output_path)
|
| 81 |
+
|
| 82 |
+
print(f"\nSuccessfully exported {args.task_name} to {args.output_path}")
|
| 83 |
+
|
| 84 |
+
print("\n" + "=" * 60)
|
| 85 |
+
print("Export completed successfully!")
|
| 86 |
+
print("=" * 60)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
if __name__ == "__main__":
|
| 90 |
+
main()
|
src/dytr/cli/train.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Training CLI for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides a command-line interface for training models.
|
| 19 |
+
|
| 20 |
+
Author: Akram Alsubari
|
| 21 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import json
|
| 26 |
+
import sys
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def parse_args():
|
| 31 |
+
"""Parse command-line arguments."""
|
| 32 |
+
parser = argparse.ArgumentParser(description="Train a Dynamic Transformer model")
|
| 33 |
+
|
| 34 |
+
parser.add_argument(
|
| 35 |
+
"--config", type=str, required=True, help="Path to configuration file (JSON)"
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
parser.add_argument("--train_data", type=str, required=True, help="Path to training data file")
|
| 39 |
+
|
| 40 |
+
parser.add_argument("--val_data", type=str, help="Path to validation data file (optional)")
|
| 41 |
+
|
| 42 |
+
parser.add_argument(
|
| 43 |
+
"--output_dir",
|
| 44 |
+
type=str,
|
| 45 |
+
default="./experiments",
|
| 46 |
+
help="Directory to save model checkpoints",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
parser.add_argument(
|
| 50 |
+
"--device",
|
| 51 |
+
type=str,
|
| 52 |
+
choices=["cuda", "cpu"],
|
| 53 |
+
default=None,
|
| 54 |
+
help="Device to use for training",
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
parser.add_argument("--seed", type=int, default=42, help="Random seed for reproducibility")
|
| 58 |
+
|
| 59 |
+
return parser.parse_args()
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def main():
|
| 63 |
+
"""Main training function."""
|
| 64 |
+
args = parse_args()
|
| 65 |
+
|
| 66 |
+
print("=" * 60)
|
| 67 |
+
print("Dynamic Transformers - Training CLI")
|
| 68 |
+
print("=" * 60)
|
| 69 |
+
|
| 70 |
+
# Load configuration
|
| 71 |
+
print(f"\nLoading configuration from: {args.config}")
|
| 72 |
+
with open(args.config, "r") as f:
|
| 73 |
+
config_dict = json.load(f)
|
| 74 |
+
|
| 75 |
+
# Load data
|
| 76 |
+
print(f"\nLoading training data from: {args.train_data}")
|
| 77 |
+
import pandas as pd
|
| 78 |
+
|
| 79 |
+
train_df = (
|
| 80 |
+
pd.read_csv(args.train_data)
|
| 81 |
+
if args.train_data.endswith(".csv")
|
| 82 |
+
else pd.read_json(args.train_data)
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
val_df = None
|
| 86 |
+
if args.val_data:
|
| 87 |
+
print(f"Loading validation data from: {args.val_data}")
|
| 88 |
+
val_df = (
|
| 89 |
+
pd.read_csv(args.val_data)
|
| 90 |
+
if args.val_data.endswith(".csv")
|
| 91 |
+
else pd.read_json(args.val_data)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# Import library components
|
| 95 |
+
from dytr import (
|
| 96 |
+
DynamicTransformer,
|
| 97 |
+
ModelConfig,
|
| 98 |
+
SingleDatasetProcessing,
|
| 99 |
+
TaskConfig,
|
| 100 |
+
Trainer,
|
| 101 |
+
TrainingStrategy,
|
| 102 |
+
set_seed,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# Set seed
|
| 106 |
+
set_seed(args.seed)
|
| 107 |
+
|
| 108 |
+
# Create configuration
|
| 109 |
+
config = ModelConfig(**config_dict.get("model", {}))
|
| 110 |
+
if args.device:
|
| 111 |
+
config.device = args.device
|
| 112 |
+
|
| 113 |
+
# Create task configuration
|
| 114 |
+
task_config = TaskConfig(
|
| 115 |
+
task_name=config_dict.get("task_name", "task"),
|
| 116 |
+
training_strategy=TrainingStrategy(config_dict.get("strategy", "sentence_classification")),
|
| 117 |
+
datasets=[{"train": train_df}],
|
| 118 |
+
num_labels=config_dict.get("num_labels"),
|
| 119 |
+
text_column=config_dict.get("text_column", "text"),
|
| 120 |
+
label_column=config_dict.get("label_column", "label"),
|
| 121 |
+
max_length=config_dict.get("max_length", 256),
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
# Initialize model
|
| 125 |
+
print("\nInitializing model...")
|
| 126 |
+
model = DynamicTransformer(config)
|
| 127 |
+
|
| 128 |
+
# Prepare datasets
|
| 129 |
+
print("Preparing datasets...")
|
| 130 |
+
train_dataset = SingleDatasetProcessing(
|
| 131 |
+
df=train_df,
|
| 132 |
+
tokenizer=model.tokenizer,
|
| 133 |
+
max_len=task_config.max_length,
|
| 134 |
+
task_name=task_config.task_name,
|
| 135 |
+
strategy=task_config.training_strategy,
|
| 136 |
+
num_labels=task_config.num_labels,
|
| 137 |
+
text_column=task_config.text_column,
|
| 138 |
+
label_column=task_config.label_column,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
train_datasets = {task_config.task_name: (train_dataset, task_config.training_strategy)}
|
| 142 |
+
|
| 143 |
+
val_datasets = {}
|
| 144 |
+
if val_df is not None:
|
| 145 |
+
val_dataset = SingleDatasetProcessing(
|
| 146 |
+
df=val_df,
|
| 147 |
+
tokenizer=model.tokenizer,
|
| 148 |
+
max_len=task_config.max_length,
|
| 149 |
+
task_name=task_config.task_name,
|
| 150 |
+
strategy=task_config.training_strategy,
|
| 151 |
+
num_labels=task_config.num_labels,
|
| 152 |
+
text_column=task_config.text_column,
|
| 153 |
+
label_column=task_config.label_column,
|
| 154 |
+
)
|
| 155 |
+
val_datasets = {task_config.task_name: (val_dataset, task_config.training_strategy)}
|
| 156 |
+
|
| 157 |
+
# Train
|
| 158 |
+
print("Starting training...")
|
| 159 |
+
trainer = Trainer(model, config, exp_dir=args.output_dir)
|
| 160 |
+
model = trainer.train([task_config], train_datasets, val_datasets)
|
| 161 |
+
|
| 162 |
+
# Save final model
|
| 163 |
+
output_path = Path(args.output_dir) / "final_model.pt"
|
| 164 |
+
model.save_model(str(output_path))
|
| 165 |
+
print(f"\nModel saved to: {output_path}")
|
| 166 |
+
|
| 167 |
+
print("\n" + "=" * 60)
|
| 168 |
+
print("Training completed successfully!")
|
| 169 |
+
print("=" * 60)
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
if __name__ == "__main__":
|
| 173 |
+
main()
|
src/dytr/core/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Core transformer components for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module contains the main model classes, configuration, and core building blocks
|
| 19 |
+
for the transformer architecture including attention mechanisms, feedforward networks,
|
| 20 |
+
and the main DynamicTransformer class.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
from dytr.core.config import ModelConfig, TaskConfig, TrainingStrategy
|
| 24 |
+
from dytr.core.decoder import DecoderLayer, TransformerDecoder
|
| 25 |
+
from dytr.core.encoder import EncoderLayer, TransformerEncoder
|
| 26 |
+
from dytr.core.exporter import ModelExporter
|
| 27 |
+
from dytr.core.layers import (
|
| 28 |
+
FeedForward,
|
| 29 |
+
MultiHeadAttention,
|
| 30 |
+
RotaryEmbedding,
|
| 31 |
+
apply_rotary_pos_emb,
|
| 32 |
+
rotate_half,
|
| 33 |
+
)
|
| 34 |
+
from dytr.core.model import DynamicTransformer
|
| 35 |
+
|
| 36 |
+
__all__ = [
|
| 37 |
+
"DynamicTransformer",
|
| 38 |
+
"ModelConfig",
|
| 39 |
+
"TaskConfig",
|
| 40 |
+
"TrainingStrategy",
|
| 41 |
+
"RotaryEmbedding",
|
| 42 |
+
"MultiHeadAttention",
|
| 43 |
+
"FeedForward",
|
| 44 |
+
"rotate_half",
|
| 45 |
+
"apply_rotary_pos_emb",
|
| 46 |
+
"TransformerEncoder",
|
| 47 |
+
"EncoderLayer",
|
| 48 |
+
"TransformerDecoder",
|
| 49 |
+
"DecoderLayer",
|
| 50 |
+
"ModelExporter",
|
| 51 |
+
]
|
| 52 |
+
|
src/dytr/core/config.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Configuration classes for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module defines the configuration dataclasses for model architecture,
|
| 19 |
+
training parameters, and task-specific settings.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from dataclasses import dataclass, field
|
| 26 |
+
from enum import Enum
|
| 27 |
+
from typing import Any, Dict, List, Optional
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TrainingStrategy(Enum):
|
| 31 |
+
"""Enumeration of supported training strategies."""
|
| 32 |
+
|
| 33 |
+
CAUSAL_LM = "causal_lm"
|
| 34 |
+
SEQ2SEQ = "seq2seq"
|
| 35 |
+
SENTENCE_CLASSIFICATION = "sentence_classification"
|
| 36 |
+
TOKEN_CLASSIFICATION = "token_classification"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@dataclass
|
| 40 |
+
class ModelConfig:
|
| 41 |
+
"""
|
| 42 |
+
Configuration class for the Dynamic Transformer model.
|
| 43 |
+
|
| 44 |
+
Contains all hyperparameters and settings for model architecture,
|
| 45 |
+
training, and continual learning features.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
# Architecture parameters
|
| 49 |
+
embed_dim: int = 256
|
| 50 |
+
num_layers: int = 6
|
| 51 |
+
num_heads: int = 8
|
| 52 |
+
head_dim: int = 256//8 # embed_dim//num_heads
|
| 53 |
+
ff_mult: int = 4
|
| 54 |
+
dropout: float = 0.1
|
| 55 |
+
max_seq_len: int = 256
|
| 56 |
+
|
| 57 |
+
# Training parameters
|
| 58 |
+
learning_rate: float = 3e-4
|
| 59 |
+
batch_size: int = 16
|
| 60 |
+
weight_decay: float = 0.01
|
| 61 |
+
gradient_clip: float = 1.0
|
| 62 |
+
warmup_steps: int = 1000
|
| 63 |
+
max_learning_rate: float = 5e-4
|
| 64 |
+
min_learning_rate: float = 1e-6
|
| 65 |
+
adam_epsilon: float = 1e-8
|
| 66 |
+
label_smoothing: float = 0.1
|
| 67 |
+
|
| 68 |
+
# Advanced training features
|
| 69 |
+
fp16: bool = False
|
| 70 |
+
gradient_accumulation_steps: int = 1
|
| 71 |
+
max_grad_norm: float = 1.0
|
| 72 |
+
|
| 73 |
+
# Evaluation and checkpointing
|
| 74 |
+
patience: int = 3
|
| 75 |
+
evaluation_strategy: str = "steps"
|
| 76 |
+
logging_steps: int = 50
|
| 77 |
+
validation_check_interval: int = 500
|
| 78 |
+
load_best_model_at_end: bool = True
|
| 79 |
+
metric_for_best_model: str = "loss"
|
| 80 |
+
early_stopping_patience: int = 10
|
| 81 |
+
|
| 82 |
+
# Training duration
|
| 83 |
+
max_train_steps: int = 100000
|
| 84 |
+
num_train_epochs: int = 3
|
| 85 |
+
lr_scheduler_type: str = "cosine"
|
| 86 |
+
|
| 87 |
+
# Data loading
|
| 88 |
+
per_device_train_batch_size: int = 8
|
| 89 |
+
per_device_eval_batch_size: int = 8
|
| 90 |
+
dataloader_num_workers: int = 2
|
| 91 |
+
dataloader_pin_memory: bool = True
|
| 92 |
+
|
| 93 |
+
# Randomness
|
| 94 |
+
seed: int = 42
|
| 95 |
+
|
| 96 |
+
# Model enhancements
|
| 97 |
+
task_specific_lr: Dict[str, float] = field(default_factory=dict)
|
| 98 |
+
task_weights: Dict[str, float] = field(default_factory=dict)
|
| 99 |
+
use_rotary_embedding: bool = True
|
| 100 |
+
use_flash_attention: bool = False
|
| 101 |
+
gradient_checkpointing: bool = False
|
| 102 |
+
training_from_scratch: bool=False
|
| 103 |
+
|
| 104 |
+
# Special tokens
|
| 105 |
+
special_tokens: Dict[str, str] = field(
|
| 106 |
+
default_factory=lambda: {
|
| 107 |
+
#"task_sep": "<|tasksep|>",
|
| 108 |
+
#"doc_sep": "<|docsep|>",
|
| 109 |
+
#"answer_start": "<|answer|>",
|
| 110 |
+
#"bos": "<s>",
|
| 111 |
+
#"eos": "</s>",
|
| 112 |
+
}
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Task configuration
|
| 116 |
+
window_size: int = 256
|
| 117 |
+
stride: int = 64
|
| 118 |
+
tasks: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
| 119 |
+
|
| 120 |
+
# Tokenizer IDs
|
| 121 |
+
vocab_size: Optional[int] = None
|
| 122 |
+
tokenizer_name: str = "bert-base-multilingual-cased"
|
| 123 |
+
add_tab_newline_vocab: bool= False
|
| 124 |
+
use_simple_tokenizer: bool = True
|
| 125 |
+
tokenizer_type: str = 'wordpiece'
|
| 126 |
+
bos_token_id: Optional[int] = None
|
| 127 |
+
eos_token_id: Optional[int] = None
|
| 128 |
+
|
| 129 |
+
# Continual learning
|
| 130 |
+
adapter_bottleneck: int = 64
|
| 131 |
+
use_task_adapters: bool = True
|
| 132 |
+
ewc_lambda: float = 1000.0
|
| 133 |
+
replay_buffer_size: int = 1000
|
| 134 |
+
use_ewc: bool = False
|
| 135 |
+
use_replay: bool = False
|
| 136 |
+
|
| 137 |
+
# Task-specific learning rate multipliers
|
| 138 |
+
causal_lm_window_size: int = 256
|
| 139 |
+
causal_lm_stride: int = 128
|
| 140 |
+
head_lr_mult: float = 1.0 # Learning rate multiplier for task heads
|
| 141 |
+
decoder_lr_mult: float = 1.0 # Learning rate multiplier for task decoders
|
| 142 |
+
shared_lr_mult: float = 0.5 # Learning rate multiplier for shared components
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
# Device
|
| 146 |
+
device: str = "cuda" if __import__("torch").cuda.is_available() else "cpu"
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@dataclass
|
| 150 |
+
class TaskConfig:
|
| 151 |
+
"""
|
| 152 |
+
Configuration class for individual tasks.
|
| 153 |
+
|
| 154 |
+
Defines the dataset, training strategy, and task-specific parameters
|
| 155 |
+
for a single task in multi-task learning.
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
# Basic task info
|
| 159 |
+
task_name: str
|
| 160 |
+
training_strategy: TrainingStrategy
|
| 161 |
+
|
| 162 |
+
# Dataset configuration
|
| 163 |
+
datasets: List[Dict[str, Any]] = None
|
| 164 |
+
num_labels: Optional[int] = None
|
| 165 |
+
|
| 166 |
+
# Data processing
|
| 167 |
+
max_length: int = 256
|
| 168 |
+
train_split: str = "train"
|
| 169 |
+
validation_split: str = "validation"
|
| 170 |
+
test_split: str = "test"
|
| 171 |
+
label_to_ids: Optional[Dict[str,int]]= None
|
| 172 |
+
|
| 173 |
+
# Column names
|
| 174 |
+
text_column: str = "text"
|
| 175 |
+
label_column: str = "label"
|
| 176 |
+
source_column: str = "source"
|
| 177 |
+
target_column: str = "target"
|
| 178 |
+
|
| 179 |
+
# Sampling
|
| 180 |
+
sample_size: Optional[int] = None
|
| 181 |
+
validation_sample_size: int = 100
|
| 182 |
+
test_sample_size: int = 100
|
| 183 |
+
|
| 184 |
+
# Task-specific options
|
| 185 |
+
prompt_template: Optional[str] = None
|
| 186 |
+
metrics: List[str] = field(default_factory=lambda: ["loss", "accuracy"])
|
| 187 |
+
shuffle_data: bool = True
|
| 188 |
+
min_text_length: int = 10
|
| 189 |
+
balance_classes: bool = False
|
| 190 |
+
dataset_config: Optional[Dict[str, Any]] = None
|
| 191 |
+
is_generation: bool = False
|
| 192 |
+
|
| 193 |
+
# QA specific
|
| 194 |
+
question_column: Optional[str] = None
|
| 195 |
+
answer_column: Optional[str] = None
|
| 196 |
+
conversations_column: Optional[str] = None
|
src/dytr/core/decoder.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Transformer decoder implementation for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module contains the decoder layers and the complete transformer decoder
|
| 19 |
+
with support for cross-attention, causal masking, and text generation.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn as nn
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
|
| 32 |
+
from dytr.core.layers import FeedForward, MultiHeadAttention
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class DecoderLayer(nn.Module):
|
| 36 |
+
"""
|
| 37 |
+
Single transformer decoder layer with self-attention, cross-attention, and feedforward network.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
layer_id: Layer index
|
| 41 |
+
embed_dim: Embedding dimension
|
| 42 |
+
config: Model configuration
|
| 43 |
+
has_cross_attention: Whether to include cross-attention (for seq2seq)
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, layer_id: int, embed_dim: int, config, has_cross_attention: bool = True):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.layer_id = layer_id
|
| 49 |
+
self.embed_dim = embed_dim
|
| 50 |
+
self.config = config
|
| 51 |
+
self.has_cross_attention = has_cross_attention
|
| 52 |
+
|
| 53 |
+
# Self-attention (causal)
|
| 54 |
+
self.self_attention = MultiHeadAttention(
|
| 55 |
+
embed_dim, config.num_heads, config.head_dim, config, layer_id, is_cross_attention=False
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
# Cross-attention (if enabled)
|
| 59 |
+
if has_cross_attention:
|
| 60 |
+
self.cross_attention = MultiHeadAttention(
|
| 61 |
+
embed_dim,
|
| 62 |
+
config.num_heads,
|
| 63 |
+
config.head_dim,
|
| 64 |
+
config,
|
| 65 |
+
layer_id,
|
| 66 |
+
is_cross_attention=True,
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Feedforward network
|
| 70 |
+
self.ffn = FeedForward(embed_dim, config.ff_mult, config, layer_id)
|
| 71 |
+
|
| 72 |
+
# Layer normalizations
|
| 73 |
+
self.norm1 = nn.LayerNorm(embed_dim)
|
| 74 |
+
if has_cross_attention:
|
| 75 |
+
self.norm2 = nn.LayerNorm(embed_dim)
|
| 76 |
+
self.norm3 = nn.LayerNorm(embed_dim)
|
| 77 |
+
|
| 78 |
+
# Task adapters
|
| 79 |
+
if config.use_task_adapters:
|
| 80 |
+
self.self_attn_adapter = nn.Sequential(
|
| 81 |
+
nn.Linear(embed_dim, config.adapter_bottleneck),
|
| 82 |
+
nn.GELU(),
|
| 83 |
+
nn.Linear(config.adapter_bottleneck, embed_dim),
|
| 84 |
+
)
|
| 85 |
+
if has_cross_attention:
|
| 86 |
+
self.cross_attn_adapter = nn.Sequential(
|
| 87 |
+
nn.Linear(embed_dim, config.adapter_bottleneck),
|
| 88 |
+
nn.GELU(),
|
| 89 |
+
nn.Linear(config.adapter_bottleneck, embed_dim),
|
| 90 |
+
)
|
| 91 |
+
self.ffn_adapter = nn.Sequential(
|
| 92 |
+
nn.Linear(embed_dim, config.adapter_bottleneck),
|
| 93 |
+
nn.GELU(),
|
| 94 |
+
nn.Linear(config.adapter_bottleneck, embed_dim),
|
| 95 |
+
)
|
| 96 |
+
else:
|
| 97 |
+
self.self_attn_adapter = None
|
| 98 |
+
self.cross_attn_adapter = None
|
| 99 |
+
self.ffn_adapter = None
|
| 100 |
+
|
| 101 |
+
def forward(
|
| 102 |
+
self,
|
| 103 |
+
x: torch.Tensor,
|
| 104 |
+
encoder_output: Optional[torch.Tensor] = None,
|
| 105 |
+
src_mask: Optional[torch.Tensor] = None,
|
| 106 |
+
tgt_mask: Optional[torch.Tensor] = None,
|
| 107 |
+
task_name: Optional[str] = None,
|
| 108 |
+
):
|
| 109 |
+
"""
|
| 110 |
+
Forward pass through decoder layer.
|
| 111 |
+
|
| 112 |
+
Args:
|
| 113 |
+
x: Input tensor [batch_size, seq_len, embed_dim]
|
| 114 |
+
encoder_output: Encoder output for cross-attention
|
| 115 |
+
src_mask: Source attention mask
|
| 116 |
+
tgt_mask: Target attention mask (causal)
|
| 117 |
+
task_name: Current task name for adapter selection
|
| 118 |
+
"""
|
| 119 |
+
# Self-attention with residual connection
|
| 120 |
+
residual = x
|
| 121 |
+
x = self.norm1(x)
|
| 122 |
+
x = self.self_attention(x, encoder_output=None, mask=tgt_mask, is_causal=True)
|
| 123 |
+
if self.self_attn_adapter is not None and task_name is not None:
|
| 124 |
+
x = x + self.self_attn_adapter(x)
|
| 125 |
+
x = residual + x
|
| 126 |
+
|
| 127 |
+
# Cross-attention with residual connection (if enabled)
|
| 128 |
+
if self.has_cross_attention and encoder_output is not None:
|
| 129 |
+
residual = x
|
| 130 |
+
x = self.norm2(x)
|
| 131 |
+
x = self.cross_attention(
|
| 132 |
+
x, encoder_output=encoder_output, mask=src_mask, is_causal=False
|
| 133 |
+
)
|
| 134 |
+
if self.cross_attn_adapter is not None and task_name is not None:
|
| 135 |
+
x = x + self.cross_attn_adapter(x)
|
| 136 |
+
x = residual + x
|
| 137 |
+
|
| 138 |
+
# Feedforward with residual connection
|
| 139 |
+
residual = x
|
| 140 |
+
x = self.norm3(x)
|
| 141 |
+
x = self.ffn(x)
|
| 142 |
+
if self.ffn_adapter is not None and task_name is not None:
|
| 143 |
+
x = x + self.ffn_adapter(x)
|
| 144 |
+
x = residual + x
|
| 145 |
+
|
| 146 |
+
return x
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class TransformerDecoder(nn.Module):
|
| 150 |
+
"""
|
| 151 |
+
Complete transformer decoder consisting of multiple decoder layers.
|
| 152 |
+
|
| 153 |
+
Supports both causal LM (no cross-attention) and seq2seq (with cross-attention) modes.
|
| 154 |
+
|
| 155 |
+
Args:
|
| 156 |
+
config: Model configuration
|
| 157 |
+
task_name: Name of the task this decoder belongs to
|
| 158 |
+
has_cross_attention: Whether to include cross-attention layers
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
def __init__(self, config, task_name: str, has_cross_attention: bool = True):
|
| 162 |
+
super().__init__()
|
| 163 |
+
self.config = config
|
| 164 |
+
self.task_name = task_name
|
| 165 |
+
self.has_cross_attention = has_cross_attention
|
| 166 |
+
|
| 167 |
+
# Embedding will be set later to share with encoder
|
| 168 |
+
self.embedding = None
|
| 169 |
+
|
| 170 |
+
# Decoder layers
|
| 171 |
+
self.layers = nn.ModuleList(
|
| 172 |
+
[
|
| 173 |
+
DecoderLayer(i, config.embed_dim, config, has_cross_attention)
|
| 174 |
+
for i in range(config.num_layers)
|
| 175 |
+
]
|
| 176 |
+
)
|
| 177 |
+
|
| 178 |
+
# Final layer norm and output projection
|
| 179 |
+
self.norm = nn.LayerNorm(config.embed_dim)
|
| 180 |
+
self.output_proj = nn.Linear(config.embed_dim, config.vocab_size)
|
| 181 |
+
|
| 182 |
+
def set_shared_embedding(self, embedding: nn.Embedding):
|
| 183 |
+
"""Share embedding weights with encoder."""
|
| 184 |
+
self.embedding = embedding
|
| 185 |
+
|
| 186 |
+
def forward(
|
| 187 |
+
self,
|
| 188 |
+
input_ids: torch.Tensor,
|
| 189 |
+
encoder_output: Optional[torch.Tensor] = None,
|
| 190 |
+
src_mask: Optional[torch.Tensor] = None,
|
| 191 |
+
tgt_mask: Optional[torch.Tensor] = None,
|
| 192 |
+
task_name: Optional[str] = None,
|
| 193 |
+
):
|
| 194 |
+
"""
|
| 195 |
+
Forward pass through transformer decoder.
|
| 196 |
+
|
| 197 |
+
Args:
|
| 198 |
+
input_ids: Input token IDs [batch_size, seq_len]
|
| 199 |
+
encoder_output: Encoder output for cross-attention
|
| 200 |
+
src_mask: Source attention mask
|
| 201 |
+
tgt_mask: Target attention mask
|
| 202 |
+
task_name: Current task name for adapter selection
|
| 203 |
+
"""
|
| 204 |
+
x = self.embedding(input_ids)
|
| 205 |
+
|
| 206 |
+
for layer in self.layers:
|
| 207 |
+
if self.has_cross_attention:
|
| 208 |
+
x = layer(x, encoder_output, src_mask, tgt_mask, task_name or self.task_name)
|
| 209 |
+
else:
|
| 210 |
+
x = layer(x, None, None, tgt_mask, task_name or self.task_name)
|
| 211 |
+
|
| 212 |
+
x = self.norm(x)
|
| 213 |
+
logits = self.output_proj(x)
|
| 214 |
+
return logits
|
| 215 |
+
|
| 216 |
+
def generate(
|
| 217 |
+
self,
|
| 218 |
+
input_ids: Optional[torch.Tensor] = None,
|
| 219 |
+
encoder_output: Optional[torch.Tensor] = None,
|
| 220 |
+
src_mask: Optional[torch.Tensor] = None,
|
| 221 |
+
max_len: int = 50,
|
| 222 |
+
temperature: float = 1.0,
|
| 223 |
+
top_k: int = 0,
|
| 224 |
+
top_p: float = 0.0,
|
| 225 |
+
repetition_penalty: float = 1.0,
|
| 226 |
+
):
|
| 227 |
+
"""
|
| 228 |
+
Generate text using the decoder.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
input_ids: Optional input token IDs for causal LM
|
| 232 |
+
encoder_output: Encoder output for seq2seq
|
| 233 |
+
src_mask: Source attention mask
|
| 234 |
+
max_len: Maximum generation length
|
| 235 |
+
temperature: Sampling temperature (higher = more random)
|
| 236 |
+
top_k: Top-k sampling (0 to disable)
|
| 237 |
+
top_p: Top-p (nucleus) sampling (0 to disable)
|
| 238 |
+
repetition_penalty: Penalty for repeating tokens
|
| 239 |
+
"""
|
| 240 |
+
batch_size = encoder_output.size(0) if encoder_output is not None else input_ids.size(0)
|
| 241 |
+
device = encoder_output.device if encoder_output is not None else input_ids.device
|
| 242 |
+
|
| 243 |
+
bos_id = self.config.bos_token_id or 101 # Default BOS token
|
| 244 |
+
eos_id = self.config.eos_token_id or 102 # Default EOS token
|
| 245 |
+
|
| 246 |
+
# Initialize generated sequence
|
| 247 |
+
if input_ids is not None:
|
| 248 |
+
generated = input_ids.clone()
|
| 249 |
+
else:
|
| 250 |
+
generated = torch.ones(batch_size, 1, dtype=torch.long, device=device) * bos_id
|
| 251 |
+
|
| 252 |
+
# Generate tokens autoregressively
|
| 253 |
+
for _ in range(max_len):
|
| 254 |
+
# Create causal mask for current sequence
|
| 255 |
+
tgt_mask = torch.ones(batch_size, generated.size(1), device=device)
|
| 256 |
+
|
| 257 |
+
# Forward pass
|
| 258 |
+
if self.has_cross_attention:
|
| 259 |
+
logits = self.forward(generated, encoder_output, src_mask, tgt_mask, self.task_name)
|
| 260 |
+
else:
|
| 261 |
+
logits = self.forward(generated, tgt_mask=tgt_mask, task_name=self.task_name)
|
| 262 |
+
|
| 263 |
+
# Get next token logits and apply temperature
|
| 264 |
+
next_token_logits = logits[:, -1, :] / temperature
|
| 265 |
+
|
| 266 |
+
# Apply repetition penalty
|
| 267 |
+
for token_id in set(generated[0].tolist()):
|
| 268 |
+
next_token_logits[:, token_id] /= repetition_penalty
|
| 269 |
+
|
| 270 |
+
# Top-k filtering
|
| 271 |
+
if top_k > 0:
|
| 272 |
+
indices_to_remove = (
|
| 273 |
+
next_token_logits < torch.topk(next_token_logits, top_k)[0][..., -1, None]
|
| 274 |
+
)
|
| 275 |
+
next_token_logits[indices_to_remove] = float("-inf")
|
| 276 |
+
|
| 277 |
+
# Top-p (nucleus) filtering
|
| 278 |
+
if top_p > 0.0 and top_p < 1.0:
|
| 279 |
+
sorted_logits, sorted_indices = torch.sort(next_token_logits, descending=True)
|
| 280 |
+
cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
|
| 281 |
+
sorted_indices_to_remove = cumulative_probs > top_p
|
| 282 |
+
sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
|
| 283 |
+
sorted_indices_to_remove[..., 0] = 0
|
| 284 |
+
|
| 285 |
+
for batch_idx in range(next_token_logits.size(0)):
|
| 286 |
+
indices_to_remove = sorted_indices[batch_idx][
|
| 287 |
+
sorted_indices_to_remove[batch_idx]
|
| 288 |
+
]
|
| 289 |
+
next_token_logits[batch_idx, indices_to_remove] = float("-inf")
|
| 290 |
+
|
| 291 |
+
# Sample next token
|
| 292 |
+
probs = F.softmax(next_token_logits, dim=-1)
|
| 293 |
+
next_token = torch.multinomial(probs, num_samples=1)
|
| 294 |
+
generated = torch.cat([generated, next_token], dim=-1)
|
| 295 |
+
|
| 296 |
+
# Stop if all sequences have generated EOS
|
| 297 |
+
if (next_token == eos_id).all():
|
| 298 |
+
break
|
| 299 |
+
|
| 300 |
+
return generated
|
src/dytr/core/encoder.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Transformer encoder implementation for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module contains the encoder layers and the complete transformer encoder
|
| 19 |
+
with support for task adapters.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from typing import Optional
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
|
| 30 |
+
from dytr.core.layers import FeedForward, MultiHeadAttention
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class EncoderLayer(nn.Module):
|
| 34 |
+
"""
|
| 35 |
+
Single transformer encoder layer with self-attention and feedforward network.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
layer_id: Layer index
|
| 39 |
+
embed_dim: Embedding dimension
|
| 40 |
+
config: Model configuration
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self, layer_id: int, embed_dim: int, config):
|
| 44 |
+
super().__init__()
|
| 45 |
+
self.layer_id = layer_id
|
| 46 |
+
self.embed_dim = embed_dim
|
| 47 |
+
self.config = config
|
| 48 |
+
|
| 49 |
+
# Self-attention components
|
| 50 |
+
self.attention = MultiHeadAttention(
|
| 51 |
+
embed_dim, config.num_heads, config.head_dim, config, layer_id, is_cross_attention=False
|
| 52 |
+
)
|
| 53 |
+
self.attention_norm = nn.LayerNorm(embed_dim)
|
| 54 |
+
|
| 55 |
+
# Feedforward components
|
| 56 |
+
self.ffn_norm = nn.LayerNorm(embed_dim)
|
| 57 |
+
self.ffn = FeedForward(embed_dim, config.ff_mult, config, layer_id)
|
| 58 |
+
|
| 59 |
+
# Task adapter
|
| 60 |
+
if config.use_task_adapters:
|
| 61 |
+
self.adapter = nn.Sequential(
|
| 62 |
+
nn.Linear(embed_dim, config.adapter_bottleneck),
|
| 63 |
+
nn.GELU(),
|
| 64 |
+
nn.Linear(config.adapter_bottleneck, embed_dim),
|
| 65 |
+
)
|
| 66 |
+
else:
|
| 67 |
+
self.adapter = None
|
| 68 |
+
|
| 69 |
+
def forward(
|
| 70 |
+
self, x: torch.Tensor, mask: Optional[torch.Tensor] = None, task_name: Optional[str] = None
|
| 71 |
+
):
|
| 72 |
+
"""
|
| 73 |
+
Forward pass through encoder layer.
|
| 74 |
+
|
| 75 |
+
Args:
|
| 76 |
+
x: Input tensor [batch_size, seq_len, embed_dim]
|
| 77 |
+
mask: Attention mask
|
| 78 |
+
task_name: Current task name for adapter selection
|
| 79 |
+
"""
|
| 80 |
+
# Self-attention with residual connection
|
| 81 |
+
residual = x
|
| 82 |
+
x = self.attention_norm(x)
|
| 83 |
+
x = self.attention(x, encoder_output=None, mask=mask, is_causal=False)
|
| 84 |
+
x = residual + x
|
| 85 |
+
|
| 86 |
+
# Apply task adapter if available
|
| 87 |
+
if self.adapter is not None and task_name is not None:
|
| 88 |
+
x = x + self.adapter(x)
|
| 89 |
+
|
| 90 |
+
# Feedforward with residual connection
|
| 91 |
+
residual = x
|
| 92 |
+
x = self.ffn_norm(x)
|
| 93 |
+
x = self.ffn(x)
|
| 94 |
+
x = residual + x
|
| 95 |
+
|
| 96 |
+
return x
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class TransformerEncoder(nn.Module):
|
| 100 |
+
"""
|
| 101 |
+
Complete transformer encoder consisting of multiple encoder layers.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
config: Model configuration
|
| 105 |
+
"""
|
| 106 |
+
|
| 107 |
+
def __init__(self, config):
|
| 108 |
+
super().__init__()
|
| 109 |
+
self.config = config
|
| 110 |
+
|
| 111 |
+
# Token embedding
|
| 112 |
+
self.embedding = nn.Embedding(config.vocab_size, config.embed_dim, padding_idx=0)
|
| 113 |
+
nn.init.normal_(self.embedding.weight, mean=0.0, std=0.02)
|
| 114 |
+
|
| 115 |
+
# Encoder layers
|
| 116 |
+
self.layers = nn.ModuleList(
|
| 117 |
+
[EncoderLayer(i, config.embed_dim, config) for i in range(config.num_layers)]
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Final layer norm
|
| 121 |
+
self.norm = nn.LayerNorm(config.embed_dim)
|
| 122 |
+
|
| 123 |
+
def forward(
|
| 124 |
+
self,
|
| 125 |
+
input_ids: torch.Tensor,
|
| 126 |
+
mask: Optional[torch.Tensor] = None,
|
| 127 |
+
task_name: Optional[str] = None,
|
| 128 |
+
):
|
| 129 |
+
"""
|
| 130 |
+
Forward pass through transformer encoder.
|
| 131 |
+
|
| 132 |
+
Args:
|
| 133 |
+
input_ids: Input token IDs [batch_size, seq_len]
|
| 134 |
+
mask: Attention mask
|
| 135 |
+
task_name: Current task name for adapter selection
|
| 136 |
+
"""
|
| 137 |
+
x = self.embedding(input_ids)
|
| 138 |
+
|
| 139 |
+
for layer in self.layers:
|
| 140 |
+
x = layer(x, mask, task_name)
|
| 141 |
+
|
| 142 |
+
x = self.norm(x)
|
| 143 |
+
return x
|
src/dytr/core/exporter.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Model exporter for extracting single-task models from multi-task DynamicTransformer.
|
| 17 |
+
|
| 18 |
+
This module provides functionality to export individual task models for inference,
|
| 19 |
+
supporting all training strategies.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
from typing import Dict
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn as nn
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
|
| 32 |
+
from dytr.core.config import TrainingStrategy
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ModelExporter:
|
| 36 |
+
"""
|
| 37 |
+
Exports single-task models from a multi-task DynamicTransformer.
|
| 38 |
+
|
| 39 |
+
This class allows extracting individual task models for deployment,
|
| 40 |
+
reducing memory footprint and simplifying inference.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
model: The DynamicTransformer instance to export from
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, model):
|
| 47 |
+
self.model = model
|
| 48 |
+
self.tokenizer = model.tokenizer
|
| 49 |
+
self.config = model.config
|
| 50 |
+
|
| 51 |
+
def export_single_task(self, task_name: str, save_path: str):
|
| 52 |
+
"""
|
| 53 |
+
Export a single task model to a file.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
task_name: Name of the task to export
|
| 57 |
+
save_path: Path where to save the exported model
|
| 58 |
+
"""
|
| 59 |
+
if task_name not in self.model.current_tasks:
|
| 60 |
+
raise ValueError(f"Task {task_name} not found in model")
|
| 61 |
+
|
| 62 |
+
strategy = TrainingStrategy(self.model.current_tasks[task_name]["strategy"])
|
| 63 |
+
num_labels = self.model.current_tasks[task_name].get("num_labels")
|
| 64 |
+
|
| 65 |
+
# Sentence Classification Task
|
| 66 |
+
if strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 67 |
+
if num_labels is None:
|
| 68 |
+
task_head = self.model.task_heads[task_name]
|
| 69 |
+
num_labels = (
|
| 70 |
+
task_head[-1].out_features
|
| 71 |
+
if isinstance(task_head, nn.Sequential)
|
| 72 |
+
else task_head.out_features
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
class SingleTaskModel(nn.Module):
|
| 76 |
+
def __init__(self, encoder, task_head, config, tokenizer, num_labels, task_name):
|
| 77 |
+
super().__init__()
|
| 78 |
+
self.encoder = encoder
|
| 79 |
+
self.task_head = task_head
|
| 80 |
+
self.config = config
|
| 81 |
+
self.tokenizer = tokenizer
|
| 82 |
+
self.num_labels = num_labels
|
| 83 |
+
self.task_name = task_name
|
| 84 |
+
self.strategy = "sentence_classification"
|
| 85 |
+
|
| 86 |
+
def forward(self, input_ids, attention_mask=None):
|
| 87 |
+
x = self.encoder(input_ids, attention_mask, self.task_name)
|
| 88 |
+
if attention_mask is not None:
|
| 89 |
+
mask_sum = attention_mask.sum(dim=1, keepdim=True).clamp(min=1)
|
| 90 |
+
pooled = (x * attention_mask.unsqueeze(-1)).sum(dim=1) / mask_sum
|
| 91 |
+
else:
|
| 92 |
+
pooled = x.mean(dim=1)
|
| 93 |
+
logits = self.task_head(pooled)
|
| 94 |
+
return logits
|
| 95 |
+
|
| 96 |
+
def generate(self, text, **kwargs):
|
| 97 |
+
self.eval()
|
| 98 |
+
inputs = self.tokenizer(text, return_tensors="pt", add_special_tokens=False)
|
| 99 |
+
inputs = {k: v.to(self.config.device) for k, v in inputs.items()}
|
| 100 |
+
with torch.no_grad():
|
| 101 |
+
logits = self.forward(inputs["input_ids"], inputs.get("attention_mask"))
|
| 102 |
+
probs = F.softmax(logits, dim=-1)
|
| 103 |
+
pred_id = probs.argmax().item()
|
| 104 |
+
return {
|
| 105 |
+
"text": text,
|
| 106 |
+
"prediction": pred_id,
|
| 107 |
+
"probabilities": probs[0].cpu().tolist(),
|
| 108 |
+
"logits": logits[0].cpu().tolist(),
|
| 109 |
+
"strategy": self.strategy,
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
model = SingleTaskModel(
|
| 113 |
+
self.model.encoder,
|
| 114 |
+
self.model.task_heads[task_name],
|
| 115 |
+
self.config,
|
| 116 |
+
self.tokenizer,
|
| 117 |
+
num_labels,
|
| 118 |
+
task_name,
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
torch.save(
|
| 122 |
+
{
|
| 123 |
+
"model_state": model.state_dict(),
|
| 124 |
+
"config": self.config,
|
| 125 |
+
"task_name": task_name,
|
| 126 |
+
"strategy": strategy.value,
|
| 127 |
+
"tokenizer_name": self.config.tokenizer_name,
|
| 128 |
+
"num_labels": num_labels,
|
| 129 |
+
},
|
| 130 |
+
save_path,
|
| 131 |
+
)
|
| 132 |
+
return model
|
| 133 |
+
|
| 134 |
+
# Token Classification Task
|
| 135 |
+
elif strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 136 |
+
if num_labels is None:
|
| 137 |
+
task_head = self.model.task_heads[task_name]
|
| 138 |
+
num_labels = (
|
| 139 |
+
task_head[-1].out_features
|
| 140 |
+
if isinstance(task_head, nn.Sequential)
|
| 141 |
+
else task_head.out_features
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
class SingleTaskModel(nn.Module):
|
| 145 |
+
def __init__(self, encoder, task_head, config, tokenizer, num_labels, task_name):
|
| 146 |
+
super().__init__()
|
| 147 |
+
self.encoder = encoder
|
| 148 |
+
self.task_head = task_head
|
| 149 |
+
self.config = config
|
| 150 |
+
self.tokenizer = tokenizer
|
| 151 |
+
self.num_labels = num_labels
|
| 152 |
+
self.task_name = task_name
|
| 153 |
+
self.strategy = "token_classification"
|
| 154 |
+
|
| 155 |
+
def forward(self, input_ids, attention_mask=None):
|
| 156 |
+
x = self.encoder(input_ids, attention_mask, self.task_name)
|
| 157 |
+
logits = self.task_head(x)
|
| 158 |
+
return logits
|
| 159 |
+
|
| 160 |
+
def generate(self, text, **kwargs):
|
| 161 |
+
self.eval()
|
| 162 |
+
inputs = self.tokenizer(text, return_tensors="pt", add_special_tokens=False)
|
| 163 |
+
inputs = {k: v.to(self.config.device) for k, v in inputs.items()}
|
| 164 |
+
with torch.no_grad():
|
| 165 |
+
logits = self.forward(inputs["input_ids"], inputs.get("attention_mask"))
|
| 166 |
+
predictions = logits.argmax(dim=-1)[0].cpu().tolist()
|
| 167 |
+
tokens = self.tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
|
| 168 |
+
return {
|
| 169 |
+
"text": text,
|
| 170 |
+
"tokens": tokens,
|
| 171 |
+
"predictions": predictions,
|
| 172 |
+
"pairs": list(zip(tokens, predictions)),
|
| 173 |
+
"strategy": self.strategy,
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
model = SingleTaskModel(
|
| 177 |
+
self.model.encoder,
|
| 178 |
+
self.model.task_heads[task_name],
|
| 179 |
+
self.config,
|
| 180 |
+
self.tokenizer,
|
| 181 |
+
num_labels,
|
| 182 |
+
task_name,
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
torch.save(
|
| 186 |
+
{
|
| 187 |
+
"model_state": model.state_dict(),
|
| 188 |
+
"config": self.config,
|
| 189 |
+
"task_name": task_name,
|
| 190 |
+
"strategy": strategy.value,
|
| 191 |
+
"tokenizer_name": self.config.tokenizer_name,
|
| 192 |
+
"num_labels": num_labels,
|
| 193 |
+
},
|
| 194 |
+
save_path,
|
| 195 |
+
)
|
| 196 |
+
return model
|
| 197 |
+
|
| 198 |
+
# Seq2Seq Task
|
| 199 |
+
elif strategy == TrainingStrategy.SEQ2SEQ:
|
| 200 |
+
|
| 201 |
+
class Seq2SeqModel(nn.Module):
|
| 202 |
+
def __init__(self, encoder, decoder, config, tokenizer, task_name):
|
| 203 |
+
super().__init__()
|
| 204 |
+
self.encoder = encoder
|
| 205 |
+
self.decoder = decoder
|
| 206 |
+
self.config = config
|
| 207 |
+
self.tokenizer = tokenizer
|
| 208 |
+
self.task_name = task_name
|
| 209 |
+
self.strategy = "seq2seq"
|
| 210 |
+
|
| 211 |
+
def forward(self, input_ids, attention_mask=None, labels=None):
|
| 212 |
+
encoder_output = self.encoder(input_ids, attention_mask, self.task_name)
|
| 213 |
+
if labels is not None:
|
| 214 |
+
decoder_input = labels[:, :-1]
|
| 215 |
+
logits = self.decoder(decoder_input, encoder_output, attention_mask)
|
| 216 |
+
return logits
|
| 217 |
+
return encoder_output
|
| 218 |
+
|
| 219 |
+
def generate(
|
| 220 |
+
self,
|
| 221 |
+
text,
|
| 222 |
+
max_length=50,
|
| 223 |
+
temperature=0.8,
|
| 224 |
+
top_p=0.9,
|
| 225 |
+
top_k=40,
|
| 226 |
+
repetition_penalty=1.1,
|
| 227 |
+
**kwargs,
|
| 228 |
+
):
|
| 229 |
+
self.eval()
|
| 230 |
+
inputs = self.tokenizer(text, return_tensors="pt", add_special_tokens=False)
|
| 231 |
+
inputs = {k: v.to(self.config.device) for k, v in inputs.items()}
|
| 232 |
+
with torch.no_grad():
|
| 233 |
+
encoder_output = self.encoder(
|
| 234 |
+
inputs["input_ids"], inputs["attention_mask"], self.task_name
|
| 235 |
+
)
|
| 236 |
+
generated_ids = self.decoder.generate(
|
| 237 |
+
encoder_output=encoder_output,
|
| 238 |
+
src_mask=inputs["attention_mask"],
|
| 239 |
+
max_len=max_length,
|
| 240 |
+
temperature=temperature,
|
| 241 |
+
top_k=top_k,
|
| 242 |
+
top_p=top_p,
|
| 243 |
+
repetition_penalty=repetition_penalty,
|
| 244 |
+
)
|
| 245 |
+
generated_text = self.tokenizer.decode(
|
| 246 |
+
generated_ids[0], skip_special_tokens=True
|
| 247 |
+
)
|
| 248 |
+
return {
|
| 249 |
+
"source": text,
|
| 250 |
+
"generated": generated_text,
|
| 251 |
+
"strategy": self.strategy,
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
model = Seq2SeqModel(
|
| 255 |
+
self.model.encoder,
|
| 256 |
+
self.model.decoders[task_name],
|
| 257 |
+
self.config,
|
| 258 |
+
self.tokenizer,
|
| 259 |
+
task_name,
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
torch.save(
|
| 263 |
+
{
|
| 264 |
+
"model_state": model.state_dict(),
|
| 265 |
+
"config": self.config,
|
| 266 |
+
"task_name": task_name,
|
| 267 |
+
"strategy": strategy.value,
|
| 268 |
+
"tokenizer_name": self.config.tokenizer_name,
|
| 269 |
+
},
|
| 270 |
+
save_path,
|
| 271 |
+
)
|
| 272 |
+
return model
|
| 273 |
+
|
| 274 |
+
# Causal LM Task
|
| 275 |
+
elif strategy == TrainingStrategy.CAUSAL_LM:
|
| 276 |
+
|
| 277 |
+
class DecoderOnlyModel(nn.Module):
|
| 278 |
+
def __init__(self, decoder, config, tokenizer, task_name):
|
| 279 |
+
super().__init__()
|
| 280 |
+
self.decoder = decoder
|
| 281 |
+
self.config = config
|
| 282 |
+
self.tokenizer = tokenizer
|
| 283 |
+
self.task_name = task_name
|
| 284 |
+
self.strategy = "causal_lm"
|
| 285 |
+
|
| 286 |
+
def forward(self, input_ids, attention_mask=None):
|
| 287 |
+
return self.decoder(input_ids)
|
| 288 |
+
|
| 289 |
+
def generate(
|
| 290 |
+
self,
|
| 291 |
+
prompt,
|
| 292 |
+
max_length=50,
|
| 293 |
+
temperature=0.8,
|
| 294 |
+
top_p=0.9,
|
| 295 |
+
top_k=40,
|
| 296 |
+
repetition_penalty=1.1,
|
| 297 |
+
**kwargs,
|
| 298 |
+
):
|
| 299 |
+
self.eval()
|
| 300 |
+
inputs = self.tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
|
| 301 |
+
inputs = {k: v.to(self.config.device) for k, v in inputs.items()}
|
| 302 |
+
with torch.no_grad():
|
| 303 |
+
generated_ids = self.decoder.generate(
|
| 304 |
+
input_ids=inputs["input_ids"],
|
| 305 |
+
max_len=max_length,
|
| 306 |
+
temperature=temperature,
|
| 307 |
+
top_k=top_k,
|
| 308 |
+
top_p=top_p,
|
| 309 |
+
repetition_penalty=repetition_penalty,
|
| 310 |
+
)
|
| 311 |
+
generated_text = self.tokenizer.decode(
|
| 312 |
+
generated_ids[0], skip_special_tokens=True
|
| 313 |
+
)
|
| 314 |
+
return {
|
| 315 |
+
"prompt": prompt,
|
| 316 |
+
"generated": generated_text,
|
| 317 |
+
"strategy": self.strategy,
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
model = DecoderOnlyModel(
|
| 321 |
+
self.model.decoders[task_name], self.config, self.tokenizer, task_name
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
torch.save(
|
| 325 |
+
{
|
| 326 |
+
"model_state": model.state_dict(),
|
| 327 |
+
"config": self.config,
|
| 328 |
+
"task_name": task_name,
|
| 329 |
+
"strategy": strategy.value,
|
| 330 |
+
"tokenizer_name": self.config.tokenizer_name,
|
| 331 |
+
},
|
| 332 |
+
save_path,
|
| 333 |
+
)
|
| 334 |
+
return model
|
| 335 |
+
|
| 336 |
+
def export_all_tasks(self, save_dir: str) -> Dict[str, nn.Module]:
|
| 337 |
+
"""
|
| 338 |
+
Export all tasks from the model.
|
| 339 |
+
|
| 340 |
+
Args:
|
| 341 |
+
save_dir: Directory where to save exported models
|
| 342 |
+
|
| 343 |
+
Returns:
|
| 344 |
+
Dictionary mapping task names to exported models
|
| 345 |
+
"""
|
| 346 |
+
save_dir = Path(save_dir)
|
| 347 |
+
save_dir.mkdir(parents=True, exist_ok=True)
|
| 348 |
+
|
| 349 |
+
exported_models = {}
|
| 350 |
+
for task_name in self.model.current_tasks.keys():
|
| 351 |
+
save_path = save_dir / f"{task_name}.pt"
|
| 352 |
+
print(f" Exporting {task_name} -> {save_path}")
|
| 353 |
+
model = self.export_single_task(task_name, str(save_path))
|
| 354 |
+
exported_models[task_name] = model
|
| 355 |
+
|
| 356 |
+
return exported_models
|
src/dytr/core/layers.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Core transformer layers including attention mechanisms and feedforward networks.
|
| 17 |
+
|
| 18 |
+
This module implements the fundamental building blocks of the transformer architecture:
|
| 19 |
+
multi-head attention with rotary positional embeddings, feedforward networks,
|
| 20 |
+
and supporting utility functions.
|
| 21 |
+
|
| 22 |
+
Author: Akram Alsubari
|
| 23 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from typing import Optional
|
| 27 |
+
|
| 28 |
+
import torch
|
| 29 |
+
import torch.nn as nn
|
| 30 |
+
import torch.nn.functional as F
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class RotaryEmbedding(nn.Module):
|
| 34 |
+
"""
|
| 35 |
+
Rotary Position Embedding (RoPE) for transformer models.
|
| 36 |
+
|
| 37 |
+
Implements rotary positional embeddings that encode position information
|
| 38 |
+
by rotating query and key vectors in the complex plane.
|
| 39 |
+
|
| 40 |
+
Args:
|
| 41 |
+
dim: Dimension of the embedding
|
| 42 |
+
max_seq_len: Maximum sequence length
|
| 43 |
+
base: Base for the frequency computation
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, dim: int, max_seq_len: int = 2048, base: int = 10000):
|
| 47 |
+
super().__init__()
|
| 48 |
+
self.dim = dim
|
| 49 |
+
self.max_seq_len = max_seq_len
|
| 50 |
+
self.base = base
|
| 51 |
+
inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
|
| 52 |
+
self.register_buffer("inv_freq", inv_freq)
|
| 53 |
+
self._build_cache(max_seq_len)
|
| 54 |
+
|
| 55 |
+
def _build_cache(self, seq_len: int):
|
| 56 |
+
"""Build and cache cosine and sine values for rotary embeddings."""
|
| 57 |
+
t = torch.arange(seq_len, device=self.inv_freq.device, dtype=self.inv_freq.dtype)
|
| 58 |
+
freqs = torch.einsum("i,j->ij", t, self.inv_freq)
|
| 59 |
+
emb = torch.cat((freqs, freqs), dim=-1)
|
| 60 |
+
self.register_buffer("cos_cached", emb.cos())
|
| 61 |
+
self.register_buffer("sin_cached", emb.sin())
|
| 62 |
+
|
| 63 |
+
def forward(self, x: torch.Tensor, seq_len: int):
|
| 64 |
+
"""Get cosine and sine values for rotary embeddings."""
|
| 65 |
+
if seq_len > self.max_seq_len:
|
| 66 |
+
self._build_cache(seq_len)
|
| 67 |
+
return self.cos_cached[:seq_len], self.sin_cached[:seq_len]
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def rotate_half(x: torch.Tensor) -> torch.Tensor:
|
| 71 |
+
"""Rotate half of the hidden dimensions."""
|
| 72 |
+
x1, x2 = x.chunk(2, dim=-1)
|
| 73 |
+
return torch.cat((-x2, x1), dim=-1)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def apply_rotary_pos_emb(q: torch.Tensor, k: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
|
| 77 |
+
"""Apply rotary position embeddings to query and key tensors."""
|
| 78 |
+
cos = cos.unsqueeze(0).unsqueeze(0)
|
| 79 |
+
sin = sin.unsqueeze(0).unsqueeze(0)
|
| 80 |
+
q_embed = (q * cos) + (rotate_half(q) * sin)
|
| 81 |
+
k_embed = (k * cos) + (rotate_half(k) * sin)
|
| 82 |
+
return q_embed, k_embed
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class MultiHeadAttention(nn.Module):
|
| 86 |
+
"""
|
| 87 |
+
Multi-Head Attention mechanism with support for cross-attention and rotary embeddings.
|
| 88 |
+
|
| 89 |
+
Args:
|
| 90 |
+
embed_dim: Embedding dimension
|
| 91 |
+
num_heads: Number of attention heads
|
| 92 |
+
head_dim: Dimension of each attention head
|
| 93 |
+
config: Model configuration
|
| 94 |
+
layer_id: Layer index for debugging
|
| 95 |
+
is_cross_attention: Whether this is cross-attention layer
|
| 96 |
+
"""
|
| 97 |
+
|
| 98 |
+
def __init__(
|
| 99 |
+
self,
|
| 100 |
+
embed_dim: int,
|
| 101 |
+
num_heads: int,
|
| 102 |
+
head_dim: int,
|
| 103 |
+
config,
|
| 104 |
+
layer_id: int,
|
| 105 |
+
is_cross_attention: bool = False,
|
| 106 |
+
):
|
| 107 |
+
super().__init__()
|
| 108 |
+
self.embed_dim = embed_dim
|
| 109 |
+
self.num_heads = num_heads
|
| 110 |
+
self.head_dim = head_dim
|
| 111 |
+
self.total_dim = num_heads * head_dim
|
| 112 |
+
self.scale = head_dim**-0.5
|
| 113 |
+
self.config = config
|
| 114 |
+
self.layer_id = layer_id
|
| 115 |
+
self.is_cross_attention = is_cross_attention
|
| 116 |
+
|
| 117 |
+
self.q_proj = nn.Linear(embed_dim, self.total_dim)
|
| 118 |
+
self.k_proj = nn.Linear(embed_dim, self.total_dim)
|
| 119 |
+
self.v_proj = nn.Linear(embed_dim, self.total_dim)
|
| 120 |
+
self.out_proj = nn.Linear(self.total_dim, embed_dim)
|
| 121 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 122 |
+
|
| 123 |
+
if config.use_rotary_embedding and not is_cross_attention:
|
| 124 |
+
self.rotary = RotaryEmbedding(head_dim, config.max_seq_len)
|
| 125 |
+
|
| 126 |
+
def forward(
|
| 127 |
+
self,
|
| 128 |
+
x: torch.Tensor,
|
| 129 |
+
encoder_output: Optional[torch.Tensor] = None,
|
| 130 |
+
mask: Optional[torch.Tensor] = None,
|
| 131 |
+
is_causal: bool = False,
|
| 132 |
+
):
|
| 133 |
+
"""
|
| 134 |
+
Forward pass for multi-head attention.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
x: Input tensor [batch_size, seq_len, embed_dim]
|
| 138 |
+
encoder_output: Encoder output for cross-attention
|
| 139 |
+
mask: Attention mask
|
| 140 |
+
is_causal: Whether to apply causal masking
|
| 141 |
+
"""
|
| 142 |
+
batch_size, seq_len, _ = x.shape
|
| 143 |
+
|
| 144 |
+
# Project and reshape queries
|
| 145 |
+
q = self.q_proj(x).view(batch_size, seq_len, self.num_heads, self.head_dim).transpose(1, 2)
|
| 146 |
+
|
| 147 |
+
# Handle keys and values (self-attention or cross-attention)
|
| 148 |
+
if self.is_cross_attention and encoder_output is not None:
|
| 149 |
+
k = (
|
| 150 |
+
self.k_proj(encoder_output)
|
| 151 |
+
.view(batch_size, encoder_output.size(1), self.num_heads, self.head_dim)
|
| 152 |
+
.transpose(1, 2)
|
| 153 |
+
)
|
| 154 |
+
v = (
|
| 155 |
+
self.v_proj(encoder_output)
|
| 156 |
+
.view(batch_size, encoder_output.size(1), self.num_heads, self.head_dim)
|
| 157 |
+
.transpose(1, 2)
|
| 158 |
+
)
|
| 159 |
+
kv_seq_len = encoder_output.size(1)
|
| 160 |
+
else:
|
| 161 |
+
k = (
|
| 162 |
+
self.k_proj(x)
|
| 163 |
+
.view(batch_size, seq_len, self.num_heads, self.head_dim)
|
| 164 |
+
.transpose(1, 2)
|
| 165 |
+
)
|
| 166 |
+
v = (
|
| 167 |
+
self.v_proj(x)
|
| 168 |
+
.view(batch_size, seq_len, self.num_heads, self.head_dim)
|
| 169 |
+
.transpose(1, 2)
|
| 170 |
+
)
|
| 171 |
+
kv_seq_len = seq_len
|
| 172 |
+
|
| 173 |
+
# Apply rotary embeddings
|
| 174 |
+
if (
|
| 175 |
+
self.config.use_rotary_embedding
|
| 176 |
+
and not self.is_cross_attention
|
| 177 |
+
and encoder_output is None
|
| 178 |
+
):
|
| 179 |
+
cos, sin = self.rotary(x, seq_len)
|
| 180 |
+
q, k = apply_rotary_pos_emb(q, k, cos, sin)
|
| 181 |
+
|
| 182 |
+
# Compute attention scores
|
| 183 |
+
attn_scores = torch.matmul(q, k.transpose(-2, -1)) * self.scale
|
| 184 |
+
|
| 185 |
+
# Apply masks
|
| 186 |
+
if mask is not None:
|
| 187 |
+
if mask.dim() == 2:
|
| 188 |
+
mask = mask[:, None, None, :]
|
| 189 |
+
if mask.size(-1) != kv_seq_len:
|
| 190 |
+
if mask.size(-1) > kv_seq_len:
|
| 191 |
+
mask = mask[..., :kv_seq_len]
|
| 192 |
+
else:
|
| 193 |
+
pad_len = kv_seq_len - mask.size(-1)
|
| 194 |
+
mask = F.pad(mask, (0, pad_len), value=1)
|
| 195 |
+
if mask.size(1) != self.num_heads:
|
| 196 |
+
mask = mask.expand(-1, self.num_heads, -1, -1)
|
| 197 |
+
attn_scores = attn_scores.masked_fill(mask == 0, float("-inf"))
|
| 198 |
+
|
| 199 |
+
# Apply causal mask if needed
|
| 200 |
+
if is_causal and not self.is_cross_attention:
|
| 201 |
+
causal_mask = torch.triu(
|
| 202 |
+
torch.ones(seq_len, kv_seq_len, device=x.device) * float("-inf"), diagonal=1
|
| 203 |
+
)
|
| 204 |
+
attn_scores = attn_scores + causal_mask[None, None, :, :]
|
| 205 |
+
|
| 206 |
+
# Apply softmax and dropout
|
| 207 |
+
attn_weights = F.softmax(attn_scores, dim=-1)
|
| 208 |
+
attn_weights = self.dropout(attn_weights)
|
| 209 |
+
|
| 210 |
+
# Apply attention to values
|
| 211 |
+
context = torch.matmul(attn_weights, v)
|
| 212 |
+
context = context.transpose(1, 2).contiguous().view(batch_size, -1, self.total_dim)
|
| 213 |
+
output = self.out_proj(context)
|
| 214 |
+
|
| 215 |
+
return output
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
class FeedForward(nn.Module):
|
| 219 |
+
"""
|
| 220 |
+
FeedForward network with GELU activation and SwiGLU-style gating.
|
| 221 |
+
|
| 222 |
+
Implements a transformer feedforward layer with gating mechanism
|
| 223 |
+
similar to SwiGLU used in modern LLMs.
|
| 224 |
+
|
| 225 |
+
Args:
|
| 226 |
+
embed_dim: Embedding dimension
|
| 227 |
+
ff_mult: Multiplier for feedforward dimension
|
| 228 |
+
config: Model configuration
|
| 229 |
+
layer_id: Layer index for debugging
|
| 230 |
+
"""
|
| 231 |
+
|
| 232 |
+
def __init__(self, embed_dim: int, ff_mult: int, config, layer_id: int):
|
| 233 |
+
super().__init__()
|
| 234 |
+
self.embed_dim = embed_dim
|
| 235 |
+
self.ff_dim = embed_dim * ff_mult
|
| 236 |
+
self.config = config
|
| 237 |
+
self.layer_id = layer_id
|
| 238 |
+
|
| 239 |
+
self.gate_proj = nn.Linear(embed_dim, self.ff_dim)
|
| 240 |
+
self.up_proj = nn.Linear(embed_dim, self.ff_dim)
|
| 241 |
+
self.down_proj = nn.Linear(self.ff_dim, embed_dim)
|
| 242 |
+
self.activation = nn.GELU()
|
| 243 |
+
self.dropout = nn.Dropout(config.dropout)
|
| 244 |
+
|
| 245 |
+
def forward(self, x: torch.Tensor):
|
| 246 |
+
"""Forward pass through feedforward network."""
|
| 247 |
+
return self.down_proj(self.activation(self.gate_proj(x)) * self.up_proj(x))
|
src/dytr/core/model.py
ADDED
|
@@ -0,0 +1,619 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Main DynamicTransformer model implementation.
|
| 17 |
+
|
| 18 |
+
This module contains the core DynamicTransformer class that manages multiple tasks,
|
| 19 |
+
task-specific heads and decoders, and handles different training strategies.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from typing import Optional
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
import torch.nn.functional as F
|
| 30 |
+
|
| 31 |
+
from dytr.core.config import ModelConfig, TaskConfig, TrainingStrategy
|
| 32 |
+
from dytr.core.decoder import TransformerDecoder
|
| 33 |
+
from dytr.core.encoder import TransformerEncoder
|
| 34 |
+
from dytr.core.exporter import ModelExporter
|
| 35 |
+
from dytr.memory.ewc import EWC
|
| 36 |
+
from dytr.memory.replay import ReplayBuffer
|
| 37 |
+
|
| 38 |
+
from collections import defaultdict
|
| 39 |
+
import numpy as np
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def calculate_f_scores(preds, labels, average='macro', beta=1.0):
|
| 45 |
+
"""
|
| 46 |
+
Calculate F-beta score (F1 when beta=1, F0.5 when beta=0.5)
|
| 47 |
+
|
| 48 |
+
Args:
|
| 49 |
+
preds: Tensor of predictions
|
| 50 |
+
labels: Tensor of ground truth labels
|
| 51 |
+
average: 'macro', 'micro', 'weighted', or None
|
| 52 |
+
beta: Beta parameter for F-beta score
|
| 53 |
+
Returns:
|
| 54 |
+
f_score: F-beta score
|
| 55 |
+
per_class: Dictionary of per-class scores (if average is None)
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
if labels.numel() == 0:
|
| 60 |
+
return 0.0, {}
|
| 61 |
+
|
| 62 |
+
# Convert to CPU numpy for calculation
|
| 63 |
+
preds_np = preds.cpu().numpy()
|
| 64 |
+
labels_np = labels.cpu().numpy()
|
| 65 |
+
|
| 66 |
+
# Get unique classes
|
| 67 |
+
classes = np.unique(np.concatenate([labels_np, preds_np]))
|
| 68 |
+
n_classes = len(classes)
|
| 69 |
+
|
| 70 |
+
if n_classes == 0:
|
| 71 |
+
return 0.0, {}
|
| 72 |
+
|
| 73 |
+
# Initialize confusion matrix components
|
| 74 |
+
tp = defaultdict(int)
|
| 75 |
+
fp = defaultdict(int)
|
| 76 |
+
fn = defaultdict(int)
|
| 77 |
+
|
| 78 |
+
for pred, label in zip(preds_np, labels_np):
|
| 79 |
+
if pred == label:
|
| 80 |
+
tp[label] += 1
|
| 81 |
+
else:
|
| 82 |
+
fp[pred] += 1
|
| 83 |
+
fn[label] += 1
|
| 84 |
+
|
| 85 |
+
# Calculate precision, recall, and f-beta for each class
|
| 86 |
+
precision = {}
|
| 87 |
+
recall = {}
|
| 88 |
+
f_beta = {}
|
| 89 |
+
|
| 90 |
+
for cls in classes:
|
| 91 |
+
p = tp[cls] / (tp[cls] + fp[cls]) if (tp[cls] + fp[cls]) > 0 else 0.0
|
| 92 |
+
r = tp[cls] / (tp[cls] + fn[cls]) if (tp[cls] + fn[cls]) > 0 else 0.0
|
| 93 |
+
|
| 94 |
+
precision[cls] = p
|
| 95 |
+
recall[cls] = r
|
| 96 |
+
|
| 97 |
+
if p + r > 0:
|
| 98 |
+
beta_sq = beta * beta
|
| 99 |
+
f_beta[cls] = (1 + beta_sq) * (p * r) / ((beta_sq * p) + r)
|
| 100 |
+
else:
|
| 101 |
+
f_beta[cls] = 0.0
|
| 102 |
+
|
| 103 |
+
if average == 'macro':
|
| 104 |
+
return sum(f_beta.values()) / n_classes, f_beta
|
| 105 |
+
elif average == 'weighted':
|
| 106 |
+
# Count samples per class
|
| 107 |
+
class_counts = defaultdict(int)
|
| 108 |
+
for label in labels_np:
|
| 109 |
+
class_counts[label] += 1
|
| 110 |
+
total = len(labels_np)
|
| 111 |
+
weighted_score = sum(f_beta[cls] * (class_counts[cls] / total) for cls in classes)
|
| 112 |
+
return weighted_score, f_beta
|
| 113 |
+
elif average == 'micro':
|
| 114 |
+
# Micro averaging: aggregate all classes
|
| 115 |
+
total_tp = sum(tp.values())
|
| 116 |
+
total_fp = sum(fp.values())
|
| 117 |
+
total_fn = sum(fn.values())
|
| 118 |
+
micro_p = total_tp / (total_tp + total_fp) if (total_tp + total_fp) > 0 else 0.0
|
| 119 |
+
micro_r = total_tp / (total_tp + total_fn) if (total_tp + total_fn) > 0 else 0.0
|
| 120 |
+
beta_sq = beta * beta
|
| 121 |
+
micro_f = (1 + beta_sq) * (micro_p * micro_r) / ((beta_sq * micro_p) + micro_r) if (micro_p + micro_r) > 0 else 0.0
|
| 122 |
+
return micro_f, f_beta
|
| 123 |
+
else:
|
| 124 |
+
return f_beta, f_beta
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class DynamicTransformer(nn.Module):
|
| 129 |
+
"""
|
| 130 |
+
Main Dynamic Transformer model supporting multi-task learning and continual learning.
|
| 131 |
+
|
| 132 |
+
This model can handle multiple tasks with different training strategies simultaneously,
|
| 133 |
+
with support for EWC (Elastic Weight Consolidation) and experience replay to prevent
|
| 134 |
+
catastrophic forgetting.
|
| 135 |
+
|
| 136 |
+
Args:
|
| 137 |
+
config: Model configuration
|
| 138 |
+
"""
|
| 139 |
+
|
| 140 |
+
def __init__(self, config: ModelConfig):
|
| 141 |
+
super().__init__()
|
| 142 |
+
self.current_tasks = {}
|
| 143 |
+
expected_head_dim = config.embed_dim // config.num_heads
|
| 144 |
+
if config.embed_dim % config.num_heads != 0:
|
| 145 |
+
raise ValueError(
|
| 146 |
+
f"embed_dim ({config.embed_dim}) must be divisible by num_heads ({config.num_heads})"
|
| 147 |
+
)
|
| 148 |
+
if config.head_dim != expected_head_dim:
|
| 149 |
+
print(f"\n⚠️ WARNING: head_dim ({config.head_dim}) does not match expected value ({expected_head_dim})")
|
| 150 |
+
|
| 151 |
+
print(f" Formula: embed_dim ({config.embed_dim}) // num_heads ({config.num_heads}) = {expected_head_dim}")
|
| 152 |
+
print(f" Continuing with user-specified value: {config.head_dim}")
|
| 153 |
+
print(" (This may cause unexpected behavior)")
|
| 154 |
+
self.config = config
|
| 155 |
+
|
| 156 |
+
# Initialize tokenizer (try HuggingFace first, fallback to SimpleTokenizer)
|
| 157 |
+
try:
|
| 158 |
+
from transformers import AutoTokenizer
|
| 159 |
+
transformers_available = True
|
| 160 |
+
except:
|
| 161 |
+
transformers_available = False
|
| 162 |
+
|
| 163 |
+
if transformers_available and not config.use_simple_tokenizer:
|
| 164 |
+
self.tokenizer = AutoTokenizer.from_pretrained(config.tokenizer_name)
|
| 165 |
+
else:
|
| 166 |
+
|
| 167 |
+
from dytr.tokenization.download_manager import download_tokenizer_files
|
| 168 |
+
from dytr.tokenization.simple_tokenizer import SimpleTokenizer
|
| 169 |
+
|
| 170 |
+
if "vocab.json" in config.tokenizer_name :
|
| 171 |
+
import json
|
| 172 |
+
|
| 173 |
+
with open(config.tokenizer_name, "r", encoding="utf-8") as f:
|
| 174 |
+
vocab = json.load(f)
|
| 175 |
+
self.tokenizer = SimpleTokenizer(vocab,add_tab_newline_vocab = config.add_tab_newline_vocab, tokenizer_type=config.tokenizer_type)
|
| 176 |
+
else:
|
| 177 |
+
try:
|
| 178 |
+
vocab, special_token_map = download_tokenizer_files(str(config.tokenizer_name))
|
| 179 |
+
self.tokenizer = SimpleTokenizer(vocab, special_token_map,add_tab_newline_vocab = config.add_tab_newline_vocab,tokenizer_type=config.tokenizer_type)
|
| 180 |
+
except:
|
| 181 |
+
self.tokenizer = SimpleTokenizer.from_pretrained(config.tokenizer_name,tokenizer_type=config.tokenizer_type,add_tab_newline_vocab = config.add_tab_newline_vocab)
|
| 182 |
+
|
| 183 |
+
# Configure tokenizer
|
| 184 |
+
if self.tokenizer.pad_token is None:
|
| 185 |
+
self.tokenizer.pad_token = (
|
| 186 |
+
self.tokenizer.eos_token if self.tokenizer.eos_token else "[PAD]"
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
# Add special tokens
|
| 190 |
+
for token in config.special_tokens.values():
|
| 191 |
+
if token not in self.tokenizer.get_vocab() and config.training_from_scratch:
|
| 192 |
+
self.tokenizer.add_tokens([token])
|
| 193 |
+
|
| 194 |
+
# Update config with tokenizer info
|
| 195 |
+
config.vocab_size = len(self.tokenizer)
|
| 196 |
+
config.bos_token_id = self.tokenizer.bos_token_id or self.tokenizer.cls_token_id
|
| 197 |
+
config.eos_token_id = self.tokenizer.eos_token_id or self.tokenizer.sep_token_id
|
| 198 |
+
|
| 199 |
+
# Shared components
|
| 200 |
+
self.shared_embedding = nn.Embedding(
|
| 201 |
+
config.vocab_size, config.embed_dim, padding_idx=self.tokenizer.pad_token_id
|
| 202 |
+
)
|
| 203 |
+
nn.init.normal_(self.shared_embedding.weight, mean=0.0, std=0.02)
|
| 204 |
+
|
| 205 |
+
# Encoder (shared across all tasks)
|
| 206 |
+
self.encoder = TransformerEncoder(config)
|
| 207 |
+
self.encoder.embedding = self.shared_embedding
|
| 208 |
+
|
| 209 |
+
# Task-specific components
|
| 210 |
+
self.decoders = nn.ModuleDict()
|
| 211 |
+
self.task_heads = nn.ModuleDict()
|
| 212 |
+
|
| 213 |
+
# Training state
|
| 214 |
+
self.training_step = 0
|
| 215 |
+
self.best_val_loss = float("inf")
|
| 216 |
+
self.steps_without_improvement = 0
|
| 217 |
+
|
| 218 |
+
# Continual learning components
|
| 219 |
+
self.ewc_penalties = {}
|
| 220 |
+
self.replay_buffer = ReplayBuffer(config.replay_buffer_size)
|
| 221 |
+
self.error_mask = None
|
| 222 |
+
|
| 223 |
+
# Initialize tasks from config
|
| 224 |
+
if config.tasks:
|
| 225 |
+
for task_name, task_info in config.tasks.items():
|
| 226 |
+
strategy = TrainingStrategy(task_info["strategy"])
|
| 227 |
+
num_labels = task_info.get("num_labels", config.vocab_size)
|
| 228 |
+
self._add_task_head(task_name, strategy, num_labels)
|
| 229 |
+
self.current_tasks[task_name] = task_info
|
| 230 |
+
|
| 231 |
+
self.to(config.device)
|
| 232 |
+
|
| 233 |
+
def _add_task_head(self, task_name: str, strategy: TrainingStrategy, num_labels: int):
|
| 234 |
+
"""Add task-specific head or decoder."""
|
| 235 |
+
if strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 236 |
+
head = nn.Sequential(
|
| 237 |
+
nn.Linear(self.config.embed_dim, self.config.embed_dim // 2),
|
| 238 |
+
nn.LayerNorm(self.config.embed_dim // 2),
|
| 239 |
+
nn.GELU(),
|
| 240 |
+
nn.Dropout(self.config.dropout),
|
| 241 |
+
nn.Linear(self.config.embed_dim // 2, num_labels),
|
| 242 |
+
)
|
| 243 |
+
self.task_heads[task_name] = head.to(self.config.device)
|
| 244 |
+
|
| 245 |
+
elif strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 246 |
+
head = nn.Sequential(
|
| 247 |
+
nn.Linear(self.config.embed_dim, self.config.embed_dim),
|
| 248 |
+
nn.LayerNorm(self.config.embed_dim),
|
| 249 |
+
nn.GELU(),
|
| 250 |
+
nn.Dropout(self.config.dropout),
|
| 251 |
+
nn.Linear(self.config.embed_dim, num_labels),
|
| 252 |
+
)
|
| 253 |
+
self.task_heads[task_name] = head.to(self.config.device)
|
| 254 |
+
|
| 255 |
+
elif strategy == TrainingStrategy.CAUSAL_LM:
|
| 256 |
+
decoder = TransformerDecoder(self.config, task_name, has_cross_attention=False)
|
| 257 |
+
decoder.set_shared_embedding(self.shared_embedding)
|
| 258 |
+
self.decoders[task_name] = decoder.to(self.config.device)
|
| 259 |
+
|
| 260 |
+
elif strategy == TrainingStrategy.SEQ2SEQ:
|
| 261 |
+
decoder = TransformerDecoder(self.config, task_name, has_cross_attention=True)
|
| 262 |
+
decoder.set_shared_embedding(self.shared_embedding)
|
| 263 |
+
self.decoders[task_name] = decoder.to(self.config.device)
|
| 264 |
+
|
| 265 |
+
def add_task(self, task_config: TaskConfig):
|
| 266 |
+
"""Add a new task to the model."""
|
| 267 |
+
task_name = task_config.task_name
|
| 268 |
+
|
| 269 |
+
if task_name not in self.task_heads and task_name not in self.decoders:
|
| 270 |
+
num_labels = task_config.num_labels or self.config.vocab_size
|
| 271 |
+
self._add_task_head(task_name, task_config.training_strategy, num_labels)
|
| 272 |
+
|
| 273 |
+
task_info = {
|
| 274 |
+
"strategy": task_config.training_strategy.value,
|
| 275 |
+
"num_labels": num_labels,
|
| 276 |
+
"best_loss": float("inf"),
|
| 277 |
+
"best_metrics": {},
|
| 278 |
+
"steps_without_improvement": 0,
|
| 279 |
+
}
|
| 280 |
+
self.current_tasks[task_name] = task_info
|
| 281 |
+
self.config.tasks[task_name] = task_info
|
| 282 |
+
|
| 283 |
+
def forward(
|
| 284 |
+
self,
|
| 285 |
+
input_ids: torch.Tensor,
|
| 286 |
+
attention_mask: Optional[torch.Tensor] = None,
|
| 287 |
+
task_name: Optional[str] = None,
|
| 288 |
+
labels: Optional[torch.Tensor] = None,
|
| 289 |
+
encoder_output: Optional[torch.Tensor] = None,
|
| 290 |
+
error_mask: Optional[torch.Tensor] = None,
|
| 291 |
+
):
|
| 292 |
+
"""
|
| 293 |
+
Forward pass for the model.
|
| 294 |
+
|
| 295 |
+
Handles different training strategies:
|
| 296 |
+
- SEQ2SEQ: Encoder-decoder with cross-attention
|
| 297 |
+
- CAUSAL_LM: Decoder-only autoregressive
|
| 298 |
+
- SENTENCE_CLASSIFICATION: Classification with pooling
|
| 299 |
+
- TOKEN_CLASSIFICATION: Token-level classification
|
| 300 |
+
"""
|
| 301 |
+
if error_mask is not None:
|
| 302 |
+
self.error_mask = error_mask
|
| 303 |
+
|
| 304 |
+
if task_name and task_name in self.current_tasks:
|
| 305 |
+
strategy = TrainingStrategy(self.current_tasks[task_name]["strategy"])
|
| 306 |
+
|
| 307 |
+
# Sequence-to-Sequence
|
| 308 |
+
if strategy == TrainingStrategy.SEQ2SEQ:
|
| 309 |
+
if encoder_output is None:
|
| 310 |
+
encoder_output = self.encoder(input_ids, attention_mask, task_name)
|
| 311 |
+
|
| 312 |
+
if task_name not in self.decoders:
|
| 313 |
+
raise ValueError(f"No decoder found for task {task_name}")
|
| 314 |
+
|
| 315 |
+
decoder = self.decoders[task_name]
|
| 316 |
+
|
| 317 |
+
if labels is not None:
|
| 318 |
+
decoder_input = labels[:, :-1].clone()
|
| 319 |
+
decoder_input[decoder_input == -100] = self.tokenizer.pad_token_id or 0
|
| 320 |
+
target_labels = labels[:, 1:]
|
| 321 |
+
decoder_mask = (decoder_input != self.tokenizer.pad_token_id).float()
|
| 322 |
+
|
| 323 |
+
logits = decoder(
|
| 324 |
+
decoder_input, encoder_output, attention_mask, decoder_mask, task_name
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
loss_fn = nn.CrossEntropyLoss(
|
| 328 |
+
ignore_index=-100, label_smoothing=self.config.label_smoothing
|
| 329 |
+
)
|
| 330 |
+
loss = loss_fn(
|
| 331 |
+
logits.reshape(-1, self.config.vocab_size), target_labels.reshape(-1)
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
metrics = {}
|
| 335 |
+
if not self.training:
|
| 336 |
+
mask = target_labels != -100
|
| 337 |
+
if mask.any():
|
| 338 |
+
preds = logits.argmax(dim=-1)
|
| 339 |
+
correct = (preds[mask] == target_labels[mask]).float().sum().item()
|
| 340 |
+
total = mask.sum().item()
|
| 341 |
+
metrics["token_accuracy"] = correct / total if total > 0 else 0.0
|
| 342 |
+
|
| 343 |
+
return {
|
| 344 |
+
"logits": logits,
|
| 345 |
+
"loss": loss,
|
| 346 |
+
"encoder_output": encoder_output,
|
| 347 |
+
"metrics": metrics,
|
| 348 |
+
}
|
| 349 |
+
else:
|
| 350 |
+
return {"encoder_output": encoder_output}
|
| 351 |
+
|
| 352 |
+
# Causal Language Model
|
| 353 |
+
elif strategy == TrainingStrategy.CAUSAL_LM:
|
| 354 |
+
decoder = self.decoders[task_name]
|
| 355 |
+
|
| 356 |
+
if labels is not None:
|
| 357 |
+
decoder_input = labels[:, :-1].clone()
|
| 358 |
+
decoder_input[decoder_input == -100] = self.tokenizer.pad_token_id or 0
|
| 359 |
+
target_labels = labels[:, 1:]
|
| 360 |
+
|
| 361 |
+
logits = decoder(decoder_input, task_name=task_name)
|
| 362 |
+
loss_fn = nn.CrossEntropyLoss(
|
| 363 |
+
ignore_index=-100, label_smoothing=self.config.label_smoothing
|
| 364 |
+
)
|
| 365 |
+
loss = loss_fn(
|
| 366 |
+
logits.reshape(-1, self.config.vocab_size), target_labels.reshape(-1)
|
| 367 |
+
)
|
| 368 |
+
|
| 369 |
+
metrics = {}
|
| 370 |
+
if not self.training:
|
| 371 |
+
mask = target_labels != -100
|
| 372 |
+
if mask.any():
|
| 373 |
+
preds = logits.argmax(dim=-1)
|
| 374 |
+
metrics["perplexity"] = torch.exp(loss).item()
|
| 375 |
+
correct = (preds[mask] == target_labels[mask]).float().sum().item()
|
| 376 |
+
total = mask.sum().item()
|
| 377 |
+
metrics["token_accuracy"] = correct / total if total > 0 else 0.0
|
| 378 |
+
|
| 379 |
+
return {"logits": logits, "loss": loss, "metrics": metrics}
|
| 380 |
+
else:
|
| 381 |
+
return {"decoder": decoder}
|
| 382 |
+
|
| 383 |
+
# Sentence Classification
|
| 384 |
+
elif strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 385 |
+
x = self.shared_embedding(input_ids)
|
| 386 |
+
for layer in self.encoder.layers:
|
| 387 |
+
x = layer(x, attention_mask, task_name)
|
| 388 |
+
x = self.encoder.norm(x)
|
| 389 |
+
|
| 390 |
+
# Pooling
|
| 391 |
+
if attention_mask is not None:
|
| 392 |
+
mask_sum = attention_mask.sum(dim=1, keepdim=True).clamp(min=1)
|
| 393 |
+
pooled = (x * attention_mask.unsqueeze(-1)).sum(dim=1) / mask_sum
|
| 394 |
+
else:
|
| 395 |
+
pooled = x.mean(dim=1)
|
| 396 |
+
|
| 397 |
+
logits = self.task_heads[task_name](pooled)
|
| 398 |
+
|
| 399 |
+
if labels is not None:
|
| 400 |
+
if labels.dim() > 1:
|
| 401 |
+
labels = labels[:, 0]
|
| 402 |
+
loss_fn = nn.CrossEntropyLoss(label_smoothing=self.config.label_smoothing)
|
| 403 |
+
loss = loss_fn(logits, labels)
|
| 404 |
+
|
| 405 |
+
metrics = {}
|
| 406 |
+
if not self.training:
|
| 407 |
+
preds = logits.argmax(dim=-1)
|
| 408 |
+
correct = (preds == labels).float().sum().item()
|
| 409 |
+
total = labels.size(0)
|
| 410 |
+
metrics["accuracy"] = correct / total if total > 0 else 0.0
|
| 411 |
+
if total>0:
|
| 412 |
+
f1_macro, _ = calculate_f_scores(preds, labels, average='macro', beta=1.0)
|
| 413 |
+
f1_weighted, _ = calculate_f_scores(preds, labels, average='weighted', beta=1.0)
|
| 414 |
+
f1_micro, _ = calculate_f_scores(preds, labels, average='micro', beta=1.0)
|
| 415 |
+
metrics["f1_macro"] = f1_macro
|
| 416 |
+
metrics["f1_weighted"] = f1_weighted
|
| 417 |
+
metrics["f1_micro"] = f1_micro
|
| 418 |
+
|
| 419 |
+
return {"logits": logits, "loss": loss, "metrics": metrics}
|
| 420 |
+
return {"logits": logits}
|
| 421 |
+
|
| 422 |
+
# Token Classification
|
| 423 |
+
elif strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 424 |
+
x = self.shared_embedding(input_ids)
|
| 425 |
+
for layer in self.encoder.layers:
|
| 426 |
+
x = layer(x, attention_mask, task_name)
|
| 427 |
+
x = self.encoder.norm(x)
|
| 428 |
+
|
| 429 |
+
logits = self.task_heads[task_name](x)
|
| 430 |
+
|
| 431 |
+
if labels is not None:
|
| 432 |
+
loss_fn = nn.CrossEntropyLoss(
|
| 433 |
+
ignore_index=-100, label_smoothing=self.config.label_smoothing
|
| 434 |
+
)
|
| 435 |
+
active = labels.view(-1) != -100
|
| 436 |
+
|
| 437 |
+
if active.any():
|
| 438 |
+
logits_flat = logits.view(-1, self.task_heads[task_name][-1].out_features)
|
| 439 |
+
labels_flat = labels.view(-1)
|
| 440 |
+
loss = loss_fn(logits_flat[active], labels_flat[active])
|
| 441 |
+
else:
|
| 442 |
+
loss = torch.tensor(0.0, device=logits.device, requires_grad=True)
|
| 443 |
+
|
| 444 |
+
metrics = {}
|
| 445 |
+
if not self.training and active.any():
|
| 446 |
+
preds = logits.argmax(dim=-1).view(-1)[active]
|
| 447 |
+
labels_flat_active = labels_flat[active]
|
| 448 |
+
|
| 449 |
+
correct = (preds == labels_flat_active).float().sum().item()
|
| 450 |
+
total = active.sum().item()
|
| 451 |
+
metrics["token_accuracy"] = correct / total if total > 0 else 0.0
|
| 452 |
+
"""
|
| 453 |
+
is_error = labels_flat_active != 0
|
| 454 |
+
|
| 455 |
+
if is_error.any():
|
| 456 |
+
error_correct = (preds[is_error] == labels_flat_active[is_error]).float().sum().item()
|
| 457 |
+
error_total = is_error.sum().item()
|
| 458 |
+
metrics["token_accuracy(non-zero tags)"] = error_correct / error_total if error_total > 0 else 0.0
|
| 459 |
+
"""
|
| 460 |
+
if total > 0:
|
| 461 |
+
# Calculate macro F1 (averaged over all classes)
|
| 462 |
+
f1_macro, per_class_f1 = calculate_f_scores(preds, labels_flat_active, average='macro', beta=1.0)
|
| 463 |
+
|
| 464 |
+
#f05_macro, per_class_f05 = calculate_f_scores(preds, labels_flat_active, average='macro', beta=0.5)
|
| 465 |
+
|
| 466 |
+
# Calculate weighted F1 (accounting for class imbalance)
|
| 467 |
+
f1_weighted, _ = calculate_f_scores(preds, labels_flat_active, average='weighted', beta=1.0)
|
| 468 |
+
#f05_weighted, _ = calculate_f_scores(preds, labels_flat_active, average='weighted', beta=0.5)
|
| 469 |
+
|
| 470 |
+
# Calculate micro F1 (overall performance)
|
| 471 |
+
f1_micro, _ = calculate_f_scores(preds, labels_flat_active, average='micro', beta=1.0)
|
| 472 |
+
#f05_micro, _ = calculate_f_scores(preds, labels_flat_active, average='micro', beta=0.5)
|
| 473 |
+
|
| 474 |
+
metrics["f1_macro"] = f1_macro
|
| 475 |
+
#metrics["f0.5_macro"] = f05_macro
|
| 476 |
+
metrics["f1_weighted"] = f1_weighted
|
| 477 |
+
#metrics["f0.5_weighted"] = f05_weighted
|
| 478 |
+
metrics["f1_micro"] = f1_micro
|
| 479 |
+
#metrics["f0.5_micro"] = f05_micro
|
| 480 |
+
|
| 481 |
+
# Optional: Add per-class scores (useful for debugging)
|
| 482 |
+
# metrics["per_class_f1"] = {str(k): v for k, v in per_class_f1.items()}
|
| 483 |
+
|
| 484 |
+
|
| 485 |
+
return {"logits": logits, "loss": loss, "metrics": metrics}
|
| 486 |
+
return {"logits": logits}
|
| 487 |
+
|
| 488 |
+
# Fallback: just encode
|
| 489 |
+
x = self.shared_embedding(input_ids)
|
| 490 |
+
for layer in self.encoder.layers:
|
| 491 |
+
x = layer(x, attention_mask, None)
|
| 492 |
+
x = self.encoder.norm(x)
|
| 493 |
+
return {"hidden_states": x}
|
| 494 |
+
|
| 495 |
+
def generate(
|
| 496 |
+
self,
|
| 497 |
+
prompt: str,
|
| 498 |
+
task_name: Optional[str] = None,
|
| 499 |
+
max_new_tokens: int = 50,
|
| 500 |
+
temperature: float = 0.8,
|
| 501 |
+
top_p: float = 0.9,
|
| 502 |
+
top_k: int = 40,
|
| 503 |
+
repetition_penalty: float = 1.1,
|
| 504 |
+
decoding_with_special_tokens: str = "no",
|
| 505 |
+
):
|
| 506 |
+
"""Generate text or predictions for a given prompt."""
|
| 507 |
+
self.eval()
|
| 508 |
+
inputs = self.tokenizer(prompt, return_tensors="pt", add_special_tokens=False)
|
| 509 |
+
input_ids = inputs["input_ids"].to(self.config.device)
|
| 510 |
+
attention_mask = inputs["attention_mask"].to(self.config.device)
|
| 511 |
+
|
| 512 |
+
if task_name and task_name in self.current_tasks:
|
| 513 |
+
strategy = TrainingStrategy(self.current_tasks[task_name]["strategy"])
|
| 514 |
+
|
| 515 |
+
if strategy == TrainingStrategy.CAUSAL_LM:
|
| 516 |
+
decoder = self.decoders[task_name]
|
| 517 |
+
with torch.no_grad():
|
| 518 |
+
generated_ids = decoder.generate(
|
| 519 |
+
input_ids=input_ids,
|
| 520 |
+
max_len=max_new_tokens,
|
| 521 |
+
temperature=temperature,
|
| 522 |
+
top_k=top_k,
|
| 523 |
+
top_p=top_p,
|
| 524 |
+
repetition_penalty=repetition_penalty,
|
| 525 |
+
)
|
| 526 |
+
if decoding_with_special_tokens == "no":
|
| 527 |
+
return self.tokenizer.decode(generated_ids[0], skip_special_tokens=True)
|
| 528 |
+
elif decoding_with_special_tokens == "yes":
|
| 529 |
+
return self.tokenizer.decode(generated_ids[0], skip_special_tokens=False)
|
| 530 |
+
else:
|
| 531 |
+
return generated_ids
|
| 532 |
+
|
| 533 |
+
elif strategy == TrainingStrategy.SEQ2SEQ:
|
| 534 |
+
with torch.no_grad():
|
| 535 |
+
encoder_output = self.encoder(input_ids, attention_mask, task_name)
|
| 536 |
+
decoder = self.decoders[task_name]
|
| 537 |
+
generated_ids = decoder.generate(
|
| 538 |
+
encoder_output=encoder_output,
|
| 539 |
+
src_mask=attention_mask,
|
| 540 |
+
max_len=max_new_tokens,
|
| 541 |
+
temperature=temperature,
|
| 542 |
+
top_k=top_k,
|
| 543 |
+
top_p=top_p,
|
| 544 |
+
repetition_penalty=repetition_penalty,
|
| 545 |
+
)
|
| 546 |
+
if decoding_with_special_tokens == "no":
|
| 547 |
+
return self.tokenizer.decode(generated_ids[0], skip_special_tokens=True)
|
| 548 |
+
elif decoding_with_special_tokens == "yes":
|
| 549 |
+
return self.tokenizer.decode(generated_ids[0], skip_special_tokens=False)
|
| 550 |
+
else:
|
| 551 |
+
return generated_ids
|
| 552 |
+
|
| 553 |
+
elif strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 554 |
+
with torch.no_grad():
|
| 555 |
+
outputs = self.forward(input_ids, attention_mask, task_name=task_name)
|
| 556 |
+
logits = outputs["logits"]
|
| 557 |
+
probs = F.softmax(logits, dim=-1)
|
| 558 |
+
pred_id = probs.argmax().item()
|
| 559 |
+
return {
|
| 560 |
+
"prediction": pred_id,
|
| 561 |
+
"probabilities": probs[0].cpu().tolist(),
|
| 562 |
+
"logits": logits[0].cpu().tolist(),
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
elif strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 566 |
+
with torch.no_grad():
|
| 567 |
+
outputs = self.forward(input_ids, attention_mask, task_name=task_name)
|
| 568 |
+
logits = outputs["logits"]
|
| 569 |
+
predictions = logits.argmax(dim=-1)[0].cpu().tolist()
|
| 570 |
+
tokens = self.tokenizer.convert_ids_to_tokens(input_ids[0])
|
| 571 |
+
return {
|
| 572 |
+
"tokens": tokens,
|
| 573 |
+
"predictions": predictions,
|
| 574 |
+
"pairs": list(zip(tokens, predictions)),
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
return prompt
|
| 578 |
+
|
| 579 |
+
def save_model(self, path: str):
|
| 580 |
+
"""Save the entire model to disk."""
|
| 581 |
+
self.config.tasks = self.current_tasks
|
| 582 |
+
state = {
|
| 583 |
+
"model_state": self.state_dict(),
|
| 584 |
+
"config": self.config,
|
| 585 |
+
"current_tasks": self.current_tasks,
|
| 586 |
+
"training_step": self.training_step,
|
| 587 |
+
"best_val_loss": self.best_val_loss,
|
| 588 |
+
"steps_without_improvement": self.steps_without_improvement,
|
| 589 |
+
"tokenizer_name": self.config.tokenizer_name,
|
| 590 |
+
"special_tokens": self.config.special_tokens,
|
| 591 |
+
}
|
| 592 |
+
torch.save(state, path)
|
| 593 |
+
|
| 594 |
+
@classmethod
|
| 595 |
+
def load_model(cls, path: str, device: Optional[str] = None):
|
| 596 |
+
"""Load a saved model from disk."""
|
| 597 |
+
try:
|
| 598 |
+
if not device:
|
| 599 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 600 |
+
|
| 601 |
+
state = torch.load(path, map_location=device, weights_only=False)
|
| 602 |
+
config = state["config"]
|
| 603 |
+
config.device = device
|
| 604 |
+
|
| 605 |
+
model = cls(config)
|
| 606 |
+
model.load_state_dict(state["model_state"], strict=False)
|
| 607 |
+
model.current_tasks = state.get("current_tasks", {})
|
| 608 |
+
model.training_step = state.get("training_step", 0)
|
| 609 |
+
model.best_val_loss = state.get("best_val_loss", float("inf"))
|
| 610 |
+
model.steps_without_improvement = state.get("steps_without_improvement", 0)
|
| 611 |
+
|
| 612 |
+
return model.to(config.device)
|
| 613 |
+
except Exception as e:
|
| 614 |
+
print(f"Error loading model: {e}")
|
| 615 |
+
return None
|
| 616 |
+
|
| 617 |
+
def get_exporter(self):
|
| 618 |
+
"""Get a model exporter instance."""
|
| 619 |
+
return ModelExporter(self)
|
src/dytr/memory/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Memory and continual learning modules for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides implementations of continual learning techniques
|
| 19 |
+
including Elastic Weight Consolidation (EWC) and Experience Replay
|
| 20 |
+
to prevent catastrophic forgetting when training on sequential tasks.
|
| 21 |
+
|
| 22 |
+
Author: Akram Alsubari
|
| 23 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
from dytr.memory.ewc import EWC
|
| 27 |
+
from dytr.memory.replay import ReplayBuffer
|
| 28 |
+
|
| 29 |
+
__all__ = ["EWC", "ReplayBuffer"]
|
src/dytr/memory/ewc.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Elastic Weight Consolidation (EWC) for continual learning.
|
| 17 |
+
|
| 18 |
+
This module implements EWC to prevent catastrophic forgetting when training
|
| 19 |
+
on sequential tasks.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from typing import Dict
|
| 26 |
+
|
| 27 |
+
import torch
|
| 28 |
+
import torch.nn as nn
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class EWC:
|
| 32 |
+
"""
|
| 33 |
+
Elastic Weight Consolidation for continual learning.
|
| 34 |
+
|
| 35 |
+
EWC computes the Fisher information matrix for important parameters
|
| 36 |
+
and adds a penalty to prevent them from changing too much when learning
|
| 37 |
+
new tasks.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
model: The model to apply EWC to
|
| 41 |
+
task_name: Name of the task this EWC instance is for
|
| 42 |
+
lambda_param: Regularization strength
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __init__(self, model: nn.Module, task_name: str, lambda_param: float = 1000.0):
|
| 46 |
+
self.model = model
|
| 47 |
+
self.task_name = task_name
|
| 48 |
+
self.lambda_param = lambda_param
|
| 49 |
+
self.params = {
|
| 50 |
+
n: p.clone().detach() for n, p in model.named_parameters() if p.requires_grad
|
| 51 |
+
}
|
| 52 |
+
self.fisher: Dict[str, torch.Tensor] = {}
|
| 53 |
+
|
| 54 |
+
def compute_fisher(self, dataloader, device):
|
| 55 |
+
"""
|
| 56 |
+
Compute Fisher information matrix using the given dataloader.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
dataloader: DataLoader containing samples from the task
|
| 60 |
+
device: Device to run computation on
|
| 61 |
+
"""
|
| 62 |
+
# Initialize Fisher matrix
|
| 63 |
+
for n, p in self.model.named_parameters():
|
| 64 |
+
if p.requires_grad:
|
| 65 |
+
self.fisher[n] = torch.zeros_like(p)
|
| 66 |
+
|
| 67 |
+
self.model.eval()
|
| 68 |
+
|
| 69 |
+
for batch in dataloader:
|
| 70 |
+
# Only process batches for this task
|
| 71 |
+
if batch["task_name"][0] != self.task_name:
|
| 72 |
+
continue
|
| 73 |
+
|
| 74 |
+
batch = {
|
| 75 |
+
k: v.to(device) if isinstance(v, torch.Tensor) else v for k, v in batch.items()
|
| 76 |
+
}
|
| 77 |
+
self.model.zero_grad()
|
| 78 |
+
|
| 79 |
+
outputs = self.model(
|
| 80 |
+
input_ids=batch["input_ids"],
|
| 81 |
+
attention_mask=batch.get("attention_mask"),
|
| 82 |
+
task_name=self.task_name,
|
| 83 |
+
labels=batch.get("labels"),
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
loss = outputs["loss"]
|
| 87 |
+
if loss is not None:
|
| 88 |
+
loss.backward()
|
| 89 |
+
|
| 90 |
+
for n, p in self.model.named_parameters():
|
| 91 |
+
if p.requires_grad and p.grad is not None:
|
| 92 |
+
self.fisher[n] += p.grad.detach() ** 2
|
| 93 |
+
|
| 94 |
+
# Normalize Fisher matrix
|
| 95 |
+
for n in self.fisher:
|
| 96 |
+
self.fisher[n] /= len(dataloader)
|
| 97 |
+
|
| 98 |
+
def penalty(self, model: nn.Module) -> torch.Tensor:
|
| 99 |
+
"""
|
| 100 |
+
Compute the EWC penalty for the current model.
|
| 101 |
+
|
| 102 |
+
Args:
|
| 103 |
+
model: Current model state
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
EWC penalty loss
|
| 107 |
+
"""
|
| 108 |
+
loss = 0.0
|
| 109 |
+
for n, p in model.named_parameters():
|
| 110 |
+
if n in self.fisher and n in self.params:
|
| 111 |
+
loss += (self.fisher[n] * (p - self.params[n]) ** 2).sum()
|
| 112 |
+
return self.lambda_param * loss
|
src/dytr/memory/replay.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Experience replay buffer for continual learning.
|
| 17 |
+
|
| 18 |
+
This module implements a replay buffer that stores samples from previous tasks
|
| 19 |
+
to prevent catastrophic forgetting.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
import random
|
| 26 |
+
from collections import defaultdict
|
| 27 |
+
from typing import Any, List, Optional
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class ReplayBuffer:
|
| 31 |
+
"""
|
| 32 |
+
Experience replay buffer for storing samples from previous tasks.
|
| 33 |
+
|
| 34 |
+
The buffer maintains a fixed capacity and stores samples along with
|
| 35 |
+
their task labels for later replay during training.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
capacity: Maximum number of samples to store
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(self, capacity: int = 1000):
|
| 42 |
+
self.capacity = capacity
|
| 43 |
+
self.buffer = []
|
| 44 |
+
self.task_indices = defaultdict(list)
|
| 45 |
+
|
| 46 |
+
def add_samples(self, task_name: str, samples: List[Any]):
|
| 47 |
+
"""
|
| 48 |
+
Add samples to the replay buffer.
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
task_name: Name of the task these samples belong to
|
| 52 |
+
samples: List of samples to add
|
| 53 |
+
"""
|
| 54 |
+
for sample in samples:
|
| 55 |
+
# Remove oldest sample if buffer is full
|
| 56 |
+
if len(self.buffer) >= self.capacity:
|
| 57 |
+
oldest_task = self.buffer[0][0]
|
| 58 |
+
self.task_indices[oldest_task].pop(0)
|
| 59 |
+
self.buffer.pop(0)
|
| 60 |
+
|
| 61 |
+
self.buffer.append((task_name, sample))
|
| 62 |
+
self.task_indices[task_name].append(len(self.buffer) - 1)
|
| 63 |
+
|
| 64 |
+
def sample(self, batch_size: int, task_name: Optional[str] = None) -> List[Any]:
|
| 65 |
+
"""
|
| 66 |
+
Sample random samples from the buffer.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
batch_size: Number of samples to retrieve
|
| 70 |
+
task_name: If specified, only sample from this task
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
List of sampled samples
|
| 74 |
+
"""
|
| 75 |
+
if task_name and task_name in self.task_indices and self.task_indices[task_name]:
|
| 76 |
+
indices = random.sample(
|
| 77 |
+
self.task_indices[task_name], min(batch_size, len(self.task_indices[task_name]))
|
| 78 |
+
)
|
| 79 |
+
return [self.buffer[i][1] for i in indices]
|
| 80 |
+
elif self.buffer:
|
| 81 |
+
indices = random.sample(range(len(self.buffer)), min(batch_size, len(self.buffer)))
|
| 82 |
+
return [self.buffer[i][1] for i in indices]#, [self.buffer[i][0] for i in indices]
|
| 83 |
+
return []
|
src/dytr/pretrained/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pretrained model loading utilities for dytr.
|
| 3 |
+
|
| 4 |
+
This module provides functionality to load pretrained models from HuggingFace
|
| 5 |
+
without requiring the transformers library.
|
| 6 |
+
|
| 7 |
+
Author: Dr. Akram Alsubari
|
| 8 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
from dytr.pretrained.loader import PretrainedModelLoader
|
| 12 |
+
|
| 13 |
+
__all__ = ["PretrainedModelLoader"]
|
src/dytr/pretrained/loader.py
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Pretrained model loader for Dynamic Transformer.
|
| 3 |
+
Supports loading encoder-only models (BERT, RoBERTa, DistilBERT, ALBERT) as the shared encoder.
|
| 4 |
+
|
| 5 |
+
Author: Dr. Akram Alsubari
|
| 6 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import torch
|
| 12 |
+
import torch.nn as nn
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from typing import Dict, Optional, Union, List, Tuple
|
| 15 |
+
import requests
|
| 16 |
+
from tqdm import tqdm
|
| 17 |
+
|
| 18 |
+
from dytr.core.model import DynamicTransformer
|
| 19 |
+
from dytr.core.config import ModelConfig
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class PretrainedModelLoader:
|
| 23 |
+
"""
|
| 24 |
+
Load pretrained encoder models from HuggingFace without transformers library.
|
| 25 |
+
Supports BERT, RoBERTa, DistilBERT, ALBERT architectures.
|
| 26 |
+
|
| 27 |
+
These models are loaded as the shared encoder in DynamicTransformer.
|
| 28 |
+
|
| 29 |
+
Example:
|
| 30 |
+
>>> from dytr import PretrainedModelLoader, ModelConfig
|
| 31 |
+
>>> loader = PretrainedModelLoader()
|
| 32 |
+
>>> config = ModelConfig(tokenizer_name='prajjwal1/bert-tiny', use_simple_tokenizer=False)
|
| 33 |
+
>>> model = loader.load_pretrained('prajjwal1/bert-tiny', config)
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
# Supported encoder-only model types
|
| 37 |
+
SUPPORTED_MODELS = ['bert', 'roberta', 'distilbert', 'albert']
|
| 38 |
+
|
| 39 |
+
# Models that are NOT supported (decoder-only or encoder-decoder)
|
| 40 |
+
UNSUPPORTED_MODELS = ['gpt2', 'gpt', 't5', 'bart', 'pegasus', 'bloom', 'llama']
|
| 41 |
+
|
| 42 |
+
def __init__(self, cache_dir: str = "./pretrained_cache"):
|
| 43 |
+
"""
|
| 44 |
+
Initialize the pretrained model loader.
|
| 45 |
+
|
| 46 |
+
Args:
|
| 47 |
+
cache_dir: Directory to cache downloaded model files
|
| 48 |
+
"""
|
| 49 |
+
self.cache_dir = Path(cache_dir)
|
| 50 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
|
| 52 |
+
def load_pretrained(self, model_name: str, config: Optional[ModelConfig] = None,
|
| 53 |
+
config_override: Optional[Dict] = None) -> DynamicTransformer:
|
| 54 |
+
"""
|
| 55 |
+
Load a pretrained encoder model from HuggingFace.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
model_name: HuggingFace model name (e.g., 'prajjwal1/bert-tiny', 'bert-base-uncased')
|
| 59 |
+
config: ModelConfig instance (will be created if not provided)
|
| 60 |
+
config_override: Override configuration parameters
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
DynamicTransformer model with pretrained encoder weights
|
| 64 |
+
|
| 65 |
+
Raises:
|
| 66 |
+
ValueError: If model type is not supported (decoder-only models)
|
| 67 |
+
"""
|
| 68 |
+
# Detect and validate model type
|
| 69 |
+
model_type = self._detect_model_type(model_name)
|
| 70 |
+
|
| 71 |
+
if model_type in self.UNSUPPORTED_MODELS:
|
| 72 |
+
raise ValueError(
|
| 73 |
+
f"\n{'='*60}\n"
|
| 74 |
+
f"ERROR: Model '{model_name}' is a {model_type.upper()} model which is NOT supported.\n"
|
| 75 |
+
f"{'='*60}\n"
|
| 76 |
+
f"DynamicTransformer only supports encoder-only models as the shared encoder.\n\n"
|
| 77 |
+
f"Supported model types: {self.SUPPORTED_MODELS}\n"
|
| 78 |
+
f"Examples:\n"
|
| 79 |
+
f" - BERT: 'prajjwal1/bert-tiny', 'bert-base-uncased'\n"
|
| 80 |
+
f" - RoBERTa: 'roberta-base'\n"
|
| 81 |
+
f" - DistilBERT: 'distilbert-base-uncased'\n"
|
| 82 |
+
f" - ALBERT: 'albert-base-v2'\n\n"
|
| 83 |
+
f"Unsupported model types: {self.UNSUPPORTED_MODELS}\n"
|
| 84 |
+
f"{'='*60}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
if model_type not in self.SUPPORTED_MODELS:
|
| 88 |
+
raise ValueError(
|
| 89 |
+
f"Unsupported model type: {model_type}. "
|
| 90 |
+
f"Supported: {self.SUPPORTED_MODELS}"
|
| 91 |
+
)
|
| 92 |
+
|
| 93 |
+
if model_type == 'bert':
|
| 94 |
+
return self.load_bert(model_name, config, config_override)
|
| 95 |
+
elif model_type == 'roberta':
|
| 96 |
+
return self.load_roberta(model_name, config, config_override)
|
| 97 |
+
elif model_type == 'distilbert':
|
| 98 |
+
return self.load_distilbert(model_name, config, config_override)
|
| 99 |
+
elif model_type == 'albert':
|
| 100 |
+
return self.load_albert(model_name, config, config_override)
|
| 101 |
+
else:
|
| 102 |
+
raise ValueError(f"Unsupported model type: {model_type}")
|
| 103 |
+
|
| 104 |
+
def _detect_model_type(self, model_name: str) -> str:
|
| 105 |
+
"""Detect model type from name or config."""
|
| 106 |
+
model_name_lower = model_name.lower()
|
| 107 |
+
|
| 108 |
+
# Check by name first
|
| 109 |
+
if 'xlm-roberta' in model_name_lower:
|
| 110 |
+
return 'xlm'
|
| 111 |
+
elif 'roberta' in model_name_lower:
|
| 112 |
+
return 'roberta'
|
| 113 |
+
elif 'distilbert' in model_name_lower:
|
| 114 |
+
return 'distilbert'
|
| 115 |
+
elif 'albert' in model_name_lower:
|
| 116 |
+
return 'albert'
|
| 117 |
+
elif 'bert' in model_name_lower:
|
| 118 |
+
return 'bert'
|
| 119 |
+
elif 'gpt' in model_name_lower:
|
| 120 |
+
return 'gpt2'
|
| 121 |
+
elif 't5' in model_name_lower:
|
| 122 |
+
return 't5'
|
| 123 |
+
elif 'bart' in model_name_lower:
|
| 124 |
+
return 'bart'
|
| 125 |
+
elif 'bloom' in model_name_lower:
|
| 126 |
+
return 'bloom'
|
| 127 |
+
elif 'llama' in model_name_lower:
|
| 128 |
+
return 'llama'
|
| 129 |
+
|
| 130 |
+
# Try to detect from config if already downloaded
|
| 131 |
+
try:
|
| 132 |
+
config_path = self.cache_dir / model_name / 'config.json'
|
| 133 |
+
if config_path.exists():
|
| 134 |
+
with open(config_path, 'r') as f:
|
| 135 |
+
config_data = json.load(f)
|
| 136 |
+
if 'model_type' in config_data:
|
| 137 |
+
model_type = config_data['model_type']
|
| 138 |
+
if model_type in self.SUPPORTED_MODELS:
|
| 139 |
+
return model_type
|
| 140 |
+
else:
|
| 141 |
+
return model_type
|
| 142 |
+
except:
|
| 143 |
+
pass
|
| 144 |
+
|
| 145 |
+
raise ValueError(f"Could not detect model type for {model_name}")
|
| 146 |
+
|
| 147 |
+
def download_model_files(self, model_name: str, model_type: str) -> Dict[str, Path]:
|
| 148 |
+
"""
|
| 149 |
+
Download model files from HuggingFace.
|
| 150 |
+
|
| 151 |
+
Args:
|
| 152 |
+
model_name: HuggingFace model name
|
| 153 |
+
model_type: Type of model ('bert', 'roberta', etc.)
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
Dictionary of downloaded file paths
|
| 157 |
+
"""
|
| 158 |
+
model_dir = self.cache_dir / model_name
|
| 159 |
+
model_dir.mkdir(parents=True, exist_ok=True)
|
| 160 |
+
|
| 161 |
+
files_to_download = {
|
| 162 |
+
'config.json': f"https://huggingface.co/{model_name}/resolve/main/config.json",
|
| 163 |
+
'pytorch_model.bin': f"https://huggingface.co/{model_name}/resolve/main/pytorch_model.bin"
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
# Add tokenizer files based on model type
|
| 167 |
+
if model_type in ['bert', 'roberta', 'distilbert', 'albert']:
|
| 168 |
+
files_to_download['vocab.txt'] = f"https://huggingface.co/{model_name}/resolve/main/vocab.txt"
|
| 169 |
+
|
| 170 |
+
downloaded_files = {}
|
| 171 |
+
|
| 172 |
+
for filename, url in files_to_download.items():
|
| 173 |
+
filepath = model_dir / filename
|
| 174 |
+
if not filepath.exists():
|
| 175 |
+
print(f"Downloading {filename}...")
|
| 176 |
+
try:
|
| 177 |
+
self._download_file(url, filepath)
|
| 178 |
+
except:
|
| 179 |
+
|
| 180 |
+
print(f"Error on Downloading A file:\n {filename}\nplease downlaod it manualy and store it as following path:\n{filepath} ")
|
| 181 |
+
input("press ENTER when completed .....")
|
| 182 |
+
continue
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
else:
|
| 186 |
+
print(f"Using cached {filename}")
|
| 187 |
+
downloaded_files[filename] = filepath
|
| 188 |
+
|
| 189 |
+
return downloaded_files
|
| 190 |
+
|
| 191 |
+
def _download_file(self, url: str, filepath: Path):
|
| 192 |
+
"""Download file with progress bar."""
|
| 193 |
+
try:
|
| 194 |
+
response = requests.get(url, stream=True, timeout=30)
|
| 195 |
+
response.raise_for_status()
|
| 196 |
+
|
| 197 |
+
total_size = int(response.headers.get('content-length', 0))
|
| 198 |
+
|
| 199 |
+
with open(filepath, 'wb') as f:
|
| 200 |
+
with tqdm(total=total_size, unit='B', unit_scale=True, desc=filepath.name) as pbar:
|
| 201 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 202 |
+
if chunk:
|
| 203 |
+
f.write(chunk)
|
| 204 |
+
pbar.update(len(chunk))
|
| 205 |
+
except Exception as e:
|
| 206 |
+
print(f"Error downloading {url}: {e}")
|
| 207 |
+
raise
|
| 208 |
+
|
| 209 |
+
def load_bert(self, model_name: str,
|
| 210 |
+
config: Optional[ModelConfig] = None,
|
| 211 |
+
config_override: Optional[Dict] = None) -> DynamicTransformer:
|
| 212 |
+
"""
|
| 213 |
+
Load BERT model as shared encoder.
|
| 214 |
+
|
| 215 |
+
Args:
|
| 216 |
+
model_name: BERT model name (e.g., 'prajjwal1/bert-tiny', 'bert-base-uncased')
|
| 217 |
+
config: ModelConfig instance
|
| 218 |
+
config_override: Override configuration parameters
|
| 219 |
+
|
| 220 |
+
Returns:
|
| 221 |
+
DynamicTransformer model with BERT encoder weights
|
| 222 |
+
"""
|
| 223 |
+
print(f"\n{'='*60}")
|
| 224 |
+
print(f"Loading BERT model: {model_name}")
|
| 225 |
+
print(f"{'='*60}")
|
| 226 |
+
if config and not config.tokenizer_name:
|
| 227 |
+
config.tokenizer_name=model_name
|
| 228 |
+
# Download model files
|
| 229 |
+
files = self.download_model_files(model_name, 'bert')
|
| 230 |
+
|
| 231 |
+
# Load config
|
| 232 |
+
with open(files['config.json'], 'r') as f:
|
| 233 |
+
bert_config = json.load(f)
|
| 234 |
+
|
| 235 |
+
print(f"BERT config:")
|
| 236 |
+
print(f" Hidden size: {bert_config.get('hidden_size', 'N/A')}")
|
| 237 |
+
print(f" Layers: {bert_config.get('num_hidden_layers', 'N/A')}")
|
| 238 |
+
print(f" Attention heads: {bert_config.get('num_attention_heads', 'N/A')}")
|
| 239 |
+
print(f" Max position embeddings: {bert_config.get('max_position_embeddings', 'N/A')}")
|
| 240 |
+
print(f" Vocabulary size: {bert_config.get('vocab_size', 'N/A')}")
|
| 241 |
+
|
| 242 |
+
# Create or update model config
|
| 243 |
+
if config is None:
|
| 244 |
+
config = ModelConfig(
|
| 245 |
+
embed_dim=bert_config.get('hidden_size', 768),
|
| 246 |
+
num_layers=bert_config.get('num_hidden_layers', 12),
|
| 247 |
+
num_heads=bert_config.get('num_attention_heads', 12),
|
| 248 |
+
head_dim=bert_config.get('hidden_size', 768) // bert_config.get('num_attention_heads', 12),
|
| 249 |
+
ff_mult=4,
|
| 250 |
+
tokenizer_name=model_name,
|
| 251 |
+
#use_simple_tokenizer=False,
|
| 252 |
+
max_seq_len=bert_config.get('max_position_embeddings', 512),
|
| 253 |
+
dropout=bert_config.get('hidden_dropout_prob', 0.1),
|
| 254 |
+
#use_rotary_embedding=False,
|
| 255 |
+
#use_task_adapters=False,
|
| 256 |
+
special_tokens={}
|
| 257 |
+
)
|
| 258 |
+
else:
|
| 259 |
+
# Update config with BERT settings while preserving user settings
|
| 260 |
+
#config.tokenizer_name = model_name
|
| 261 |
+
#config.use_simple_tokenizer = False
|
| 262 |
+
config.special_tokens = {}
|
| 263 |
+
|
| 264 |
+
#if not hasattr(config, 'embed_dim') or config.embed_dim == 256:
|
| 265 |
+
config.embed_dim = bert_config.get('hidden_size', config.embed_dim )
|
| 266 |
+
#if not hasattr(config, 'num_layers') or config.num_layers == 6:
|
| 267 |
+
config.num_layers = bert_config.get('num_hidden_layers', config.num_layers)
|
| 268 |
+
#if not hasattr(config, 'num_heads') or config.num_heads == 8:
|
| 269 |
+
config.num_heads = bert_config.get('num_attention_heads', config.num_heads )
|
| 270 |
+
config.head_dim = config.embed_dim // config.num_heads
|
| 271 |
+
#if not hasattr(config, 'max_seq_len') or config.max_seq_len == 256:
|
| 272 |
+
config.max_seq_len = bert_config.get('max_position_embeddings', config.max_seq_len)
|
| 273 |
+
config.use_task_adapters=False
|
| 274 |
+
config.use_rotary_embedding=False
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
# Override config if provided
|
| 278 |
+
if config_override:
|
| 279 |
+
for key, value in config_override.items():
|
| 280 |
+
setattr(config, key, value)
|
| 281 |
+
print(f"Overriding {key}: {value}")
|
| 282 |
+
|
| 283 |
+
# Initialize DynamicTransformer
|
| 284 |
+
print("\nInitializing DynamicTransformer...")
|
| 285 |
+
model = DynamicTransformer(config)
|
| 286 |
+
|
| 287 |
+
print(f"Tokenizer loaded with vocab size: {len(model.tokenizer)}")
|
| 288 |
+
|
| 289 |
+
# Load weights
|
| 290 |
+
print("\nLoading model weights...")
|
| 291 |
+
state_dict = torch.load(files['pytorch_model.bin'], map_location='cpu')
|
| 292 |
+
|
| 293 |
+
# Map BERT weights to encoder
|
| 294 |
+
mapped_state_dict = self._map_bert_to_encoder(state_dict, config)
|
| 295 |
+
|
| 296 |
+
# Load mapped weights into encoder only
|
| 297 |
+
missing_keys, unexpected_keys = model.encoder.load_state_dict(mapped_state_dict, strict=False)
|
| 298 |
+
|
| 299 |
+
if missing_keys:
|
| 300 |
+
print(f" Missing keys: {missing_keys}..." if len(missing_keys) > 5 else f" Missing keys: {missing_keys}")
|
| 301 |
+
if unexpected_keys:
|
| 302 |
+
print(f" Unexpected keys: {unexpected_keys[:5]}..." if len(unexpected_keys) > 5 else f" Unexpected keys: {unexpected_keys}")
|
| 303 |
+
|
| 304 |
+
print(f"\n✓ Successfully loaded BERT model as encoder")
|
| 305 |
+
print(f" Encoder parameters: {sum(p.numel() for p in model.encoder.parameters()):,}")
|
| 306 |
+
print(f" Total model parameters: {sum(p.numel() for p in model.parameters()):,}")
|
| 307 |
+
print(f" Embed dim: {config.embed_dim}")
|
| 308 |
+
print(f" Layers: {config.num_layers}")
|
| 309 |
+
print(f" Heads: {config.num_heads}")
|
| 310 |
+
print(f" Vocabulary size: {len(model.tokenizer)}")
|
| 311 |
+
print("\n📝 Note: This model has no Tasks: Add tasks using model.add_task() or Train the model on different Tasks to be added")
|
| 312 |
+
|
| 313 |
+
return model
|
| 314 |
+
|
| 315 |
+
def _map_bert_to_encoder(self, bert_state: Dict, config: ModelConfig) -> Dict:
|
| 316 |
+
"""
|
| 317 |
+
Map BERT weights to encoder only (no decoders).
|
| 318 |
+
"""
|
| 319 |
+
mapped = {}
|
| 320 |
+
|
| 321 |
+
# Map word embeddings
|
| 322 |
+
if 'bert.embeddings.word_embeddings.weight' in bert_state:
|
| 323 |
+
src_weight = bert_state['bert.embeddings.word_embeddings.weight']
|
| 324 |
+
if src_weight.shape[1] == config.embed_dim:
|
| 325 |
+
mapped['embedding.weight'] = src_weight
|
| 326 |
+
print(f" Mapped word embeddings: shape {src_weight.shape}")
|
| 327 |
+
else:
|
| 328 |
+
print(f" Warning: Word embedding dimension mismatch. Expected {config.embed_dim}, got {src_weight.shape[1]}")
|
| 329 |
+
|
| 330 |
+
# Map position embeddings (if needed)
|
| 331 |
+
if 'bert.embeddings.position_embeddings.weight' in bert_state:
|
| 332 |
+
# BERT uses learned position embeddings, we use rotary so skip
|
| 333 |
+
pass
|
| 334 |
+
|
| 335 |
+
# Map token type embeddings (if needed)
|
| 336 |
+
if 'bert.embeddings.token_type_embeddings.weight' in bert_state:
|
| 337 |
+
# Not used in our model, skip
|
| 338 |
+
# future work
|
| 339 |
+
pass
|
| 340 |
+
|
| 341 |
+
# Map layer norm before encoder
|
| 342 |
+
if 'bert.embeddings.LayerNorm.weight' in bert_state:
|
| 343 |
+
# Not used in our encoder structure, skip
|
| 344 |
+
# future work
|
| 345 |
+
pass
|
| 346 |
+
|
| 347 |
+
# Map encoder layers
|
| 348 |
+
for i in range(config.num_layers):
|
| 349 |
+
prefix = f'bert.encoder.layer.{i}'
|
| 350 |
+
target_prefix = f'layers.{i}'
|
| 351 |
+
|
| 352 |
+
# Check if this layer exists in checkpoint
|
| 353 |
+
if f'{prefix}.attention.self.query.weight' not in bert_state:
|
| 354 |
+
if i == 0:
|
| 355 |
+
print(f" Warning: Layer {i} not found in checkpoint")
|
| 356 |
+
continue
|
| 357 |
+
|
| 358 |
+
# Self-attention Q, K, V projections
|
| 359 |
+
if f'{prefix}.attention.self.query.weight' in bert_state:
|
| 360 |
+
mapped[f'{target_prefix}.attention.q_proj.weight'] = bert_state[f'{prefix}.attention.self.query.weight']
|
| 361 |
+
mapped[f'{target_prefix}.attention.q_proj.bias'] = bert_state[f'{prefix}.attention.self.query.bias']
|
| 362 |
+
|
| 363 |
+
mapped[f'{target_prefix}.attention.k_proj.weight'] = bert_state[f'{prefix}.attention.self.key.weight']
|
| 364 |
+
mapped[f'{target_prefix}.attention.k_proj.bias'] = bert_state[f'{prefix}.attention.self.key.bias']
|
| 365 |
+
|
| 366 |
+
mapped[f'{target_prefix}.attention.v_proj.weight'] = bert_state[f'{prefix}.attention.self.value.weight']
|
| 367 |
+
mapped[f'{target_prefix}.attention.v_proj.bias'] = bert_state[f'{prefix}.attention.self.value.bias']
|
| 368 |
+
|
| 369 |
+
# Attention output projection
|
| 370 |
+
if f'{prefix}.attention.output.dense.weight' in bert_state:
|
| 371 |
+
mapped[f'{target_prefix}.attention.out_proj.weight'] = bert_state[f'{prefix}.attention.output.dense.weight']
|
| 372 |
+
mapped[f'{target_prefix}.attention.out_proj.bias'] = bert_state[f'{prefix}.attention.output.dense.bias']
|
| 373 |
+
|
| 374 |
+
# Attention layer norm
|
| 375 |
+
if f'{prefix}.attention.output.LayerNorm.weight' in bert_state:
|
| 376 |
+
mapped[f'{target_prefix}.attention_norm.weight'] = bert_state[f'{prefix}.attention.output.LayerNorm.weight']
|
| 377 |
+
mapped[f'{target_prefix}.attention_norm.bias'] = bert_state[f'{prefix}.attention.output.LayerNorm.bias']
|
| 378 |
+
|
| 379 |
+
# Feed-forward intermediate (gate and up projections)
|
| 380 |
+
if f'{prefix}.intermediate.dense.weight' in bert_state:
|
| 381 |
+
mapped[f'{target_prefix}.ffn.gate_proj.weight'] = bert_state[f'{prefix}.intermediate.dense.weight']
|
| 382 |
+
mapped[f'{target_prefix}.ffn.gate_proj.bias'] = bert_state[f'{prefix}.intermediate.dense.bias']
|
| 383 |
+
mapped[f'{target_prefix}.ffn.up_proj.weight'] = bert_state[f'{prefix}.intermediate.dense.weight']
|
| 384 |
+
mapped[f'{target_prefix}.ffn.up_proj.bias'] = bert_state[f'{prefix}.intermediate.dense.bias']
|
| 385 |
+
|
| 386 |
+
# Feed-forward output
|
| 387 |
+
if f'{prefix}.output.dense.weight' in bert_state:
|
| 388 |
+
mapped[f'{target_prefix}.ffn.down_proj.weight'] = bert_state[f'{prefix}.output.dense.weight']
|
| 389 |
+
mapped[f'{target_prefix}.ffn.down_proj.bias'] = bert_state[f'{prefix}.output.dense.bias']
|
| 390 |
+
|
| 391 |
+
# Feed-forward layer norm
|
| 392 |
+
if f'{prefix}.output.LayerNorm.weight' in bert_state:
|
| 393 |
+
mapped[f'{target_prefix}.ffn_norm.weight'] = bert_state[f'{prefix}.output.LayerNorm.weight']
|
| 394 |
+
mapped[f'{target_prefix}.ffn_norm.bias'] = bert_state[f'{prefix}.output.LayerNorm.bias']
|
| 395 |
+
|
| 396 |
+
if i < 4: # Print first few layers
|
| 397 |
+
print(f" Mapped layer {i}")
|
| 398 |
+
|
| 399 |
+
# Map final layer norm
|
| 400 |
+
if 'bert.encoder.LayerNorm.weight' in bert_state:
|
| 401 |
+
mapped['norm.weight'] = bert_state['bert.encoder.LayerNorm.weight']
|
| 402 |
+
mapped['norm.bias'] = bert_state['bert.encoder.LayerNorm.bias']
|
| 403 |
+
print(" Mapped final layer norm")
|
| 404 |
+
else:
|
| 405 |
+
|
| 406 |
+
mapped['norm.weight'] = torch.ones(config.embed_dim)
|
| 407 |
+
mapped['norm.bias'] = torch.zeros(config.embed_dim)
|
| 408 |
+
print(" Initialized final layer norm with identity")
|
| 409 |
+
if 'bert.pooler.dense.weight' in bert_state:
|
| 410 |
+
# Some BERT variants use pooler instead
|
| 411 |
+
# future work
|
| 412 |
+
pass
|
| 413 |
+
|
| 414 |
+
return mapped
|
| 415 |
+
|
| 416 |
+
def load_roberta(self, model_name: str,
|
| 417 |
+
config: Optional[ModelConfig] = None,
|
| 418 |
+
config_override: Optional[Dict] = None) -> DynamicTransformer:
|
| 419 |
+
"""
|
| 420 |
+
Load RoBERTa model as shared encoder.
|
| 421 |
+
RoBERTa uses the same architecture as BERT with different training.
|
| 422 |
+
"""
|
| 423 |
+
print(f"\n{'='*60}")
|
| 424 |
+
print(f"Loading RoBERTa model: {model_name}")
|
| 425 |
+
print(f"{'='*60}")
|
| 426 |
+
if config and not config.tokenizer_name:
|
| 427 |
+
config.tokenizer_name=model_name
|
| 428 |
+
# Download model files
|
| 429 |
+
files = self.download_model_files(model_name, 'roberta')
|
| 430 |
+
|
| 431 |
+
# Load config
|
| 432 |
+
with open(files['config.json'], 'r') as f:
|
| 433 |
+
roberta_config = json.load(f)
|
| 434 |
+
|
| 435 |
+
print(f"RoBERTa config:")
|
| 436 |
+
print(f" Hidden size: {roberta_config.get('hidden_size', 'N/A')}")
|
| 437 |
+
print(f" Layers: {roberta_config.get('num_hidden_layers', 'N/A')}")
|
| 438 |
+
print(f" Attention heads: {roberta_config.get('num_attention_heads', 'N/A')}")
|
| 439 |
+
|
| 440 |
+
# Create or update model config
|
| 441 |
+
if config is None:
|
| 442 |
+
config = ModelConfig(
|
| 443 |
+
embed_dim=roberta_config.get('hidden_size', 768),
|
| 444 |
+
num_layers=roberta_config.get('num_hidden_layers', 12),
|
| 445 |
+
num_heads=roberta_config.get('num_attention_heads', 12),
|
| 446 |
+
head_dim=roberta_config.get('hidden_size', 768) // roberta_config.get('num_attention_heads', 12),
|
| 447 |
+
ff_mult=4,
|
| 448 |
+
tokenizer_name=model_name,
|
| 449 |
+
#use_simple_tokenizer=False,
|
| 450 |
+
max_seq_len=roberta_config.get('max_position_embeddings', 514),
|
| 451 |
+
dropout=roberta_config.get('hidden_dropout_prob', 0.1),
|
| 452 |
+
use_rotary_embedding=False,
|
| 453 |
+
use_task_adapters=False,
|
| 454 |
+
special_tokens={}
|
| 455 |
+
)
|
| 456 |
+
else:
|
| 457 |
+
#config.tokenizer_name = model_name
|
| 458 |
+
#config.use_simple_tokenizer = False
|
| 459 |
+
config.special_tokens = {}
|
| 460 |
+
#if not hasattr(config, 'embed_dim') or config.embed_dim == 256:
|
| 461 |
+
config.embed_dim = roberta_config.get('hidden_size', config.embed_dim)
|
| 462 |
+
#if not hasattr(config, 'num_layers') or config.num_layers == 6:
|
| 463 |
+
config.num_layers = roberta_config.get('num_hidden_layers', config.num_layers)
|
| 464 |
+
#if not hasattr(config, 'num_heads') or config.num_heads == 8:
|
| 465 |
+
config.num_heads = roberta_config.get('num_attention_heads', config.num_heads )
|
| 466 |
+
config.head_dim = config.embed_dim // config.num_heads
|
| 467 |
+
config.use_task_adapters=False
|
| 468 |
+
config.use_rotary_embedding=False
|
| 469 |
+
|
| 470 |
+
# Override config if provided
|
| 471 |
+
if config_override:
|
| 472 |
+
for key, value in config_override.items():
|
| 473 |
+
setattr(config, key, value)
|
| 474 |
+
print(f"Overriding {key}: {value}")
|
| 475 |
+
|
| 476 |
+
# Initialize DynamicTransformer
|
| 477 |
+
print("\nInitializing DynamicTransformer...")
|
| 478 |
+
model = DynamicTransformer(config)
|
| 479 |
+
|
| 480 |
+
print(f"Tokenizer loaded with vocab size: {len(model.tokenizer)}")
|
| 481 |
+
|
| 482 |
+
# Load weights
|
| 483 |
+
print("\nLoading model weights...")
|
| 484 |
+
state_dict = torch.load(files['pytorch_model.bin'], map_location='cpu')
|
| 485 |
+
|
| 486 |
+
# RoBERTa uses similar structure to BERT
|
| 487 |
+
# Need to map 'roberta.' prefix to 'bert.' prefix
|
| 488 |
+
renamed_state_dict = {}
|
| 489 |
+
for key, value in state_dict.items():
|
| 490 |
+
if key.startswith('roberta.'):
|
| 491 |
+
new_key = 'bert.' + key[8:]
|
| 492 |
+
renamed_state_dict[new_key] = value
|
| 493 |
+
else:
|
| 494 |
+
renamed_state_dict[key] = value
|
| 495 |
+
|
| 496 |
+
mapped_state_dict = self._map_bert_to_encoder(renamed_state_dict, config)
|
| 497 |
+
model.encoder.load_state_dict(mapped_state_dict, strict=False)
|
| 498 |
+
|
| 499 |
+
print(f"\n✓ Successfully loaded RoBERTa model as encoder")
|
| 500 |
+
print(f" Encoder parameters: {sum(p.numel() for p in model.encoder.parameters()):,}")
|
| 501 |
+
print(f" Embed dim: {config.embed_dim}")
|
| 502 |
+
print(f" Layers: {config.num_layers}")
|
| 503 |
+
print(f" Vocabulary size: {len(model.tokenizer)}")
|
| 504 |
+
|
| 505 |
+
return model
|
| 506 |
+
|
| 507 |
+
def load_distilbert(self, model_name: str,
|
| 508 |
+
config: Optional[ModelConfig] = None,
|
| 509 |
+
config_override: Optional[Dict] = None) -> DynamicTransformer:
|
| 510 |
+
"""
|
| 511 |
+
Load DistilBERT model as shared encoder.
|
| 512 |
+
DistilBERT is a distilled version of BERT with fewer layers.
|
| 513 |
+
"""
|
| 514 |
+
print(f"\n{'='*60}")
|
| 515 |
+
print(f"Loading DistilBERT model: {model_name}")
|
| 516 |
+
print(f"{'='*60}")
|
| 517 |
+
|
| 518 |
+
# Download model files
|
| 519 |
+
files = self.download_model_files(model_name, 'distilbert')
|
| 520 |
+
|
| 521 |
+
# Load config
|
| 522 |
+
with open(files['config.json'], 'r') as f:
|
| 523 |
+
distilbert_config = json.load(f)
|
| 524 |
+
|
| 525 |
+
print(f"DistilBERT config:")
|
| 526 |
+
print(f" Hidden size: {distilbert_config.get('dim', 'N/A')}")
|
| 527 |
+
print(f" Layers: {distilbert_config.get('n_layers', 'N/A')}")
|
| 528 |
+
print(f" Attention heads: {distilbert_config.get('n_heads', 'N/A')}")
|
| 529 |
+
|
| 530 |
+
# Create or update model config
|
| 531 |
+
if config is None:
|
| 532 |
+
config = ModelConfig(
|
| 533 |
+
embed_dim=distilbert_config.get('dim', 768),
|
| 534 |
+
num_layers=distilbert_config.get('n_layers', 6),
|
| 535 |
+
num_heads=distilbert_config.get('n_heads', 12),
|
| 536 |
+
head_dim=distilbert_config.get('dim', 768) // distilbert_config.get('n_heads', 12),
|
| 537 |
+
ff_mult=4,
|
| 538 |
+
tokenizer_name=model_name,
|
| 539 |
+
#use_simple_tokenizer=False,
|
| 540 |
+
max_seq_len=distilbert_config.get('max_position_embeddings', 512),
|
| 541 |
+
dropout=distilbert_config.get('dropout', 0.1),
|
| 542 |
+
use_rotary_embedding=False,
|
| 543 |
+
use_task_adapters=False,
|
| 544 |
+
special_tokens={}
|
| 545 |
+
)
|
| 546 |
+
else:
|
| 547 |
+
#config.tokenizer_name = model_name
|
| 548 |
+
#config.use_simple_tokenizer = False
|
| 549 |
+
config.special_tokens = {}
|
| 550 |
+
#if not hasattr(config, 'embed_dim') or config.embed_dim == 256:
|
| 551 |
+
config.embed_dim = distilbert_config.get('dim', config.embed_dim)
|
| 552 |
+
#if not hasattr(config, 'num_layers') or config.num_layers == 6:
|
| 553 |
+
config.num_layers = distilbert_config.get('n_layers', config.num_layers )
|
| 554 |
+
#if not hasattr(config, 'num_heads') or config.num_heads == 8:
|
| 555 |
+
config.num_heads = distilbert_config.get('n_heads', config.num_heads)
|
| 556 |
+
config.head_dim = config.embed_dim // config.num_heads
|
| 557 |
+
config.use_task_adapters=False
|
| 558 |
+
config.use_rotary_embedding=False
|
| 559 |
+
|
| 560 |
+
# Override config if provided
|
| 561 |
+
if config_override:
|
| 562 |
+
for key, value in config_override.items():
|
| 563 |
+
setattr(config, key, value)
|
| 564 |
+
print(f"Overriding {key}: {value}")
|
| 565 |
+
|
| 566 |
+
# Initialize DynamicTransformer
|
| 567 |
+
print("\nInitializing DynamicTransformer...")
|
| 568 |
+
model = DynamicTransformer(config)
|
| 569 |
+
|
| 570 |
+
print(f"Tokenizer loaded with vocab size: {len(model.tokenizer)}")
|
| 571 |
+
|
| 572 |
+
# Load weights
|
| 573 |
+
print("\nLoading model weights...")
|
| 574 |
+
state_dict = torch.load(files['pytorch_model.bin'], map_location='cpu')
|
| 575 |
+
|
| 576 |
+
# Map DistilBERT weights to encoder
|
| 577 |
+
mapped_state_dict = self._map_distilbert_to_encoder(state_dict, config)
|
| 578 |
+
|
| 579 |
+
# Load mapped weights
|
| 580 |
+
model.encoder.load_state_dict(mapped_state_dict, strict=False)
|
| 581 |
+
|
| 582 |
+
print(f"\n✓ Successfully loaded DistilBERT model as encoder")
|
| 583 |
+
print(f" Encoder parameters: {sum(p.numel() for p in model.encoder.parameters()):,}")
|
| 584 |
+
print(f" Embed dim: {config.embed_dim}")
|
| 585 |
+
print(f" Layers: {config.num_layers}")
|
| 586 |
+
print(f" Vocabulary size: {len(model.tokenizer)}")
|
| 587 |
+
|
| 588 |
+
return model
|
| 589 |
+
|
| 590 |
+
def _map_distilbert_to_encoder(self, state_dict: Dict, config: ModelConfig) -> Dict:
|
| 591 |
+
"""
|
| 592 |
+
Map DistilBERT weights to encoder.
|
| 593 |
+
DistilBERT has a simpler structure than BERT.
|
| 594 |
+
"""
|
| 595 |
+
mapped = {}
|
| 596 |
+
|
| 597 |
+
# Word embeddings
|
| 598 |
+
if 'distilbert.embeddings.word_embeddings.weight' in state_dict:
|
| 599 |
+
src_weight = state_dict['distilbert.embeddings.word_embeddings.weight']
|
| 600 |
+
if src_weight.shape[1] == config.embed_dim:
|
| 601 |
+
mapped['embedding.weight'] = src_weight
|
| 602 |
+
print(f" Mapped word embeddings: shape {src_weight.shape}")
|
| 603 |
+
|
| 604 |
+
# Position embeddings (if present)
|
| 605 |
+
if 'distilbert.embeddings.position_embeddings.weight' in state_dict:
|
| 606 |
+
# DistilBERT uses learned position embeddings, we skip
|
| 607 |
+
# future work
|
| 608 |
+
pass
|
| 609 |
+
|
| 610 |
+
# Transformer layers
|
| 611 |
+
for i in range(config.num_layers):
|
| 612 |
+
prefix = f'distilbert.transformer.layer.{i}'
|
| 613 |
+
target_prefix = f'layers.{i}'
|
| 614 |
+
|
| 615 |
+
# Check if layer exists
|
| 616 |
+
if f'{prefix}.attention.q_lin.weight' not in state_dict:
|
| 617 |
+
continue
|
| 618 |
+
|
| 619 |
+
# Q, K, V projections (DistilBERT has separate linear layers)
|
| 620 |
+
if f'{prefix}.attention.q_lin.weight' in state_dict:
|
| 621 |
+
mapped[f'{target_prefix}.attention.q_proj.weight'] = state_dict[f'{prefix}.attention.q_lin.weight']
|
| 622 |
+
mapped[f'{target_prefix}.attention.q_proj.bias'] = state_dict[f'{prefix}.attention.q_lin.bias']
|
| 623 |
+
|
| 624 |
+
mapped[f'{target_prefix}.attention.k_proj.weight'] = state_dict[f'{prefix}.attention.k_lin.weight']
|
| 625 |
+
mapped[f'{target_prefix}.attention.k_proj.bias'] = state_dict[f'{prefix}.attention.k_lin.bias']
|
| 626 |
+
|
| 627 |
+
mapped[f'{target_prefix}.attention.v_proj.weight'] = state_dict[f'{prefix}.attention.v_lin.weight']
|
| 628 |
+
mapped[f'{target_prefix}.attention.v_proj.bias'] = state_dict[f'{prefix}.attention.v_lin.bias']
|
| 629 |
+
|
| 630 |
+
# Attention output projection
|
| 631 |
+
if f'{prefix}.attention.out_lin.weight' in state_dict:
|
| 632 |
+
mapped[f'{target_prefix}.attention.out_proj.weight'] = state_dict[f'{prefix}.attention.out_lin.weight']
|
| 633 |
+
mapped[f'{target_prefix}.attention.out_proj.bias'] = state_dict[f'{prefix}.attention.out_lin.bias']
|
| 634 |
+
|
| 635 |
+
# Self-attention layer norm
|
| 636 |
+
if f'{prefix}.sa_layer_norm.weight' in state_dict:
|
| 637 |
+
mapped[f'{target_prefix}.attention_norm.weight'] = state_dict[f'{prefix}.sa_layer_norm.weight']
|
| 638 |
+
mapped[f'{target_prefix}.attention_norm.bias'] = state_dict[f'{prefix}.sa_layer_norm.bias']
|
| 639 |
+
|
| 640 |
+
# Feed-forward first linear (gate and up share same weights)
|
| 641 |
+
if f'{prefix}.ffn.lin1.weight' in state_dict:
|
| 642 |
+
mapped[f'{target_prefix}.ffn.gate_proj.weight'] = state_dict[f'{prefix}.ffn.lin1.weight']
|
| 643 |
+
mapped[f'{target_prefix}.ffn.gate_proj.bias'] = state_dict[f'{prefix}.ffn.lin1.bias']
|
| 644 |
+
mapped[f'{target_prefix}.ffn.up_proj.weight'] = state_dict[f'{prefix}.ffn.lin1.weight']
|
| 645 |
+
mapped[f'{target_prefix}.ffn.up_proj.bias'] = state_dict[f'{prefix}.ffn.lin1.bias']
|
| 646 |
+
|
| 647 |
+
# Feed-forward second linear
|
| 648 |
+
if f'{prefix}.ffn.lin2.weight' in state_dict:
|
| 649 |
+
mapped[f'{target_prefix}.ffn.down_proj.weight'] = state_dict[f'{prefix}.ffn.lin2.weight']
|
| 650 |
+
mapped[f'{target_prefix}.ffn.down_proj.bias'] = state_dict[f'{prefix}.ffn.lin2.bias']
|
| 651 |
+
|
| 652 |
+
# Feed-forward layer norm
|
| 653 |
+
if f'{prefix}.output_layer_norm.weight' in state_dict:
|
| 654 |
+
mapped[f'{target_prefix}.ffn_norm.weight'] = state_dict[f'{prefix}.output_layer_norm.weight']
|
| 655 |
+
mapped[f'{target_prefix}.ffn_norm.bias'] = state_dict[f'{prefix}.output_layer_norm.bias']
|
| 656 |
+
|
| 657 |
+
if i < 2:
|
| 658 |
+
print(f" Mapped layer {i}")
|
| 659 |
+
|
| 660 |
+
# Final layer norm (if exists)
|
| 661 |
+
if 'distilbert.transformer.LayerNorm.weight' in state_dict:
|
| 662 |
+
mapped['norm.weight'] = state_dict['distilbert.transformer.LayerNorm.weight']
|
| 663 |
+
mapped['norm.bias'] = state_dict['distilbert.transformer.LayerNorm.bias']
|
| 664 |
+
print(f" Mapped final layer norm")
|
| 665 |
+
|
| 666 |
+
return mapped
|
| 667 |
+
|
| 668 |
+
def load_albert(self, model_name: str,
|
| 669 |
+
config: Optional[ModelConfig] = None,
|
| 670 |
+
config_override: Optional[Dict] = None) -> DynamicTransformer:
|
| 671 |
+
"""
|
| 672 |
+
Load ALBERT model as shared encoder.
|
| 673 |
+
ALBERT uses parameter sharing across layers.
|
| 674 |
+
"""
|
| 675 |
+
print(f"\n{'='*60}")
|
| 676 |
+
print(f"Loading ALBERT model: {model_name}")
|
| 677 |
+
print(f"{'='*60}")
|
| 678 |
+
|
| 679 |
+
# Download model files
|
| 680 |
+
files = self.download_model_files(model_name, 'albert')
|
| 681 |
+
|
| 682 |
+
# Load config
|
| 683 |
+
with open(files['config.json'], 'r') as f:
|
| 684 |
+
albert_config = json.load(f)
|
| 685 |
+
|
| 686 |
+
print(f"ALBERT config:")
|
| 687 |
+
print(f" Hidden size: {albert_config.get('hidden_size', 'N/A')}")
|
| 688 |
+
print(f" Layers: {albert_config.get('num_hidden_layers', 'N/A')}")
|
| 689 |
+
print(f" Attention heads: {albert_config.get('num_attention_heads', 'N/A')}")
|
| 690 |
+
print(f" Embedding size: {albert_config.get('embedding_size', 'N/A')}")
|
| 691 |
+
|
| 692 |
+
# Create or update model config
|
| 693 |
+
if config is None:
|
| 694 |
+
config = ModelConfig(
|
| 695 |
+
embed_dim=albert_config.get('hidden_size', 768),
|
| 696 |
+
num_layers=albert_config.get('num_hidden_layers', 12),
|
| 697 |
+
num_heads=albert_config.get('num_attention_heads', 12),
|
| 698 |
+
head_dim=albert_config.get('hidden_size', 768) // albert_config.get('num_attention_heads', 12),
|
| 699 |
+
ff_mult=4,
|
| 700 |
+
tokenizer_name=model_name,
|
| 701 |
+
#use_simple_tokenizer=False,
|
| 702 |
+
max_seq_len=albert_config.get('max_position_embeddings', 512),
|
| 703 |
+
dropout=albert_config.get('hidden_dropout_prob', 0.1),
|
| 704 |
+
use_rotary_embedding=False,
|
| 705 |
+
use_task_adapters=False,
|
| 706 |
+
special_tokens={}
|
| 707 |
+
)
|
| 708 |
+
else:
|
| 709 |
+
#config.tokenizer_name = model_name
|
| 710 |
+
#config.use_simple_tokenizer = False
|
| 711 |
+
config.special_tokens = {}
|
| 712 |
+
|
| 713 |
+
config.embed_dim = albert_config.get('hidden_size', config.embed_dim)
|
| 714 |
+
|
| 715 |
+
config.num_layers = albert_config.get('num_hidden_layers', config.num_layers)
|
| 716 |
+
|
| 717 |
+
config.num_heads = albert_config.get('num_attention_heads', config.num_heads)
|
| 718 |
+
config.head_dim = config.embed_dim // config.num_heads
|
| 719 |
+
config.use_task_adapters=False
|
| 720 |
+
config.use_rotary_embedding=False
|
| 721 |
+
|
| 722 |
+
|
| 723 |
+
# Override config if provided
|
| 724 |
+
if config_override:
|
| 725 |
+
for key, value in config_override.items():
|
| 726 |
+
setattr(config, key, value)
|
| 727 |
+
print(f"Overriding {key}: {value}")
|
| 728 |
+
|
| 729 |
+
# Initialize DynamicTransformer
|
| 730 |
+
print("\nInitializing DynamicTransformer...")
|
| 731 |
+
model = DynamicTransformer(config)
|
| 732 |
+
|
| 733 |
+
print(f"Tokenizer loaded with vocab size: {len(model.tokenizer)}")
|
| 734 |
+
|
| 735 |
+
# Load weights
|
| 736 |
+
print("\nLoading model weights...")
|
| 737 |
+
state_dict = torch.load(files['pytorch_model.bin'], map_location='cpu')
|
| 738 |
+
|
| 739 |
+
# ALBERT has a similar structure to BERT but with embedding projection
|
| 740 |
+
# We'll map using BERT mapping with prefix adjustments
|
| 741 |
+
renamed_state_dict = {}
|
| 742 |
+
for key, value in state_dict.items():
|
| 743 |
+
if key.startswith('albert.'):
|
| 744 |
+
new_key = 'bert.' + key[7:]
|
| 745 |
+
renamed_state_dict[new_key] = value
|
| 746 |
+
else:
|
| 747 |
+
renamed_state_dict[key] = value
|
| 748 |
+
|
| 749 |
+
mapped_state_dict = self._map_bert_to_encoder(renamed_state_dict, config)
|
| 750 |
+
model.encoder.load_state_dict(mapped_state_dict, strict=False)
|
| 751 |
+
|
| 752 |
+
print(f"\n✓ Successfully loaded ALBERT model as encoder")
|
| 753 |
+
print(f" Encoder parameters: {sum(p.numel() for p in model.encoder.parameters()):,}")
|
| 754 |
+
print(f" Embed dim: {config.embed_dim}")
|
| 755 |
+
print(f" Layers: {config.num_layers} (note: ALBERT shares parameters across layers)")
|
| 756 |
+
print(f" Vocabulary size: {len(model.tokenizer)}")
|
| 757 |
+
|
| 758 |
+
return model
|
| 759 |
+
|
| 760 |
+
def list_available_models(self) -> Dict[str, List[str]]:
|
| 761 |
+
"""
|
| 762 |
+
List available pretrained models for encoder loading.
|
| 763 |
+
|
| 764 |
+
Returns:
|
| 765 |
+
Dictionary of model types with list of recommended models
|
| 766 |
+
"""
|
| 767 |
+
return {
|
| 768 |
+
'bert': [
|
| 769 |
+
'prajjwal1/bert-tiny', # 2 layers, 128 dim (~4.4M params)
|
| 770 |
+
'prajjwal1/bert-mini', # 4 layers, 256 dim (~11M params)
|
| 771 |
+
'prajjwal1/bert-small', # 4 layers, 512 dim (~28M params)
|
| 772 |
+
'prajjwal1/bert-medium', # 8 layers, 512 dim (~41M params)
|
| 773 |
+
'bert-base-uncased', # 12 layers, 768 dim (~110M params)
|
| 774 |
+
'bert-large-uncased', # 24 layers, 1024 dim (~336M params)
|
| 775 |
+
'bert-base-multilingual-cased', # Multilingual support
|
| 776 |
+
'bert-base-arabic' # Arabic BERT
|
| 777 |
+
],
|
| 778 |
+
'roberta': [
|
| 779 |
+
'roberta-base', # 12 layers, 768 dim (~125M params)
|
| 780 |
+
'roberta-large', # 24 layers, 1024 dim (~355M params)
|
| 781 |
+
#'xlm-roberta-base', # Multilingual RoBERTa
|
| 782 |
+
#'xlm-roberta-large' # Large multilingual
|
| 783 |
+
],
|
| 784 |
+
'distilbert': [
|
| 785 |
+
'distilbert-base-uncased', # 6 layers, 768 dim (~66M params)
|
| 786 |
+
'distilbert-base-multilingual-cased' # Multilingual
|
| 787 |
+
],
|
| 788 |
+
'albert': [
|
| 789 |
+
'albert-base-v2', # 12 layers, 768 dim (~12M params)
|
| 790 |
+
'albert-large-v2', # 24 layers, 1024 dim (~18M params)
|
| 791 |
+
'albert-xlarge-v2', # 24 layers, 2048 dim (~60M params)
|
| 792 |
+
'albert-xxlarge-v2' # 12 layers, 4096 dim (~235M params)
|
| 793 |
+
]
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
def get_model_info(self, model_name: str) -> Dict:
|
| 797 |
+
"""
|
| 798 |
+
Get information about a pretrained model without loading it.
|
| 799 |
+
|
| 800 |
+
Args:
|
| 801 |
+
model_name: HuggingFace model name
|
| 802 |
+
|
| 803 |
+
Returns:
|
| 804 |
+
Dictionary with model information
|
| 805 |
+
"""
|
| 806 |
+
try:
|
| 807 |
+
model_type = self._detect_model_type(model_name)
|
| 808 |
+
|
| 809 |
+
# Try to download config
|
| 810 |
+
model_dir = self.cache_dir / model_name
|
| 811 |
+
config_path = model_dir / 'config.json'
|
| 812 |
+
|
| 813 |
+
if not config_path.exists():
|
| 814 |
+
url = f"https://huggingface.co/{model_name}/resolve/main/config.json"
|
| 815 |
+
response = requests.get(url, timeout=10)
|
| 816 |
+
if response.status_code == 200:
|
| 817 |
+
config = response.json()
|
| 818 |
+
else:
|
| 819 |
+
return {"error": "Could not fetch config"}
|
| 820 |
+
else:
|
| 821 |
+
with open(config_path, 'r') as f:
|
| 822 |
+
config = json.load(f)
|
| 823 |
+
|
| 824 |
+
info = {
|
| 825 |
+
"model_name": model_name,
|
| 826 |
+
"model_type": model_type,
|
| 827 |
+
"supported": model_type in self.SUPPORTED_MODELS,
|
| 828 |
+
"architecture": {}
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
if model_type == 'bert':
|
| 832 |
+
info["architecture"] = {
|
| 833 |
+
"hidden_size": config.get('hidden_size', 'N/A'),
|
| 834 |
+
"num_layers": config.get('num_hidden_layers', 'N/A'),
|
| 835 |
+
"num_heads": config.get('num_attention_heads', 'N/A'),
|
| 836 |
+
"vocab_size": config.get('vocab_size', 'N/A'),
|
| 837 |
+
"max_position_embeddings": config.get('max_position_embeddings', 'N/A')
|
| 838 |
+
}
|
| 839 |
+
elif model_type == 'roberta':
|
| 840 |
+
info["architecture"] = {
|
| 841 |
+
"hidden_size": config.get('hidden_size', 'N/A'),
|
| 842 |
+
"num_layers": config.get('num_hidden_layers', 'N/A'),
|
| 843 |
+
"num_heads": config.get('num_attention_heads', 'N/A'),
|
| 844 |
+
"vocab_size": config.get('vocab_size', 'N/A'),
|
| 845 |
+
"max_position_embeddings": config.get('max_position_embeddings', 'N/A')
|
| 846 |
+
}
|
| 847 |
+
elif model_type == 'distilbert':
|
| 848 |
+
info["architecture"] = {
|
| 849 |
+
"hidden_size": config.get('dim', 'N/A'),
|
| 850 |
+
"num_layers": config.get('n_layers', 'N/A'),
|
| 851 |
+
"num_heads": config.get('n_heads', 'N/A'),
|
| 852 |
+
"vocab_size": config.get('vocab_size', 'N/A'),
|
| 853 |
+
"max_position_embeddings": config.get('max_position_embeddings', 'N/A')
|
| 854 |
+
}
|
| 855 |
+
elif model_type == 'albert':
|
| 856 |
+
info["architecture"] = {
|
| 857 |
+
"hidden_size": config.get('hidden_size', 'N/A'),
|
| 858 |
+
"num_layers": config.get('num_hidden_layers', 'N/A'),
|
| 859 |
+
"num_heads": config.get('num_attention_heads', 'N/A'),
|
| 860 |
+
"vocab_size": config.get('vocab_size', 'N/A'),
|
| 861 |
+
"embedding_size": config.get('embedding_size', 'N/A')
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
return info
|
| 865 |
+
|
| 866 |
+
except Exception as e:
|
| 867 |
+
return {"error": str(e)}
|
src/dytr/tokenization/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Tokenization module for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides tokenizer implementations and download utilities.
|
| 19 |
+
|
| 20 |
+
Author: Akram Alsubari
|
| 21 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
from dytr.tokenization.download_manager import (
|
| 26 |
+
DownloadManager,
|
| 27 |
+
download_file,
|
| 28 |
+
download_tokenizer_files,
|
| 29 |
+
get_url_from_HF,
|
| 30 |
+
process_vocab_text,
|
| 31 |
+
)
|
| 32 |
+
from dytr.tokenization.simple_tokenizer import SimpleTokenizer
|
| 33 |
+
|
| 34 |
+
__all__ = [
|
| 35 |
+
"SimpleTokenizer",
|
| 36 |
+
"DownloadManager",
|
| 37 |
+
"download_file",
|
| 38 |
+
"download_tokenizer_files",
|
| 39 |
+
"get_url_from_HF",
|
| 40 |
+
"process_vocab_text",
|
| 41 |
+
]
|
src/dytr/tokenization/download_manager.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Download manager for tokenizer files from HuggingFace.
|
| 17 |
+
|
| 18 |
+
This module provides functionality to download tokenizer files from HuggingFace
|
| 19 |
+
with resume support and caching.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
import hashlib
|
| 27 |
+
import json
|
| 28 |
+
import pickle
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
from typing import Dict, Optional, Union
|
| 31 |
+
|
| 32 |
+
import pandas as pd
|
| 33 |
+
import requests
|
| 34 |
+
from tqdm import tqdm
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def get_url_from_HF(HF_path: str):
|
| 38 |
+
"""
|
| 39 |
+
Generate download URLs for HuggingFace tokenizer files.
|
| 40 |
+
|
| 41 |
+
Args:
|
| 42 |
+
HF_path: HuggingFace model path (e.g., 'bert-base-uncased')
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
Tuple of (directory_name, vocab_url, special_tokens_map_url)
|
| 46 |
+
"""
|
| 47 |
+
vocab_url = f"https://huggingface.co/{HF_path}/resolve/main/vocab.txt?download=true"
|
| 48 |
+
special_token_map = (
|
| 49 |
+
f"https://huggingface.co/{HF_path}/resolve/main/special_tokens_map.json?download=true"
|
| 50 |
+
)
|
| 51 |
+
file_dir = HF_path.split("/")[-1].replace("-", "_")
|
| 52 |
+
return file_dir, vocab_url, special_token_map
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def process_vocab_text(path: Union[str, Path]) -> Dict[str, int]:
|
| 56 |
+
"""
|
| 57 |
+
Process vocabulary text file into token-to-id mapping.
|
| 58 |
+
|
| 59 |
+
Args:
|
| 60 |
+
path: Path to vocabulary text file
|
| 61 |
+
|
| 62 |
+
Returns:
|
| 63 |
+
Dictionary mapping tokens to IDs
|
| 64 |
+
"""
|
| 65 |
+
path = Path(path)
|
| 66 |
+
json_path = path.with_suffix(".json")
|
| 67 |
+
|
| 68 |
+
if json_path.exists():
|
| 69 |
+
with open(json_path, "r", encoding="utf-8") as f:
|
| 70 |
+
token_to_id = json.load(f)
|
| 71 |
+
print("JSON OF VOCAB ALREADY SAVED")
|
| 72 |
+
return token_to_id
|
| 73 |
+
|
| 74 |
+
print("*" * 60)
|
| 75 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 76 |
+
data = f.read()
|
| 77 |
+
|
| 78 |
+
token_to_id = {k: idx for idx, k in enumerate(data.split("\n")) if k}
|
| 79 |
+
return token_to_id
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
class DownloadManager:
|
| 83 |
+
"""
|
| 84 |
+
Manages file downloads with resume support and caching.
|
| 85 |
+
|
| 86 |
+
This class handles downloading files from URLs, supports resuming
|
| 87 |
+
interrupted downloads, and caches downloaded files to avoid
|
| 88 |
+
re-downloading.
|
| 89 |
+
|
| 90 |
+
Args:
|
| 91 |
+
base_dir: Base directory for storing downloads
|
| 92 |
+
registry_file: Name of the registry file for tracking downloads
|
| 93 |
+
"""
|
| 94 |
+
|
| 95 |
+
def __init__(self, base_dir: str = "downloads", registry_file: str = "download_registry.pkl"):
|
| 96 |
+
self.base_dir = Path(base_dir)
|
| 97 |
+
self.base_dir.mkdir(exist_ok=True)
|
| 98 |
+
self.registry_file = self.base_dir / registry_file
|
| 99 |
+
self.registry = self._load_registry()
|
| 100 |
+
|
| 101 |
+
def _load_registry(self) -> Dict[str, str]:
|
| 102 |
+
"""Load download registry from disk."""
|
| 103 |
+
if self.registry_file.exists():
|
| 104 |
+
with open(self.registry_file, "rb") as f:
|
| 105 |
+
return pickle.load(f)
|
| 106 |
+
return {}
|
| 107 |
+
|
| 108 |
+
def _save_registry(self):
|
| 109 |
+
"""Save download registry to disk."""
|
| 110 |
+
with open(self.registry_file, "wb") as f:
|
| 111 |
+
pickle.dump(self.registry, f)
|
| 112 |
+
|
| 113 |
+
def _get_file_path(self, url: str, subfolder: Optional[str] = None) -> Path:
|
| 114 |
+
"""
|
| 115 |
+
Generate file path for a given URL.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
url: Download URL
|
| 119 |
+
subfolder: Optional subfolder within base directory
|
| 120 |
+
|
| 121 |
+
Returns:
|
| 122 |
+
Path object for the file
|
| 123 |
+
"""
|
| 124 |
+
url_hash = hashlib.md5(url.encode()).hexdigest()[:8]
|
| 125 |
+
original_filename = url.split("/")[-1].split("?")[0]
|
| 126 |
+
|
| 127 |
+
if not original_filename:
|
| 128 |
+
original_filename = f"download_{url_hash}"
|
| 129 |
+
|
| 130 |
+
if subfolder:
|
| 131 |
+
file_path = self.base_dir / subfolder / original_filename
|
| 132 |
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
| 133 |
+
else:
|
| 134 |
+
file_path = self.base_dir / original_filename
|
| 135 |
+
|
| 136 |
+
return file_path
|
| 137 |
+
|
| 138 |
+
def download_file(
|
| 139 |
+
self, url: str, subfolder: Optional[str] = None
|
| 140 |
+
) -> Union[str, pd.DataFrame, dict, list]:
|
| 141 |
+
"""
|
| 142 |
+
Download a file from URL with resume support.
|
| 143 |
+
|
| 144 |
+
Args:
|
| 145 |
+
url: URL to download
|
| 146 |
+
subfolder: Optional subfolder to store the file
|
| 147 |
+
|
| 148 |
+
Returns:
|
| 149 |
+
Parsed file content (dict for JSON, DataFrame for CSV/Excel, str for text)
|
| 150 |
+
"""
|
| 151 |
+
file_path = self._get_file_path(url, subfolder)
|
| 152 |
+
|
| 153 |
+
# Check if file already exists in registry
|
| 154 |
+
if str(file_path) in self.registry.values():
|
| 155 |
+
if file_path.exists():
|
| 156 |
+
print(f"File already exists: {file_path}")
|
| 157 |
+
return self._read_file(file_path)
|
| 158 |
+
|
| 159 |
+
# Check if file exists directly
|
| 160 |
+
if file_path.exists():
|
| 161 |
+
print(f"File already exists: {file_path}")
|
| 162 |
+
return self._read_file(file_path)
|
| 163 |
+
|
| 164 |
+
# Download file
|
| 165 |
+
self._download_with_resume(url, file_path)
|
| 166 |
+
|
| 167 |
+
# Update registry
|
| 168 |
+
self.registry[url] = str(file_path)
|
| 169 |
+
self._save_registry()
|
| 170 |
+
|
| 171 |
+
return self._read_file(file_path)
|
| 172 |
+
|
| 173 |
+
def _download_with_resume(self, url: str, file_path: Path):
|
| 174 |
+
"""
|
| 175 |
+
Download file with resume support for interrupted downloads.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
url: URL to download
|
| 179 |
+
file_path: Path where to save the file
|
| 180 |
+
"""
|
| 181 |
+
existing_size = file_path.stat().st_size if file_path.exists() else 0
|
| 182 |
+
|
| 183 |
+
headers = {"Range": f"bytes={existing_size}-"} if existing_size > 0 else {}
|
| 184 |
+
|
| 185 |
+
response = requests.get(url, headers=headers, stream=True)
|
| 186 |
+
response.raise_for_status()
|
| 187 |
+
|
| 188 |
+
total_size = int(response.headers.get("content-length", 0)) + existing_size
|
| 189 |
+
|
| 190 |
+
mode = "ab" if existing_size > 0 else "wb"
|
| 191 |
+
|
| 192 |
+
with open(file_path, mode) as f:
|
| 193 |
+
with tqdm(
|
| 194 |
+
total=total_size,
|
| 195 |
+
unit="B",
|
| 196 |
+
unit_scale=True,
|
| 197 |
+
unit_divisor=1024,
|
| 198 |
+
desc=f"Downloading {file_path.name}",
|
| 199 |
+
initial=existing_size,
|
| 200 |
+
bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]",
|
| 201 |
+
) as pbar:
|
| 202 |
+
for chunk in response.iter_content(chunk_size=8192):
|
| 203 |
+
if chunk:
|
| 204 |
+
f.write(chunk)
|
| 205 |
+
pbar.update(len(chunk))
|
| 206 |
+
|
| 207 |
+
def _read_file(self, file_path: Path) -> Union[str, pd.DataFrame, dict, list]:
|
| 208 |
+
"""
|
| 209 |
+
Read and parse a downloaded file based on its extension.
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
file_path: Path to the file
|
| 213 |
+
|
| 214 |
+
Returns:
|
| 215 |
+
Parsed file content
|
| 216 |
+
"""
|
| 217 |
+
file_extension = file_path.suffix.lower()
|
| 218 |
+
|
| 219 |
+
if file_extension == ".json":
|
| 220 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 221 |
+
return json.load(f)
|
| 222 |
+
elif file_extension == ".csv":
|
| 223 |
+
return pd.read_csv(file_path)
|
| 224 |
+
elif file_extension == ".txt":
|
| 225 |
+
if "vocab" in str(file_path).lower():
|
| 226 |
+
return process_vocab_text(file_path)
|
| 227 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 228 |
+
return f.read()
|
| 229 |
+
elif file_extension in [".xlsx", ".xls"]:
|
| 230 |
+
return pd.read_excel(file_path)
|
| 231 |
+
else:
|
| 232 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 233 |
+
return f.read()
|
| 234 |
+
|
| 235 |
+
def get_downloaded_files(self) -> Dict[str, str]:
|
| 236 |
+
"""
|
| 237 |
+
Get all downloaded files from registry.
|
| 238 |
+
|
| 239 |
+
Returns:
|
| 240 |
+
Dictionary mapping URLs to file paths
|
| 241 |
+
"""
|
| 242 |
+
return self.registry.copy()
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def download_file(
|
| 246 |
+
url: str, subfolder: Optional[str] = None, base_dir: str = "downloads"
|
| 247 |
+
) -> Union[str, pd.DataFrame, dict, list]:
|
| 248 |
+
"""
|
| 249 |
+
Convenience function to download a single file.
|
| 250 |
+
|
| 251 |
+
Args:
|
| 252 |
+
url: URL to download
|
| 253 |
+
subfolder: Optional subfolder for the file
|
| 254 |
+
base_dir: Base directory for downloads
|
| 255 |
+
|
| 256 |
+
Returns:
|
| 257 |
+
Parsed file content
|
| 258 |
+
"""
|
| 259 |
+
manager = DownloadManager(base_dir)
|
| 260 |
+
return manager.download_file(url, subfolder)
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def download_tokenizer_files(HF_tokenizer_path: str) -> tuple:
|
| 264 |
+
"""
|
| 265 |
+
Download tokenizer files from HuggingFace.
|
| 266 |
+
|
| 267 |
+
Args:
|
| 268 |
+
HF_tokenizer_path: HuggingFace tokenizer path (e.g., 'bert-base-uncased')
|
| 269 |
+
|
| 270 |
+
Returns:
|
| 271 |
+
Tuple of (vocab_dict, special_tokens_map)
|
| 272 |
+
"""
|
| 273 |
+
sub_dir, vocab_path, special_token_map_path = get_url_from_HF(HF_tokenizer_path)
|
| 274 |
+
vocab = download_file(vocab_path, sub_dir)
|
| 275 |
+
try:
|
| 276 |
+
special_token_map = download_file(special_token_map_path, sub_dir)
|
| 277 |
+
except:
|
| 278 |
+
special_token_map = {}
|
| 279 |
+
|
| 280 |
+
return vocab, special_token_map
|
src/dytr/tokenization/simple_tokenizer.py
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Simple tokenizer implementation with multiple tokenization strategies.
|
| 17 |
+
|
| 18 |
+
This module provides a lightweight tokenizer that supports various tokenization
|
| 19 |
+
methods including WordPiece, BPE, Unigram, and Byte-Level encoding.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
import json
|
| 27 |
+
import os
|
| 28 |
+
from typing import Any, Dict, List, Optional, Union
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SimpleTokenizer:
|
| 32 |
+
"""
|
| 33 |
+
A flexible tokenizer supporting multiple tokenization strategies.
|
| 34 |
+
|
| 35 |
+
Supports:
|
| 36 |
+
- WordPiece tokenization
|
| 37 |
+
- Byte-Pair Encoding (BPE)
|
| 38 |
+
- Unigram tokenization
|
| 39 |
+
- Character-level tokenization
|
| 40 |
+
- Space-based tokenization
|
| 41 |
+
- Byte-level tokenization (GPT-2 style)
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
vocab: Vocabulary dictionary mapping tokens to IDs
|
| 45 |
+
special_tokens_map: Dictionary of special tokens
|
| 46 |
+
tokenizer_type: Type of tokenizer ('wordpiece', 'bpe', 'unigram', 'char', 'space', 'byte_level')
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(
|
| 50 |
+
self,
|
| 51 |
+
vocab: Dict[str, int],
|
| 52 |
+
special_tokens_map: Optional[Dict] = None,
|
| 53 |
+
tokenizer_type: str = "wordpiece", add_tab_newline_vocab: bool =False
|
| 54 |
+
):
|
| 55 |
+
self.vocab = vocab
|
| 56 |
+
self.inv_vocab = {v: k for k, v in vocab.items()}
|
| 57 |
+
self.special_tokens_map = special_tokens_map or {}
|
| 58 |
+
self.tokenizer_type = tokenizer_type
|
| 59 |
+
self.add_tab_newline_vocab = add_tab_newline_vocab
|
| 60 |
+
|
| 61 |
+
# Special tokens
|
| 62 |
+
self.pad_token = self.special_tokens_map.get("pad_token", "[PAD]")
|
| 63 |
+
self.unk_token = self.special_tokens_map.get("unk_token", "[UNK]")
|
| 64 |
+
self.cls_token = self.special_tokens_map.get("cls_token", "[CLS]")
|
| 65 |
+
self.sep_token = self.special_tokens_map.get("sep_token", "[SEP]")
|
| 66 |
+
self.mask_token = self.special_tokens_map.get("mask_token", "[MASK]")
|
| 67 |
+
self.bos_token = self.special_tokens_map.get("bos_token", "<s>")
|
| 68 |
+
self.eos_token = self.special_tokens_map.get("eos_token", "</s>")
|
| 69 |
+
|
| 70 |
+
# Token IDs
|
| 71 |
+
self.pad_token_id = self.vocab.get(self.pad_token)
|
| 72 |
+
self.unk_token_id = self.vocab.get(self.unk_token)
|
| 73 |
+
self.cls_token_id = self.vocab.get(self.cls_token)
|
| 74 |
+
self.sep_token_id = self.vocab.get(self.sep_token)
|
| 75 |
+
self.mask_token_id = self.vocab.get(self.mask_token)
|
| 76 |
+
|
| 77 |
+
# Add BOS and EOS if not present
|
| 78 |
+
if self.bos_token not in self.vocab:
|
| 79 |
+
self.bos_token=self.cls_token
|
| 80 |
+
|
| 81 |
+
#self.add_tokens([self.bos_token])
|
| 82 |
+
if self.eos_token not in self.vocab:
|
| 83 |
+
self.eos_token=self.sep_token
|
| 84 |
+
#self.add_tokens([self.eos_token])
|
| 85 |
+
|
| 86 |
+
self.bos_token_id = self.vocab.get(self.bos_token)
|
| 87 |
+
self.eos_token_id = self.vocab.get(self.eos_token)
|
| 88 |
+
|
| 89 |
+
# All special tokens
|
| 90 |
+
self.all_special_tokens = [
|
| 91 |
+
self.pad_token,
|
| 92 |
+
self.unk_token,
|
| 93 |
+
self.cls_token,
|
| 94 |
+
self.sep_token,
|
| 95 |
+
self.mask_token,
|
| 96 |
+
self.bos_token,
|
| 97 |
+
self.eos_token,
|
| 98 |
+
]
|
| 99 |
+
self.all_special_ids = [
|
| 100 |
+
self.pad_token_id,
|
| 101 |
+
self.unk_token_id,
|
| 102 |
+
self.cls_token_id,
|
| 103 |
+
self.sep_token_id,
|
| 104 |
+
self.mask_token_id,
|
| 105 |
+
self.bos_token_id,
|
| 106 |
+
self.eos_token_id,
|
| 107 |
+
]
|
| 108 |
+
|
| 109 |
+
if self.add_tab_newline_vocab:
|
| 110 |
+
# Add newline and tab representations
|
| 111 |
+
self.add_tokens(["ĉ", "Ċ","Ġ"])
|
| 112 |
+
self.name_or_path = "local_tokenizer"
|
| 113 |
+
|
| 114 |
+
# Tokenization function mapping
|
| 115 |
+
self.tokenization_functions = {
|
| 116 |
+
"wordpiece": self._wordpiece_tokenize,
|
| 117 |
+
"bpe": self._bpe_tokenize,
|
| 118 |
+
"unigram": self._unigram_tokenize,
|
| 119 |
+
"char": self._char_tokenize,
|
| 120 |
+
"space": self._space_tokenize,
|
| 121 |
+
"byte_level": self._byte_level_tokenize,
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
# BPE merges
|
| 125 |
+
self.merges = self.special_tokens_map.get("merges", [])
|
| 126 |
+
|
| 127 |
+
# Byte-level encoder
|
| 128 |
+
self.byte_encoder = self._build_byte_encoder() if tokenizer_type == "byte_level" else None
|
| 129 |
+
|
| 130 |
+
def _build_byte_encoder(self) -> Dict[str, str]:
|
| 131 |
+
"""Build byte-level encoder for GPT-2 style tokenization."""
|
| 132 |
+
# Printable ASCII range
|
| 133 |
+
bs = list(range(ord("!"), ord("~") + 1))
|
| 134 |
+
bs += list(range(ord("¡"), ord("¬") + 1))
|
| 135 |
+
bs += list(range(ord("®"), ord("ÿ") + 1))
|
| 136 |
+
cs = bs[:]
|
| 137 |
+
n = 0
|
| 138 |
+
for b in range(2**8):
|
| 139 |
+
if b not in bs:
|
| 140 |
+
bs.append(b)
|
| 141 |
+
cs.append(2**8 + n)
|
| 142 |
+
n += 1
|
| 143 |
+
return {chr(b): chr(c) for b, c in zip(bs, cs)}
|
| 144 |
+
|
| 145 |
+
def tokenize(self, text: str) -> List[str]:
|
| 146 |
+
"""Tokenize text using the configured tokenization strategy."""
|
| 147 |
+
if not text:
|
| 148 |
+
return []
|
| 149 |
+
|
| 150 |
+
tokenizer_func = self.tokenization_functions.get(
|
| 151 |
+
self.tokenizer_type, self._wordpiece_tokenize
|
| 152 |
+
)
|
| 153 |
+
return tokenizer_func(text)
|
| 154 |
+
|
| 155 |
+
def _space_tokenize(self, text: str) -> List[str]:
|
| 156 |
+
"""Simple space-based tokenization."""
|
| 157 |
+
return text.split()
|
| 158 |
+
|
| 159 |
+
def _char_tokenize(self, text: str) -> List[str]:
|
| 160 |
+
"""Character-level tokenization."""
|
| 161 |
+
tokens = []
|
| 162 |
+
for char in text:
|
| 163 |
+
if char in self.vocab:
|
| 164 |
+
tokens.append(char)
|
| 165 |
+
else:
|
| 166 |
+
tokens.append(self.unk_token)
|
| 167 |
+
return tokens
|
| 168 |
+
|
| 169 |
+
def _wordpiece_tokenize(self, text: str) -> List[str]:
|
| 170 |
+
"""WordPiece tokenization."""
|
| 171 |
+
if self.add_tab_newline_vocab:
|
| 172 |
+
text = text.replace("\n", "Ċ").replace(" ", "\t").replace("\t", "ĉ")
|
| 173 |
+
words = text.split(" ")
|
| 174 |
+
tokens = []
|
| 175 |
+
|
| 176 |
+
for word in words:
|
| 177 |
+
if word in self.vocab:
|
| 178 |
+
tokens.append(word)
|
| 179 |
+
continue
|
| 180 |
+
subwords = self._wordpiece_split(word)
|
| 181 |
+
tokens.extend(subwords)
|
| 182 |
+
|
| 183 |
+
return tokens
|
| 184 |
+
|
| 185 |
+
def _wordpiece_split(self, word: str) -> List[str]:
|
| 186 |
+
"""Split a word into WordPiece subwords."""
|
| 187 |
+
if not word:
|
| 188 |
+
return []
|
| 189 |
+
if word in self.vocab:
|
| 190 |
+
return [word]
|
| 191 |
+
|
| 192 |
+
tokens = []
|
| 193 |
+
remaining = word
|
| 194 |
+
start = 0
|
| 195 |
+
|
| 196 |
+
while start < len(remaining):
|
| 197 |
+
end = len(remaining)
|
| 198 |
+
best_token = None
|
| 199 |
+
|
| 200 |
+
while start < end:
|
| 201 |
+
subword = remaining[start:end]
|
| 202 |
+
if start > 0:
|
| 203 |
+
subword = "##" + subword
|
| 204 |
+
if subword in self.vocab:
|
| 205 |
+
best_token = subword
|
| 206 |
+
break
|
| 207 |
+
end -= 1
|
| 208 |
+
|
| 209 |
+
if best_token is None:
|
| 210 |
+
# Unknown characters
|
| 211 |
+
for char in remaining[start:]:
|
| 212 |
+
if char in self.vocab:
|
| 213 |
+
tokens.append(char)
|
| 214 |
+
else:
|
| 215 |
+
tokens.append(self.unk_token)
|
| 216 |
+
break
|
| 217 |
+
else:
|
| 218 |
+
tokens.append(best_token)
|
| 219 |
+
start = end
|
| 220 |
+
|
| 221 |
+
return tokens
|
| 222 |
+
|
| 223 |
+
def _bpe_tokenize(self, text: str) -> List[str]:
|
| 224 |
+
"""Byte-Pair Encoding tokenization."""
|
| 225 |
+
words = self._preprocess_text(text)
|
| 226 |
+
tokens = []
|
| 227 |
+
|
| 228 |
+
for word in words:
|
| 229 |
+
if word in self.vocab:
|
| 230 |
+
tokens.append(word)
|
| 231 |
+
else:
|
| 232 |
+
bpe_tokens = self._apply_bpe(word)
|
| 233 |
+
tokens.extend(bpe_tokens)
|
| 234 |
+
|
| 235 |
+
return tokens
|
| 236 |
+
|
| 237 |
+
def _preprocess_text(self, text: str) -> List[str]:
|
| 238 |
+
"""Preprocess text for BPE tokenization."""
|
| 239 |
+
processed = []
|
| 240 |
+
for i, char in enumerate(text):
|
| 241 |
+
if char == " ":
|
| 242 |
+
processed.append("Ġ")
|
| 243 |
+
elif char == "\n":
|
| 244 |
+
count = 1
|
| 245 |
+
idx = i + 1
|
| 246 |
+
while idx < len(text) and text[idx] == "\n":
|
| 247 |
+
count += 1
|
| 248 |
+
idx += 1
|
| 249 |
+
processed.append("Ċ" * count)
|
| 250 |
+
elif char == "\t":
|
| 251 |
+
processed.append("ĉ")
|
| 252 |
+
else:
|
| 253 |
+
processed.append(char)
|
| 254 |
+
return processed
|
| 255 |
+
|
| 256 |
+
def _apply_bpe(self, word: Union[str, List[str]]) -> List[str]:
|
| 257 |
+
"""Apply BPE merges to a word."""
|
| 258 |
+
if isinstance(word, list):
|
| 259 |
+
word = "".join(word)
|
| 260 |
+
|
| 261 |
+
if not self.merges:
|
| 262 |
+
return [word] if word in self.vocab else [self.unk_token]
|
| 263 |
+
|
| 264 |
+
tokens = list(word)
|
| 265 |
+
|
| 266 |
+
for merge in self.merges:
|
| 267 |
+
pair = merge.split()
|
| 268 |
+
if len(pair) != 2:
|
| 269 |
+
continue
|
| 270 |
+
|
| 271 |
+
new_tokens = []
|
| 272 |
+
i = 0
|
| 273 |
+
while i < len(tokens):
|
| 274 |
+
if i < len(tokens) - 1 and tokens[i] == pair[0] and tokens[i + 1] == pair[1]:
|
| 275 |
+
new_tokens.append("".join(pair))
|
| 276 |
+
i += 2
|
| 277 |
+
else:
|
| 278 |
+
new_tokens.append(tokens[i])
|
| 279 |
+
i += 1
|
| 280 |
+
tokens = new_tokens
|
| 281 |
+
|
| 282 |
+
return [token if token in self.vocab else self.unk_token for token in tokens]
|
| 283 |
+
|
| 284 |
+
def _unigram_tokenize(self, text: str) -> List[str]:
|
| 285 |
+
"""Unigram tokenization using Viterbi algorithm."""
|
| 286 |
+
chars = list(text)
|
| 287 |
+
n = len(chars)
|
| 288 |
+
dp = [-1] * (n + 1)
|
| 289 |
+
dp[0] = 0
|
| 290 |
+
|
| 291 |
+
# Find best segmentation
|
| 292 |
+
for i in range(n):
|
| 293 |
+
if dp[i] == -1:
|
| 294 |
+
continue
|
| 295 |
+
for j in range(i + 1, min(i + 20, n + 1)):
|
| 296 |
+
token = "".join(chars[i:j])
|
| 297 |
+
if token in self.vocab or token in self.all_special_tokens:
|
| 298 |
+
if dp[j] == -1 or dp[j] > dp[i] + 1:
|
| 299 |
+
dp[j] = dp[i] + 1
|
| 300 |
+
|
| 301 |
+
if dp[n] == -1:
|
| 302 |
+
return [self.unk_token]
|
| 303 |
+
|
| 304 |
+
# Reconstruct tokens
|
| 305 |
+
tokens = []
|
| 306 |
+
i = n
|
| 307 |
+
while i > 0:
|
| 308 |
+
for j in range(max(0, i - 20), i):
|
| 309 |
+
if dp[j] != -1 and dp[i] == dp[j] + 1:
|
| 310 |
+
token = "".join(chars[j:i])
|
| 311 |
+
if token in self.vocab:
|
| 312 |
+
tokens.insert(0, token)
|
| 313 |
+
else:
|
| 314 |
+
tokens.insert(0, self.unk_token)
|
| 315 |
+
i = j
|
| 316 |
+
break
|
| 317 |
+
|
| 318 |
+
return tokens
|
| 319 |
+
|
| 320 |
+
def _byte_level_tokenize(self, text: str) -> List[str]:
|
| 321 |
+
"""Byte-level tokenization (GPT-2 style)."""
|
| 322 |
+
if not self.byte_encoder:
|
| 323 |
+
return self._char_tokenize(text)
|
| 324 |
+
|
| 325 |
+
# Encode bytes
|
| 326 |
+
bytes_text = text.encode("utf-8")
|
| 327 |
+
encoded_chars = []
|
| 328 |
+
for b in bytes_text:
|
| 329 |
+
if b < 128:
|
| 330 |
+
encoded_chars.append(chr(b))
|
| 331 |
+
else:
|
| 332 |
+
encoded_chars.append(self.byte_encoder.get(chr(b), chr(b)))
|
| 333 |
+
|
| 334 |
+
# Preprocess
|
| 335 |
+
processed = []
|
| 336 |
+
i = 0
|
| 337 |
+
while i < len(encoded_chars):
|
| 338 |
+
if encoded_chars[i] == " ":
|
| 339 |
+
processed.append("Ġ")
|
| 340 |
+
i += 1
|
| 341 |
+
elif encoded_chars[i] == "\n":
|
| 342 |
+
count = 1
|
| 343 |
+
while i + count < len(encoded_chars) and encoded_chars[i + count] == "\n":
|
| 344 |
+
count += 1
|
| 345 |
+
processed.append("Ċ" * count)
|
| 346 |
+
i += count
|
| 347 |
+
elif encoded_chars[i] == "\t":
|
| 348 |
+
processed.append("ĉ")
|
| 349 |
+
i += 1
|
| 350 |
+
else:
|
| 351 |
+
processed.append(encoded_chars[i])
|
| 352 |
+
i += 1
|
| 353 |
+
|
| 354 |
+
# Apply BPE if merges exist
|
| 355 |
+
if self.merges:
|
| 356 |
+
return self._apply_bpe(processed)
|
| 357 |
+
|
| 358 |
+
return [p if p in self.vocab else self.unk_token for p in processed]
|
| 359 |
+
|
| 360 |
+
def convert_tokens_to_ids(self, tokens: Union[str, List[str]]) -> List[int]:
|
| 361 |
+
"""Convert tokens to token IDs."""
|
| 362 |
+
if isinstance(tokens, str):
|
| 363 |
+
tokens = [tokens]
|
| 364 |
+
return [self.vocab.get(token, self.unk_token_id) for token in tokens]
|
| 365 |
+
|
| 366 |
+
def convert_ids_to_tokens(
|
| 367 |
+
self, ids: Union[int, List[int]], skip_special_tokens: bool = False
|
| 368 |
+
) -> List[str]:
|
| 369 |
+
"""Convert token IDs to tokens."""
|
| 370 |
+
if hasattr(ids, "cpu"):
|
| 371 |
+
ids = ids.cpu().tolist()
|
| 372 |
+
elif isinstance(ids, int):
|
| 373 |
+
ids = [ids]
|
| 374 |
+
|
| 375 |
+
tokens = []
|
| 376 |
+
for idx in ids:
|
| 377 |
+
token = self.inv_vocab.get(idx, self.unk_token)
|
| 378 |
+
if skip_special_tokens and token in self.all_special_tokens:
|
| 379 |
+
continue
|
| 380 |
+
tokens.append(token)
|
| 381 |
+
|
| 382 |
+
return tokens
|
| 383 |
+
|
| 384 |
+
def convert_tokens_to_string(self, tokens: List[str]) -> str:
|
| 385 |
+
"""Convert a list of tokens to a string."""
|
| 386 |
+
text = ""
|
| 387 |
+
for i, token in enumerate(tokens):
|
| 388 |
+
if token.startswith("##"):
|
| 389 |
+
text += token[2:]
|
| 390 |
+
elif token in ["Ġ", "Ċ", "ĉ"] or (
|
| 391 |
+
self.tokenizer_type == "byte_level" and token in ["Ġ", "Ċ", "ĉ"]
|
| 392 |
+
):
|
| 393 |
+
if token == "Ġ":
|
| 394 |
+
text += " "
|
| 395 |
+
elif token == "Ċ":
|
| 396 |
+
text += "\n"
|
| 397 |
+
elif token == "ĉ":
|
| 398 |
+
text += "\t"
|
| 399 |
+
elif token in self.all_special_tokens:
|
| 400 |
+
if token == self.sep_token and i == len(tokens) - 1:
|
| 401 |
+
continue
|
| 402 |
+
text += " " + token
|
| 403 |
+
else:
|
| 404 |
+
if (
|
| 405 |
+
text
|
| 406 |
+
and not text.endswith(" ")
|
| 407 |
+
and not text.endswith("\n")
|
| 408 |
+
and not text.endswith("\t")
|
| 409 |
+
):
|
| 410 |
+
text += " "
|
| 411 |
+
text += token
|
| 412 |
+
|
| 413 |
+
text = text.strip()
|
| 414 |
+
for special in self.all_special_tokens:
|
| 415 |
+
text = text.replace(" " + special, "")
|
| 416 |
+
text = text.replace(special + " ", "")
|
| 417 |
+
|
| 418 |
+
return text.strip()
|
| 419 |
+
|
| 420 |
+
def _get_word_ids(
|
| 421 |
+
self, input_ids: List[int], offset_mapping: Optional[List] = None
|
| 422 |
+
) -> List[int]:
|
| 423 |
+
"""Get word IDs for token classification."""
|
| 424 |
+
if offset_mapping is not None:
|
| 425 |
+
word_ids = []
|
| 426 |
+
current_word_idx = 0
|
| 427 |
+
last_end = 0
|
| 428 |
+
|
| 429 |
+
for i, (start, end) in enumerate(offset_mapping):
|
| 430 |
+
if start == 0 and end == 0:
|
| 431 |
+
word_ids.append(None)
|
| 432 |
+
else:
|
| 433 |
+
if i > 0 and start > last_end:
|
| 434 |
+
current_word_idx += 1
|
| 435 |
+
word_ids.append(current_word_idx)
|
| 436 |
+
last_end = end
|
| 437 |
+
|
| 438 |
+
return word_ids
|
| 439 |
+
|
| 440 |
+
tokens = self.convert_ids_to_tokens(input_ids)
|
| 441 |
+
word_ids = []
|
| 442 |
+
current_word_idx = 0
|
| 443 |
+
|
| 444 |
+
for i, token in enumerate(tokens):
|
| 445 |
+
if token.startswith("##"):
|
| 446 |
+
word_ids.append(current_word_idx)
|
| 447 |
+
else:
|
| 448 |
+
if i > 0 and not tokens[i - 1].startswith("##"):
|
| 449 |
+
current_word_idx += 1
|
| 450 |
+
word_ids.append(current_word_idx)
|
| 451 |
+
|
| 452 |
+
return word_ids
|
| 453 |
+
|
| 454 |
+
def word_ids(self, batch_encoding: Any) -> Optional[List[int]]:
|
| 455 |
+
"""Extract word IDs from a batch encoding."""
|
| 456 |
+
if hasattr(batch_encoding, "encodings"):
|
| 457 |
+
word_ids_list = []
|
| 458 |
+
for encoding in batch_encoding.encodings:
|
| 459 |
+
word_ids_list.append(
|
| 460 |
+
self._get_word_ids(encoding.ids, getattr(encoding, "offsets", None))
|
| 461 |
+
)
|
| 462 |
+
return word_ids_list
|
| 463 |
+
elif isinstance(batch_encoding, dict) and "input_ids" in batch_encoding:
|
| 464 |
+
return self._get_word_ids(
|
| 465 |
+
batch_encoding["input_ids"], batch_encoding.get("offset_mapping")
|
| 466 |
+
)
|
| 467 |
+
return None
|
| 468 |
+
|
| 469 |
+
def encode(
|
| 470 |
+
self,
|
| 471 |
+
text: str,
|
| 472 |
+
add_special_tokens: bool = False,
|
| 473 |
+
max_length: Optional[int] = None,
|
| 474 |
+
truncation: bool = False,
|
| 475 |
+
padding: bool = False,
|
| 476 |
+
return_tensors: Optional[str] = None,
|
| 477 |
+
return_token_type_ids: bool = False,
|
| 478 |
+
return_attention_mask: bool = True,
|
| 479 |
+
return_offsets_mapping: bool = False,
|
| 480 |
+
) -> Dict:
|
| 481 |
+
"""Encode text to token IDs."""
|
| 482 |
+
tokens = self.tokenize(text)
|
| 483 |
+
|
| 484 |
+
# Add special tokens
|
| 485 |
+
if add_special_tokens:
|
| 486 |
+
if self.bos_token and self.eos_token:
|
| 487 |
+
tokens = [self.bos_token] + tokens + [self.eos_token]
|
| 488 |
+
else:
|
| 489 |
+
tokens = [self.cls_token] + tokens + [self.sep_token]
|
| 490 |
+
|
| 491 |
+
input_ids = self.convert_tokens_to_ids(tokens)
|
| 492 |
+
attention_mask = [1] * len(input_ids)
|
| 493 |
+
|
| 494 |
+
# Build offset mapping for token classification
|
| 495 |
+
offset_mapping = None
|
| 496 |
+
if return_offsets_mapping:
|
| 497 |
+
offset_mapping = []
|
| 498 |
+
char_pos = 0
|
| 499 |
+
original_text = text
|
| 500 |
+
|
| 501 |
+
for token in tokens:
|
| 502 |
+
if token in self.all_special_tokens:
|
| 503 |
+
offset_mapping.append((0, 0))
|
| 504 |
+
else:
|
| 505 |
+
token_text = token
|
| 506 |
+
if token_text.startswith("##"):
|
| 507 |
+
token_text = token_text[2:]
|
| 508 |
+
elif token_text in ["Ġ", "Ċ", "ĉ"]:
|
| 509 |
+
token_text = {"Ġ": " ", "Ċ": "\n", "ĉ": "\t"}.get(token_text, token_text)
|
| 510 |
+
|
| 511 |
+
start = original_text.find(token_text, char_pos)
|
| 512 |
+
if start == -1:
|
| 513 |
+
start = char_pos
|
| 514 |
+
end = start + len(token_text)
|
| 515 |
+
offset_mapping.append((start, end))
|
| 516 |
+
char_pos = end
|
| 517 |
+
|
| 518 |
+
# Truncation
|
| 519 |
+
if truncation and max_length and len(input_ids) > max_length:
|
| 520 |
+
input_ids = input_ids[:max_length]
|
| 521 |
+
attention_mask = attention_mask[:max_length]
|
| 522 |
+
if offset_mapping:
|
| 523 |
+
offset_mapping = offset_mapping[:max_length]
|
| 524 |
+
|
| 525 |
+
# Padding
|
| 526 |
+
if padding and max_length and len(input_ids) < max_length:
|
| 527 |
+
pad_length = max_length - len(input_ids)
|
| 528 |
+
input_ids = input_ids + [self.pad_token_id] * pad_length
|
| 529 |
+
attention_mask = attention_mask + [0] * pad_length
|
| 530 |
+
if offset_mapping:
|
| 531 |
+
offset_mapping = offset_mapping + [(0, 0)] * pad_length
|
| 532 |
+
|
| 533 |
+
result = {"input_ids": input_ids, "attention_mask": attention_mask}
|
| 534 |
+
|
| 535 |
+
if return_offsets_mapping:
|
| 536 |
+
result["offset_mapping"] = offset_mapping
|
| 537 |
+
|
| 538 |
+
if return_token_type_ids:
|
| 539 |
+
result["token_type_ids"] = [0] * len(input_ids)
|
| 540 |
+
|
| 541 |
+
# Convert to tensors if requested
|
| 542 |
+
if return_tensors == "pt":
|
| 543 |
+
import torch
|
| 544 |
+
|
| 545 |
+
result = {
|
| 546 |
+
k: (
|
| 547 |
+
torch.tensor([v], dtype=torch.long)
|
| 548 |
+
if k != "offset_mapping"
|
| 549 |
+
else [torch.tensor(v, dtype=torch.long)]
|
| 550 |
+
)
|
| 551 |
+
for k, v in result.items()
|
| 552 |
+
}
|
| 553 |
+
elif return_tensors == "np":
|
| 554 |
+
import numpy as np
|
| 555 |
+
|
| 556 |
+
result = {
|
| 557 |
+
k: np.array([v]) if k != "offset_mapping" else [np.array(v)]
|
| 558 |
+
for k, v in result.items()
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
return result
|
| 562 |
+
|
| 563 |
+
def decode(
|
| 564 |
+
self,
|
| 565 |
+
token_ids: Union[int, List[int]],
|
| 566 |
+
skip_special_tokens: bool = False,
|
| 567 |
+
clean_up_tokenization_spaces: bool = True,
|
| 568 |
+
) -> str:
|
| 569 |
+
"""Decode token IDs back to text."""
|
| 570 |
+
if hasattr(token_ids, "cpu"):
|
| 571 |
+
token_ids = token_ids.cpu().tolist()
|
| 572 |
+
elif isinstance(token_ids, int):
|
| 573 |
+
token_ids = [token_ids]
|
| 574 |
+
|
| 575 |
+
tokens = self.convert_ids_to_tokens(token_ids, skip_special_tokens=skip_special_tokens)
|
| 576 |
+
text = self.convert_tokens_to_string(tokens)
|
| 577 |
+
|
| 578 |
+
if clean_up_tokenization_spaces:
|
| 579 |
+
text = " ".join(text.split())
|
| 580 |
+
|
| 581 |
+
return text
|
| 582 |
+
|
| 583 |
+
def batch_encode_plus(
|
| 584 |
+
self,
|
| 585 |
+
batch_text_or_pairs: List[str],
|
| 586 |
+
add_special_tokens: bool = False,
|
| 587 |
+
max_length: Optional[int] = None,
|
| 588 |
+
truncation: bool = False,
|
| 589 |
+
padding: bool = False,
|
| 590 |
+
return_tensors: Optional[str] = None,
|
| 591 |
+
return_attention_mask: bool = True,
|
| 592 |
+
return_offsets_mapping: bool = False,
|
| 593 |
+
return_token_type_ids: bool = False,
|
| 594 |
+
) -> Dict:
|
| 595 |
+
"""Encode a batch of texts."""
|
| 596 |
+
if isinstance(batch_text_or_pairs, str):
|
| 597 |
+
batch_text_or_pairs = [batch_text_or_pairs]
|
| 598 |
+
|
| 599 |
+
batch_output = []
|
| 600 |
+
for text in batch_text_or_pairs:
|
| 601 |
+
encoding = self.encode(
|
| 602 |
+
text,
|
| 603 |
+
add_special_tokens=add_special_tokens,
|
| 604 |
+
max_length=max_length,
|
| 605 |
+
truncation=truncation,
|
| 606 |
+
padding=False,
|
| 607 |
+
return_tensors=None,
|
| 608 |
+
return_attention_mask=return_attention_mask,
|
| 609 |
+
return_offsets_mapping=return_offsets_mapping,
|
| 610 |
+
return_token_type_ids=return_token_type_ids,
|
| 611 |
+
)
|
| 612 |
+
batch_output.append(encoding)
|
| 613 |
+
|
| 614 |
+
# Apply padding across batch
|
| 615 |
+
if padding:
|
| 616 |
+
max_len = max(len(enc["input_ids"]) for enc in batch_output)
|
| 617 |
+
for enc in batch_output:
|
| 618 |
+
pad_length = max_len - len(enc["input_ids"])
|
| 619 |
+
if pad_length > 0:
|
| 620 |
+
enc["input_ids"] = enc["input_ids"] + [self.pad_token_id] * pad_length
|
| 621 |
+
enc["attention_mask"] = enc["attention_mask"] + [0] * pad_length
|
| 622 |
+
if return_offsets_mapping and "offset_mapping" in enc:
|
| 623 |
+
enc["offset_mapping"] = enc["offset_mapping"] + [(0, 0)] * pad_length
|
| 624 |
+
if return_token_type_ids and "token_type_ids" in enc:
|
| 625 |
+
enc["token_type_ids"] = enc["token_type_ids"] + [0] * pad_length
|
| 626 |
+
|
| 627 |
+
batch_result = {
|
| 628 |
+
"input_ids": [enc["input_ids"] for enc in batch_output],
|
| 629 |
+
"attention_mask": [enc["attention_mask"] for enc in batch_output],
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
if return_offsets_mapping:
|
| 633 |
+
batch_result["offset_mapping"] = [enc.get("offset_mapping", []) for enc in batch_output]
|
| 634 |
+
if return_token_type_ids:
|
| 635 |
+
batch_result["token_type_ids"] = [enc.get("token_type_ids", []) for enc in batch_output]
|
| 636 |
+
|
| 637 |
+
# Convert to tensors if requested
|
| 638 |
+
if return_tensors == "pt":
|
| 639 |
+
import torch
|
| 640 |
+
|
| 641 |
+
batch_result = {
|
| 642 |
+
"input_ids": torch.tensor(batch_result["input_ids"], dtype=torch.long),
|
| 643 |
+
"attention_mask": torch.tensor(batch_result["attention_mask"], dtype=torch.long),
|
| 644 |
+
}
|
| 645 |
+
if return_offsets_mapping:
|
| 646 |
+
batch_result["offset_mapping"] = [
|
| 647 |
+
torch.tensor(m, dtype=torch.long) for m in batch_result["offset_mapping"]
|
| 648 |
+
]
|
| 649 |
+
if return_token_type_ids:
|
| 650 |
+
batch_result["token_type_ids"] = torch.tensor(
|
| 651 |
+
batch_result["token_type_ids"], dtype=torch.long
|
| 652 |
+
)
|
| 653 |
+
elif return_tensors == "np":
|
| 654 |
+
import numpy as np
|
| 655 |
+
|
| 656 |
+
batch_result = {
|
| 657 |
+
"input_ids": np.array(batch_result["input_ids"]),
|
| 658 |
+
"attention_mask": np.array(batch_result["attention_mask"]),
|
| 659 |
+
}
|
| 660 |
+
if return_offsets_mapping:
|
| 661 |
+
batch_result["offset_mapping"] = [
|
| 662 |
+
np.array(m) for m in batch_result["offset_mapping"]
|
| 663 |
+
]
|
| 664 |
+
if return_token_type_ids:
|
| 665 |
+
batch_result["token_type_ids"] = np.array(batch_result["token_type_ids"])
|
| 666 |
+
|
| 667 |
+
return batch_result
|
| 668 |
+
|
| 669 |
+
def __call__(
|
| 670 |
+
self,
|
| 671 |
+
text: str,
|
| 672 |
+
add_special_tokens: bool = False,
|
| 673 |
+
max_length: Optional[int] = None,
|
| 674 |
+
truncation: bool = False,
|
| 675 |
+
padding: bool = False,
|
| 676 |
+
return_tensors: Optional[str] = None,
|
| 677 |
+
return_attention_mask: bool = True,
|
| 678 |
+
return_offsets_mapping: bool = True,
|
| 679 |
+
return_token_type_ids: bool = False,
|
| 680 |
+
) -> Dict:
|
| 681 |
+
"""Call method for easy encoding."""
|
| 682 |
+
return self.encode(
|
| 683 |
+
text,
|
| 684 |
+
add_special_tokens=add_special_tokens,
|
| 685 |
+
max_length=max_length,
|
| 686 |
+
truncation=truncation,
|
| 687 |
+
padding=padding,
|
| 688 |
+
return_tensors=return_tensors,
|
| 689 |
+
return_attention_mask=return_attention_mask,
|
| 690 |
+
return_offsets_mapping=return_offsets_mapping,
|
| 691 |
+
return_token_type_ids=return_token_type_ids,
|
| 692 |
+
)
|
| 693 |
+
|
| 694 |
+
def get_vocab(self) -> Dict[str, int]:
|
| 695 |
+
"""Get the vocabulary."""
|
| 696 |
+
return self.vocab
|
| 697 |
+
|
| 698 |
+
def save_pretrained(self, save_directory: str):
|
| 699 |
+
"""Save tokenizer to directory."""
|
| 700 |
+
os.makedirs(save_directory, exist_ok=True)
|
| 701 |
+
|
| 702 |
+
tokenizer_config = {
|
| 703 |
+
"vocab": self.vocab,
|
| 704 |
+
"special_tokens_map": self.special_tokens_map,
|
| 705 |
+
"tokenizer_type": self.tokenizer_type,
|
| 706 |
+
"merges": self.merges,
|
| 707 |
+
"pad_token": self.pad_token,
|
| 708 |
+
"unk_token": self.unk_token,
|
| 709 |
+
"cls_token": self.cls_token,
|
| 710 |
+
"sep_token": self.sep_token,
|
| 711 |
+
"mask_token": self.mask_token,
|
| 712 |
+
"bos_token": self.bos_token,
|
| 713 |
+
"eos_token": self.eos_token,
|
| 714 |
+
"pad_token_id": self.pad_token_id,
|
| 715 |
+
"unk_token_id": self.unk_token_id,
|
| 716 |
+
"cls_token_id": self.cls_token_id,
|
| 717 |
+
"sep_token_id": self.sep_token_id,
|
| 718 |
+
"mask_token_id": self.mask_token_id,
|
| 719 |
+
"bos_token_id": self.bos_token_id,
|
| 720 |
+
"eos_token_id": self.eos_token_id,
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
# Save config files
|
| 724 |
+
with open(
|
| 725 |
+
os.path.join(save_directory, "tokenizer_config.json"), "w", encoding="utf-8"
|
| 726 |
+
) as f:
|
| 727 |
+
json.dump(tokenizer_config, f, ensure_ascii=False, indent=2)
|
| 728 |
+
|
| 729 |
+
with open(os.path.join(save_directory, "vocab.json"), "w", encoding="utf-8") as f:
|
| 730 |
+
json.dump(self.vocab, f, ensure_ascii=False, indent=2)
|
| 731 |
+
|
| 732 |
+
with open(
|
| 733 |
+
os.path.join(save_directory, "special_tokens_map.json"), "w", encoding="utf-8"
|
| 734 |
+
) as f:
|
| 735 |
+
json.dump(self.special_tokens_map, f, ensure_ascii=False, indent=2)
|
| 736 |
+
|
| 737 |
+
if self.merges:
|
| 738 |
+
with open(os.path.join(save_directory, "merges.txt"), "w", encoding="utf-8") as f:
|
| 739 |
+
for merge in self.merges:
|
| 740 |
+
f.write(merge + "\n")
|
| 741 |
+
|
| 742 |
+
@classmethod
|
| 743 |
+
def from_pretrained(
|
| 744 |
+
cls, save_directory: str, tokenizer_type: str = "wordpiece", adding_new_token_path: str = "",add_tab_newline_vocab: bool=False
|
| 745 |
+
):
|
| 746 |
+
"""Load tokenizer from directory."""
|
| 747 |
+
with open(os.path.join(save_directory, "vocab.json"), "r", encoding="utf-8") as f:
|
| 748 |
+
vocab = json.load(f)
|
| 749 |
+
|
| 750 |
+
special_tokens_map = {}
|
| 751 |
+
special_tokens_path = os.path.join(save_directory, "special_tokens_map.json")
|
| 752 |
+
if os.path.exists(special_tokens_path):
|
| 753 |
+
with open(special_tokens_path, "r", encoding="utf-8") as f:
|
| 754 |
+
special_tokens_map = json.load(f)
|
| 755 |
+
|
| 756 |
+
# Add new tokens if provided
|
| 757 |
+
if adding_new_token_path and os.path.exists(adding_new_token_path):
|
| 758 |
+
with open(adding_new_token_path, "r", encoding="utf-8") as f:
|
| 759 |
+
adding_new_tokens = f.read().split("\n")
|
| 760 |
+
|
| 761 |
+
updated = False
|
| 762 |
+
c = 0
|
| 763 |
+
for ch in adding_new_tokens:
|
| 764 |
+
if ch not in vocab and ch != " ":
|
| 765 |
+
vocab[ch] = len(vocab)
|
| 766 |
+
updated = True
|
| 767 |
+
c += 1
|
| 768 |
+
|
| 769 |
+
if updated:
|
| 770 |
+
with open(os.path.join(save_directory, "vocab.json"), "w", encoding="utf-8") as f:
|
| 771 |
+
json.dump(vocab, f, ensure_ascii=False, indent=2)
|
| 772 |
+
print(f"Updating vocab with {c} new tokens")
|
| 773 |
+
|
| 774 |
+
# Load merges for BPE
|
| 775 |
+
merges = []
|
| 776 |
+
merges_path = os.path.join(save_directory, "merges.txt")
|
| 777 |
+
if os.path.exists(merges_path):
|
| 778 |
+
with open(merges_path, "r", encoding="utf-8") as f:
|
| 779 |
+
merges = [line.strip() for line in f.readlines()]
|
| 780 |
+
|
| 781 |
+
tokenizer = cls(
|
| 782 |
+
vocab = vocab,
|
| 783 |
+
special_tokens_map = special_tokens_map,
|
| 784 |
+
tokenizer_type = tokenizer_type,
|
| 785 |
+
add_tab_newline_vocab = add_tab_newline_vocab,
|
| 786 |
+
|
| 787 |
+
)
|
| 788 |
+
tokenizer.merges = merges
|
| 789 |
+
|
| 790 |
+
return tokenizer
|
| 791 |
+
|
| 792 |
+
def add_tokens(self, new_tokens: Union[str, List[str]]) -> int:
|
| 793 |
+
"""Add new tokens to the vocabulary."""
|
| 794 |
+
if isinstance(new_tokens, str):
|
| 795 |
+
new_tokens = [new_tokens]
|
| 796 |
+
|
| 797 |
+
added_count = 0
|
| 798 |
+
for token in new_tokens:
|
| 799 |
+
if token not in self.vocab:
|
| 800 |
+
self.vocab[token] = len(self.vocab)
|
| 801 |
+
self.inv_vocab[len(self.vocab) - 1] = token
|
| 802 |
+
added_count += 1
|
| 803 |
+
return added_count
|
| 804 |
+
|
| 805 |
+
def add_special_tokens(self, special_tokens_dict: Dict[str, str]) -> int:
|
| 806 |
+
"""Add special tokens to the tokenizer."""
|
| 807 |
+
added_count = 0
|
| 808 |
+
for key, token in special_tokens_dict.items():
|
| 809 |
+
if hasattr(self, key):
|
| 810 |
+
setattr(self, key, token)
|
| 811 |
+
if token not in self.vocab:
|
| 812 |
+
self.vocab[token] = len(self.vocab)
|
| 813 |
+
self.inv_vocab[len(self.vocab) - 1] = token
|
| 814 |
+
added_count += 1
|
| 815 |
+
|
| 816 |
+
self.all_special_tokens = [
|
| 817 |
+
self.pad_token,
|
| 818 |
+
self.unk_token,
|
| 819 |
+
self.cls_token,
|
| 820 |
+
self.sep_token,
|
| 821 |
+
self.mask_token,
|
| 822 |
+
self.bos_token,
|
| 823 |
+
self.eos_token,
|
| 824 |
+
]
|
| 825 |
+
self.all_special_ids = [
|
| 826 |
+
self.pad_token_id,
|
| 827 |
+
self.unk_token_id,
|
| 828 |
+
self.cls_token_id,
|
| 829 |
+
self.sep_token_id,
|
| 830 |
+
self.mask_token_id,
|
| 831 |
+
self.bos_token_id,
|
| 832 |
+
self.eos_token_id,
|
| 833 |
+
]
|
| 834 |
+
|
| 835 |
+
return added_count
|
| 836 |
+
|
| 837 |
+
def __len__(self) -> int:
|
| 838 |
+
"""Return vocabulary size."""
|
| 839 |
+
return len(self.vocab)
|
src/dytr/training/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Training modules for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides the trainer class, dataset handling utilities,
|
| 19 |
+
and data processing functions for multi-task training with continual learning support.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
from dytr.training.data import MultiTaskDataset, TaskAwareBatchSampler, collate_fn
|
| 28 |
+
from dytr.training.dataset import SingleDatasetProcessing
|
| 29 |
+
from dytr.training.trainer import Trainer
|
| 30 |
+
from dytr.training.utils import process_qa_dataset, set_seed
|
| 31 |
+
|
| 32 |
+
__all__ = [
|
| 33 |
+
"Trainer",
|
| 34 |
+
"SingleDatasetProcessing",
|
| 35 |
+
"MultiTaskDataset",
|
| 36 |
+
"TaskAwareBatchSampler",
|
| 37 |
+
"collate_fn",
|
| 38 |
+
"set_seed",
|
| 39 |
+
"process_qa_dataset",
|
| 40 |
+
]
|
| 41 |
+
|
src/dytr/training/data.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Data utilities for multi-task training.
|
| 17 |
+
|
| 18 |
+
This module provides dataset classes and samplers for handling multiple tasks
|
| 19 |
+
during training.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
import bisect
|
| 28 |
+
from collections import defaultdict
|
| 29 |
+
from typing import Optional
|
| 30 |
+
|
| 31 |
+
import numpy as np
|
| 32 |
+
import torch
|
| 33 |
+
import torch.nn.functional as F
|
| 34 |
+
from torch.utils.data import Dataset
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def collate_fn(batch, fixed_max_len: Optional[int] = None):
|
| 38 |
+
"""
|
| 39 |
+
Custom collate function for multi-task batches.
|
| 40 |
+
|
| 41 |
+
Handles variable-length sequences and different task types.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
batch: List of samples from the dataset
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
Dictionary with batched tensors
|
| 48 |
+
"""
|
| 49 |
+
if not isinstance(batch, list):
|
| 50 |
+
batch = [batch]
|
| 51 |
+
|
| 52 |
+
batch_dict = {}
|
| 53 |
+
|
| 54 |
+
for key in batch[0].keys():
|
| 55 |
+
if key in ["input_ids", "attention_mask", "labels"]:
|
| 56 |
+
values = []
|
| 57 |
+
for item in batch:
|
| 58 |
+
if key in item and isinstance(item[key], torch.Tensor):
|
| 59 |
+
if item[key].dim() > 0:
|
| 60 |
+
values.append(item[key])
|
| 61 |
+
|
| 62 |
+
if not values:
|
| 63 |
+
continue
|
| 64 |
+
|
| 65 |
+
# Find max length for padding
|
| 66 |
+
if fixed_max_len is not None:
|
| 67 |
+
# Use fixed length for all sequences
|
| 68 |
+
max_len = batch[0]["max_length"] # fixed_max_len
|
| 69 |
+
else:
|
| 70 |
+
# Dynamic: use max length in this batch
|
| 71 |
+
max_len = max(v.size(0) for v in values if v.dim() > 0)
|
| 72 |
+
# max_len = max(v.size(0) for v in values if v.dim() > 0)
|
| 73 |
+
padded_values = []
|
| 74 |
+
|
| 75 |
+
for v in values:
|
| 76 |
+
if v.dim() > 0:
|
| 77 |
+
if v.size(0) < max_len:
|
| 78 |
+
pad_size = max_len - v.size(0)
|
| 79 |
+
if key == "labels":
|
| 80 |
+
padded = F.pad(v, (0, pad_size), value=-100)
|
| 81 |
+
else:
|
| 82 |
+
padded = F.pad(v, (0, pad_size), value=0)
|
| 83 |
+
else:
|
| 84 |
+
padded = v
|
| 85 |
+
padded_values.append(padded.unsqueeze(0))
|
| 86 |
+
|
| 87 |
+
if padded_values:
|
| 88 |
+
batch_dict[key] = torch.cat(padded_values, dim=0)
|
| 89 |
+
else:
|
| 90 |
+
# For non-tensor fields, keep as list
|
| 91 |
+
batch_dict[key] = [item[key] for item in batch if key in item]
|
| 92 |
+
|
| 93 |
+
return batch_dict
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class TaskAwareBatchSampler:
|
| 97 |
+
"""
|
| 98 |
+
Batch sampler that creates batches with samples from the same task.
|
| 99 |
+
|
| 100 |
+
Ensures that each batch contains samples from only one task to simplify
|
| 101 |
+
task-specific processing.
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
dataset: MultiTaskDataset instance
|
| 105 |
+
batch_size: Batch size
|
| 106 |
+
shuffle: Whether to shuffle batches
|
| 107 |
+
drop_last: Whether to drop incomplete batches
|
| 108 |
+
"""
|
| 109 |
+
|
| 110 |
+
def __init__(self, dataset, batch_size, shuffle=True, drop_last=True):
|
| 111 |
+
self.dataset = dataset
|
| 112 |
+
self.batch_size = batch_size
|
| 113 |
+
self.shuffle = shuffle
|
| 114 |
+
self.drop_last = drop_last
|
| 115 |
+
|
| 116 |
+
# Group indices by task
|
| 117 |
+
self.indices_by_task = defaultdict(list)
|
| 118 |
+
for idx in range(len(dataset)):
|
| 119 |
+
dataset_idx = bisect.bisect_right(dataset.cumulative_sizes, idx) - 1
|
| 120 |
+
task_name = dataset.task_names[dataset_idx]
|
| 121 |
+
self.indices_by_task[task_name].append(idx)
|
| 122 |
+
|
| 123 |
+
# Create batches
|
| 124 |
+
self.batches = []
|
| 125 |
+
self.batch_task_map = []
|
| 126 |
+
|
| 127 |
+
for task_name, indices in self.indices_by_task.items():
|
| 128 |
+
if shuffle:
|
| 129 |
+
np.random.shuffle(indices)
|
| 130 |
+
|
| 131 |
+
for i in range(0, len(indices), batch_size):
|
| 132 |
+
batch_indices = indices[i : i + batch_size]
|
| 133 |
+
if drop_last and len(batch_indices) < batch_size:
|
| 134 |
+
continue
|
| 135 |
+
self.batches.append(batch_indices)
|
| 136 |
+
self.batch_task_map.append(task_name)
|
| 137 |
+
|
| 138 |
+
# Shuffle batches if needed
|
| 139 |
+
if shuffle:
|
| 140 |
+
combined = list(zip(self.batches, self.batch_task_map))
|
| 141 |
+
np.random.shuffle(combined)
|
| 142 |
+
if combined:
|
| 143 |
+
self.batches, self.batch_task_map = zip(*combined)
|
| 144 |
+
self.batches = list(self.batches)
|
| 145 |
+
self.batch_task_map = list(self.batch_task_map)
|
| 146 |
+
|
| 147 |
+
def __iter__(self):
|
| 148 |
+
"""Iterate over batches."""
|
| 149 |
+
for batch_indices in self.batches:
|
| 150 |
+
yield batch_indices
|
| 151 |
+
|
| 152 |
+
def __len__(self):
|
| 153 |
+
"""Number of batches."""
|
| 154 |
+
return len(self.batches)
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
class MultiTaskDataset(Dataset):
|
| 158 |
+
"""
|
| 159 |
+
Dataset that combines multiple single-task datasets.
|
| 160 |
+
|
| 161 |
+
Creates a unified dataset from multiple task-specific datasets,
|
| 162 |
+
allowing seamless multi-task training.
|
| 163 |
+
|
| 164 |
+
Args:
|
| 165 |
+
datasets_dict: Dictionary mapping task names to (dataset, strategy) tuples
|
| 166 |
+
tokenizer: Tokenizer instance
|
| 167 |
+
task_configs: List of task configurations
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
def __init__(self, datasets_dict, tokenizer, task_configs):
|
| 171 |
+
self.datasets = []
|
| 172 |
+
self.task_names = []
|
| 173 |
+
self.max_lengths ={t:datasets_dict[t][0].max_len for t in datasets_dict.keys() }
|
| 174 |
+
|
| 175 |
+
#self.max_lengths = {t.task_name: t.max_length for t in task_configs}
|
| 176 |
+
self.strategies = []
|
| 177 |
+
self.cumulative_sizes = [0]
|
| 178 |
+
|
| 179 |
+
print("Max lengths per task:", self.max_lengths)
|
| 180 |
+
|
| 181 |
+
for task_name, (dataset, strategy) in datasets_dict.items():
|
| 182 |
+
if len(dataset) > 0:
|
| 183 |
+
self.datasets.append(dataset)
|
| 184 |
+
self.task_names.append(task_name)
|
| 185 |
+
self.strategies.append(strategy)
|
| 186 |
+
self.cumulative_sizes.append(self.cumulative_sizes[-1] + len(dataset))
|
| 187 |
+
|
| 188 |
+
def __len__(self):
|
| 189 |
+
"""Total number of samples across all tasks."""
|
| 190 |
+
return self.cumulative_sizes[-1]
|
| 191 |
+
|
| 192 |
+
def __getitem__(self, idx):
|
| 193 |
+
"""
|
| 194 |
+
Get a sample by index.
|
| 195 |
+
|
| 196 |
+
Args:
|
| 197 |
+
idx: Index in the combined dataset
|
| 198 |
+
|
| 199 |
+
Returns:
|
| 200 |
+
Sample with task_name and strategy added
|
| 201 |
+
"""
|
| 202 |
+
if idx < 0:
|
| 203 |
+
idx = len(self) + idx
|
| 204 |
+
|
| 205 |
+
# Find which dataset this index belongs to
|
| 206 |
+
dataset_idx = bisect.bisect_right(self.cumulative_sizes, idx) - 1
|
| 207 |
+
sample_idx = idx - self.cumulative_sizes[dataset_idx]
|
| 208 |
+
|
| 209 |
+
# Get the sample from the specific dataset
|
| 210 |
+
item = self.datasets[dataset_idx][sample_idx]
|
| 211 |
+
|
| 212 |
+
if isinstance(item, dict):
|
| 213 |
+
item["task_name"] = self.task_names[dataset_idx]
|
| 214 |
+
item["strategy"] = self.strategies[dataset_idx].value
|
| 215 |
+
item["max_length"] = self.max_lengths[self.task_names[dataset_idx]]
|
| 216 |
+
|
| 217 |
+
return item
|
src/dytr/training/dataset.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Dataset classes for single-task data processing.
|
| 17 |
+
|
| 18 |
+
This module provides the SingleDatasetProcessing class which handles
|
| 19 |
+
dataset preparation for all training strategies.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
import hashlib
|
| 28 |
+
import pickle
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
#from typing import Optional
|
| 31 |
+
|
| 32 |
+
#import numpy as np
|
| 33 |
+
import pandas as pd
|
| 34 |
+
import torch
|
| 35 |
+
from torch.utils.data import Dataset
|
| 36 |
+
from tqdm import tqdm
|
| 37 |
+
|
| 38 |
+
from dytr.core.config import TrainingStrategy
|
| 39 |
+
|
| 40 |
+
def process_tags_column(df, tags_column='tags',label_to_ids={},calculate_distribution=False):
|
| 41 |
+
"""
|
| 42 |
+
Process tags column: convert string tags to numeric labels.
|
| 43 |
+
|
| 44 |
+
Args:
|
| 45 |
+
df: DataFrame with tags column
|
| 46 |
+
tags_column: Name of tags column (default: 'tags')
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
df: DataFrame with processed tags column
|
| 50 |
+
label_to_ids: Dictionary mapping tag labels to IDs
|
| 51 |
+
distribution: Dictionary of tag distribution
|
| 52 |
+
"""
|
| 53 |
+
from collections import Counter
|
| 54 |
+
df[tags_column] = df[tags_column].apply(lambda x: ' '.join(map(str, x)) if isinstance(x, list) else x)
|
| 55 |
+
# Collect all unique tags
|
| 56 |
+
all_tags = set()
|
| 57 |
+
for tags_str in df[tags_column].dropna():
|
| 58 |
+
if isinstance(tags_str, str):
|
| 59 |
+
tags = tags_str.split()
|
| 60 |
+
all_tags.update(tags)
|
| 61 |
+
|
| 62 |
+
if not label_to_ids:
|
| 63 |
+
# Create label mapping (O -> 0, others start from 1)
|
| 64 |
+
|
| 65 |
+
label_to_ids = {'O': 0}
|
| 66 |
+
|
| 67 |
+
for tag in sorted(all_tags):
|
| 68 |
+
if tag != 'O' and not tag.isdigit():
|
| 69 |
+
label_to_ids[tag] = len(label_to_ids)
|
| 70 |
+
elif tag.isdigit() and int(tag) !=0:
|
| 71 |
+
label_to_ids[tag] = int(tag)#len(label_to_ids)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# Convert tags to numeric labels
|
| 75 |
+
def convert_tags(tags_str):
|
| 76 |
+
if not isinstance(tags_str, str):
|
| 77 |
+
return ''
|
| 78 |
+
tags = tags_str.split()
|
| 79 |
+
return ' '.join(str(label_to_ids.get(tag, 0)) for tag in tags)
|
| 80 |
+
|
| 81 |
+
df[tags_column] = df[tags_column].apply(convert_tags)
|
| 82 |
+
if not calculate_distribution:
|
| 83 |
+
return df, label_to_ids, {}
|
| 84 |
+
# Calculate distribution
|
| 85 |
+
distribution = Counter()
|
| 86 |
+
for tags_str in df[tags_column]:
|
| 87 |
+
if tags_str:
|
| 88 |
+
for label in map(int, tags_str.split()):
|
| 89 |
+
distribution[label] += 1
|
| 90 |
+
|
| 91 |
+
# Convert to readable format
|
| 92 |
+
readable_dist = {label: count for label, count in distribution.items()}
|
| 93 |
+
|
| 94 |
+
return df, label_to_ids, readable_dist
|
| 95 |
+
|
| 96 |
+
class SingleDatasetProcessing(Dataset):
|
| 97 |
+
"""
|
| 98 |
+
Dataset class for processing single tasks with different training strategies.
|
| 99 |
+
|
| 100 |
+
Supports:
|
| 101 |
+
- Sentence Classification
|
| 102 |
+
- Token Classification
|
| 103 |
+
- Seq2Seq
|
| 104 |
+
- Causal Language Modeling
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
df: DataFrame containing the data
|
| 108 |
+
tokenizer: Tokenizer instance
|
| 109 |
+
max_len: Maximum sequence length
|
| 110 |
+
task_name: Name of the task
|
| 111 |
+
strategy: Training strategy
|
| 112 |
+
num_labels: Number of labels (for classification)
|
| 113 |
+
text_column: Column name for text
|
| 114 |
+
label_column: Column name for labels
|
| 115 |
+
source_column: Column name for source text (seq2seq)
|
| 116 |
+
target_column: Column name for target text (seq2seq)
|
| 117 |
+
tags_column: Column name for token tags
|
| 118 |
+
stride: Stride for windowing (causal LM)
|
| 119 |
+
cache_dir: Directory for caching processed data
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
def __init__(
|
| 123 |
+
self,
|
| 124 |
+
df,
|
| 125 |
+
tokenizer,
|
| 126 |
+
max_len,
|
| 127 |
+
task_name,
|
| 128 |
+
strategy,
|
| 129 |
+
num_labels=None,
|
| 130 |
+
text_column="text",
|
| 131 |
+
label_column="label",
|
| 132 |
+
source_column="source",
|
| 133 |
+
target_column="target",
|
| 134 |
+
tags_column="tags",
|
| 135 |
+
stride=None,
|
| 136 |
+
cache_dir="./dataset_cache",
|
| 137 |
+
token_labeling_first_only=True,
|
| 138 |
+
label_to_ids={}
|
| 139 |
+
):
|
| 140 |
+
|
| 141 |
+
self.df = df
|
| 142 |
+
self.tokenizer = tokenizer
|
| 143 |
+
self.max_len = max_len
|
| 144 |
+
self.task_name = task_name
|
| 145 |
+
self.strategy = strategy
|
| 146 |
+
self.num_labels = num_labels
|
| 147 |
+
self.text_column = text_column
|
| 148 |
+
self.stride = stride if stride is not None else max_len // 2
|
| 149 |
+
self.samples = []
|
| 150 |
+
self.pad_id = tokenizer.pad_token_id or 0
|
| 151 |
+
self.cache_dir = Path(cache_dir)
|
| 152 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 153 |
+
self.token_labeling_first_only = token_labeling_first_only
|
| 154 |
+
self.label_to_ids=label_to_ids
|
| 155 |
+
# Process based on strategy
|
| 156 |
+
if strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 157 |
+
self._process_classification(label_column)
|
| 158 |
+
elif strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 159 |
+
self._process_token_classification(tags_column)
|
| 160 |
+
elif strategy == TrainingStrategy.SEQ2SEQ:
|
| 161 |
+
self._process_seq2seq(source_column, target_column)
|
| 162 |
+
elif strategy == TrainingStrategy.CAUSAL_LM:
|
| 163 |
+
self._process_causal_lm(text_column)
|
| 164 |
+
|
| 165 |
+
print(f" Created {len(self.samples)} samples for {self.task_name} >> {strategy.value}")
|
| 166 |
+
|
| 167 |
+
def _process_classification(self, label_column):
|
| 168 |
+
"""Process data for sentence classification."""
|
| 169 |
+
if self.num_labels is None:
|
| 170 |
+
self.num_labels = len(self.df[label_column].unique())
|
| 171 |
+
|
| 172 |
+
for _, row in self.df.iterrows():
|
| 173 |
+
label = int(row[label_column])
|
| 174 |
+
self.samples.append({"text": row[self.text_column], "label": label})
|
| 175 |
+
if len(self.samples) == 1:
|
| 176 |
+
print(f" Sample text: {str(row[self.text_column])}...")
|
| 177 |
+
print(f" Sample label: {label}")
|
| 178 |
+
|
| 179 |
+
def _process_token_classification(self, tags_column):
|
| 180 |
+
"""Process data for token classification."""
|
| 181 |
+
if not self.label_to_ids:
|
| 182 |
+
self.df, self.label_to_ids, _=process_tags_column(self.df, tags_column)
|
| 183 |
+
else:
|
| 184 |
+
self.df, _, _=process_tags_column(self.df, tags_column,label_to_ids=self.label_to_ids)
|
| 185 |
+
"""
|
| 186 |
+
all_tags = set()
|
| 187 |
+
for tags in self.df[tags_column]:
|
| 188 |
+
if isinstance(tags, str):
|
| 189 |
+
for tag in tags.split():
|
| 190 |
+
all_tags.add(int(tag))
|
| 191 |
+
elif isinstance(tags, list):
|
| 192 |
+
for tag in tags:
|
| 193 |
+
all_tags.add(int(tag))
|
| 194 |
+
all_tags = sorted(all_tags)"""
|
| 195 |
+
self.num_labels = max(self.label_to_ids.values())+1 #len(all_tags)
|
| 196 |
+
|
| 197 |
+
valid_samples = 0
|
| 198 |
+
for _, row in self.df.iterrows():
|
| 199 |
+
text = row[self.text_column]
|
| 200 |
+
if isinstance(row[tags_column], str):
|
| 201 |
+
tags = [int(i) for i in row[tags_column].split()]
|
| 202 |
+
else:
|
| 203 |
+
tags = [int(i) for i in row[tags_column]]
|
| 204 |
+
|
| 205 |
+
words = text.split()
|
| 206 |
+
if len(tags) == len(words):
|
| 207 |
+
self.samples.append({"text": text, "tags": tags})
|
| 208 |
+
valid_samples += 1
|
| 209 |
+
if valid_samples == 1:
|
| 210 |
+
print(f" Sample text: {text}...")
|
| 211 |
+
print(f" Sample tags: {tags}...")
|
| 212 |
+
|
| 213 |
+
def _process_seq2seq(self, source_column, target_column):
|
| 214 |
+
"""Process data for seq2seq."""
|
| 215 |
+
self.num_labels=len(self.tokenizer)
|
| 216 |
+
for _, row in self.df.iterrows():
|
| 217 |
+
self.samples.append({"source": row[source_column], "target": row[target_column]})
|
| 218 |
+
if len(self.samples) == 1:
|
| 219 |
+
print(f" Sample source: {str(row[source_column])}...")
|
| 220 |
+
print(f" Sample target: {str(row[target_column])}...")
|
| 221 |
+
|
| 222 |
+
def _process_causal_lm(self, text_column):
|
| 223 |
+
"""Process data for causal language modeling with windowing."""
|
| 224 |
+
self.num_labels=len(self.tokenizer)
|
| 225 |
+
cache_key = hashlib.md5(
|
| 226 |
+
f"{len(self.df)}_{self.max_len}_{self.stride}_{text_column}_{self.tokenizer.name_or_path}".encode()
|
| 227 |
+
).hexdigest()
|
| 228 |
+
cache_file = self.cache_dir / f"causal_windows_{cache_key}.pkl"
|
| 229 |
+
|
| 230 |
+
if cache_file.exists():
|
| 231 |
+
print(f" Loading cached window texts from {cache_file}")
|
| 232 |
+
with open(cache_file, "rb") as f:
|
| 233 |
+
cache_data = pickle.load(f)
|
| 234 |
+
self.samples = cache_data["window_texts"]
|
| 235 |
+
print(f" Loaded {len(self.samples)} cached windows")
|
| 236 |
+
else:
|
| 237 |
+
print(" Creating causal LM dataset with dynamic window sampling...")
|
| 238 |
+
|
| 239 |
+
if isinstance(self.df, pd.DataFrame):
|
| 240 |
+
texts = self.df[text_column].astype(str).tolist()
|
| 241 |
+
else:
|
| 242 |
+
texts = [t[text_column] for t in self.df]
|
| 243 |
+
|
| 244 |
+
for doc_idx, text in enumerate(tqdm(texts, desc="Processing documents")):
|
| 245 |
+
tokens = self.tokenizer.tokenize(text)
|
| 246 |
+
token_count = len(tokens)
|
| 247 |
+
|
| 248 |
+
if token_count <= self.max_len:
|
| 249 |
+
self.samples.append(text)
|
| 250 |
+
else:
|
| 251 |
+
num_windows = (token_count - self.max_len) // self.stride + 1
|
| 252 |
+
for window_idx in range(num_windows):
|
| 253 |
+
start_token = window_idx * self.stride
|
| 254 |
+
end_token = min(start_token + self.max_len, token_count)
|
| 255 |
+
|
| 256 |
+
window_tokens = tokens[start_token:end_token]
|
| 257 |
+
if len(window_tokens) < 2:
|
| 258 |
+
continue
|
| 259 |
+
window_text = self.tokenizer.convert_tokens_to_string(window_tokens)
|
| 260 |
+
self.samples.append(window_text)
|
| 261 |
+
|
| 262 |
+
# Cache the processed windows
|
| 263 |
+
cache_data = {"window_texts": self.samples}
|
| 264 |
+
response=input("Do you want to save Causal Data Window in cach dir?[Y/n]")
|
| 265 |
+
if response=="Y":
|
| 266 |
+
with open(cache_file, "wb") as f:
|
| 267 |
+
pickle.dump(cache_data, f)
|
| 268 |
+
print(f"data saved on dir: {cache_file}")
|
| 269 |
+
|
| 270 |
+
print(f" Created and cached {len(self.samples)} windows from {len(texts)} documents")
|
| 271 |
+
|
| 272 |
+
def __len__(self):
|
| 273 |
+
return len(self.samples)
|
| 274 |
+
|
| 275 |
+
def __getitem__(self, idx):
|
| 276 |
+
"""Get a single sample based on the strategy."""
|
| 277 |
+
if self.strategy == TrainingStrategy.SENTENCE_CLASSIFICATION:
|
| 278 |
+
return self._get_classification_item(idx)
|
| 279 |
+
elif self.strategy == TrainingStrategy.TOKEN_CLASSIFICATION:
|
| 280 |
+
return self._get_token_classification_item(idx)
|
| 281 |
+
elif self.strategy == TrainingStrategy.SEQ2SEQ:
|
| 282 |
+
return self._get_seq2seq_item(idx)
|
| 283 |
+
elif self.strategy == TrainingStrategy.CAUSAL_LM:
|
| 284 |
+
return self._get_causal_lm_item(idx)
|
| 285 |
+
|
| 286 |
+
def _get_classification_item(self, idx):
|
| 287 |
+
"""Get item for sentence classification."""
|
| 288 |
+
sample = self.samples[idx]
|
| 289 |
+
encoding = self.tokenizer(
|
| 290 |
+
sample["text"],
|
| 291 |
+
max_length=self.max_len,
|
| 292 |
+
truncation=True,
|
| 293 |
+
padding=False,
|
| 294 |
+
add_special_tokens=False,
|
| 295 |
+
)
|
| 296 |
+
input_ids = encoding["input_ids"]
|
| 297 |
+
if not input_ids:
|
| 298 |
+
input_ids = [self.tokenizer.pad_token_id or 0]
|
| 299 |
+
attention_mask = [1] * len(input_ids)
|
| 300 |
+
labels = torch.full((len(input_ids),), -100, dtype=torch.long)
|
| 301 |
+
labels[0] = sample["label"]
|
| 302 |
+
|
| 303 |
+
return {
|
| 304 |
+
"input_ids": torch.tensor(input_ids, dtype=torch.long),
|
| 305 |
+
"attention_mask": torch.tensor(attention_mask, dtype=torch.long),
|
| 306 |
+
"labels": labels,
|
| 307 |
+
"task_name": self.task_name,
|
| 308 |
+
"strategy": self.strategy.value,
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
def _get_token_classification_item(self, idx):
|
| 312 |
+
"""Get item for token classification."""
|
| 313 |
+
sample = self.samples[idx]
|
| 314 |
+
encoding = self.tokenizer(
|
| 315 |
+
sample["text"],
|
| 316 |
+
max_length=self.max_len,
|
| 317 |
+
truncation=True,
|
| 318 |
+
padding=False,
|
| 319 |
+
return_offsets_mapping=True,
|
| 320 |
+
add_special_tokens=False,
|
| 321 |
+
)
|
| 322 |
+
input_ids = encoding["input_ids"]
|
| 323 |
+
if not input_ids:
|
| 324 |
+
input_ids = [self.tokenizer.pad_token_id or 0]
|
| 325 |
+
attention_mask = [1] * len(input_ids)
|
| 326 |
+
|
| 327 |
+
# Get word ids for token alignment
|
| 328 |
+
try:
|
| 329 |
+
word_ids = self.tokenizer.word_ids(encoding)
|
| 330 |
+
except:
|
| 331 |
+
word_ids = encoding.word_ids()
|
| 332 |
+
|
| 333 |
+
labels = [-100] * len(input_ids)
|
| 334 |
+
if self.token_labeling_first_only:
|
| 335 |
+
previous_word_idx = None
|
| 336 |
+
for i, word_idx in enumerate(word_ids):
|
| 337 |
+
if word_idx is None:
|
| 338 |
+
continue
|
| 339 |
+
if word_idx != previous_word_idx:
|
| 340 |
+
if word_idx < len(sample["tags"]):
|
| 341 |
+
tag = sample["tags"][word_idx]
|
| 342 |
+
label_id = int(tag)
|
| 343 |
+
if label_id < self.num_labels:
|
| 344 |
+
labels[i] = label_id
|
| 345 |
+
previous_word_idx = word_idx
|
| 346 |
+
else:
|
| 347 |
+
|
| 348 |
+
# Label every token
|
| 349 |
+
for i, word_idx in enumerate(word_ids):
|
| 350 |
+
if word_idx is None:
|
| 351 |
+
continue
|
| 352 |
+
if word_idx < len(sample["tags"]):
|
| 353 |
+
tag = sample["tags"][word_idx]
|
| 354 |
+
label_id = int(tag)
|
| 355 |
+
if label_id < self.num_labels:
|
| 356 |
+
labels[i] = label_id
|
| 357 |
+
|
| 358 |
+
return {
|
| 359 |
+
"input_ids": torch.tensor(input_ids, dtype=torch.long),
|
| 360 |
+
"attention_mask": torch.tensor(attention_mask, dtype=torch.long),
|
| 361 |
+
"labels": torch.tensor(labels, dtype=torch.long),
|
| 362 |
+
"task_name": self.task_name,
|
| 363 |
+
"strategy": self.strategy.value,
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
def _get_seq2seq_item(self, idx):
|
| 367 |
+
"""Get item for seq2seq."""
|
| 368 |
+
sample = self.samples[idx]
|
| 369 |
+
source_encoding = self.tokenizer(
|
| 370 |
+
sample["source"],
|
| 371 |
+
max_length=self.max_len,
|
| 372 |
+
truncation=True,
|
| 373 |
+
padding=False,
|
| 374 |
+
add_special_tokens=False,
|
| 375 |
+
)
|
| 376 |
+
target_encoding = self.tokenizer(
|
| 377 |
+
sample["target"],
|
| 378 |
+
max_length=self.max_len,
|
| 379 |
+
truncation=True,
|
| 380 |
+
padding=False,
|
| 381 |
+
add_special_tokens=False,
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
return {
|
| 385 |
+
"input_ids": torch.tensor(source_encoding["input_ids"], dtype=torch.long),
|
| 386 |
+
"labels": torch.tensor(target_encoding["input_ids"], dtype=torch.long),
|
| 387 |
+
"attention_mask": torch.ones(len(source_encoding["input_ids"]), dtype=torch.long),
|
| 388 |
+
"task_name": self.task_name,
|
| 389 |
+
"strategy": self.strategy.value,
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
def _get_causal_lm_item(self, idx):
|
| 393 |
+
"""Get item for causal language modeling."""
|
| 394 |
+
window_text = self.samples[idx]
|
| 395 |
+
encoding = self.tokenizer(
|
| 396 |
+
window_text,
|
| 397 |
+
add_special_tokens=False,
|
| 398 |
+
truncation=True,
|
| 399 |
+
max_length=self.max_len,
|
| 400 |
+
padding=False,
|
| 401 |
+
)
|
| 402 |
+
input_ids = encoding["input_ids"]
|
| 403 |
+
|
| 404 |
+
# Shift labels for next token prediction
|
| 405 |
+
labels = input_ids[1:] + [self.pad_id]
|
| 406 |
+
|
| 407 |
+
return {
|
| 408 |
+
"input_ids": torch.tensor(input_ids, dtype=torch.long),
|
| 409 |
+
"labels": torch.tensor(labels, dtype=torch.long),
|
| 410 |
+
"attention_mask": torch.ones(len(input_ids), dtype=torch.long),
|
| 411 |
+
"task_name": self.task_name,
|
| 412 |
+
"strategy": self.strategy.value,
|
| 413 |
+
}
|
src/dytr/training/trainer.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Trainer class for multi-task continual learning.
|
| 17 |
+
|
| 18 |
+
This module provides the Trainer class which handles training,
|
| 19 |
+
validation, and continual learning features like EWC and experience replay.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
import math
|
| 28 |
+
import os
|
| 29 |
+
from collections import defaultdict
|
| 30 |
+
from pathlib import Path
|
| 31 |
+
from typing import Dict, List
|
| 32 |
+
|
| 33 |
+
import torch
|
| 34 |
+
#import torch.nn as nn
|
| 35 |
+
import torch.optim as optim
|
| 36 |
+
from torch.utils.data import DataLoader
|
| 37 |
+
from tqdm import tqdm
|
| 38 |
+
|
| 39 |
+
from dytr.core.config import ModelConfig, TaskConfig
|
| 40 |
+
from dytr.core.model import DynamicTransformer
|
| 41 |
+
from dytr.training.data import MultiTaskDataset, TaskAwareBatchSampler, collate_fn
|
| 42 |
+
from dytr import EWC
|
| 43 |
+
from dytr.utils.logging import get_logger
|
| 44 |
+
|
| 45 |
+
class Trainer:
|
| 46 |
+
"""
|
| 47 |
+
Trainer class for multi-task continual learning.
|
| 48 |
+
|
| 49 |
+
Handles training across multiple tasks with support for:
|
| 50 |
+
- EWC (Elastic Weight Consolidation)
|
| 51 |
+
- Experience replay
|
| 52 |
+
- Learning rate scheduling
|
| 53 |
+
- Gradient accumulation
|
| 54 |
+
- Early stopping
|
| 55 |
+
- Model checkpointing
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
model: DynamicTransformer model
|
| 59 |
+
config: Model configuration
|
| 60 |
+
exp_dir: Directory for saving experiment outputs
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
def __init__(self, model: DynamicTransformer, config: ModelConfig, exp_dir: str):
|
| 64 |
+
self.logger = get_logger(__name__)
|
| 65 |
+
self.model = model
|
| 66 |
+
self.config = config
|
| 67 |
+
self.device = config.device
|
| 68 |
+
self.exp_dir = Path(exp_dir)
|
| 69 |
+
self.exp_dir.mkdir(parents=True, exist_ok=True)
|
| 70 |
+
self.best_val_loss = model.best_val_loss
|
| 71 |
+
self.best_metrics = {}
|
| 72 |
+
self.patience_counter = 0
|
| 73 |
+
self.loss_history = []
|
| 74 |
+
self.num_labels_per_task={}
|
| 75 |
+
self.val_loss_history = []
|
| 76 |
+
self.metrics_history = []
|
| 77 |
+
self.logger.info("Trainer initialized")
|
| 78 |
+
#self.logger.debug(f"Experiment directory: {exp_dir}")
|
| 79 |
+
self.logger.debug("Device: %s ",self.config.device)
|
| 80 |
+
|
| 81 |
+
def count_parameters(self):
|
| 82 |
+
"""Count and display model parameters."""
|
| 83 |
+
total = sum(p.numel() for p in self.model.parameters())
|
| 84 |
+
trainable = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
|
| 85 |
+
|
| 86 |
+
self.logger.info(" Model size: %s parameters",total)
|
| 87 |
+
self.logger.info(" Trainable: %s" ,trainable)
|
| 88 |
+
self.logger.info("\nParameter breakdown:")
|
| 89 |
+
|
| 90 |
+
for name, module in self.model.task_heads.items():
|
| 91 |
+
self.logger.info(" Head %s: %s",name, sum(p.numel() for p in module.parameters()))
|
| 92 |
+
|
| 93 |
+
for name, module in self.model.decoders.items():
|
| 94 |
+
self.logger.info(" Decoder %s: %s",name, sum(p.numel() for p in module.parameters()))
|
| 95 |
+
|
| 96 |
+
self.logger.info(f" Shared encoder: {sum(p.numel() for p in self.model.encoder.parameters()):,}")
|
| 97 |
+
self.logger.info(
|
| 98 |
+
f" Shared embedding: {sum(p.numel() for p in self.model.shared_embedding.parameters()):,}"
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
return total
|
| 102 |
+
def freeze_all(self):
|
| 103 |
+
"""
|
| 104 |
+
Freeze all parameters in the model.
|
| 105 |
+
Used as initial state before selectively unfreezing tasks.
|
| 106 |
+
"""
|
| 107 |
+
for param in self.model.parameters():
|
| 108 |
+
param.requires_grad = False
|
| 109 |
+
self.logger.info(" Frozen all model parameters")
|
| 110 |
+
|
| 111 |
+
def unfreeze_shared_encoder(self):
|
| 112 |
+
"""
|
| 113 |
+
Unfreeze the shared encoder and embedding layers.
|
| 114 |
+
These are shared across all tasks and should be trainable for all tasks.
|
| 115 |
+
"""
|
| 116 |
+
# Unfreeze shared embedding
|
| 117 |
+
for param in self.model.shared_embedding.parameters():
|
| 118 |
+
param.requires_grad = True
|
| 119 |
+
|
| 120 |
+
# Unfreeze encoder
|
| 121 |
+
for param in self.model.encoder.parameters():
|
| 122 |
+
param.requires_grad = True
|
| 123 |
+
|
| 124 |
+
self.logger.info(" Unfrozen shared encoder and embeddings")
|
| 125 |
+
|
| 126 |
+
def unfreeze_task_components(self, task_name: str):
|
| 127 |
+
"""
|
| 128 |
+
Unfreeze task-specific components for a given task.
|
| 129 |
+
|
| 130 |
+
Args:
|
| 131 |
+
task_name: Name of the task to unfreeze
|
| 132 |
+
"""
|
| 133 |
+
# Unfreeze task head if it exists
|
| 134 |
+
if task_name in self.model.task_heads:
|
| 135 |
+
for param in self.model.task_heads[task_name].parameters():
|
| 136 |
+
param.requires_grad = True
|
| 137 |
+
self.logger.info(f" Unfrozen head for task: {task_name}")
|
| 138 |
+
|
| 139 |
+
# Unfreeze decoder if it exists
|
| 140 |
+
if task_name in self.model.decoders:
|
| 141 |
+
for param in self.model.decoders[task_name].parameters():
|
| 142 |
+
param.requires_grad = True
|
| 143 |
+
self.logger.info(f" Unfrozen decoder for task: {task_name}")
|
| 144 |
+
|
| 145 |
+
def unfreeze_for_tasks(self, task_names: list, unfreeze_shared: bool = True):
|
| 146 |
+
"""
|
| 147 |
+
Unfreeze model components for specific tasks.
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
task_names: List of tasks to unfreeze
|
| 151 |
+
unfreeze_shared: Whether to unfreeze shared encoder (default: True)
|
| 152 |
+
"""
|
| 153 |
+
# Unfreeze shared components first (if needed)
|
| 154 |
+
if unfreeze_shared:
|
| 155 |
+
self.unfreeze_shared_encoder()
|
| 156 |
+
|
| 157 |
+
# Unfreeze each task's specific components
|
| 158 |
+
for task_name in task_names:
|
| 159 |
+
self.unfreeze_task_components(task_name)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def train(self, task_configs: List[TaskConfig], train_datasets: Dict, val_datasets: Dict):
|
| 163 |
+
"""
|
| 164 |
+
Train the model on multiple tasks.
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
task_configs: List of task configurations
|
| 168 |
+
train_datasets: Dictionary of training datasets per task
|
| 169 |
+
val_datasets: Dictionary of validation datasets per task
|
| 170 |
+
|
| 171 |
+
Returns:
|
| 172 |
+
Trained model
|
| 173 |
+
"""
|
| 174 |
+
self.logger.info("=" * 60)
|
| 175 |
+
self.logger.info("Starting training session")
|
| 176 |
+
self.logger.info("=" * 60)
|
| 177 |
+
training_task_names = list(train_datasets.keys())
|
| 178 |
+
encoder_unfreeze = not all(train_datasets[i][1] == "causal_lm" for i in train_datasets.keys())
|
| 179 |
+
|
| 180 |
+
self.logger.debug(f"Task configs: {[tc.task_name for tc in task_configs]}")
|
| 181 |
+
self.logger.debug(f"Train datasets: {training_task_names}")
|
| 182 |
+
self.logger.debug(f"Val datasets: {list(val_datasets.keys())}")
|
| 183 |
+
# Add tasks to model
|
| 184 |
+
self.freeze_all()
|
| 185 |
+
for task_config in task_configs:
|
| 186 |
+
self.model.add_task(task_config)
|
| 187 |
+
if task_config.task_name not in self.num_labels_per_task:
|
| 188 |
+
self.num_labels_per_task[task_config.task_name]=task_config.num_labels
|
| 189 |
+
|
| 190 |
+
self.unfreeze_for_tasks(training_task_names,encoder_unfreeze)
|
| 191 |
+
if len(train_datasets.keys())<1:
|
| 192 |
+
self.logger.error("No training datasets found for tasks!")
|
| 193 |
+
return self.model
|
| 194 |
+
|
| 195 |
+
self.logger.info("Model parameters after adding tasks:")
|
| 196 |
+
self.count_parameters()
|
| 197 |
+
|
| 198 |
+
# Create multi-task datasets
|
| 199 |
+
train_dataset = MultiTaskDataset(train_datasets, self.model.tokenizer, task_configs)
|
| 200 |
+
val_dataset = MultiTaskDataset(val_datasets, self.model.tokenizer, task_configs)
|
| 201 |
+
|
| 202 |
+
# Create samplers and dataloaders
|
| 203 |
+
train_sampler = TaskAwareBatchSampler(
|
| 204 |
+
train_dataset, self.config.per_device_train_batch_size, shuffle=True
|
| 205 |
+
)
|
| 206 |
+
use_fixed_padding = self.config.use_replay and len(self.model.replay_buffer.buffer) > 0
|
| 207 |
+
|
| 208 |
+
def train_collate(batch):
|
| 209 |
+
if use_fixed_padding:
|
| 210 |
+
# Use fixed max length from config
|
| 211 |
+
return collate_fn(batch, fixed_max_len=self.config.max_seq_len)
|
| 212 |
+
else:
|
| 213 |
+
# Use dynamic padding
|
| 214 |
+
return collate_fn(batch)
|
| 215 |
+
|
| 216 |
+
train_loader = DataLoader(
|
| 217 |
+
train_dataset, batch_sampler=train_sampler, collate_fn=train_collate, num_workers=0
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
if len(val_dataset) > 0:
|
| 221 |
+
val_sampler = TaskAwareBatchSampler(
|
| 222 |
+
val_dataset, self.config.per_device_eval_batch_size, shuffle=False
|
| 223 |
+
)
|
| 224 |
+
val_loader = DataLoader(
|
| 225 |
+
val_dataset, batch_sampler=val_sampler, collate_fn=collate_fn, num_workers=0
|
| 226 |
+
)
|
| 227 |
+
else:
|
| 228 |
+
val_loader = None
|
| 229 |
+
self.logger.warning(" Warning: No validation data provided")
|
| 230 |
+
|
| 231 |
+
# Setup optimizer with task-specific learning rates
|
| 232 |
+
param_groups = []
|
| 233 |
+
assigned = set()
|
| 234 |
+
|
| 235 |
+
# Task heads parameters
|
| 236 |
+
head_params = []
|
| 237 |
+
for th in self.model.task_heads.values():
|
| 238 |
+
for p in th.parameters():
|
| 239 |
+
if id(p) not in assigned:
|
| 240 |
+
head_params.append(p)
|
| 241 |
+
assigned.add(id(p))
|
| 242 |
+
if head_params:
|
| 243 |
+
param_groups.append(
|
| 244 |
+
{
|
| 245 |
+
"params": head_params,
|
| 246 |
+
"lr": self.config.learning_rate * self.config.head_lr_mult,
|
| 247 |
+
"name": "task_heads",
|
| 248 |
+
}
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Decoder parameters
|
| 252 |
+
decoder_params = []
|
| 253 |
+
for d in self.model.decoders.values():
|
| 254 |
+
for p in d.parameters():
|
| 255 |
+
if id(p) not in assigned:
|
| 256 |
+
decoder_params.append(p)
|
| 257 |
+
assigned.add(id(p))
|
| 258 |
+
if decoder_params:
|
| 259 |
+
param_groups.append(
|
| 260 |
+
{
|
| 261 |
+
"params": decoder_params,
|
| 262 |
+
"lr": self.config.learning_rate * self.config.decoder_lr_mult,
|
| 263 |
+
"name": "decoders",
|
| 264 |
+
}
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
# Shared parameters
|
| 268 |
+
shared_params = []
|
| 269 |
+
for p in self.model.parameters():
|
| 270 |
+
if id(p) not in assigned:
|
| 271 |
+
shared_params.append(p)
|
| 272 |
+
assigned.add(id(p))
|
| 273 |
+
if shared_params:
|
| 274 |
+
param_groups.append(
|
| 275 |
+
{
|
| 276 |
+
"params": shared_params,
|
| 277 |
+
"lr": self.config.learning_rate * self.config.shared_lr_mult,
|
| 278 |
+
"name": "shared",
|
| 279 |
+
}
|
| 280 |
+
)
|
| 281 |
+
|
| 282 |
+
# Optimizer and scheduler
|
| 283 |
+
optimizer = optim.AdamW(param_groups, weight_decay=self.config.weight_decay)
|
| 284 |
+
total_steps = len(train_loader) * self.config.num_train_epochs
|
| 285 |
+
|
| 286 |
+
def lr_lambda(step):
|
| 287 |
+
if step < self.config.warmup_steps:
|
| 288 |
+
return step / max(1, self.config.warmup_steps)
|
| 289 |
+
progress = (step - self.config.warmup_steps) / max(
|
| 290 |
+
1, total_steps - self.config.warmup_steps
|
| 291 |
+
)
|
| 292 |
+
return max(
|
| 293 |
+
self.config.min_learning_rate / self.config.learning_rate,
|
| 294 |
+
0.5 * (1 + math.cos(math.pi * progress)),
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
scheduler = optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
|
| 298 |
+
best_model_path = os.path.join(self.exp_dir, "best_model.pt")
|
| 299 |
+
steps=0
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
# Training loop
|
| 305 |
+
for epoch in range(self.config.num_train_epochs):
|
| 306 |
+
self.model.train()
|
| 307 |
+
total_loss = 0
|
| 308 |
+
num_batches = 0
|
| 309 |
+
task_losses = defaultdict(list)
|
| 310 |
+
epoch_metrics = defaultdict(lambda: defaultdict(list))
|
| 311 |
+
|
| 312 |
+
pbar = tqdm(train_loader, desc=f"Epoch {epoch+1}/{self.config.num_train_epochs}")
|
| 313 |
+
|
| 314 |
+
for batch_idx, batch in enumerate(pbar):
|
| 315 |
+
try:
|
| 316 |
+
task_name = batch["task_name"][0]
|
| 317 |
+
strategy=batch["strategy"][0]
|
| 318 |
+
|
| 319 |
+
# Move batch to device
|
| 320 |
+
|
| 321 |
+
batch = {
|
| 322 |
+
k: v.to(self.device) if isinstance(v, torch.Tensor) else v
|
| 323 |
+
for k, v in batch.items()
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
# Add replay samples if available
|
| 327 |
+
if self.config.use_replay and len(self.model.replay_buffer.buffer) > 0:
|
| 328 |
+
replay_samples = self.model.replay_buffer.sample(
|
| 329 |
+
self.config.per_device_train_batch_size // 4
|
| 330 |
+
)
|
| 331 |
+
if replay_samples:
|
| 332 |
+
|
| 333 |
+
for replay in replay_samples:
|
| 334 |
+
if task_name in self.num_labels_per_task and replay["task_name"] in self.num_labels_per_task and self.num_labels_per_task[replay["task_name"]]!=self.num_labels_per_task[task_name]:
|
| 335 |
+
break
|
| 336 |
+
elif task_name not in self.num_labels_per_task or replay["task_name"] not in self.num_labels_per_task:
|
| 337 |
+
break
|
| 338 |
+
elif replay["strategy"]!=strategy:
|
| 339 |
+
break
|
| 340 |
+
#print(replay["strategy"])
|
| 341 |
+
for k, v in replay.items():
|
| 342 |
+
if isinstance(v, torch.Tensor):
|
| 343 |
+
if batch[k].dim() == v.dim():
|
| 344 |
+
# Same dimensions, can concatenate
|
| 345 |
+
batch[k] = torch.cat(
|
| 346 |
+
[batch[k], v.to(self.device)], dim=0
|
| 347 |
+
)
|
| 348 |
+
else:
|
| 349 |
+
# Different dimensions, need to handle specially
|
| 350 |
+
# print(f" Warning: Cannot concatenate {k} with dim {batch[k].dim()} and {v.dim()}")
|
| 351 |
+
# Skip this replay sample for this key
|
| 352 |
+
pass
|
| 353 |
+
# batch[k] = torch.cat([batch[k], v.to(self.device)], dim=0)
|
| 354 |
+
|
| 355 |
+
# Forward pass
|
| 356 |
+
outputs = self.model(
|
| 357 |
+
input_ids=batch["input_ids"],
|
| 358 |
+
attention_mask=batch.get("attention_mask"),
|
| 359 |
+
task_name=task_name,
|
| 360 |
+
labels=batch.get("labels"),
|
| 361 |
+
)
|
| 362 |
+
|
| 363 |
+
if outputs is not None and "loss" in outputs and outputs["loss"] is not None:
|
| 364 |
+
loss = outputs["loss"]
|
| 365 |
+
|
| 366 |
+
# Add EWC penalty if enabled
|
| 367 |
+
if self.config.use_ewc and len(self.model.ewc_penalties) > 0:
|
| 368 |
+
ewc_loss = 0
|
| 369 |
+
for ts in self.model.ewc_penalties.keys():
|
| 370 |
+
if ts in training_task_names:
|
| 371 |
+
continue
|
| 372 |
+
ewc_loss += self.model.ewc_penalties[ts].penalty(self.model)
|
| 373 |
+
|
| 374 |
+
#for ewc in self.model.ewc_penalties.values():
|
| 375 |
+
# ewc_loss += ewc.penalty(self.model)
|
| 376 |
+
loss = loss + self.config.ewc_lambda * ewc_loss
|
| 377 |
+
|
| 378 |
+
if torch.isfinite(loss):
|
| 379 |
+
loss.backward()
|
| 380 |
+
torch.nn.utils.clip_grad_norm_(
|
| 381 |
+
self.model.parameters(), self.config.max_grad_norm
|
| 382 |
+
)
|
| 383 |
+
optimizer.step()
|
| 384 |
+
scheduler.step()
|
| 385 |
+
optimizer.zero_grad()
|
| 386 |
+
|
| 387 |
+
total_loss += loss.item()
|
| 388 |
+
num_batches += 1
|
| 389 |
+
steps+=1
|
| 390 |
+
self.loss_history.append(loss.item())
|
| 391 |
+
task_losses[task_name].append(loss.item())
|
| 392 |
+
|
| 393 |
+
# Collect metrics
|
| 394 |
+
if "metrics" in outputs and outputs["metrics"]:
|
| 395 |
+
for metric_name, value in outputs["metrics"].items():
|
| 396 |
+
if isinstance(value, (int, float)):
|
| 397 |
+
epoch_metrics[task_name][metric_name].append(value)
|
| 398 |
+
elif isinstance(value, torch.Tensor):
|
| 399 |
+
epoch_metrics[task_name][metric_name].append(value.item())
|
| 400 |
+
else:
|
| 401 |
+
self.logger.warning(f" Warning: Non-finite loss at batch {batch_idx}: {loss}")
|
| 402 |
+
|
| 403 |
+
# Logging
|
| 404 |
+
if steps % self.config.logging_steps == 0 and num_batches > 0:
|
| 405 |
+
task_avg = {
|
| 406 |
+
task: sum(losses[-100:]) / len(losses[-100:])
|
| 407 |
+
for task, losses in task_losses.items()
|
| 408 |
+
if losses
|
| 409 |
+
}
|
| 410 |
+
pbar.set_postfix(
|
| 411 |
+
{"loss": f"{total_loss/num_batches:.4f}", "tasks": str(task_avg)}
|
| 412 |
+
)
|
| 413 |
+
|
| 414 |
+
# Validation during training (if steps strategy)
|
| 415 |
+
if (
|
| 416 |
+
self.config.evaluation_strategy == "steps"
|
| 417 |
+
and steps % self.config.validation_check_interval == 0
|
| 418 |
+
and num_batches > 0
|
| 419 |
+
and val_loader is not None
|
| 420 |
+
):
|
| 421 |
+
val_results = self.validate(val_loader)
|
| 422 |
+
if val_results:
|
| 423 |
+
self._handle_validation(val_results, best_model_path)
|
| 424 |
+
|
| 425 |
+
except RuntimeError as e:
|
| 426 |
+
if "out of memory" in str(e):
|
| 427 |
+
if torch.cuda.is_available():
|
| 428 |
+
torch.cuda.empty_cache()
|
| 429 |
+
optimizer.zero_grad()
|
| 430 |
+
self.logger.warning(f" Error in training batch: {e}")
|
| 431 |
+
continue
|
| 432 |
+
else:
|
| 433 |
+
raise e
|
| 434 |
+
except Exception as e:
|
| 435 |
+
self.logger.warning(f" Error in training batch: {e}")
|
| 436 |
+
continue
|
| 437 |
+
|
| 438 |
+
# Epoch end
|
| 439 |
+
if num_batches > 0:
|
| 440 |
+
avg_epoch_loss = total_loss / num_batches
|
| 441 |
+
print(f"\n Epoch {epoch+1} average training loss: {avg_epoch_loss:.4f}")
|
| 442 |
+
|
| 443 |
+
for task, metrics in epoch_metrics.items():
|
| 444 |
+
print(f" {task} training metrics:")
|
| 445 |
+
for metric_name, values in metrics.items():
|
| 446 |
+
if values:
|
| 447 |
+
avg_value = sum(values) / len(values)
|
| 448 |
+
print(f" {metric_name}: {avg_value:.4f}")
|
| 449 |
+
|
| 450 |
+
# Validation at epoch end
|
| 451 |
+
if val_loader is not None and self.config.evaluation_strategy != "steps":
|
| 452 |
+
val_results = self.validate(val_loader)
|
| 453 |
+
if val_results:
|
| 454 |
+
self._handle_validation(val_results, best_model_path)
|
| 455 |
+
|
| 456 |
+
if self.patience_counter >= self.config.patience:
|
| 457 |
+
print(f" Early stopping triggered after {epoch+1} Validation")
|
| 458 |
+
response=input("Insert letter Y to stop the training: [Y/n]")
|
| 459 |
+
if response=='Y':
|
| 460 |
+
break
|
| 461 |
+
self.patience_counter=0
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
self.logger.debug(f"\n Training completed. Best validation loss: {self.best_val_loss:.4f}")
|
| 466 |
+
if self.config.use_ewc:# and use_fixed_padding:
|
| 467 |
+
for task_name in train_datasets.keys():
|
| 468 |
+
if task_name in self.model.ewc_penalties: continue
|
| 469 |
+
ewc = EWC(self.model, task_name, lambda_param=self.config.ewc_lambda)
|
| 470 |
+
fisher_loader = DataLoader(train_datasets[task_name][0], batch_size= 8, shuffle=True, collate_fn=collate_fn)
|
| 471 |
+
ewc.compute_fisher(fisher_loader, self.config.device)
|
| 472 |
+
self.model.ewc_penalties[task_name] = ewc
|
| 473 |
+
return self.model
|
| 474 |
+
|
| 475 |
+
def _handle_validation(self, val_results, best_model_path):
|
| 476 |
+
"""Handle validation results and model checkpointing."""
|
| 477 |
+
val_loss = val_results["avg_loss"]
|
| 478 |
+
val_metrics = val_results["metrics"]
|
| 479 |
+
|
| 480 |
+
self.val_loss_history.append(val_loss)
|
| 481 |
+
self.metrics_history.append(val_metrics)
|
| 482 |
+
print(f"\n Validation loss: {val_loss:.4f}")
|
| 483 |
+
|
| 484 |
+
for task, metrics in val_metrics.items():
|
| 485 |
+
print(f" {task} validation:")
|
| 486 |
+
for metric_name, value in metrics.items():
|
| 487 |
+
if isinstance(value, dict):
|
| 488 |
+
print(f" {metric_name}:")
|
| 489 |
+
for k, v in value.items():
|
| 490 |
+
print(f" {k}: {v:.4f}")
|
| 491 |
+
else:
|
| 492 |
+
print(f" {metric_name}: {value:.4f}")
|
| 493 |
+
|
| 494 |
+
if val_loss < self.best_val_loss:
|
| 495 |
+
improvement = self.best_val_loss - val_loss
|
| 496 |
+
self.model.steps_without_improvement = 0
|
| 497 |
+
self.best_val_loss = val_loss
|
| 498 |
+
self.model.best_val_loss = val_loss
|
| 499 |
+
self.best_metrics = val_metrics
|
| 500 |
+
self.patience_counter = 0
|
| 501 |
+
self.model.save_model(best_model_path)
|
| 502 |
+
self.logger.warning(" ✓ Best model saved improvement: %s",improvement)
|
| 503 |
+
else:
|
| 504 |
+
self.model.steps_without_improvement += 1
|
| 505 |
+
self.patience_counter += 1
|
| 506 |
+
self.logger.warning(" No improvement for %s epochs",self.patience_counter)
|
| 507 |
+
|
| 508 |
+
def validate(self, val_loader):
|
| 509 |
+
"""
|
| 510 |
+
Validate the model on validation data.
|
| 511 |
+
|
| 512 |
+
Args:
|
| 513 |
+
val_loader: DataLoader for validation data
|
| 514 |
+
|
| 515 |
+
Returns:
|
| 516 |
+
Dictionary with average loss and metrics per task
|
| 517 |
+
"""
|
| 518 |
+
self.model.eval()
|
| 519 |
+
total_loss = 0
|
| 520 |
+
val_batches = 0
|
| 521 |
+
task_metrics = defaultdict(lambda: defaultdict(list))
|
| 522 |
+
|
| 523 |
+
with torch.no_grad():
|
| 524 |
+
for batch in val_loader:
|
| 525 |
+
task_name = batch["task_name"][0]
|
| 526 |
+
|
| 527 |
+
batch = {
|
| 528 |
+
k: v.to(self.device) if isinstance(v, torch.Tensor) else v
|
| 529 |
+
for k, v in batch.items()
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
outputs = self.model(
|
| 533 |
+
input_ids=batch["input_ids"],
|
| 534 |
+
attention_mask=batch.get("attention_mask"),
|
| 535 |
+
task_name=task_name,
|
| 536 |
+
labels=batch.get("labels"),
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
if outputs is not None and "loss" in outputs and outputs["loss"] is not None:
|
| 540 |
+
if torch.isfinite(outputs["loss"]):
|
| 541 |
+
total_loss += outputs["loss"].item()
|
| 542 |
+
val_batches += 1
|
| 543 |
+
|
| 544 |
+
if "metrics" in outputs and outputs["metrics"]:
|
| 545 |
+
for metric_name, value in outputs["metrics"].items():
|
| 546 |
+
if isinstance(value, torch.Tensor):
|
| 547 |
+
task_metrics[task_name][metric_name].append(value.cpu().item())
|
| 548 |
+
elif isinstance(value, dict):
|
| 549 |
+
for sub_metric, sub_value in value.items():
|
| 550 |
+
combined_key = f"{metric_name}_{sub_metric}"
|
| 551 |
+
if isinstance(sub_value, (int, float, torch.Tensor)):
|
| 552 |
+
if isinstance(sub_value, torch.Tensor):
|
| 553 |
+
task_metrics[task_name][combined_key].append(
|
| 554 |
+
sub_value.item()
|
| 555 |
+
)
|
| 556 |
+
else:
|
| 557 |
+
task_metrics[task_name][combined_key].append(
|
| 558 |
+
sub_value
|
| 559 |
+
)
|
| 560 |
+
else:
|
| 561 |
+
task_metrics[task_name][metric_name].append(value)
|
| 562 |
+
|
| 563 |
+
if val_batches > 0:
|
| 564 |
+
avg_loss = total_loss / val_batches
|
| 565 |
+
aggregated_metrics = {}
|
| 566 |
+
for task, metrics in task_metrics.items():
|
| 567 |
+
aggregated_metrics[task] = {}
|
| 568 |
+
for metric_name, values in metrics.items():
|
| 569 |
+
if values:
|
| 570 |
+
aggregated_metrics[task][metric_name] = sum(values) / len(values)
|
| 571 |
+
return {"avg_loss": avg_loss, "metrics": aggregated_metrics}
|
| 572 |
+
return None
|
src/dytr/training/utils.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Training utilities for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides helper functions for training, including seed setting
|
| 19 |
+
and dataset processing for QA tasks.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
import random
|
| 28 |
+
from typing import Dict, List, Optional
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
#import pandas as pd
|
| 32 |
+
import torch
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def set_seed(seed: int):
|
| 36 |
+
"""
|
| 37 |
+
Set random seeds for reproducibility.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
seed: Random seed value
|
| 41 |
+
"""
|
| 42 |
+
random.seed(seed)
|
| 43 |
+
np.random.seed(seed)
|
| 44 |
+
torch.manual_seed(seed)
|
| 45 |
+
if torch.cuda.is_available():
|
| 46 |
+
torch.cuda.manual_seed_all(seed)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def process_qa_dataset(
|
| 50 |
+
ds,
|
| 51 |
+
config,
|
| 52 |
+
num_samples: int = 200000,
|
| 53 |
+
text_column: Optional[str] = None,
|
| 54 |
+
conversations_col: Optional[str] = None,
|
| 55 |
+
question_col: Optional[str] = None,
|
| 56 |
+
answer_col: Optional[str] = None,
|
| 57 |
+
min_text_length: int = 50,
|
| 58 |
+
) -> List[Dict[str, str]]:
|
| 59 |
+
"""
|
| 60 |
+
Process question-answering datasets into text format for language modeling.
|
| 61 |
+
|
| 62 |
+
This function processes QA datasets from various formats (conversations,
|
| 63 |
+
question-answer pairs) into a unified text format suitable for training.
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
ds: Dataset (list of dictionaries or DataFrame)
|
| 67 |
+
config: Model configuration (for special tokens)
|
| 68 |
+
num_samples: Maximum number of samples to process
|
| 69 |
+
text_column: Column containing text (if applicable)
|
| 70 |
+
conversations_col: Column containing conversation history
|
| 71 |
+
question_col: Column containing questions
|
| 72 |
+
answer_col: Column containing answers
|
| 73 |
+
min_text_length: Minimum text length to include
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
List of processed text samples
|
| 77 |
+
"""
|
| 78 |
+
processed_texts = []
|
| 79 |
+
all_processed_texts = []
|
| 80 |
+
answer_start = config.special_tokens.get("answer_start", "<|answer|>")
|
| 81 |
+
|
| 82 |
+
c = 0
|
| 83 |
+
for i, item in enumerate(ds):
|
| 84 |
+
if c >= 20000:
|
| 85 |
+
c = 0
|
| 86 |
+
all_processed_texts.extend([{"text": t.lower()} for t in processed_texts])
|
| 87 |
+
processed_texts = []
|
| 88 |
+
|
| 89 |
+
try:
|
| 90 |
+
# Handle conversation format
|
| 91 |
+
if conversations_col in item and isinstance(item[conversations_col], list):
|
| 92 |
+
conversations = item[conversations_col]
|
| 93 |
+
if len(conversations) >= 2:
|
| 94 |
+
human_text = ""
|
| 95 |
+
gpt_text = ""
|
| 96 |
+
for conv in conversations:
|
| 97 |
+
if isinstance(conv, dict):
|
| 98 |
+
from_val = conv.get("from", "").lower()
|
| 99 |
+
value_val = conv.get("value", "")
|
| 100 |
+
if "human" in from_val or "user" in from_val or "question" in from_val:
|
| 101 |
+
human_text = value_val
|
| 102 |
+
elif (
|
| 103 |
+
"gpt" in from_val or "assistant" in from_val or "answer" in from_val
|
| 104 |
+
):
|
| 105 |
+
gpt_text = value_val
|
| 106 |
+
|
| 107 |
+
if human_text and gpt_text:
|
| 108 |
+
merged = f"Q: {human_text} \n {answer_start} \n {gpt_text} \n \n"
|
| 109 |
+
if len(merged) >= min_text_length:
|
| 110 |
+
processed_texts.append(merged)
|
| 111 |
+
c += len(merged)
|
| 112 |
+
|
| 113 |
+
# Handle question-answer format
|
| 114 |
+
elif (
|
| 115 |
+
question_col
|
| 116 |
+
and answer_col
|
| 117 |
+
and question_col in item
|
| 118 |
+
and item[question_col]
|
| 119 |
+
and answer_col in item
|
| 120 |
+
and item[answer_col]
|
| 121 |
+
):
|
| 122 |
+
question = item[question_col]
|
| 123 |
+
if text_column and text_column in item and item[text_column]:
|
| 124 |
+
question = question + f"\n {item[text_column]}"
|
| 125 |
+
|
| 126 |
+
answer = item[answer_col]
|
| 127 |
+
if question and answer and isinstance(question, str) and isinstance(answer, str):
|
| 128 |
+
merged = f"Q: {question} \n {answer_start} \n {answer} \n \n"
|
| 129 |
+
if len(merged) >= min_text_length:
|
| 130 |
+
processed_texts.append(merged)
|
| 131 |
+
c += len(merged)
|
| 132 |
+
|
| 133 |
+
# Try to auto-detect question and answer columns
|
| 134 |
+
else:
|
| 135 |
+
question = None
|
| 136 |
+
answer = None
|
| 137 |
+
question_candidates = [
|
| 138 |
+
"question",
|
| 139 |
+
"Question",
|
| 140 |
+
"query",
|
| 141 |
+
"Query",
|
| 142 |
+
"input",
|
| 143 |
+
"Input",
|
| 144 |
+
"prompt",
|
| 145 |
+
"Prompt",
|
| 146 |
+
"instruction",
|
| 147 |
+
"Instruction",
|
| 148 |
+
]
|
| 149 |
+
answer_candidates = [
|
| 150 |
+
"answer",
|
| 151 |
+
"Answer",
|
| 152 |
+
"response",
|
| 153 |
+
"Response",
|
| 154 |
+
"output",
|
| 155 |
+
"Output",
|
| 156 |
+
"completion",
|
| 157 |
+
"Completion",
|
| 158 |
+
"target",
|
| 159 |
+
"Target",
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
for q_col in question_candidates:
|
| 163 |
+
if q_col in item and isinstance(item[q_col], str) and item[q_col].strip():
|
| 164 |
+
question = item[q_col]
|
| 165 |
+
break
|
| 166 |
+
|
| 167 |
+
for a_col in answer_candidates:
|
| 168 |
+
if a_col in item and isinstance(item[a_col], str) and item[a_col].strip():
|
| 169 |
+
answer = item[a_col]
|
| 170 |
+
break
|
| 171 |
+
|
| 172 |
+
if not question and not answer and text_column in item and item[text_column]:
|
| 173 |
+
text = item[text_column]
|
| 174 |
+
if isinstance(text, str) and len(text.strip()) >= min_text_length:
|
| 175 |
+
processed_texts.append(text + "\n\n")
|
| 176 |
+
c += len(text)
|
| 177 |
+
|
| 178 |
+
elif question and answer:
|
| 179 |
+
merged = f"Q: {question} \n {answer_start} \n {answer} \n \n"
|
| 180 |
+
if len(merged) >= min_text_length:
|
| 181 |
+
processed_texts.append(merged)
|
| 182 |
+
c += len(merged)
|
| 183 |
+
|
| 184 |
+
if i == 0 and processed_texts:
|
| 185 |
+
print(f" Sample processed text: {processed_texts[0]}...")
|
| 186 |
+
|
| 187 |
+
except Exception as e:
|
| 188 |
+
print(f" Error in QA processing: {e}")
|
| 189 |
+
continue
|
| 190 |
+
|
| 191 |
+
if processed_texts:
|
| 192 |
+
all_processed_texts.extend([{"text": t.lower()} for t in processed_texts])
|
| 193 |
+
#all_processed_texts.append({"text": "\n".join(processed_texts)})
|
| 194 |
+
print(f"Total Questions Answers: {len(all_processed_texts)}")
|
| 195 |
+
|
| 196 |
+
return all_processed_texts
|
src/dytr/utils/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Utility modules for Dynamic Transformers.
|
| 17 |
+
|
| 18 |
+
This module provides utility functions and classes for caching, file management,
|
| 19 |
+
and other helper functionalities.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
__version__ = "0.1.0"
|
| 25 |
+
__author__ = "Akram Alsubari"
|
| 26 |
+
__email__ = "akram.alsubari@outlook.com"
|
| 27 |
+
__license__ = "Apache 2.0"
|
| 28 |
+
|
| 29 |
+
from dytr.utils.caching import CacheManager, get_cache_manager
|
| 30 |
+
from dytr.utils.logging import get_logger, set_log_level,disable_logging
|
| 31 |
+
__all__ = ["CacheManager", "get_cache_manager","get_logger","set_log_level","disable_logging"]
|
src/dytr/utils/caching.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
"""
|
| 16 |
+
Caching utilities for dataset and model caching.
|
| 17 |
+
|
| 18 |
+
This module provides helper functions for managing cache directories
|
| 19 |
+
and file operations.
|
| 20 |
+
|
| 21 |
+
Author: Akram Alsubari
|
| 22 |
+
Email: akram.alsubari@outlook.com / akram.alsubari87@gmail.com
|
| 23 |
+
"""
|
| 24 |
+
__version__ = "0.1.0"
|
| 25 |
+
__author__ = "Akram Alsubari"
|
| 26 |
+
__email__ = "akram.alsubari@outlook.com"
|
| 27 |
+
__license__ = "Apache 2.0"
|
| 28 |
+
#import os
|
| 29 |
+
from pathlib import Path
|
| 30 |
+
from typing import Optional
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class CacheManager:
|
| 34 |
+
"""
|
| 35 |
+
Manages cache directories for datasets and models.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
cache_dir: Root cache directory path
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
def __init__(self, cache_dir: str = "./dataset_cache"):
|
| 42 |
+
self.cache_dir = Path(cache_dir)
|
| 43 |
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
| 44 |
+
|
| 45 |
+
def get_dataset_cache_path(
|
| 46 |
+
self, dataset_name: str, task_name: str, max_len: int, stride: int
|
| 47 |
+
) -> Path:
|
| 48 |
+
"""Get cache path for a processed dataset."""
|
| 49 |
+
cache_subdir = self.cache_dir / "datasets" / task_name
|
| 50 |
+
cache_subdir.mkdir(parents=True, exist_ok=True)
|
| 51 |
+
filename = f"{dataset_name}_{max_len}_{stride}.pkl"
|
| 52 |
+
return cache_subdir / filename
|
| 53 |
+
|
| 54 |
+
def get_model_cache_path(self, model_name: str, task_name: str) -> Path:
|
| 55 |
+
"""Get cache path for a model."""
|
| 56 |
+
cache_subdir = self.cache_dir / "models" / task_name
|
| 57 |
+
cache_subdir.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
return cache_subdir / f"{model_name}.pt"
|
| 59 |
+
|
| 60 |
+
def get_tokenizer_cache_path(self, tokenizer_name: str) -> Path:
|
| 61 |
+
"""Get cache path for a tokenizer."""
|
| 62 |
+
cache_subdir = self.cache_dir / "tokenizers"
|
| 63 |
+
cache_subdir.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
return cache_subdir / tokenizer_name
|
| 65 |
+
|
| 66 |
+
def clear_cache(self, cache_type: Optional[str] = None):
|
| 67 |
+
"""Clear cache files."""
|
| 68 |
+
if cache_type == "datasets":
|
| 69 |
+
cache_path = self.cache_dir / "datasets"
|
| 70 |
+
elif cache_type == "models":
|
| 71 |
+
cache_path = self.cache_dir / "models"
|
| 72 |
+
elif cache_type == "tokenizers":
|
| 73 |
+
cache_path = self.cache_dir / "tokenizers"
|
| 74 |
+
else:
|
| 75 |
+
cache_path = self.cache_dir
|
| 76 |
+
|
| 77 |
+
if cache_path.exists():
|
| 78 |
+
import shutil
|
| 79 |
+
|
| 80 |
+
shutil.rmtree(cache_path)
|
| 81 |
+
print(f"Cleared cache at {cache_path}")
|
| 82 |
+
|
| 83 |
+
def get_cache_size(self) -> int:
|
| 84 |
+
"""Get total cache size in bytes."""
|
| 85 |
+
total_size = 0
|
| 86 |
+
for file_path in self.cache_dir.rglob("*"):
|
| 87 |
+
if file_path.is_file():
|
| 88 |
+
total_size += file_path.stat().st_size
|
| 89 |
+
return total_size
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# Global cache manager instance
|
| 93 |
+
_default_cache_manager = None
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def get_cache_manager(cache_dir: str = "./dataset_cache") -> CacheManager:
|
| 97 |
+
"""Get or create a cache manager instance."""
|
| 98 |
+
global _default_cache_manager
|
| 99 |
+
if _default_cache_manager is None or _default_cache_manager.cache_dir != Path(cache_dir):
|
| 100 |
+
_default_cache_manager = CacheManager(cache_dir)
|
| 101 |
+
return _default_cache_manager
|
src/dytr/utils/logging.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Akram Alsubari
|
| 2 |
+
#
|
| 3 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 4 |
+
# you may not use this file except in compliance with the License.
|
| 5 |
+
# You may obtain a copy of the License at
|
| 6 |
+
#
|
| 7 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 8 |
+
#
|
| 9 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 10 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 11 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 12 |
+
# See the License for the specific language governing permissions and
|
| 13 |
+
# limitations under the License.
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
import logging
|
| 18 |
+
import sys
|
| 19 |
+
from typing import Optional
|
| 20 |
+
|
| 21 |
+
_DEFAULT_LOG_LEVEL = logging.INFO
|
| 22 |
+
_ROOT_LOGGER_NAME = "dytr"
|
| 23 |
+
|
| 24 |
+
def get_logger(name: str, level: Optional[int] = None) -> logging.Logger:
|
| 25 |
+
"""
|
| 26 |
+
Get a logger for a module with consistent formatting.
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
name: Logger name (typically __name__)
|
| 30 |
+
level: Logging level (default: INFO)
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
Configured logger instance
|
| 34 |
+
"""
|
| 35 |
+
logger = logging.getLogger(name)
|
| 36 |
+
|
| 37 |
+
if not logger.handlers:
|
| 38 |
+
handler = logging.StreamHandler(sys.stderr)
|
| 39 |
+
formatter = logging.Formatter(
|
| 40 |
+
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 41 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 42 |
+
)
|
| 43 |
+
handler.setFormatter(formatter)
|
| 44 |
+
logger.addHandler(handler)
|
| 45 |
+
|
| 46 |
+
if level is None:
|
| 47 |
+
level = _DEFAULT_LOG_LEVEL
|
| 48 |
+
logger.setLevel(level)
|
| 49 |
+
logger.propagate = False
|
| 50 |
+
|
| 51 |
+
return logger
|
| 52 |
+
|
| 53 |
+
def set_log_level(level: int):
|
| 54 |
+
"""
|
| 55 |
+
Set log level for all dytr loggers.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
level: Logging level (logging.DEBUG, logging.INFO, etc.)
|
| 59 |
+
"""
|
| 60 |
+
logging.getLogger(_ROOT_LOGGER_NAME).setLevel(level)
|
| 61 |
+
|
| 62 |
+
def disable_logging():
|
| 63 |
+
"""Disable all dytr logging."""
|
| 64 |
+
logging.getLogger(_ROOT_LOGGER_NAME).setLevel(logging.WARNING)
|