alsubari commited on
Commit
9981d07
·
verified ·
1 Parent(s): 1481c39

Sync from GitHub via hub-sync

Browse files
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
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
38
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
39
  [![PyPI version](https://badge.fury.io/py/dytr.svg)](https://badge.fury.io/py/dytr)
40
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://github.com/AAlsubari/dytr/blob/main/dytr_bert_finetune_demo.ipynb)
41
 
42
- **Build dynamic transformers that learn multiple tasks with supports to load and modify pretrained model such as bert.**
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
- ![unnamed](https://cdn-uploads.huggingface.co/production/uploads/645a5b8e333fb1835783d500/TdQZhE7EPeGU9zgYJMKbl.png)
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='bert-base-uncased',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)
168
 
169
  # Load pretrained BERT as your encoder
170
- model = loader.load_pretrained('bert-base-uncased', config)
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
- - akram.alsubari@outlook.com
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
  [![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
8
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
9
  [![PyPI version](https://badge.fury.io/py/dytr.svg)](https://badge.fury.io/py/dytr)
10
  [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](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
+ ![unnamed](https://cdn-uploads.huggingface.co/production/uploads/645a5b8e333fb1835783d500/TdQZhE7EPeGU9zgYJMKbl.png)
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)