zico: checkpoint, code, model card
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- LICENSE-CODE +202 -0
- LICENSE.md +7 -0
- NOTICE +7 -0
- README.md +325 -0
- backbone/config.json +83 -0
- backbone/model.safetensors +3 -0
- eval.json +1504 -0
- examples/request.json +30 -0
- examples/systemone_request.json +31 -0
- head.safetensors +3 -0
- pyproject.toml +33 -0
- scripts/add_vision.py +55 -0
- scripts/bench_ours.py +109 -0
- scripts/build_data.py +135 -0
- scripts/build_image_data.py +171 -0
- scripts/check_kernels.py +79 -0
- scripts/check_mlx_parity.py +88 -0
- scripts/eval.py +126 -0
- scripts/eval_images.py +82 -0
- scripts/infer.py +37 -0
- scripts/make_soft_targets.py +50 -0
- scripts/run_4b.sh +34 -0
- scripts/run_img.sh +34 -0
- scripts/serve.sh +63 -0
- scripts/setup_box.sh +56 -0
- scripts/train.py +279 -0
- tests/__init__.py +0 -0
- tests/test_core.py +217 -0
- tests/test_mlx.py +80 -0
- tests/test_server.py +122 -0
- tests/test_vision.py +248 -0
- tokenizer/chat_template.jinja +154 -0
- tokenizer/tokenizer.json +3 -0
- tokenizer/tokenizer_config.json +32 -0
- vision/config.json +36 -0
- vision/model.safetensors +3 -0
- vision/preprocessor_config.json +26 -0
- vision/zico_vision.json +8 -0
- zico.json +12 -0
- zico/__init__.py +13 -0
- zico/answers.py +24 -0
- zico/backbone.py +53 -0
- zico/batching.py +182 -0
- zico/benchmark.py +51 -0
- zico/calibration.py +86 -0
- zico/data.py +298 -0
- zico/evaluate.py +49 -0
- zico/image_sources.py +99 -0
- zico/losses.py +67 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
LICENSE-CODE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
LICENSE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Licences
|
| 2 |
+
|
| 3 |
+
* **Model weights** (`backbone/`, `head.safetensors`): Creative Commons Attribution-NonCommercial 4.0 International
|
| 4 |
+
(CC-BY-NC-4.0), https://creativecommons.org/licenses/by-nc/4.0/legalcode. The base model is Apache-2.0, but part of the
|
| 5 |
+
training data is licensed for non-commercial or research use only; see the model card.
|
| 6 |
+
* **Code** (`zico/`, `scripts/`, `tests/`, `examples/`): Apache License 2.0, see `LICENSE-CODE`.
|
| 7 |
+
* **`vision/` and `tokenizer/`**: unmodified from `Qwen/Qwen3.5-4B-Base`, Apache License 2.0, see `NOTICE`.
|
NOTICE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
zico
|
| 2 |
+
Copyright 2026 AmeenAhmed2
|
| 3 |
+
|
| 4 |
+
This checkpoint is a fine-tune of Qwen/Qwen3.5-4B-Base (https://huggingface.co/Qwen/Qwen3.5-4B-Base),
|
| 5 |
+
Copyright Alibaba Cloud, licensed under the Apache License, Version 2.0. The files in `vision/` (vision tower and image
|
| 6 |
+
preprocessor configuration) and `tokenizer/` are redistributed from that model without modification; the language-model
|
| 7 |
+
weights in `backbone/` were modified by fine-tuning.
|
README.md
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: cc-by-nc-4.0
|
| 3 |
+
base_model: Qwen/Qwen3.5-4B-Base
|
| 4 |
+
base_model_relation: finetune
|
| 5 |
+
language:
|
| 6 |
+
- en
|
| 7 |
+
pipeline_tag: zero-shot-classification
|
| 8 |
+
tags:
|
| 9 |
+
- classification
|
| 10 |
+
- zero-shot
|
| 11 |
+
- calibration
|
| 12 |
+
- decision-model
|
| 13 |
+
- system-one
|
| 14 |
+
- routing
|
| 15 |
+
- guardrails
|
| 16 |
+
- image-classification
|
| 17 |
+
- qwen3.5
|
| 18 |
+
- mlx
|
| 19 |
+
- pytorch
|
| 20 |
+
datasets:
|
| 21 |
+
- google/boolq
|
| 22 |
+
- fancyzhx/ag_news
|
| 23 |
+
- Yelp/yelp_review_full
|
| 24 |
+
- google-research-datasets/go_emotions
|
| 25 |
+
- mteb/banking77
|
| 26 |
+
- nyu-mll/glue
|
| 27 |
+
- ehovy/race
|
| 28 |
+
- google/civil_comments
|
| 29 |
+
- allenai/openbookqa
|
| 30 |
+
- tau/commonsense_qa
|
| 31 |
+
- allenai/qasc
|
| 32 |
+
- allenai/sciq
|
| 33 |
+
- Rowan/hellaswag
|
| 34 |
+
- allenai/winogrande
|
| 35 |
+
- aps/super_glue
|
| 36 |
+
- facebook/anli
|
| 37 |
+
- stanfordnlp/snli
|
| 38 |
+
- google-research-datasets/paws
|
| 39 |
+
- tals/vitaminc
|
| 40 |
+
- rajpurkar/squad_v2
|
| 41 |
+
- clinc/clinc_oos
|
| 42 |
+
- fancyzhx/dbpedia_14
|
| 43 |
+
- community-datasets/yahoo_answers_topics
|
| 44 |
+
- SetFit/20_newsgroups
|
| 45 |
+
- cardiffnlp/tweet_eval
|
| 46 |
+
- SetFit/sst5
|
| 47 |
+
- SetFit/amazon_reviews_multi_en
|
| 48 |
+
- deepset/prompt-injections
|
| 49 |
+
- jackhhao/jailbreak-classification
|
| 50 |
+
- lmsys/toxic-chat
|
| 51 |
+
- microsoft/ms_marco
|
| 52 |
+
- Team-ACE/ToolACE
|
| 53 |
+
- openlifescienceai/medmcqa
|
| 54 |
+
- derek-thomas/ScienceQA
|
| 55 |
+
- bitext/Bitext-customer-support-llm-chatbot-training-dataset
|
| 56 |
+
- benayas/snips
|
| 57 |
+
- benayas/atis
|
| 58 |
+
- zeroshot/twitter-financial-news-sentiment
|
| 59 |
+
- Muennighoff/natural-instructions
|
| 60 |
+
- chainyo/rvl-cdip
|
| 61 |
+
- clane9/imagenet-100
|
| 62 |
+
- uoft-cs/cifar100
|
| 63 |
+
- tanganke/stanford_cars
|
| 64 |
+
- bentrevett/caltech-ucsd-birds-200-2011
|
| 65 |
+
- Multimodal-Fatima/FGVC_Aircraft_train
|
| 66 |
+
- tanganke/dtd
|
| 67 |
+
- tanganke/eurosat
|
| 68 |
+
- tanganke/gtsrb
|
| 69 |
+
- tanganke/resisc45
|
| 70 |
+
- tanganke/sun397
|
| 71 |
+
- ashraq/fashion-product-images-small
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
# zico
|
| 75 |
+
|
| 76 |
+
**zico is a 4B decision model.** You give it a *state* (a message, a document, a JSON object, an image) and a set of
|
| 77 |
+
*typed questions*. It answers every question with a **calibrated probability distribution over the options you
|
| 78 |
+
listed** — in one forward pass, without generating any text.
|
| 79 |
+
|
| 80 |
+
```jsonc
|
| 81 |
+
// request // response
|
| 82 |
+
{ "state": { "ticket": { "body": "Charged twice, refund me { "answers": {
|
| 83 |
+
or I cancel." } }, "wants_refund": { "type": "noul", "noul": 0.89 },
|
| 84 |
+
"questions": { "team": { "type": "choice", "choice": "billing",
|
| 85 |
+
"wants_refund": { "type": "noul", "probabilities": { "billing": 0.94, "technical": 0.02, … },
|
| 86 |
+
"instructions": "The customer asks for a refund." }, "confidence": 0.81 },
|
| 87 |
+
"team": { "type": "choice", "frustration": { "type": "score", "score": 2.26,
|
| 88 |
+
"instructions": "Which team should handle this?", "probabilities": { "0": 0.04, "1": 0.08, "2": 0.54, … } } },
|
| 89 |
+
"criteria": { "billing": "Charges, refunds", "usage": { "input_tokens": 240, "output_tokens": 0 } }
|
| 90 |
+
"technical": "Bugs, outages", … } },
|
| 91 |
+
"frustration": { "type": "score",
|
| 92 |
+
"criteria": ["Calm", "Annoyed", "Frustrated", "Angry", "Furious"] } } }
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
It is meant for the many small decisions inside software — routing, triage, moderation, guardrails, tool selection,
|
| 96 |
+
"is this answer grounded?", document and image sorting — where you want a fast, cheap, *typed* answer with a
|
| 97 |
+
probability you can threshold, not a paragraph to parse.
|
| 98 |
+
|
| 99 |
+
| Question type | You provide | You get |
|
| 100 |
+
|---|---|---|
|
| 101 |
+
| **Choice** | 2–255 named options, each with an optional description | the most likely option, the full distribution, a confidence |
|
| 102 |
+
| **Score** | an ordered rubric of 2–10 level descriptions, lowest first | the expected level (e.g. `2.26`), the distribution over levels |
|
| 103 |
+
| **Noul** | a statement that is true or false about the state | the probability that it is true |
|
| 104 |
+
|
| 105 |
+
* **Type-safe by construction.** The output is a softmax over exactly the options in your request. An answer outside
|
| 106 |
+
your schema cannot happen, so there is nothing to parse, retry or validate.
|
| 107 |
+
* **Calibrated.** Trained with strictly proper scoring rules; on held-out tasks its stated confidence tracks its accuracy
|
| 108 |
+
(expected calibration error ≈ 0.04–0.06, see below). "0.9" means roughly nine in ten.
|
| 109 |
+
* **Many questions, one pass.** The state is encoded once and every question branches off it, so ten questions cost
|
| 110 |
+
little more than one. Answers are identical whether questions are asked together or alone.
|
| 111 |
+
* **Text and images.** Images can sit anywhere inside the state, next to text fields.
|
| 112 |
+
* **Fast.** ≈110 ms for 1–16 questions about a 500-token state on an H100 (64 questions: ≈260 ms); ≈300 ms for a
|
| 113 |
+
4-question request on an M4 Max laptop with the included MLX backend.
|
| 114 |
+
|
| 115 |
+
## Quick start
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
pip install -U "huggingface_hub[cli]"
|
| 119 |
+
hf download AmeenAhmed2/zico --local-dir zico && cd zico
|
| 120 |
+
python -m venv .venv && source .venv/bin/activate
|
| 121 |
+
pip install -e ".[serve]" # Apple silicon: pip install -e ".[serve,mlx]"
|
| 122 |
+
python scripts/infer.py . examples/request.json
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
The repository is the checkpoint *and* the code that runs it (`zico/`, a small Python package). The model cannot be
|
| 126 |
+
loaded with `transformers.AutoModel` alone: the language backbone is a standard Qwen3.5 checkpoint, but the decision
|
| 127 |
+
head and the input format are zico's own.
|
| 128 |
+
|
| 129 |
+
```python
|
| 130 |
+
import torch
|
| 131 |
+
from zico import DecisionModel, Choice, Score, Noul
|
| 132 |
+
|
| 133 |
+
model, renderer, _ = DecisionModel.load(".", device="cuda", dtype=torch.bfloat16) # or "mps" / "cpu"
|
| 134 |
+
model.autocast_dtype = torch.bfloat16
|
| 135 |
+
|
| 136 |
+
answers = model.answer(
|
| 137 |
+
renderer,
|
| 138 |
+
state={"message": "hey can u move my 3pm with dr patel to next week, mornings are better"},
|
| 139 |
+
questions={
|
| 140 |
+
"intent": Choice("What does the patient want?", {"book": None, "reschedule": None, "cancel": None, "billing": None, "other": None}),
|
| 141 |
+
"time_preference": Noul("The patient states a preferred time of day."),
|
| 142 |
+
"urgency": Score("How urgent is this?", ["Whenever", "This week", "Today", "Emergency"]),
|
| 143 |
+
},
|
| 144 |
+
)
|
| 145 |
+
print(answers["answers"]["intent"]) # {'type': 'choice', 'choice': 'reschedule', 'probabilities': {...}, 'confidence': ...}
|
| 146 |
+
```
|
| 147 |
+
|
| 148 |
+
An image is a JSON object inside the state — `{"type": "image", "data": "<base64>"}`, optionally with
|
| 149 |
+
`"detail": "low" | "high"` (64 to 1,024 image tokens; default ≤ 256):
|
| 150 |
+
|
| 151 |
+
```python
|
| 152 |
+
import base64
|
| 153 |
+
photo = {"type": "image", "data": base64.b64encode(open("scan.png", "rb").read()).decode()}
|
| 154 |
+
model.answer(renderer, {"scan": photo}, {"kind": Choice("What type of document is this?", {"invoice": None, "letter": None, "form": None, "resume": None})})
|
| 155 |
+
```
|
| 156 |
+
|
| 157 |
+
### HTTP server and test page
|
| 158 |
+
|
| 159 |
+
```bash
|
| 160 |
+
ZICO_CKPT=. scripts/serve.sh start # also: stop | restart | status | log | key-path
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
This starts a small FastAPI server on `http://127.0.0.1:8787` (on Apple silicon it uses the MLX backend automatically):
|
| 164 |
+
|
| 165 |
+
* `POST /v1/systemone` with `Authorization: Bearer <key>` takes the JSON request shown at the top (plus
|
| 166 |
+
`"model": "zico"`) and returns the response shown there. A key is generated on first start; `scripts/serve.sh key-path`
|
| 167 |
+
says where it is.
|
| 168 |
+
* `GET /` is a **test page**: write a state, drop in an image, build questions, and watch the probability bars. "Live"
|
| 169 |
+
mode re-runs on every edit and shows how far each probability moved.
|
| 170 |
+
|
| 171 |
+
The request and response shapes follow the *System One* wire format that TypeSafe AI documents publicly for its API, so
|
| 172 |
+
client code written for that format can be pointed at this server (`ZICO_MODEL_ALIASES` adds any fixed model name such a
|
| 173 |
+
client sends). See "Relation to other work" below.
|
| 174 |
+
|
| 175 |
+
## How it works
|
| 176 |
+
|
| 177 |
+
```
|
| 178 |
+
row = ### State\n{state}\n\n ### Choice\n{instructions}\n- billing: Charges…<OPT>\n- technical: Bugs…<OPT>\nAnswer:<ANS>
|
| 179 |
+
└──── shared prefix ────┘ └──────────────────────────── one question ──────────────────────────────────────┘
|
| 180 |
+
|
| 181 |
+
logit_i = q(h[<ANS>]) · k(h[<OPT>_i]) / √d + u(h[<OPT>_i]) p = softmax over this question's own options
|
| 182 |
+
```
|
| 183 |
+
|
| 184 |
+
* **Backbone.** `Qwen/Qwen3.5-4B-Base` (a hybrid of Gated-DeltaNet and attention layers), fully fine-tuned, language-model
|
| 185 |
+
head removed. It is only ever used for *prefill*: there is no decoding loop.
|
| 186 |
+
* **Slots, not tokens.** Each option ends in an `<OPT>` slot and each question in an `<ANS>` slot. Their input
|
| 187 |
+
embeddings come from a learned 4-row table, so the tokenizer and embedding matrix are untouched.
|
| 188 |
+
* **Pointer head.** The hidden state at `<ANS>` is scored against the hidden state at every `<OPT>`; the softmax runs
|
| 189 |
+
over exactly those positions. Choice → arg-max key, Score → Σ i·pᵢ, Noul → p(true), confidence = 1 − H(p)/log K. The
|
| 190 |
+
head has 1.3M parameters.
|
| 191 |
+
* **One state, many questions.** Training uses one `(state + question)` row per question. At inference the state is
|
| 192 |
+
prefilled once and its cache — attention KV *and* the recurrent / convolution state of the DeltaNet layers — is
|
| 193 |
+
forked across the questions, which run as one batch. Because the backbone is causal both compute the same function;
|
| 194 |
+
the test-suite asserts it.
|
| 195 |
+
* **Images** enter as embeddings from Qwen3.5's own vision tower (frozen, shipped in `vision/`), placed where the image
|
| 196 |
+
sits in the state's JSON, with Qwen's 3-part rotary positions. They are part of the shared prefix, so an image is
|
| 197 |
+
encoded once for all questions.
|
| 198 |
+
* **Loss.** Cross-entropy + 0.5 · Brier score, plus the ranked probability score for Score questions: all strictly proper
|
| 199 |
+
scoring rules, so the optimum is the true conditional distribution — accuracy and calibration are the same objective.
|
| 200 |
+
A per-type temperature fitted on validation data is stored in the checkpoint (1.09 / 1.10 / 1.23).
|
| 201 |
+
|
| 202 |
+
## Training
|
| 203 |
+
|
| 204 |
+
1. **Text pass** — 1 epoch over ≈40 public classification, NLI, reading-comprehension, multiple-choice, intent, moderation,
|
| 205 |
+
retrieval-relevance and tool-selection datasets plus ≈300 Super-NaturalInstructions tasks, all recast as typed
|
| 206 |
+
questions. To make the model *read the schema* rather than memorise label sets, records randomise option order,
|
| 207 |
+
option subsets, the presence of descriptions, instruction wording, whether the state is text or JSON, and add
|
| 208 |
+
"none of the above" cases. Whole datasets and whole instruction tasks are held out, and training text is de-duplicated
|
| 209 |
+
against every held-out set. ≈2.5 h on one H100 (bf16, 8-bit AdamW, ≈8k tokens/s).
|
| 210 |
+
2. **Image pass** — 1 epoch over 90k images from 12 public image-classification sets (documents, objects, fine-grained
|
| 211 |
+
species / cars / aircraft, textures, satellite and aerial scenes, traffic signs, product attributes), vision tower
|
| 212 |
+
frozen. To stop the text skills drifting, 80k text rows were replayed with the *previous model's own probabilities*
|
| 213 |
+
as soft targets — not the gold labels. Replaying hard labels made the model markedly overconfident on tasks it had
|
| 214 |
+
never seen (ECE 0.045 → 0.131); anchoring to its own distributions avoided that (0.055) while keeping every image
|
| 215 |
+
gain. ≈85 min on one H100.
|
| 216 |
+
|
| 217 |
+
Training and data-building code is in `scripts/` (`build_data.py`, `build_image_data.py`, `make_soft_targets.py`,
|
| 218 |
+
`train.py`, `eval.py`).
|
| 219 |
+
|
| 220 |
+
## Evaluation
|
| 221 |
+
|
| 222 |
+
All numbers are this checkpoint, bf16, raw probabilities unless stated. "Held out" means the dataset (or the whole
|
| 223 |
+
instruction task) never appeared in training, so the label set and the wording are new to the model. Sample sizes are
|
| 224 |
+
small (150–500 per set; 5.5k for the unseen-task pool): differences under ≈3 points are noise.
|
| 225 |
+
|
| 226 |
+
**Text, held-out (zero-shot)**
|
| 227 |
+
|
| 228 |
+
| Set | Question | Accuracy | ECE |
|
| 229 |
+
|---|---|---|---|
|
| 230 |
+
| ARC-Easy | Choice, free-text options | 97.6 % | 0.031 |
|
| 231 |
+
| IMDB sentiment | Noul | 94.0 % | 0.033 |
|
| 232 |
+
| MASSIVE, 60 intents | Choice ×60 | 74.6 % | 0.090 |
|
| 233 |
+
| 19 unseen Super-NaturalInstructions tasks | mixed | 75.2 % | 0.055 |
|
| 234 |
+
| PubMedQA | Choice ×3 | 70.8 % | 0.088 |
|
| 235 |
+
| dair-ai/emotion | Choice ×6 | 58.0 % | 0.200 |
|
| 236 |
+
| AskUbuntu duplicate reranking (pick 1 of many) | Choice | 51.8 % | 0.161 |
|
| 237 |
+
|
| 238 |
+
**Text, in-distribution test split:** 86.2 % over 37 sources (3,000 rows). Examples: Banking77 96.7 %, CLINC-150 with
|
| 239 |
+
out-of-scope 95.3 %, AG News 93.0 %, MNLI 92.0 %, BoolQ 92.0 %, VitaminC fact-checking 92.9 %, SQuAD-v2 answerability
|
| 240 |
+
88.0 %, jailbreak / prompt-injection detection 100 % (small sets), SST-5 67.3 %, MS MARCO passage selection 57.1 %.
|
| 241 |
+
By question type over the whole test split (text and images): Choice 90.0 %, Noul 92.3 %, Score 68.5 % exact level.
|
| 242 |
+
|
| 243 |
+
**Images, held-out sets (zero-shot: never trained on, full label set in every question)**
|
| 244 |
+
|
| 245 |
+
| Set | Options per question | Accuracy | ECE |
|
| 246 |
+
|---|---|---|---|
|
| 247 |
+
| CIFAR-10 | 10 | 97.7 % | 0.024 |
|
| 248 |
+
| MNIST digits | 10 | 95.3 % | 0.032 |
|
| 249 |
+
| POPE object presence | Noul | 90.0 % | 0.073 |
|
| 250 |
+
| Food-101 | 101 | 87.7 % | 0.043 |
|
| 251 |
+
| Oxford-IIIT pets | 37 | 85.0 % | 0.064 |
|
| 252 |
+
|
| 253 |
+
**Images, test splits of the training sets.** As in training, a question lists either the full label set or a random
|
| 254 |
+
subset that contains the right answer, and about a quarter are "the image shows X" Noul questions — so these are easier
|
| 255 |
+
than standard full-label benchmarks and are not comparable to published leaderboard numbers. 150–300 questions per set.
|
| 256 |
+
|
| 257 |
+
| Set | Options per question | Accuracy | ECE |
|
| 258 |
+
|---|---|---|---|
|
| 259 |
+
| RVL-CDIP scanned document types | 6 or all 16 | 92.3 % | 0.036 |
|
| 260 |
+
| Stanford Cars | 8–64 of 196 | 98.5 % | 0.023 |
|
| 261 |
+
| EuroSAT land use | 5 or all 10 | 97.0 % | 0.023 |
|
| 262 |
+
| SUN397 scenes | 8–64 of 397 | 96.6 % | 0.028 |
|
| 263 |
+
| ImageNet-100 | 8–100 | 95.2 % | 0.017 |
|
| 264 |
+
| RESISC45 aerial scenes | 8–45 | 94.5 % | 0.032 |
|
| 265 |
+
| CIFAR-100 | 10–100 | 93.6 % | 0.026 |
|
| 266 |
+
| DTD textures | 8–47 | 93.3 % | 0.046 |
|
| 267 |
+
| CUB-200 birds | 8–64 of 200 | 92.8 % | 0.034 |
|
| 268 |
+
| GTSRB traffic signs | 8–43 | 92.7 % | 0.035 |
|
| 269 |
+
|
| 270 |
+
**Calibration.** Over all 9.9k held-out rows (text and images): ECE 0.055 raw, 0.041 with the stored temperatures. On the
|
| 271 |
+
in-distribution test split: 0.011 raw. Full per-source results are in `eval.json`.
|
| 272 |
+
|
| 273 |
+
**Consistency.** Forked (batched) answers equal single-question answers to ≈1e-6 in fp32 (6e-5 in bf16 on an H100). The MLX backend matches PyTorch:
|
| 274 |
+
388 probabilities from real requests differ by 0.0003 on average, with the same top answer every time.
|
| 275 |
+
|
| 276 |
+
## Limitations
|
| 277 |
+
|
| 278 |
+
* **One forward pass, no reasoning.** Weak at counting, arithmetic, dates, multi-hop logic and double negatives. If a
|
| 279 |
+
question needs a chain of thought, this is the wrong tool.
|
| 280 |
+
* **It classifies; it does not extract or generate.** No spans, no free text, no numbers beyond a 2–10 level rubric
|
| 281 |
+
(and on rubrics the exact level is right about two times in three; the expected value is the more useful output).
|
| 282 |
+
* **Trained on rows up to ≈1k tokens.** The server accepts states up to 8k tokens, but accuracy and calibration on long
|
| 283 |
+
inputs are untested.
|
| 284 |
+
* **Mostly English.** The backbone is multilingual; the fine-tuning data is almost entirely English.
|
| 285 |
+
* **Fine-grained subjective labels are hard** (emotion 58 %, 5-star sentiment 67 %), and there the model is overconfident.
|
| 286 |
+
Calibration is a property of the distributions it was tested on; check it on your own data before relying on a threshold.
|
| 287 |
+
* **Images:** single still images, classification-style questions. It is not an OCR engine and was not trained on
|
| 288 |
+
charts, screenshots, multi-page documents or video. Small text inside a photo may not be read.
|
| 289 |
+
* **Safety-related questions** (toxicity, jailbreaks, prompt injection) were trained on small public sets. Treat the
|
| 290 |
+
output as one signal, not as a complete moderation or security system, and keep a human in the loop for decisions that
|
| 291 |
+
matter to people.
|
| 292 |
+
* The training data carries the usual biases of public web, review and social-media corpora.
|
| 293 |
+
|
| 294 |
+
## Licence and training data
|
| 295 |
+
|
| 296 |
+
* **Weights: CC-BY-NC-4.0 (non-commercial).** The base model is Apache-2.0, but some of the training data is licensed
|
| 297 |
+
for non-commercial or research use only — notably MS MARCO and ToxicChat (text), and ImageNet, FGVC-Aircraft,
|
| 298 |
+
CUB-200, Stanford Cars, SUN397 and RVL-CDIP (images). The cautious reading is that a model trained on them should not
|
| 299 |
+
be used commercially. `scripts/build_data.py --exclude-nc` builds the text set without the two non-commercial text
|
| 300 |
+
sources if you want to train a variant.
|
| 301 |
+
* **Code** (`zico/`, `scripts/`, `tests/`): Apache-2.0.
|
| 302 |
+
* `vision/` is the unmodified vision tower of `Qwen/Qwen3.5-4B-Base` (Apache-2.0, © Alibaba Cloud), redistributed so the
|
| 303 |
+
checkpoint is self-contained. See `NOTICE`.
|
| 304 |
+
* The datasets are listed in this card's metadata. They remain under their own licences; none of their content is
|
| 305 |
+
redistributed here.
|
| 306 |
+
|
| 307 |
+
## Relation to other work
|
| 308 |
+
|
| 309 |
+
* Built on **Qwen3.5-4B-Base** by the Qwen team; the Apple-silicon backend builds on **mlx-lm**.
|
| 310 |
+
* The task format — a state plus typed Choice / Score / Noul questions answered with probabilities — and the HTTP wire
|
| 311 |
+
format follow the *System One* API that **TypeSafe AI** documents publicly. zico is an independent, from-scratch
|
| 312 |
+
project: it is not affiliated with or endorsed by TypeSafe AI, shares no code or weights with their models, and **no
|
| 313 |
+
output of their models was used to train or tune it**. The architecture described above is this project's own design
|
| 314 |
+
and says nothing about how theirs works.
|
| 315 |
+
|
| 316 |
+
## Citation
|
| 317 |
+
|
| 318 |
+
```bibtex
|
| 319 |
+
@misc{zico2026,
|
| 320 |
+
title = {zico: a calibrated, typed decision model in a single forward pass},
|
| 321 |
+
author = {AmeenAhmed2},
|
| 322 |
+
year = {2026},
|
| 323 |
+
url = {https://huggingface.co/AmeenAhmed2/zico}
|
| 324 |
+
}
|
| 325 |
+
```
|
backbone/config.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen3_5TextModel"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"attn_output_gate": true,
|
| 8 |
+
"bos_token_id": null,
|
| 9 |
+
"dtype": "bfloat16",
|
| 10 |
+
"eos_token_id": 248044,
|
| 11 |
+
"full_attention_interval": 4,
|
| 12 |
+
"head_dim": 256,
|
| 13 |
+
"hidden_act": "silu",
|
| 14 |
+
"hidden_size": 2560,
|
| 15 |
+
"initializer_range": 0.02,
|
| 16 |
+
"intermediate_size": 9216,
|
| 17 |
+
"layer_types": [
|
| 18 |
+
"linear_attention",
|
| 19 |
+
"linear_attention",
|
| 20 |
+
"linear_attention",
|
| 21 |
+
"full_attention",
|
| 22 |
+
"linear_attention",
|
| 23 |
+
"linear_attention",
|
| 24 |
+
"linear_attention",
|
| 25 |
+
"full_attention",
|
| 26 |
+
"linear_attention",
|
| 27 |
+
"linear_attention",
|
| 28 |
+
"linear_attention",
|
| 29 |
+
"full_attention",
|
| 30 |
+
"linear_attention",
|
| 31 |
+
"linear_attention",
|
| 32 |
+
"linear_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"linear_attention",
|
| 35 |
+
"linear_attention",
|
| 36 |
+
"linear_attention",
|
| 37 |
+
"full_attention",
|
| 38 |
+
"linear_attention",
|
| 39 |
+
"linear_attention",
|
| 40 |
+
"linear_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"linear_attention",
|
| 43 |
+
"linear_attention",
|
| 44 |
+
"linear_attention",
|
| 45 |
+
"full_attention",
|
| 46 |
+
"linear_attention",
|
| 47 |
+
"linear_attention",
|
| 48 |
+
"linear_attention",
|
| 49 |
+
"full_attention"
|
| 50 |
+
],
|
| 51 |
+
"linear_conv_kernel_dim": 4,
|
| 52 |
+
"linear_key_head_dim": 128,
|
| 53 |
+
"linear_num_key_heads": 16,
|
| 54 |
+
"linear_num_value_heads": 32,
|
| 55 |
+
"linear_value_head_dim": 128,
|
| 56 |
+
"mamba_ssm_dtype": "float32",
|
| 57 |
+
"max_position_embeddings": 262144,
|
| 58 |
+
"mlp_only_layers": [],
|
| 59 |
+
"model_type": "qwen3_5_text",
|
| 60 |
+
"mtp_num_hidden_layers": 1,
|
| 61 |
+
"mtp_use_dedicated_embeddings": false,
|
| 62 |
+
"num_attention_heads": 16,
|
| 63 |
+
"num_hidden_layers": 32,
|
| 64 |
+
"num_key_value_heads": 4,
|
| 65 |
+
"pad_token_id": null,
|
| 66 |
+
"partial_rotary_factor": 0.25,
|
| 67 |
+
"rms_norm_eps": 1e-06,
|
| 68 |
+
"rope_parameters": {
|
| 69 |
+
"mrope_interleaved": true,
|
| 70 |
+
"mrope_section": [
|
| 71 |
+
11,
|
| 72 |
+
11,
|
| 73 |
+
10
|
| 74 |
+
],
|
| 75 |
+
"partial_rotary_factor": 0.25,
|
| 76 |
+
"rope_theta": 10000000,
|
| 77 |
+
"rope_type": "default"
|
| 78 |
+
},
|
| 79 |
+
"tie_word_embeddings": true,
|
| 80 |
+
"transformers_version": "5.17.0",
|
| 81 |
+
"use_cache": true,
|
| 82 |
+
"vocab_size": 248320
|
| 83 |
+
}
|
backbone/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ea11662c9f4eda45d498db013c3f1d5de6cf89079b763f6e7d169bc367a72de
|
| 3 |
+
size 8411549456
|
eval.json
ADDED
|
@@ -0,0 +1,1504 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"meta": {
|
| 3 |
+
"base_model": "Qwen/Qwen3.5-4B-Base",
|
| 4 |
+
"d_head": 256,
|
| 5 |
+
"lora": false,
|
| 6 |
+
"renderer": {
|
| 7 |
+
"max_state_tokens": 768,
|
| 8 |
+
"max_instruction_tokens": 256,
|
| 9 |
+
"max_option_tokens": 64
|
| 10 |
+
},
|
| 11 |
+
"step": 734,
|
| 12 |
+
"val_nll": 0.27321311831474304
|
| 13 |
+
},
|
| 14 |
+
"temperatures": {
|
| 15 |
+
"choice": 1.0929638744012644,
|
| 16 |
+
"score": 1.1011288758276154,
|
| 17 |
+
"noul": 1.228405177254357
|
| 18 |
+
},
|
| 19 |
+
"test": {
|
| 20 |
+
"raw": {
|
| 21 |
+
"overall": {
|
| 22 |
+
"n": 5070,
|
| 23 |
+
"acc": 0.8964496850967407,
|
| 24 |
+
"nll": 0.276126503944397,
|
| 25 |
+
"brier": 0.1475353091955185,
|
| 26 |
+
"ece": 0.010809999694056847,
|
| 27 |
+
"mean_conf": 0.9048414826393127,
|
| 28 |
+
"score_mae": 0.41506025195121765
|
| 29 |
+
},
|
| 30 |
+
"by_type": {
|
| 31 |
+
"choice": {
|
| 32 |
+
"n": 3041,
|
| 33 |
+
"acc": 0.9003617167472839,
|
| 34 |
+
"nll": 0.2871852517127991,
|
| 35 |
+
"brier": 0.1437402367591858,
|
| 36 |
+
"ece": 0.013187985173783867,
|
| 37 |
+
"mean_conf": 0.9091227054595947
|
| 38 |
+
},
|
| 39 |
+
"score": {
|
| 40 |
+
"n": 273,
|
| 41 |
+
"acc": 0.6849817037582397,
|
| 42 |
+
"nll": 0.7681964039802551,
|
| 43 |
+
"brier": 0.4400661289691925,
|
| 44 |
+
"ece": 0.047672064850177796,
|
| 45 |
+
"mean_conf": 0.6837503910064697,
|
| 46 |
+
"score_mae": 0.41506025195121765
|
| 47 |
+
},
|
| 48 |
+
"noul": {
|
| 49 |
+
"n": 1756,
|
| 50 |
+
"acc": 0.9225512742996216,
|
| 51 |
+
"nll": 0.18047469854354858,
|
| 52 |
+
"brier": 0.10862862318754196,
|
| 53 |
+
"ece": 0.013375402451816704,
|
| 54 |
+
"mean_conf": 0.9318000674247742
|
| 55 |
+
}
|
| 56 |
+
},
|
| 57 |
+
"by_source": {
|
| 58 |
+
"ag_news": {
|
| 59 |
+
"n": 86,
|
| 60 |
+
"acc": 0.930232584476471,
|
| 61 |
+
"nll": 0.23873434960842133,
|
| 62 |
+
"brier": 0.10834771394729614,
|
| 63 |
+
"ece": 0.056332710342662184,
|
| 64 |
+
"mean_conf": 0.9181896448135376
|
| 65 |
+
},
|
| 66 |
+
"amazon_stars": {
|
| 67 |
+
"n": 86,
|
| 68 |
+
"acc": 0.5930232405662537,
|
| 69 |
+
"nll": 0.8701136112213135,
|
| 70 |
+
"brier": 0.5058151483535767,
|
| 71 |
+
"ece": 0.10280728207642115,
|
| 72 |
+
"mean_conf": 0.6330943703651428,
|
| 73 |
+
"score_mae": 0.4689413011074066
|
| 74 |
+
},
|
| 75 |
+
"anli": {
|
| 76 |
+
"n": 79,
|
| 77 |
+
"acc": 0.746835470199585,
|
| 78 |
+
"nll": 0.6076061129570007,
|
| 79 |
+
"brier": 0.3763708174228668,
|
| 80 |
+
"ece": 0.15590721699211704,
|
| 81 |
+
"mean_conf": 0.8249932527542114
|
| 82 |
+
},
|
| 83 |
+
"banking77": {
|
| 84 |
+
"n": 91,
|
| 85 |
+
"acc": 0.9670329689979553,
|
| 86 |
+
"nll": 0.109474316239357,
|
| 87 |
+
"brier": 0.05170481652021408,
|
| 88 |
+
"ece": 0.04274910372363963,
|
| 89 |
+
"mean_conf": 0.9463766813278198
|
| 90 |
+
},
|
| 91 |
+
"boolq": {
|
| 92 |
+
"n": 75,
|
| 93 |
+
"acc": 0.9200000166893005,
|
| 94 |
+
"nll": 0.22674815356731415,
|
| 95 |
+
"brier": 0.14536462724208832,
|
| 96 |
+
"ece": 0.07328386494371486,
|
| 97 |
+
"mean_conf": 0.908048689365387
|
| 98 |
+
},
|
| 99 |
+
"cifar100": {
|
| 100 |
+
"n": 297,
|
| 101 |
+
"acc": 0.936026930809021,
|
| 102 |
+
"nll": 0.19756226241588593,
|
| 103 |
+
"brier": 0.096889927983284,
|
| 104 |
+
"ece": 0.02602774308840307,
|
| 105 |
+
"mean_conf": 0.9325385689735413
|
| 106 |
+
},
|
| 107 |
+
"civil_comments": {
|
| 108 |
+
"n": 154,
|
| 109 |
+
"acc": 0.9545454382896423,
|
| 110 |
+
"nll": 0.1635725051164627,
|
| 111 |
+
"brier": 0.08106027543544769,
|
| 112 |
+
"ece": 0.08850163645923792,
|
| 113 |
+
"mean_conf": 0.8801278471946716
|
| 114 |
+
},
|
| 115 |
+
"clinc_oos": {
|
| 116 |
+
"n": 85,
|
| 117 |
+
"acc": 0.9529411792755127,
|
| 118 |
+
"nll": 0.10286867618560791,
|
| 119 |
+
"brier": 0.057649165391922,
|
| 120 |
+
"ece": 0.028260532641420116,
|
| 121 |
+
"mean_conf": 0.9765596389770508
|
| 122 |
+
},
|
| 123 |
+
"cola": {
|
| 124 |
+
"n": 89,
|
| 125 |
+
"acc": 0.8202247023582458,
|
| 126 |
+
"nll": 0.3538511097431183,
|
| 127 |
+
"brier": 0.23637528717517853,
|
| 128 |
+
"ece": 0.07856890498333513,
|
| 129 |
+
"mean_conf": 0.8428974747657776
|
| 130 |
+
},
|
| 131 |
+
"commonsense_qa": {
|
| 132 |
+
"n": 79,
|
| 133 |
+
"acc": 0.797468364238739,
|
| 134 |
+
"nll": 0.47439879179000854,
|
| 135 |
+
"brier": 0.2571961283683777,
|
| 136 |
+
"ece": 0.11401724499686991,
|
| 137 |
+
"mean_conf": 0.8429151177406311
|
| 138 |
+
},
|
| 139 |
+
"copa": {
|
| 140 |
+
"n": 19,
|
| 141 |
+
"acc": 0.9473684430122375,
|
| 142 |
+
"nll": 0.08234653621912003,
|
| 143 |
+
"brier": 0.056282687932252884,
|
| 144 |
+
"ece": 0.053942021972607,
|
| 145 |
+
"mean_conf": 0.9678375720977783
|
| 146 |
+
},
|
| 147 |
+
"cub_birds": {
|
| 148 |
+
"n": 209,
|
| 149 |
+
"acc": 0.9282296895980835,
|
| 150 |
+
"nll": 0.205092191696167,
|
| 151 |
+
"brier": 0.09457369148731232,
|
| 152 |
+
"ece": 0.0337111855618914,
|
| 153 |
+
"mean_conf": 0.9249444007873535
|
| 154 |
+
},
|
| 155 |
+
"dbpedia": {
|
| 156 |
+
"n": 83,
|
| 157 |
+
"acc": 0.9879518151283264,
|
| 158 |
+
"nll": 0.0267021507024765,
|
| 159 |
+
"brier": 0.01387640181928873,
|
| 160 |
+
"ece": 0.01773379953028531,
|
| 161 |
+
"mean_conf": 0.9858810305595398
|
| 162 |
+
},
|
| 163 |
+
"dtd": {
|
| 164 |
+
"n": 193,
|
| 165 |
+
"acc": 0.9326424598693848,
|
| 166 |
+
"nll": 0.1894713044166565,
|
| 167 |
+
"brier": 0.10953276604413986,
|
| 168 |
+
"ece": 0.0462280399812367,
|
| 169 |
+
"mean_conf": 0.9360920786857605
|
| 170 |
+
},
|
| 171 |
+
"eurosat": {
|
| 172 |
+
"n": 200,
|
| 173 |
+
"acc": 0.9700000286102295,
|
| 174 |
+
"nll": 0.09461035579442978,
|
| 175 |
+
"brier": 0.04988160729408264,
|
| 176 |
+
"ece": 0.022912378822553925,
|
| 177 |
+
"mean_conf": 0.9720562696456909
|
| 178 |
+
},
|
| 179 |
+
"go_emotions": {
|
| 180 |
+
"n": 156,
|
| 181 |
+
"acc": 0.8461538553237915,
|
| 182 |
+
"nll": 0.4075973331928253,
|
| 183 |
+
"brier": 0.2239874303340912,
|
| 184 |
+
"ece": 0.052582084089755954,
|
| 185 |
+
"mean_conf": 0.8886739015579224
|
| 186 |
+
},
|
| 187 |
+
"gtsrb": {
|
| 188 |
+
"n": 150,
|
| 189 |
+
"acc": 0.9266666769981384,
|
| 190 |
+
"nll": 0.2583926022052765,
|
| 191 |
+
"brier": 0.10076691210269928,
|
| 192 |
+
"ece": 0.0349075827405604,
|
| 193 |
+
"mean_conf": 0.9396733641624451
|
| 194 |
+
},
|
| 195 |
+
"hellaswag": {
|
| 196 |
+
"n": 71,
|
| 197 |
+
"acc": 0.9718309640884399,
|
| 198 |
+
"nll": 0.07258392125368118,
|
| 199 |
+
"brier": 0.03423016890883446,
|
| 200 |
+
"ece": 0.037721984350516546,
|
| 201 |
+
"mean_conf": 0.9680467844009399
|
| 202 |
+
},
|
| 203 |
+
"imagenet100": {
|
| 204 |
+
"n": 209,
|
| 205 |
+
"acc": 0.9521530866622925,
|
| 206 |
+
"nll": 0.1411520093679428,
|
| 207 |
+
"brier": 0.06291362643241882,
|
| 208 |
+
"ece": 0.01678104336341813,
|
| 209 |
+
"mean_conf": 0.9670758247375488
|
| 210 |
+
},
|
| 211 |
+
"jailbreak": {
|
| 212 |
+
"n": 43,
|
| 213 |
+
"acc": 1.0,
|
| 214 |
+
"nll": 0.003266148502007127,
|
| 215 |
+
"brier": 0.0003119981265626848,
|
| 216 |
+
"ece": 0.003183770989621637,
|
| 217 |
+
"mean_conf": 0.9968161582946777
|
| 218 |
+
},
|
| 219 |
+
"mnli": {
|
| 220 |
+
"n": 88,
|
| 221 |
+
"acc": 0.9204545617103577,
|
| 222 |
+
"nll": 0.17574532330036163,
|
| 223 |
+
"brier": 0.10608046501874924,
|
| 224 |
+
"ece": 0.039473747201574705,
|
| 225 |
+
"mean_conf": 0.9106850028038025
|
| 226 |
+
},
|
| 227 |
+
"ms_marco": {
|
| 228 |
+
"n": 77,
|
| 229 |
+
"acc": 0.5714285969734192,
|
| 230 |
+
"nll": 1.1672861576080322,
|
| 231 |
+
"brier": 0.5488265156745911,
|
| 232 |
+
"ece": 0.1069421570546632,
|
| 233 |
+
"mean_conf": 0.5573300123214722
|
| 234 |
+
},
|
| 235 |
+
"newsgroups": {
|
| 236 |
+
"n": 72,
|
| 237 |
+
"acc": 0.8194444179534912,
|
| 238 |
+
"nll": 0.656489372253418,
|
| 239 |
+
"brier": 0.24557192623615265,
|
| 240 |
+
"ece": 0.10580966421453894,
|
| 241 |
+
"mean_conf": 0.8494588732719421
|
| 242 |
+
},
|
| 243 |
+
"openbookqa": {
|
| 244 |
+
"n": 73,
|
| 245 |
+
"acc": 0.9452054500579834,
|
| 246 |
+
"nll": 0.2053627371788025,
|
| 247 |
+
"brier": 0.10051807016134262,
|
| 248 |
+
"ece": 0.07717989225561772,
|
| 249 |
+
"mean_conf": 0.8888569474220276
|
| 250 |
+
},
|
| 251 |
+
"paws": {
|
| 252 |
+
"n": 75,
|
| 253 |
+
"acc": 0.8799999952316284,
|
| 254 |
+
"nll": 0.252999871969223,
|
| 255 |
+
"brier": 0.16616970300674438,
|
| 256 |
+
"ece": 0.07703955284745612,
|
| 257 |
+
"mean_conf": 0.957039475440979
|
| 258 |
+
},
|
| 259 |
+
"prompt_injection": {
|
| 260 |
+
"n": 19,
|
| 261 |
+
"acc": 1.0,
|
| 262 |
+
"nll": 0.07026468217372894,
|
| 263 |
+
"brier": 0.02881462872028351,
|
| 264 |
+
"ece": 0.06134651247871403,
|
| 265 |
+
"mean_conf": 0.9386534690856934
|
| 266 |
+
},
|
| 267 |
+
"qasc": {
|
| 268 |
+
"n": 78,
|
| 269 |
+
"acc": 0.8974359035491943,
|
| 270 |
+
"nll": 0.22243788838386536,
|
| 271 |
+
"brier": 0.11997074633836746,
|
| 272 |
+
"ece": 0.058650574809634426,
|
| 273 |
+
"mean_conf": 0.9030769467353821
|
| 274 |
+
},
|
| 275 |
+
"qnli": {
|
| 276 |
+
"n": 79,
|
| 277 |
+
"acc": 0.9620253443717957,
|
| 278 |
+
"nll": 0.12711185216903687,
|
| 279 |
+
"brier": 0.06058496609330177,
|
| 280 |
+
"ece": 0.02656769830988215,
|
| 281 |
+
"mean_conf": 0.9400109648704529
|
| 282 |
+
},
|
| 283 |
+
"qqp": {
|
| 284 |
+
"n": 67,
|
| 285 |
+
"acc": 0.8507462739944458,
|
| 286 |
+
"nll": 0.32717540860176086,
|
| 287 |
+
"brier": 0.20558716356754303,
|
| 288 |
+
"ece": 0.05535818888270405,
|
| 289 |
+
"mean_conf": 0.8827617168426514
|
| 290 |
+
},
|
| 291 |
+
"race": {
|
| 292 |
+
"n": 71,
|
| 293 |
+
"acc": 0.8169013857841492,
|
| 294 |
+
"nll": 0.5218772292137146,
|
| 295 |
+
"brier": 0.26671260595321655,
|
| 296 |
+
"ece": 0.08260945939768694,
|
| 297 |
+
"mean_conf": 0.868141770362854
|
| 298 |
+
},
|
| 299 |
+
"resisc45": {
|
| 300 |
+
"n": 201,
|
| 301 |
+
"acc": 0.9452736377716064,
|
| 302 |
+
"nll": 0.17759159207344055,
|
| 303 |
+
"brier": 0.07590000331401825,
|
| 304 |
+
"ece": 0.03162269839005066,
|
| 305 |
+
"mean_conf": 0.9709147810935974
|
| 306 |
+
},
|
| 307 |
+
"rte": {
|
| 308 |
+
"n": 42,
|
| 309 |
+
"acc": 0.9047619104385376,
|
| 310 |
+
"nll": 0.21782515943050385,
|
| 311 |
+
"brier": 0.12485634535551071,
|
| 312 |
+
"ece": 0.05018191372233538,
|
| 313 |
+
"mean_conf": 0.9342974424362183
|
| 314 |
+
},
|
| 315 |
+
"rvl_cdip": {
|
| 316 |
+
"n": 207,
|
| 317 |
+
"acc": 0.9227052927017212,
|
| 318 |
+
"nll": 0.24853183329105377,
|
| 319 |
+
"brier": 0.1186482235789299,
|
| 320 |
+
"ece": 0.035717121188601514,
|
| 321 |
+
"mean_conf": 0.9394121170043945
|
| 322 |
+
},
|
| 323 |
+
"sciq": {
|
| 324 |
+
"n": 70,
|
| 325 |
+
"acc": 0.9571428298950195,
|
| 326 |
+
"nll": 0.08418832719326019,
|
| 327 |
+
"brier": 0.05511568486690521,
|
| 328 |
+
"ece": 0.05332319163331367,
|
| 329 |
+
"mean_conf": 0.9662564992904663
|
| 330 |
+
},
|
| 331 |
+
"snli": {
|
| 332 |
+
"n": 78,
|
| 333 |
+
"acc": 0.9615384340286255,
|
| 334 |
+
"nll": 0.13747791945934296,
|
| 335 |
+
"brier": 0.07794130593538284,
|
| 336 |
+
"ece": 0.06147045345163815,
|
| 337 |
+
"mean_conf": 0.9255906343460083
|
| 338 |
+
},
|
| 339 |
+
"squad_v2": {
|
| 340 |
+
"n": 83,
|
| 341 |
+
"acc": 0.8795180916786194,
|
| 342 |
+
"nll": 0.3885398805141449,
|
| 343 |
+
"brier": 0.18468858301639557,
|
| 344 |
+
"ece": 0.08604057614296412,
|
| 345 |
+
"mean_conf": 0.9535071849822998
|
| 346 |
+
},
|
| 347 |
+
"sst5": {
|
| 348 |
+
"n": 101,
|
| 349 |
+
"acc": 0.6732673048973083,
|
| 350 |
+
"nll": 0.8455938696861267,
|
| 351 |
+
"brier": 0.4966614544391632,
|
| 352 |
+
"ece": 0.10307709063944676,
|
| 353 |
+
"mean_conf": 0.6207720041275024,
|
| 354 |
+
"score_mae": 0.45541465282440186
|
| 355 |
+
},
|
| 356 |
+
"stanford_cars": {
|
| 357 |
+
"n": 199,
|
| 358 |
+
"acc": 0.9849246144294739,
|
| 359 |
+
"nll": 0.06568868458271027,
|
| 360 |
+
"brier": 0.033190734684467316,
|
| 361 |
+
"ece": 0.02284979613965564,
|
| 362 |
+
"mean_conf": 0.9779478907585144
|
| 363 |
+
},
|
| 364 |
+
"sun397": {
|
| 365 |
+
"n": 205,
|
| 366 |
+
"acc": 0.9658536314964294,
|
| 367 |
+
"nll": 0.13169661164283752,
|
| 368 |
+
"brier": 0.059237346053123474,
|
| 369 |
+
"ece": 0.027937838926008762,
|
| 370 |
+
"mean_conf": 0.9778258800506592
|
| 371 |
+
},
|
| 372 |
+
"toxic_chat": {
|
| 373 |
+
"n": 103,
|
| 374 |
+
"acc": 0.9805825352668762,
|
| 375 |
+
"nll": 0.04486699774861336,
|
| 376 |
+
"brier": 0.02695567160844803,
|
| 377 |
+
"ece": 0.011975512422466195,
|
| 378 |
+
"mean_conf": 0.9775879383087158
|
| 379 |
+
},
|
| 380 |
+
"tweet_irony": {
|
| 381 |
+
"n": 73,
|
| 382 |
+
"acc": 0.7397260069847107,
|
| 383 |
+
"nll": 0.47673916816711426,
|
| 384 |
+
"brier": 0.32497861981391907,
|
| 385 |
+
"ece": 0.10002183849680613,
|
| 386 |
+
"mean_conf": 0.8234904408454895
|
| 387 |
+
},
|
| 388 |
+
"tweet_offensive": {
|
| 389 |
+
"n": 95,
|
| 390 |
+
"acc": 0.800000011920929,
|
| 391 |
+
"nll": 0.42309972643852234,
|
| 392 |
+
"brier": 0.2671454846858978,
|
| 393 |
+
"ece": 0.11630791231693371,
|
| 394 |
+
"mean_conf": 0.8510119915008545
|
| 395 |
+
},
|
| 396 |
+
"tweet_sentiment": {
|
| 397 |
+
"n": 96,
|
| 398 |
+
"acc": 0.7604166865348816,
|
| 399 |
+
"nll": 0.6009823679924011,
|
| 400 |
+
"brier": 0.3709958493709564,
|
| 401 |
+
"ece": 0.08339798031360968,
|
| 402 |
+
"mean_conf": 0.7406583428382874
|
| 403 |
+
},
|
| 404 |
+
"vitaminc": {
|
| 405 |
+
"n": 84,
|
| 406 |
+
"acc": 0.9285714030265808,
|
| 407 |
+
"nll": 0.20010635256767273,
|
| 408 |
+
"brier": 0.11288775503635406,
|
| 409 |
+
"ece": 0.055092808689364614,
|
| 410 |
+
"mean_conf": 0.8885759711265564
|
| 411 |
+
},
|
| 412 |
+
"winogrande": {
|
| 413 |
+
"n": 82,
|
| 414 |
+
"acc": 0.8292682766914368,
|
| 415 |
+
"nll": 0.3929581940174103,
|
| 416 |
+
"brier": 0.2536102533340454,
|
| 417 |
+
"ece": 0.09293118971498115,
|
| 418 |
+
"mean_conf": 0.9037488698959351
|
| 419 |
+
},
|
| 420 |
+
"yahoo_topics": {
|
| 421 |
+
"n": 71,
|
| 422 |
+
"acc": 0.7746478915214539,
|
| 423 |
+
"nll": 0.6088139414787292,
|
| 424 |
+
"brier": 0.28299495577812195,
|
| 425 |
+
"ece": 0.06163965007121763,
|
| 426 |
+
"mean_conf": 0.7804079055786133
|
| 427 |
+
},
|
| 428 |
+
"yelp": {
|
| 429 |
+
"n": 127,
|
| 430 |
+
"acc": 0.834645688533783,
|
| 431 |
+
"nll": 0.4275256395339966,
|
| 432 |
+
"brier": 0.2361738681793213,
|
| 433 |
+
"ece": 0.047331056411305306,
|
| 434 |
+
"mean_conf": 0.858354926109314,
|
| 435 |
+
"score_mae": 0.31378617882728577
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"calibrated": {
|
| 440 |
+
"overall": {
|
| 441 |
+
"n": 5070,
|
| 442 |
+
"acc": 0.8964496850967407,
|
| 443 |
+
"nll": 0.2756565809249878,
|
| 444 |
+
"brier": 0.14737455546855927,
|
| 445 |
+
"ece": 0.0079650055739778,
|
| 446 |
+
"mean_conf": 0.8922349810600281,
|
| 447 |
+
"score_mae": 0.42143115401268005
|
| 448 |
+
},
|
| 449 |
+
"by_type": {
|
| 450 |
+
"choice": {
|
| 451 |
+
"n": 3041,
|
| 452 |
+
"acc": 0.9003617167472839,
|
| 453 |
+
"nll": 0.28574398159980774,
|
| 454 |
+
"brier": 0.14335514605045319,
|
| 455 |
+
"ece": 0.007999743385107894,
|
| 456 |
+
"mean_conf": 0.8997042179107666
|
| 457 |
+
},
|
| 458 |
+
"score": {
|
| 459 |
+
"n": 273,
|
| 460 |
+
"acc": 0.6849817037582397,
|
| 461 |
+
"nll": 0.7671821713447571,
|
| 462 |
+
"brier": 0.439628005027771,
|
| 463 |
+
"ece": 0.046648137280912105,
|
| 464 |
+
"mean_conf": 0.6634480953216553,
|
| 465 |
+
"score_mae": 0.42143115401268005
|
| 466 |
+
},
|
| 467 |
+
"noul": {
|
| 468 |
+
"n": 1756,
|
| 469 |
+
"acc": 0.9225512742996216,
|
| 470 |
+
"nll": 0.18177150189876556,
|
| 471 |
+
"brier": 0.10889948904514313,
|
| 472 |
+
"ece": 0.019957571878301106,
|
| 473 |
+
"mean_conf": 0.9148688316345215
|
| 474 |
+
}
|
| 475 |
+
},
|
| 476 |
+
"by_source": {
|
| 477 |
+
"ag_news": {
|
| 478 |
+
"n": 86,
|
| 479 |
+
"acc": 0.930232584476471,
|
| 480 |
+
"nll": 0.2372349351644516,
|
| 481 |
+
"brier": 0.10982950031757355,
|
| 482 |
+
"ece": 0.06062877569098146,
|
| 483 |
+
"mean_conf": 0.909740686416626
|
| 484 |
+
},
|
| 485 |
+
"amazon_stars": {
|
| 486 |
+
"n": 86,
|
| 487 |
+
"acc": 0.5930232405662537,
|
| 488 |
+
"nll": 0.8707610964775085,
|
| 489 |
+
"brier": 0.5034118294715881,
|
| 490 |
+
"ece": 0.07147133660142413,
|
| 491 |
+
"mean_conf": 0.6115179657936096,
|
| 492 |
+
"score_mae": 0.4738217294216156
|
| 493 |
+
},
|
| 494 |
+
"anli": {
|
| 495 |
+
"n": 79,
|
| 496 |
+
"acc": 0.746835470199585,
|
| 497 |
+
"nll": 0.5904105305671692,
|
| 498 |
+
"brier": 0.3706202208995819,
|
| 499 |
+
"ece": 0.12736005320452623,
|
| 500 |
+
"mean_conf": 0.8066393733024597
|
| 501 |
+
},
|
| 502 |
+
"banking77": {
|
| 503 |
+
"n": 91,
|
| 504 |
+
"acc": 0.9670329689979553,
|
| 505 |
+
"nll": 0.11517834663391113,
|
| 506 |
+
"brier": 0.05284213647246361,
|
| 507 |
+
"ece": 0.04865624303995164,
|
| 508 |
+
"mean_conf": 0.9390164613723755
|
| 509 |
+
},
|
| 510 |
+
"boolq": {
|
| 511 |
+
"n": 75,
|
| 512 |
+
"acc": 0.9200000166893005,
|
| 513 |
+
"nll": 0.230869323015213,
|
| 514 |
+
"brier": 0.14464128017425537,
|
| 515 |
+
"ece": 0.05709565622368751,
|
| 516 |
+
"mean_conf": 0.8859748244285583
|
| 517 |
+
},
|
| 518 |
+
"cifar100": {
|
| 519 |
+
"n": 297,
|
| 520 |
+
"acc": 0.936026930809021,
|
| 521 |
+
"nll": 0.19978056848049164,
|
| 522 |
+
"brier": 0.09738589078187943,
|
| 523 |
+
"ece": 0.03686069778541466,
|
| 524 |
+
"mean_conf": 0.9233465194702148
|
| 525 |
+
},
|
| 526 |
+
"civil_comments": {
|
| 527 |
+
"n": 154,
|
| 528 |
+
"acc": 0.9545454382896423,
|
| 529 |
+
"nll": 0.1945398449897766,
|
| 530 |
+
"brier": 0.09554325044155121,
|
| 531 |
+
"ece": 0.11094526093531587,
|
| 532 |
+
"mean_conf": 0.8504656553268433
|
| 533 |
+
},
|
| 534 |
+
"clinc_oos": {
|
| 535 |
+
"n": 85,
|
| 536 |
+
"acc": 0.9529411792755127,
|
| 537 |
+
"nll": 0.10213149338960648,
|
| 538 |
+
"brier": 0.05607043579220772,
|
| 539 |
+
"ece": 0.03434847114305911,
|
| 540 |
+
"mean_conf": 0.9722728133201599
|
| 541 |
+
},
|
| 542 |
+
"cola": {
|
| 543 |
+
"n": 89,
|
| 544 |
+
"acc": 0.8202247023582458,
|
| 545 |
+
"nll": 0.3592163622379303,
|
| 546 |
+
"brier": 0.2345142364501953,
|
| 547 |
+
"ece": 0.07689192332149841,
|
| 548 |
+
"mean_conf": 0.8134418725967407
|
| 549 |
+
},
|
| 550 |
+
"commonsense_qa": {
|
| 551 |
+
"n": 79,
|
| 552 |
+
"acc": 0.797468364238739,
|
| 553 |
+
"nll": 0.47140878438949585,
|
| 554 |
+
"brier": 0.25501152873039246,
|
| 555 |
+
"ece": 0.08131545514081495,
|
| 556 |
+
"mean_conf": 0.8276560306549072
|
| 557 |
+
},
|
| 558 |
+
"copa": {
|
| 559 |
+
"n": 19,
|
| 560 |
+
"acc": 0.9473684430122375,
|
| 561 |
+
"nll": 0.08365084230899811,
|
| 562 |
+
"brier": 0.05513031780719757,
|
| 563 |
+
"ece": 0.05683338684464534,
|
| 564 |
+
"mean_conf": 0.9632882475852966
|
| 565 |
+
},
|
| 566 |
+
"cub_birds": {
|
| 567 |
+
"n": 209,
|
| 568 |
+
"acc": 0.9282296895980835,
|
| 569 |
+
"nll": 0.20787712931632996,
|
| 570 |
+
"brier": 0.09552589058876038,
|
| 571 |
+
"ece": 0.03924435088578615,
|
| 572 |
+
"mean_conf": 0.9157379269599915
|
| 573 |
+
},
|
| 574 |
+
"dbpedia": {
|
| 575 |
+
"n": 83,
|
| 576 |
+
"acc": 0.9879518151283264,
|
| 577 |
+
"nll": 0.02914765290915966,
|
| 578 |
+
"brier": 0.013987519778311253,
|
| 579 |
+
"ece": 0.020033360317049276,
|
| 580 |
+
"mean_conf": 0.9829169511795044
|
| 581 |
+
},
|
| 582 |
+
"dtd": {
|
| 583 |
+
"n": 193,
|
| 584 |
+
"acc": 0.9326424598693848,
|
| 585 |
+
"nll": 0.19226685166358948,
|
| 586 |
+
"brier": 0.10932108014822006,
|
| 587 |
+
"ece": 0.04905586869892781,
|
| 588 |
+
"mean_conf": 0.926359236240387
|
| 589 |
+
},
|
| 590 |
+
"eurosat": {
|
| 591 |
+
"n": 200,
|
| 592 |
+
"acc": 0.9700000286102295,
|
| 593 |
+
"nll": 0.09493371099233627,
|
| 594 |
+
"brier": 0.04979003965854645,
|
| 595 |
+
"ece": 0.019272321772114326,
|
| 596 |
+
"mean_conf": 0.964755117893219
|
| 597 |
+
},
|
| 598 |
+
"go_emotions": {
|
| 599 |
+
"n": 156,
|
| 600 |
+
"acc": 0.8461538553237915,
|
| 601 |
+
"nll": 0.3998943567276001,
|
| 602 |
+
"brier": 0.2204635888338089,
|
| 603 |
+
"ece": 0.06369368834835576,
|
| 604 |
+
"mean_conf": 0.8688168525695801
|
| 605 |
+
},
|
| 606 |
+
"gtsrb": {
|
| 607 |
+
"n": 150,
|
| 608 |
+
"acc": 0.9266666769981384,
|
| 609 |
+
"nll": 0.25059908628463745,
|
| 610 |
+
"brier": 0.09959184378385544,
|
| 611 |
+
"ece": 0.04895997661024053,
|
| 612 |
+
"mean_conf": 0.9357502460479736
|
| 613 |
+
},
|
| 614 |
+
"hellaswag": {
|
| 615 |
+
"n": 71,
|
| 616 |
+
"acc": 0.9718309640884399,
|
| 617 |
+
"nll": 0.07610221207141876,
|
| 618 |
+
"brier": 0.034769345074892044,
|
| 619 |
+
"ece": 0.0420044008320708,
|
| 620 |
+
"mean_conf": 0.9625624418258667
|
| 621 |
+
},
|
| 622 |
+
"imagenet100": {
|
| 623 |
+
"n": 209,
|
| 624 |
+
"acc": 0.9521530866622925,
|
| 625 |
+
"nll": 0.140834778547287,
|
| 626 |
+
"brier": 0.06210394203662872,
|
| 627 |
+
"ece": 0.02685701278059019,
|
| 628 |
+
"mean_conf": 0.9609060883522034
|
| 629 |
+
},
|
| 630 |
+
"jailbreak": {
|
| 631 |
+
"n": 43,
|
| 632 |
+
"acc": 1.0,
|
| 633 |
+
"nll": 0.007107510231435299,
|
| 634 |
+
"brier": 0.0007506122929044068,
|
| 635 |
+
"ece": 0.006904559023125811,
|
| 636 |
+
"mean_conf": 0.9930953979492188
|
| 637 |
+
},
|
| 638 |
+
"mnli": {
|
| 639 |
+
"n": 88,
|
| 640 |
+
"acc": 0.9204545617103577,
|
| 641 |
+
"nll": 0.18531149625778198,
|
| 642 |
+
"brier": 0.10737426578998566,
|
| 643 |
+
"ece": 0.06684734037705664,
|
| 644 |
+
"mean_conf": 0.8966826796531677
|
| 645 |
+
},
|
| 646 |
+
"ms_marco": {
|
| 647 |
+
"n": 77,
|
| 648 |
+
"acc": 0.5714285969734192,
|
| 649 |
+
"nll": 1.1738694906234741,
|
| 650 |
+
"brier": 0.5511745810508728,
|
| 651 |
+
"ece": 0.1849330931448978,
|
| 652 |
+
"mean_conf": 0.5303053855895996
|
| 653 |
+
},
|
| 654 |
+
"newsgroups": {
|
| 655 |
+
"n": 72,
|
| 656 |
+
"acc": 0.8194444179534912,
|
| 657 |
+
"nll": 0.6413953304290771,
|
| 658 |
+
"brier": 0.2439313530921936,
|
| 659 |
+
"ece": 0.0672152123840676,
|
| 660 |
+
"mean_conf": 0.8347481489181519
|
| 661 |
+
},
|
| 662 |
+
"openbookqa": {
|
| 663 |
+
"n": 73,
|
| 664 |
+
"acc": 0.9452054500579834,
|
| 665 |
+
"nll": 0.21645799279212952,
|
| 666 |
+
"brier": 0.10464119911193848,
|
| 667 |
+
"ece": 0.07754900295525902,
|
| 668 |
+
"mean_conf": 0.8753600716590881
|
| 669 |
+
},
|
| 670 |
+
"paws": {
|
| 671 |
+
"n": 75,
|
| 672 |
+
"acc": 0.8799999952316284,
|
| 673 |
+
"nll": 0.23008832335472107,
|
| 674 |
+
"brier": 0.15555186569690704,
|
| 675 |
+
"ece": 0.07505809075017267,
|
| 676 |
+
"mean_conf": 0.9440792798995972
|
| 677 |
+
},
|
| 678 |
+
"prompt_injection": {
|
| 679 |
+
"n": 19,
|
| 680 |
+
"acc": 1.0,
|
| 681 |
+
"nll": 0.0893387570977211,
|
| 682 |
+
"brier": 0.0387185737490654,
|
| 683 |
+
"ece": 0.07711471421890259,
|
| 684 |
+
"mean_conf": 0.9228852987289429
|
| 685 |
+
},
|
| 686 |
+
"qasc": {
|
| 687 |
+
"n": 78,
|
| 688 |
+
"acc": 0.8974359035491943,
|
| 689 |
+
"nll": 0.2294393628835678,
|
| 690 |
+
"brier": 0.12008954584598541,
|
| 691 |
+
"ece": 0.07760370314998621,
|
| 692 |
+
"mean_conf": 0.8928042650222778
|
| 693 |
+
},
|
| 694 |
+
"qnli": {
|
| 695 |
+
"n": 79,
|
| 696 |
+
"acc": 0.9620253443717957,
|
| 697 |
+
"nll": 0.13672247529029846,
|
| 698 |
+
"brier": 0.0658198893070221,
|
| 699 |
+
"ece": 0.04174679779133661,
|
| 700 |
+
"mean_conf": 0.9221668243408203
|
| 701 |
+
},
|
| 702 |
+
"qqp": {
|
| 703 |
+
"n": 67,
|
| 704 |
+
"acc": 0.8507462739944458,
|
| 705 |
+
"nll": 0.3204824924468994,
|
| 706 |
+
"brier": 0.20365533232688904,
|
| 707 |
+
"ece": 0.05547176195417913,
|
| 708 |
+
"mean_conf": 0.8596541285514832
|
| 709 |
+
},
|
| 710 |
+
"race": {
|
| 711 |
+
"n": 71,
|
| 712 |
+
"acc": 0.8169013857841492,
|
| 713 |
+
"nll": 0.508931040763855,
|
| 714 |
+
"brier": 0.2651640772819519,
|
| 715 |
+
"ece": 0.09953184393917758,
|
| 716 |
+
"mean_conf": 0.855144202709198
|
| 717 |
+
},
|
| 718 |
+
"resisc45": {
|
| 719 |
+
"n": 201,
|
| 720 |
+
"acc": 0.9452736377716064,
|
| 721 |
+
"nll": 0.1719132363796234,
|
| 722 |
+
"brier": 0.07461114972829819,
|
| 723 |
+
"ece": 0.03276641192363214,
|
| 724 |
+
"mean_conf": 0.9654509425163269
|
| 725 |
+
},
|
| 726 |
+
"rte": {
|
| 727 |
+
"n": 42,
|
| 728 |
+
"acc": 0.9047619104385376,
|
| 729 |
+
"nll": 0.2149493396282196,
|
| 730 |
+
"brier": 0.12409208714962006,
|
| 731 |
+
"ece": 0.09782490985591286,
|
| 732 |
+
"mean_conf": 0.9137647747993469
|
| 733 |
+
},
|
| 734 |
+
"rvl_cdip": {
|
| 735 |
+
"n": 207,
|
| 736 |
+
"acc": 0.9227052927017212,
|
| 737 |
+
"nll": 0.24763381481170654,
|
| 738 |
+
"brier": 0.11820948868989944,
|
| 739 |
+
"ece": 0.024818566785942367,
|
| 740 |
+
"mean_conf": 0.9276553392410278
|
| 741 |
+
},
|
| 742 |
+
"sciq": {
|
| 743 |
+
"n": 70,
|
| 744 |
+
"acc": 0.9571428298950195,
|
| 745 |
+
"nll": 0.08641297370195389,
|
| 746 |
+
"brier": 0.0542028583586216,
|
| 747 |
+
"ece": 0.05665828745125506,
|
| 748 |
+
"mean_conf": 0.9611032605171204
|
| 749 |
+
},
|
| 750 |
+
"snli": {
|
| 751 |
+
"n": 78,
|
| 752 |
+
"acc": 0.9615384340286255,
|
| 753 |
+
"nll": 0.14562663435935974,
|
| 754 |
+
"brier": 0.07991129904985428,
|
| 755 |
+
"ece": 0.05743888654096996,
|
| 756 |
+
"mean_conf": 0.9150496125221252
|
| 757 |
+
},
|
| 758 |
+
"squad_v2": {
|
| 759 |
+
"n": 83,
|
| 760 |
+
"acc": 0.8795180916786194,
|
| 761 |
+
"nll": 0.3431330621242523,
|
| 762 |
+
"brier": 0.18107561767101288,
|
| 763 |
+
"ece": 0.08313956865378169,
|
| 764 |
+
"mean_conf": 0.9385043978691101
|
| 765 |
+
},
|
| 766 |
+
"sst5": {
|
| 767 |
+
"n": 101,
|
| 768 |
+
"acc": 0.6732673048973083,
|
| 769 |
+
"nll": 0.8503657579421997,
|
| 770 |
+
"brier": 0.49845677614212036,
|
| 771 |
+
"ece": 0.0914942533168388,
|
| 772 |
+
"mean_conf": 0.5979520082473755,
|
| 773 |
+
"score_mae": 0.46103909611701965
|
| 774 |
+
},
|
| 775 |
+
"stanford_cars": {
|
| 776 |
+
"n": 199,
|
| 777 |
+
"acc": 0.9849246144294739,
|
| 778 |
+
"nll": 0.0664161965250969,
|
| 779 |
+
"brier": 0.03385894373059273,
|
| 780 |
+
"ece": 0.02054634083136521,
|
| 781 |
+
"mean_conf": 0.9744680523872375
|
| 782 |
+
},
|
| 783 |
+
"sun397": {
|
| 784 |
+
"n": 205,
|
| 785 |
+
"acc": 0.9658536314964294,
|
| 786 |
+
"nll": 0.12713490426540375,
|
| 787 |
+
"brier": 0.05907195806503296,
|
| 788 |
+
"ece": 0.03913777098139376,
|
| 789 |
+
"mean_conf": 0.973029613494873
|
| 790 |
+
},
|
| 791 |
+
"toxic_chat": {
|
| 792 |
+
"n": 103,
|
| 793 |
+
"acc": 0.9805825352668762,
|
| 794 |
+
"nll": 0.04893926531076431,
|
| 795 |
+
"brier": 0.027793018147349358,
|
| 796 |
+
"ece": 0.0175806231729555,
|
| 797 |
+
"mean_conf": 0.971018373966217
|
| 798 |
+
},
|
| 799 |
+
"tweet_irony": {
|
| 800 |
+
"n": 73,
|
| 801 |
+
"acc": 0.7397260069847107,
|
| 802 |
+
"nll": 0.46547093987464905,
|
| 803 |
+
"brier": 0.3147864043712616,
|
| 804 |
+
"ece": 0.1386445264786409,
|
| 805 |
+
"mean_conf": 0.7930445075035095
|
| 806 |
+
},
|
| 807 |
+
"tweet_offensive": {
|
| 808 |
+
"n": 95,
|
| 809 |
+
"acc": 0.800000011920929,
|
| 810 |
+
"nll": 0.4134097099304199,
|
| 811 |
+
"brier": 0.263822078704834,
|
| 812 |
+
"ece": 0.12641781317580403,
|
| 813 |
+
"mean_conf": 0.8215466737747192
|
| 814 |
+
},
|
| 815 |
+
"tweet_sentiment": {
|
| 816 |
+
"n": 96,
|
| 817 |
+
"acc": 0.7604166865348816,
|
| 818 |
+
"nll": 0.6009461283683777,
|
| 819 |
+
"brier": 0.37088480591773987,
|
| 820 |
+
"ece": 0.09440179414827354,
|
| 821 |
+
"mean_conf": 0.7233335375785828
|
| 822 |
+
},
|
| 823 |
+
"vitaminc": {
|
| 824 |
+
"n": 84,
|
| 825 |
+
"acc": 0.9285714030265808,
|
| 826 |
+
"nll": 0.21254083514213562,
|
| 827 |
+
"brier": 0.11658700555562973,
|
| 828 |
+
"ece": 0.060456716703195634,
|
| 829 |
+
"mean_conf": 0.87336665391922
|
| 830 |
+
},
|
| 831 |
+
"winogrande": {
|
| 832 |
+
"n": 82,
|
| 833 |
+
"acc": 0.8292682766914368,
|
| 834 |
+
"nll": 0.3811376690864563,
|
| 835 |
+
"brier": 0.24891741573810577,
|
| 836 |
+
"ece": 0.07569023195409919,
|
| 837 |
+
"mean_conf": 0.8934732675552368
|
| 838 |
+
},
|
| 839 |
+
"yahoo_topics": {
|
| 840 |
+
"n": 71,
|
| 841 |
+
"acc": 0.7746478915214539,
|
| 842 |
+
"nll": 0.6164750456809998,
|
| 843 |
+
"brier": 0.2843913733959198,
|
| 844 |
+
"ece": 0.10321198388129582,
|
| 845 |
+
"mean_conf": 0.7571716904640198
|
| 846 |
+
},
|
| 847 |
+
"yelp": {
|
| 848 |
+
"n": 127,
|
| 849 |
+
"acc": 0.834645688533783,
|
| 850 |
+
"nll": 0.4197368919849396,
|
| 851 |
+
"brier": 0.23375587165355682,
|
| 852 |
+
"ece": 0.036530637687290035,
|
| 853 |
+
"mean_conf": 0.8449493646621704,
|
| 854 |
+
"score_mae": 0.32252421975135803
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
},
|
| 858 |
+
"reliability_raw": [
|
| 859 |
+
{
|
| 860 |
+
"lo": 0.133,
|
| 861 |
+
"hi": 0.2,
|
| 862 |
+
"conf": 0.17327187955379486,
|
| 863 |
+
"acc": 0.0,
|
| 864 |
+
"frac": 0.0007889546104706824
|
| 865 |
+
},
|
| 866 |
+
{
|
| 867 |
+
"lo": 0.2,
|
| 868 |
+
"hi": 0.267,
|
| 869 |
+
"conf": 0.23755492269992828,
|
| 870 |
+
"acc": 0.2222222238779068,
|
| 871 |
+
"frac": 0.001775147975422442
|
| 872 |
+
},
|
| 873 |
+
{
|
| 874 |
+
"lo": 0.267,
|
| 875 |
+
"hi": 0.333,
|
| 876 |
+
"conf": 0.3051908314228058,
|
| 877 |
+
"acc": 0.2800000011920929,
|
| 878 |
+
"frac": 0.004930966533720493
|
| 879 |
+
},
|
| 880 |
+
{
|
| 881 |
+
"lo": 0.333,
|
| 882 |
+
"hi": 0.4,
|
| 883 |
+
"conf": 0.36935192346572876,
|
| 884 |
+
"acc": 0.26923078298568726,
|
| 885 |
+
"frac": 0.0051282052882015705
|
| 886 |
+
},
|
| 887 |
+
{
|
| 888 |
+
"lo": 0.4,
|
| 889 |
+
"hi": 0.467,
|
| 890 |
+
"conf": 0.43677520751953125,
|
| 891 |
+
"acc": 0.42307692766189575,
|
| 892 |
+
"frac": 0.010256410576403141
|
| 893 |
+
},
|
| 894 |
+
{
|
| 895 |
+
"lo": 0.467,
|
| 896 |
+
"hi": 0.533,
|
| 897 |
+
"conf": 0.5065982937812805,
|
| 898 |
+
"acc": 0.5289855003356934,
|
| 899 |
+
"frac": 0.027218934148550034
|
| 900 |
+
},
|
| 901 |
+
{
|
| 902 |
+
"lo": 0.533,
|
| 903 |
+
"hi": 0.6,
|
| 904 |
+
"conf": 0.5669992566108704,
|
| 905 |
+
"acc": 0.5779221057891846,
|
| 906 |
+
"frac": 0.03037475422024727
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"lo": 0.6,
|
| 910 |
+
"hi": 0.667,
|
| 911 |
+
"conf": 0.63744056224823,
|
| 912 |
+
"acc": 0.6171428561210632,
|
| 913 |
+
"frac": 0.03451676666736603
|
| 914 |
+
},
|
| 915 |
+
{
|
| 916 |
+
"lo": 0.667,
|
| 917 |
+
"hi": 0.733,
|
| 918 |
+
"conf": 0.7005413770675659,
|
| 919 |
+
"acc": 0.6280487775802612,
|
| 920 |
+
"frac": 0.03234713897109032
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"lo": 0.733,
|
| 924 |
+
"hi": 0.8,
|
| 925 |
+
"conf": 0.767250657081604,
|
| 926 |
+
"acc": 0.7511312365531921,
|
| 927 |
+
"frac": 0.043589744716882706
|
| 928 |
+
},
|
| 929 |
+
{
|
| 930 |
+
"lo": 0.8,
|
| 931 |
+
"hi": 0.867,
|
| 932 |
+
"conf": 0.834570586681366,
|
| 933 |
+
"acc": 0.80694979429245,
|
| 934 |
+
"frac": 0.051084812730550766
|
| 935 |
+
},
|
| 936 |
+
{
|
| 937 |
+
"lo": 0.867,
|
| 938 |
+
"hi": 0.933,
|
| 939 |
+
"conf": 0.9034878015518188,
|
| 940 |
+
"acc": 0.9073033928871155,
|
| 941 |
+
"frac": 0.07021696120500565
|
| 942 |
+
},
|
| 943 |
+
{
|
| 944 |
+
"lo": 0.933,
|
| 945 |
+
"hi": 1.0,
|
| 946 |
+
"conf": 0.9904627203941345,
|
| 947 |
+
"acc": 0.9853742718696594,
|
| 948 |
+
"frac": 0.687771201133728
|
| 949 |
+
}
|
| 950 |
+
],
|
| 951 |
+
"reliability_calibrated": [
|
| 952 |
+
{
|
| 953 |
+
"lo": 0.133,
|
| 954 |
+
"hi": 0.2,
|
| 955 |
+
"conf": 0.17058970034122467,
|
| 956 |
+
"acc": 0.0,
|
| 957 |
+
"frac": 0.0009861933067440987
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"lo": 0.2,
|
| 961 |
+
"hi": 0.267,
|
| 962 |
+
"conf": 0.24141208827495575,
|
| 963 |
+
"acc": 0.23076923191547394,
|
| 964 |
+
"frac": 0.0025641026441007853
|
| 965 |
+
},
|
| 966 |
+
{
|
| 967 |
+
"lo": 0.267,
|
| 968 |
+
"hi": 0.333,
|
| 969 |
+
"conf": 0.30261722207069397,
|
| 970 |
+
"acc": 0.3076923191547394,
|
| 971 |
+
"frac": 0.0051282052882015705
|
| 972 |
+
},
|
| 973 |
+
{
|
| 974 |
+
"lo": 0.333,
|
| 975 |
+
"hi": 0.4,
|
| 976 |
+
"conf": 0.3718169331550598,
|
| 977 |
+
"acc": 0.29032257199287415,
|
| 978 |
+
"frac": 0.006114398594945669
|
| 979 |
+
},
|
| 980 |
+
{
|
| 981 |
+
"lo": 0.4,
|
| 982 |
+
"hi": 0.467,
|
| 983 |
+
"conf": 0.4343971312046051,
|
| 984 |
+
"acc": 0.5079365372657776,
|
| 985 |
+
"frac": 0.012426035478711128
|
| 986 |
+
},
|
| 987 |
+
{
|
| 988 |
+
"lo": 0.467,
|
| 989 |
+
"hi": 0.533,
|
| 990 |
+
"conf": 0.5043900012969971,
|
| 991 |
+
"acc": 0.5302013158798218,
|
| 992 |
+
"frac": 0.029388559982180595
|
| 993 |
+
},
|
| 994 |
+
{
|
| 995 |
+
"lo": 0.533,
|
| 996 |
+
"hi": 0.6,
|
| 997 |
+
"conf": 0.5663641691207886,
|
| 998 |
+
"acc": 0.5602409839630127,
|
| 999 |
+
"frac": 0.032741617411375046
|
| 1000 |
+
},
|
| 1001 |
+
{
|
| 1002 |
+
"lo": 0.6,
|
| 1003 |
+
"hi": 0.667,
|
| 1004 |
+
"conf": 0.6334572434425354,
|
| 1005 |
+
"acc": 0.6188119053840637,
|
| 1006 |
+
"frac": 0.039842210710048676
|
| 1007 |
+
},
|
| 1008 |
+
{
|
| 1009 |
+
"lo": 0.667,
|
| 1010 |
+
"hi": 0.733,
|
| 1011 |
+
"conf": 0.7039550542831421,
|
| 1012 |
+
"acc": 0.6941747665405273,
|
| 1013 |
+
"frac": 0.040631163865327835
|
| 1014 |
+
},
|
| 1015 |
+
{
|
| 1016 |
+
"lo": 0.733,
|
| 1017 |
+
"hi": 0.8,
|
| 1018 |
+
"conf": 0.7678379416465759,
|
| 1019 |
+
"acc": 0.7991452813148499,
|
| 1020 |
+
"frac": 0.04615384712815285
|
| 1021 |
+
},
|
| 1022 |
+
{
|
| 1023 |
+
"lo": 0.8,
|
| 1024 |
+
"hi": 0.867,
|
| 1025 |
+
"conf": 0.8343891501426697,
|
| 1026 |
+
"acc": 0.8534798622131348,
|
| 1027 |
+
"frac": 0.05384615436196327
|
| 1028 |
+
},
|
| 1029 |
+
{
|
| 1030 |
+
"lo": 0.867,
|
| 1031 |
+
"hi": 0.933,
|
| 1032 |
+
"conf": 0.9034571051597595,
|
| 1033 |
+
"acc": 0.9114832282066345,
|
| 1034 |
+
"frac": 0.08244576305150986
|
| 1035 |
+
},
|
| 1036 |
+
{
|
| 1037 |
+
"lo": 0.933,
|
| 1038 |
+
"hi": 1.0,
|
| 1039 |
+
"conf": 0.9883154630661011,
|
| 1040 |
+
"acc": 0.9902557730674744,
|
| 1041 |
+
"frac": 0.6477317810058594
|
| 1042 |
+
}
|
| 1043 |
+
]
|
| 1044 |
+
},
|
| 1045 |
+
"heldout": {
|
| 1046 |
+
"raw": {
|
| 1047 |
+
"overall": {
|
| 1048 |
+
"n": 9888,
|
| 1049 |
+
"acc": 0.7774069309234619,
|
| 1050 |
+
"nll": 0.6259722709655762,
|
| 1051 |
+
"brier": 0.3122439980506897,
|
| 1052 |
+
"ece": 0.055361537657564505,
|
| 1053 |
+
"mean_conf": 0.8327684998512268
|
| 1054 |
+
},
|
| 1055 |
+
"by_type": {
|
| 1056 |
+
"choice": {
|
| 1057 |
+
"n": 9088,
|
| 1058 |
+
"acc": 0.7644146084785461,
|
| 1059 |
+
"nll": 0.6617501974105835,
|
| 1060 |
+
"brier": 0.3292696177959442,
|
| 1061 |
+
"ece": 0.05688635497695306,
|
| 1062 |
+
"mean_conf": 0.8213009238243103
|
| 1063 |
+
},
|
| 1064 |
+
"noul": {
|
| 1065 |
+
"n": 800,
|
| 1066 |
+
"acc": 0.925000011920929,
|
| 1067 |
+
"nll": 0.21953491866588593,
|
| 1068 |
+
"brier": 0.11883323639631271,
|
| 1069 |
+
"ece": 0.042017475422659586,
|
| 1070 |
+
"mean_conf": 0.9630394577980042
|
| 1071 |
+
}
|
| 1072 |
+
},
|
| 1073 |
+
"by_source": {
|
| 1074 |
+
"arc_easy": {
|
| 1075 |
+
"n": 500,
|
| 1076 |
+
"acc": 0.9760000109672546,
|
| 1077 |
+
"nll": 0.10126849263906479,
|
| 1078 |
+
"brier": 0.04353426396846771,
|
| 1079 |
+
"ece": 0.03141405401313018,
|
| 1080 |
+
"mean_conf": 0.9497805237770081
|
| 1081 |
+
},
|
| 1082 |
+
"askubuntu": {
|
| 1083 |
+
"n": 359,
|
| 1084 |
+
"acc": 0.5181058645248413,
|
| 1085 |
+
"nll": 1.715165376663208,
|
| 1086 |
+
"brier": 0.6542366743087769,
|
| 1087 |
+
"ece": 0.16121354500097346,
|
| 1088 |
+
"mean_conf": 0.679319441318512
|
| 1089 |
+
},
|
| 1090 |
+
"cifar10": {
|
| 1091 |
+
"n": 300,
|
| 1092 |
+
"acc": 0.9766666889190674,
|
| 1093 |
+
"nll": 0.06821396946907043,
|
| 1094 |
+
"brier": 0.033433299511671066,
|
| 1095 |
+
"ece": 0.0244025032659336,
|
| 1096 |
+
"mean_conf": 0.9716125726699829
|
| 1097 |
+
},
|
| 1098 |
+
"emotion": {
|
| 1099 |
+
"n": 500,
|
| 1100 |
+
"acc": 0.5799999833106995,
|
| 1101 |
+
"nll": 1.305448293685913,
|
| 1102 |
+
"brier": 0.6054953932762146,
|
| 1103 |
+
"ece": 0.1998288362110465,
|
| 1104 |
+
"mean_conf": 0.7798287868499756
|
| 1105 |
+
},
|
| 1106 |
+
"food101": {
|
| 1107 |
+
"n": 300,
|
| 1108 |
+
"acc": 0.8766666650772095,
|
| 1109 |
+
"nll": 0.35791701078414917,
|
| 1110 |
+
"brier": 0.15910951793193817,
|
| 1111 |
+
"ece": 0.04321668811345984,
|
| 1112 |
+
"mean_conf": 0.8755965232849121
|
| 1113 |
+
},
|
| 1114 |
+
"imdb": {
|
| 1115 |
+
"n": 500,
|
| 1116 |
+
"acc": 0.9399999976158142,
|
| 1117 |
+
"nll": 0.15117090940475464,
|
| 1118 |
+
"brier": 0.0857759416103363,
|
| 1119 |
+
"ece": 0.033096101656337,
|
| 1120 |
+
"mean_conf": 0.9650362730026245
|
| 1121 |
+
},
|
| 1122 |
+
"massive_intent": {
|
| 1123 |
+
"n": 500,
|
| 1124 |
+
"acc": 0.7459999918937683,
|
| 1125 |
+
"nll": 0.9858649969100952,
|
| 1126 |
+
"brier": 0.3682084381580353,
|
| 1127 |
+
"ece": 0.09032174787909839,
|
| 1128 |
+
"mean_conf": 0.8352954983711243
|
| 1129 |
+
},
|
| 1130 |
+
"mnist": {
|
| 1131 |
+
"n": 300,
|
| 1132 |
+
"acc": 0.95333331823349,
|
| 1133 |
+
"nll": 0.15937867760658264,
|
| 1134 |
+
"brier": 0.06808914244174957,
|
| 1135 |
+
"ece": 0.03201363153018352,
|
| 1136 |
+
"mean_conf": 0.9333906769752502
|
| 1137 |
+
},
|
| 1138 |
+
"ni_unseen_tasks": {
|
| 1139 |
+
"n": 5529,
|
| 1140 |
+
"acc": 0.752215564250946,
|
| 1141 |
+
"nll": 0.6323504447937012,
|
| 1142 |
+
"brier": 0.34408876299858093,
|
| 1143 |
+
"ece": 0.05498220219487805,
|
| 1144 |
+
"mean_conf": 0.8071978688240051
|
| 1145 |
+
},
|
| 1146 |
+
"pets": {
|
| 1147 |
+
"n": 300,
|
| 1148 |
+
"acc": 0.8500000238418579,
|
| 1149 |
+
"nll": 0.5367151498794556,
|
| 1150 |
+
"brier": 0.22500383853912354,
|
| 1151 |
+
"ece": 0.06403641861143206,
|
| 1152 |
+
"mean_conf": 0.8197709918022156
|
| 1153 |
+
},
|
| 1154 |
+
"pope": {
|
| 1155 |
+
"n": 300,
|
| 1156 |
+
"acc": 0.8999999761581421,
|
| 1157 |
+
"nll": 0.33347487449645996,
|
| 1158 |
+
"brier": 0.17392873764038086,
|
| 1159 |
+
"ece": 0.07330050353340761,
|
| 1160 |
+
"mean_conf": 0.9597114324569702
|
| 1161 |
+
},
|
| 1162 |
+
"pubmed_qa": {
|
| 1163 |
+
"n": 500,
|
| 1164 |
+
"acc": 0.7080000042915344,
|
| 1165 |
+
"nll": 0.738036036491394,
|
| 1166 |
+
"brier": 0.4015096426010132,
|
| 1167 |
+
"ece": 0.08785723579766086,
|
| 1168 |
+
"mean_conf": 0.7890949249267578
|
| 1169 |
+
}
|
| 1170 |
+
}
|
| 1171 |
+
},
|
| 1172 |
+
"calibrated": {
|
| 1173 |
+
"overall": {
|
| 1174 |
+
"n": 9888,
|
| 1175 |
+
"acc": 0.7774069309234619,
|
| 1176 |
+
"nll": 0.6107043027877808,
|
| 1177 |
+
"brier": 0.30924949049949646,
|
| 1178 |
+
"ece": 0.040644236667579256,
|
| 1179 |
+
"mean_conf": 0.8180511593818665
|
| 1180 |
+
},
|
| 1181 |
+
"by_type": {
|
| 1182 |
+
"choice": {
|
| 1183 |
+
"n": 9088,
|
| 1184 |
+
"acc": 0.7644146084785461,
|
| 1185 |
+
"nll": 0.6470264792442322,
|
| 1186 |
+
"brier": 0.32634666562080383,
|
| 1187 |
+
"ece": 0.041729311528364656,
|
| 1188 |
+
"mean_conf": 0.8061439394950867
|
| 1189 |
+
},
|
| 1190 |
+
"noul": {
|
| 1191 |
+
"n": 800,
|
| 1192 |
+
"acc": 0.925000011920929,
|
| 1193 |
+
"nll": 0.1980842649936676,
|
| 1194 |
+
"brier": 0.11502549052238464,
|
| 1195 |
+
"ece": 0.03511890266276535,
|
| 1196 |
+
"mean_conf": 0.9533174633979797
|
| 1197 |
+
}
|
| 1198 |
+
},
|
| 1199 |
+
"by_source": {
|
| 1200 |
+
"arc_easy": {
|
| 1201 |
+
"n": 500,
|
| 1202 |
+
"acc": 0.9760000109672546,
|
| 1203 |
+
"nll": 0.10715165734291077,
|
| 1204 |
+
"brier": 0.04551118239760399,
|
| 1205 |
+
"ece": 0.03949616313766471,
|
| 1206 |
+
"mean_conf": 0.9417195320129395
|
| 1207 |
+
},
|
| 1208 |
+
"askubuntu": {
|
| 1209 |
+
"n": 359,
|
| 1210 |
+
"acc": 0.5181058645248413,
|
| 1211 |
+
"nll": 1.6524147987365723,
|
| 1212 |
+
"brier": 0.6454375982284546,
|
| 1213 |
+
"ece": 0.1338557621591943,
|
| 1214 |
+
"mean_conf": 0.651961624622345
|
| 1215 |
+
},
|
| 1216 |
+
"cifar10": {
|
| 1217 |
+
"n": 300,
|
| 1218 |
+
"acc": 0.9766666889190674,
|
| 1219 |
+
"nll": 0.07086114585399628,
|
| 1220 |
+
"brier": 0.03372351825237274,
|
| 1221 |
+
"ece": 0.03409448859720851,
|
| 1222 |
+
"mean_conf": 0.9671555757522583
|
| 1223 |
+
},
|
| 1224 |
+
"emotion": {
|
| 1225 |
+
"n": 500,
|
| 1226 |
+
"acc": 0.5799999833106995,
|
| 1227 |
+
"nll": 1.2474008798599243,
|
| 1228 |
+
"brier": 0.5944139957427979,
|
| 1229 |
+
"ece": 0.17934864653363677,
|
| 1230 |
+
"mean_conf": 0.7593486309051514
|
| 1231 |
+
},
|
| 1232 |
+
"food101": {
|
| 1233 |
+
"n": 300,
|
| 1234 |
+
"acc": 0.8766666650772095,
|
| 1235 |
+
"nll": 0.3655674159526825,
|
| 1236 |
+
"brier": 0.1599694937467575,
|
| 1237 |
+
"ece": 0.05726011354152369,
|
| 1238 |
+
"mean_conf": 0.8594070672988892
|
| 1239 |
+
},
|
| 1240 |
+
"imdb": {
|
| 1241 |
+
"n": 500,
|
| 1242 |
+
"acc": 0.9399999976158142,
|
| 1243 |
+
"nll": 0.1412155032157898,
|
| 1244 |
+
"brier": 0.08349451422691345,
|
| 1245 |
+
"ece": 0.025615910650256563,
|
| 1246 |
+
"mean_conf": 0.955990195274353
|
| 1247 |
+
},
|
| 1248 |
+
"massive_intent": {
|
| 1249 |
+
"n": 500,
|
| 1250 |
+
"acc": 0.7459999918937683,
|
| 1251 |
+
"nll": 0.9505149722099304,
|
| 1252 |
+
"brier": 0.36250603199005127,
|
| 1253 |
+
"ece": 0.07815012133194074,
|
| 1254 |
+
"mean_conf": 0.8151710033416748
|
| 1255 |
+
},
|
| 1256 |
+
"mnist": {
|
| 1257 |
+
"n": 300,
|
| 1258 |
+
"acc": 0.95333331823349,
|
| 1259 |
+
"nll": 0.1676100343465805,
|
| 1260 |
+
"brier": 0.07024406641721725,
|
| 1261 |
+
"ece": 0.044357041122790236,
|
| 1262 |
+
"mean_conf": 0.9220753908157349
|
| 1263 |
+
},
|
| 1264 |
+
"ni_unseen_tasks": {
|
| 1265 |
+
"n": 5529,
|
| 1266 |
+
"acc": 0.752215564250946,
|
| 1267 |
+
"nll": 0.6206715106964111,
|
| 1268 |
+
"brier": 0.34118887782096863,
|
| 1269 |
+
"ece": 0.040373205554264546,
|
| 1270 |
+
"mean_conf": 0.7925888299942017
|
| 1271 |
+
},
|
| 1272 |
+
"pets": {
|
| 1273 |
+
"n": 300,
|
| 1274 |
+
"acc": 0.8500000238418579,
|
| 1275 |
+
"nll": 0.5368061065673828,
|
| 1276 |
+
"brier": 0.22789603471755981,
|
| 1277 |
+
"ece": 0.07454185713334927,
|
| 1278 |
+
"mean_conf": 0.8035118579864502
|
| 1279 |
+
},
|
| 1280 |
+
"pope": {
|
| 1281 |
+
"n": 300,
|
| 1282 |
+
"acc": 0.8999999761581421,
|
| 1283 |
+
"nll": 0.29286548495292664,
|
| 1284 |
+
"brier": 0.1675771325826645,
|
| 1285 |
+
"ece": 0.06612932776747837,
|
| 1286 |
+
"mean_conf": 0.9488630890846252
|
| 1287 |
+
},
|
| 1288 |
+
"pubmed_qa": {
|
| 1289 |
+
"n": 500,
|
| 1290 |
+
"acc": 0.7080000042915344,
|
| 1291 |
+
"nll": 0.7209590077400208,
|
| 1292 |
+
"brier": 0.39785510301589966,
|
| 1293 |
+
"ece": 0.07103049902030684,
|
| 1294 |
+
"mean_conf": 0.7723875641822815
|
| 1295 |
+
}
|
| 1296 |
+
}
|
| 1297 |
+
},
|
| 1298 |
+
"reliability_raw": [
|
| 1299 |
+
{
|
| 1300 |
+
"lo": 0.133,
|
| 1301 |
+
"hi": 0.2,
|
| 1302 |
+
"conf": 0.18254680931568146,
|
| 1303 |
+
"acc": 0.0,
|
| 1304 |
+
"frac": 0.00040453075780533254
|
| 1305 |
+
},
|
| 1306 |
+
{
|
| 1307 |
+
"lo": 0.2,
|
| 1308 |
+
"hi": 0.267,
|
| 1309 |
+
"conf": 0.24415171146392822,
|
| 1310 |
+
"acc": 0.125,
|
| 1311 |
+
"frac": 0.004045307636260986
|
| 1312 |
+
},
|
| 1313 |
+
{
|
| 1314 |
+
"lo": 0.267,
|
| 1315 |
+
"hi": 0.333,
|
| 1316 |
+
"conf": 0.3000350594520569,
|
| 1317 |
+
"acc": 0.2711864411830902,
|
| 1318 |
+
"frac": 0.011933657340705395
|
| 1319 |
+
},
|
| 1320 |
+
{
|
| 1321 |
+
"lo": 0.333,
|
| 1322 |
+
"hi": 0.4,
|
| 1323 |
+
"conf": 0.36993393301963806,
|
| 1324 |
+
"acc": 0.2829268276691437,
|
| 1325 |
+
"frac": 0.020732199773192406
|
| 1326 |
+
},
|
| 1327 |
+
{
|
| 1328 |
+
"lo": 0.4,
|
| 1329 |
+
"hi": 0.467,
|
| 1330 |
+
"conf": 0.434598833322525,
|
| 1331 |
+
"acc": 0.356890469789505,
|
| 1332 |
+
"frac": 0.02862055040895939
|
| 1333 |
+
},
|
| 1334 |
+
{
|
| 1335 |
+
"lo": 0.467,
|
| 1336 |
+
"hi": 0.533,
|
| 1337 |
+
"conf": 0.5052576661109924,
|
| 1338 |
+
"acc": 0.4368070960044861,
|
| 1339 |
+
"frac": 0.04561084136366844
|
| 1340 |
+
},
|
| 1341 |
+
{
|
| 1342 |
+
"lo": 0.533,
|
| 1343 |
+
"hi": 0.6,
|
| 1344 |
+
"conf": 0.5669507384300232,
|
| 1345 |
+
"acc": 0.4856114983558655,
|
| 1346 |
+
"frac": 0.05622977390885353
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"lo": 0.6,
|
| 1350 |
+
"hi": 0.667,
|
| 1351 |
+
"conf": 0.6334361433982849,
|
| 1352 |
+
"acc": 0.5653104782104492,
|
| 1353 |
+
"frac": 0.047228965908288956
|
| 1354 |
+
},
|
| 1355 |
+
{
|
| 1356 |
+
"lo": 0.667,
|
| 1357 |
+
"hi": 0.733,
|
| 1358 |
+
"conf": 0.701598048210144,
|
| 1359 |
+
"acc": 0.6368821263313293,
|
| 1360 |
+
"frac": 0.05319579318165779
|
| 1361 |
+
},
|
| 1362 |
+
{
|
| 1363 |
+
"lo": 0.733,
|
| 1364 |
+
"hi": 0.8,
|
| 1365 |
+
"conf": 0.7661896347999573,
|
| 1366 |
+
"acc": 0.7110726833343506,
|
| 1367 |
+
"frac": 0.058454692363739014
|
| 1368 |
+
},
|
| 1369 |
+
{
|
| 1370 |
+
"lo": 0.8,
|
| 1371 |
+
"hi": 0.867,
|
| 1372 |
+
"conf": 0.8357764482498169,
|
| 1373 |
+
"acc": 0.7466007471084595,
|
| 1374 |
+
"frac": 0.08181634545326233
|
| 1375 |
+
},
|
| 1376 |
+
{
|
| 1377 |
+
"lo": 0.867,
|
| 1378 |
+
"hi": 0.933,
|
| 1379 |
+
"conf": 0.9009120464324951,
|
| 1380 |
+
"acc": 0.8152466416358948,
|
| 1381 |
+
"frac": 0.1127629429101944
|
| 1382 |
+
},
|
| 1383 |
+
{
|
| 1384 |
+
"lo": 0.933,
|
| 1385 |
+
"hi": 1.0,
|
| 1386 |
+
"conf": 0.9835764169692993,
|
| 1387 |
+
"acc": 0.9503800868988037,
|
| 1388 |
+
"frac": 0.4789643883705139
|
| 1389 |
+
}
|
| 1390 |
+
],
|
| 1391 |
+
"reliability_calibrated": [
|
| 1392 |
+
{
|
| 1393 |
+
"lo": 0.133,
|
| 1394 |
+
"hi": 0.2,
|
| 1395 |
+
"conf": 0.17538046836853027,
|
| 1396 |
+
"acc": 0.0,
|
| 1397 |
+
"frac": 0.0006067961221560836
|
| 1398 |
+
},
|
| 1399 |
+
{
|
| 1400 |
+
"lo": 0.2,
|
| 1401 |
+
"hi": 0.267,
|
| 1402 |
+
"conf": 0.24302583932876587,
|
| 1403 |
+
"acc": 0.16981132328510284,
|
| 1404 |
+
"frac": 0.005360032431781292
|
| 1405 |
+
},
|
| 1406 |
+
{
|
| 1407 |
+
"lo": 0.267,
|
| 1408 |
+
"hi": 0.333,
|
| 1409 |
+
"conf": 0.30060428380966187,
|
| 1410 |
+
"acc": 0.25362318754196167,
|
| 1411 |
+
"frac": 0.013956310227513313
|
| 1412 |
+
},
|
| 1413 |
+
{
|
| 1414 |
+
"lo": 0.333,
|
| 1415 |
+
"hi": 0.4,
|
| 1416 |
+
"conf": 0.36858007311820984,
|
| 1417 |
+
"acc": 0.3234042525291443,
|
| 1418 |
+
"frac": 0.023766180500388145
|
| 1419 |
+
},
|
| 1420 |
+
{
|
| 1421 |
+
"lo": 0.4,
|
| 1422 |
+
"hi": 0.467,
|
| 1423 |
+
"conf": 0.4340149462223053,
|
| 1424 |
+
"acc": 0.3523809611797333,
|
| 1425 |
+
"frac": 0.03185679763555527
|
| 1426 |
+
},
|
| 1427 |
+
{
|
| 1428 |
+
"lo": 0.467,
|
| 1429 |
+
"hi": 0.533,
|
| 1430 |
+
"conf": 0.5055366158485413,
|
| 1431 |
+
"acc": 0.45656564831733704,
|
| 1432 |
+
"frac": 0.05006067827343941
|
| 1433 |
+
},
|
| 1434 |
+
{
|
| 1435 |
+
"lo": 0.533,
|
| 1436 |
+
"hi": 0.6,
|
| 1437 |
+
"conf": 0.5651605129241943,
|
| 1438 |
+
"acc": 0.5167548656463623,
|
| 1439 |
+
"frac": 0.05734223127365112
|
| 1440 |
+
},
|
| 1441 |
+
{
|
| 1442 |
+
"lo": 0.6,
|
| 1443 |
+
"hi": 0.667,
|
| 1444 |
+
"conf": 0.6337770223617554,
|
| 1445 |
+
"acc": 0.5630081295967102,
|
| 1446 |
+
"frac": 0.04975728318095207
|
| 1447 |
+
},
|
| 1448 |
+
{
|
| 1449 |
+
"lo": 0.667,
|
| 1450 |
+
"hi": 0.733,
|
| 1451 |
+
"conf": 0.7002703547477722,
|
| 1452 |
+
"acc": 0.6780238747596741,
|
| 1453 |
+
"frac": 0.059364885091781616
|
| 1454 |
+
},
|
| 1455 |
+
{
|
| 1456 |
+
"lo": 0.733,
|
| 1457 |
+
"hi": 0.8,
|
| 1458 |
+
"conf": 0.7682633399963379,
|
| 1459 |
+
"acc": 0.7181528806686401,
|
| 1460 |
+
"frac": 0.06351132690906525
|
| 1461 |
+
},
|
| 1462 |
+
{
|
| 1463 |
+
"lo": 0.8,
|
| 1464 |
+
"hi": 0.867,
|
| 1465 |
+
"conf": 0.8352631330490112,
|
| 1466 |
+
"acc": 0.7694915533065796,
|
| 1467 |
+
"frac": 0.08950242400169373
|
| 1468 |
+
},
|
| 1469 |
+
{
|
| 1470 |
+
"lo": 0.867,
|
| 1471 |
+
"hi": 0.933,
|
| 1472 |
+
"conf": 0.9024354219436646,
|
| 1473 |
+
"acc": 0.8416013717651367,
|
| 1474 |
+
"frac": 0.11620145291090012
|
| 1475 |
+
},
|
| 1476 |
+
{
|
| 1477 |
+
"lo": 0.933,
|
| 1478 |
+
"hi": 1.0,
|
| 1479 |
+
"conf": 0.9815592169761658,
|
| 1480 |
+
"acc": 0.9596588015556335,
|
| 1481 |
+
"frac": 0.43871358036994934
|
| 1482 |
+
}
|
| 1483 |
+
]
|
| 1484 |
+
},
|
| 1485 |
+
"fork_invariance_max_dp": 6.145238876342773e-05,
|
| 1486 |
+
"latency_ms": {
|
| 1487 |
+
"1": {
|
| 1488 |
+
"fork": 108.40368270874023,
|
| 1489 |
+
"separate": 959.8970413208008
|
| 1490 |
+
},
|
| 1491 |
+
"4": {
|
| 1492 |
+
"fork": 107.42926597595215,
|
| 1493 |
+
"separate": 1998.2314109802246
|
| 1494 |
+
},
|
| 1495 |
+
"16": {
|
| 1496 |
+
"fork": 113.10410499572754,
|
| 1497 |
+
"separate": 7827.5275230407715
|
| 1498 |
+
},
|
| 1499 |
+
"64": {
|
| 1500 |
+
"fork": 256.7019462585449,
|
| 1501 |
+
"separate": 16031.468868255615
|
| 1502 |
+
}
|
| 1503 |
+
}
|
| 1504 |
+
}
|
examples/request.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"state": {
|
| 3 |
+
"ticket": {
|
| 4 |
+
"id": 4412,
|
| 5 |
+
"plan": "pro",
|
| 6 |
+
"subject": "Charged twice??",
|
| 7 |
+
"body": "Hi, I just noticed I was charged twice for my subscription this month. This is the second time it has happened and honestly I'm pretty fed up. Please refund the extra charge today or I'm cancelling."
|
| 8 |
+
}
|
| 9 |
+
},
|
| 10 |
+
"questions": {
|
| 11 |
+
"wants_refund": {"type": "noul", "instructions": "The customer is asking for money back."},
|
| 12 |
+
"threatens_churn": {"type": "noul", "instructions": "The customer threatens to cancel or leave."},
|
| 13 |
+
"team": {
|
| 14 |
+
"type": "choice",
|
| 15 |
+
"instructions": "Which team should handle this ticket?",
|
| 16 |
+
"criteria": {
|
| 17 |
+
"billing": "Charges, invoices, refunds, payment methods",
|
| 18 |
+
"technical": "Bugs, outages, errors, performance problems",
|
| 19 |
+
"sales": "Upgrades, pricing questions, new purchases",
|
| 20 |
+
"account": "Login, password, profile and access issues"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"frustration": {
|
| 24 |
+
"type": "score",
|
| 25 |
+
"instructions": "How frustrated is the customer?",
|
| 26 |
+
"criteria": ["Calm and neutral", "Mildly annoyed", "Clearly frustrated", "Furious or abusive"]
|
| 27 |
+
},
|
| 28 |
+
"language": {"type": "choice", "instructions": "Which language is the ticket written in?", "criteria": ["english", "spanish", "german", "french"]}
|
| 29 |
+
}
|
| 30 |
+
}
|
examples/systemone_request.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "zico-latest",
|
| 3 |
+
"state": {
|
| 4 |
+
"ticket": {
|
| 5 |
+
"id": 4412,
|
| 6 |
+
"plan": "pro",
|
| 7 |
+
"subject": "Charged twice??",
|
| 8 |
+
"body": "Hi, I just noticed I was charged twice for my subscription this month. This is the second time it has happened and honestly I'm pretty fed up. Please refund the extra charge today or I'm cancelling."
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"questions": {
|
| 12 |
+
"wants_refund": {"type": "noul", "instructions": "The customer is asking for money back."},
|
| 13 |
+
"threatens_churn": {"type": "noul", "instructions": "The customer threatens to cancel or leave."},
|
| 14 |
+
"team": {
|
| 15 |
+
"type": "choice",
|
| 16 |
+
"instructions": "Which team should handle this ticket?",
|
| 17 |
+
"criteria": {
|
| 18 |
+
"billing": "Charges, invoices, refunds, payment methods",
|
| 19 |
+
"technical": "Bugs, outages, errors, performance problems",
|
| 20 |
+
"sales": "Upgrades, pricing questions, new purchases",
|
| 21 |
+
"account": "Login, password, profile and access issues"
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"frustration": {
|
| 25 |
+
"type": "score",
|
| 26 |
+
"instructions": "How frustrated is the customer?",
|
| 27 |
+
"criteria": ["Calm and neutral", "Mildly annoyed", "Clearly frustrated", "Furious or abusive"]
|
| 28 |
+
},
|
| 29 |
+
"language": {"type": "choice", "instructions": "Which language is the ticket written in?", "criteria": {"english": null, "spanish": null, "german": null, "french": null}}
|
| 30 |
+
}
|
| 31 |
+
}
|
head.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49b8b57e04c6664c067039132a8ae3c4c4e11f30768710995188d17022b99457
|
| 3 |
+
size 5315232
|
pyproject.toml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "zico"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "zico: a typed decision model - state + typed questions -> calibrated probability distributions"
|
| 5 |
+
requires-python = ">=3.10"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"torch",
|
| 8 |
+
"transformers>=5.2",
|
| 9 |
+
"datasets",
|
| 10 |
+
"peft",
|
| 11 |
+
"safetensors",
|
| 12 |
+
"numpy",
|
| 13 |
+
"pillow", # images in the state
|
| 14 |
+
"torchvision", # transformers' image processor for the vision tower
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
[project.optional-dependencies]
|
| 18 |
+
dev = ["pytest", "httpx"]
|
| 19 |
+
serve = ["fastapi", "uvicorn"]
|
| 20 |
+
mlx = ["mlx", "mlx-lm"] # Apple silicon serving backend
|
| 21 |
+
|
| 22 |
+
[build-system]
|
| 23 |
+
requires = ["setuptools>=68"]
|
| 24 |
+
build-backend = "setuptools.build_meta"
|
| 25 |
+
|
| 26 |
+
[tool.setuptools.packages.find]
|
| 27 |
+
include = ["zico*"]
|
| 28 |
+
|
| 29 |
+
[tool.setuptools.package-data]
|
| 30 |
+
zico = ["ui.html"]
|
| 31 |
+
|
| 32 |
+
[tool.pytest.ini_options]
|
| 33 |
+
testpaths = ["tests"]
|
scripts/add_vision.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Package the base checkpoint's vision tower into a zico checkpoint: <ckpt>/vision/
|
| 2 |
+
|
| 3 |
+
python scripts/add_vision.py runs/qwen35-4b-best-bf16
|
| 4 |
+
|
| 5 |
+
Only the ViT + merger weights are copied (a few hundred MB), together with the image-processor
|
| 6 |
+
settings and the special-token ids the renderer needs. The vision tower is used as shipped: our
|
| 7 |
+
fine-tuning never touched it, and the zero-shot probe showed the alignment survived.
|
| 8 |
+
"""
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
from huggingface_hub import snapshot_download
|
| 15 |
+
from safetensors import safe_open
|
| 16 |
+
from safetensors.torch import save_file
|
| 17 |
+
from transformers import AutoConfig, AutoImageProcessor
|
| 18 |
+
|
| 19 |
+
p = argparse.ArgumentParser()
|
| 20 |
+
p.add_argument("ckpt")
|
| 21 |
+
p.add_argument("--base", default=None, help="defaults to the checkpoint's base_model")
|
| 22 |
+
p.add_argument("--dtype", default="bf16", choices=["bf16", "fp32"])
|
| 23 |
+
args = p.parse_args()
|
| 24 |
+
|
| 25 |
+
ckpt = Path(args.ckpt)
|
| 26 |
+
base = args.base or json.loads((ckpt / "zico.json").read_text())["base_model"]
|
| 27 |
+
cfg = AutoConfig.from_pretrained(base)
|
| 28 |
+
src = Path(snapshot_download(base, allow_patterns=["*.safetensors", "*.json"]))
|
| 29 |
+
dtype = torch.bfloat16 if args.dtype == "bf16" else torch.float32
|
| 30 |
+
|
| 31 |
+
weights = {}
|
| 32 |
+
for shard in sorted(src.glob("*.safetensors")):
|
| 33 |
+
with safe_open(shard, framework="pt") as f:
|
| 34 |
+
for key in f.keys():
|
| 35 |
+
if key.startswith("model.visual."):
|
| 36 |
+
weights[key[len("model.visual."):]] = f.get_tensor(key).to(dtype).contiguous()
|
| 37 |
+
if not weights:
|
| 38 |
+
raise SystemExit(f"no vision weights found in {base}")
|
| 39 |
+
|
| 40 |
+
out = ckpt / "vision"
|
| 41 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 42 |
+
save_file(weights, out / "model.safetensors")
|
| 43 |
+
vision_cfg = {k: v for k, v in cfg.vision_config.to_dict().items() if k not in ("transformers_version", "architectures")}
|
| 44 |
+
(out / "config.json").write_text(json.dumps(vision_cfg, indent=2))
|
| 45 |
+
AutoImageProcessor.from_pretrained(base).save_pretrained(out)
|
| 46 |
+
(out / "zico_vision.json").write_text(json.dumps({
|
| 47 |
+
"base_model": base,
|
| 48 |
+
"image_token_id": cfg.image_token_id,
|
| 49 |
+
"vision_start_token_id": cfg.vision_start_token_id,
|
| 50 |
+
"vision_end_token_id": cfg.vision_end_token_id,
|
| 51 |
+
"patch_size": cfg.vision_config.patch_size,
|
| 52 |
+
"spatial_merge_size": cfg.vision_config.spatial_merge_size,
|
| 53 |
+
}, indent=2)) # fmt: skip
|
| 54 |
+
n = sum(w.numel() for w in weights.values())
|
| 55 |
+
print(f"packaged {len(weights)} tensors, {n/1e6:.0f}M parameters, {sum(w.numel()*w.element_size() for w in weights.values())/1e9:.2f} GB -> {out}")
|
scripts/bench_ours.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Score a zico checkpoint on the fixed benchmark rows (the same rows every other system saw).
|
| 2 |
+
|
| 3 |
+
python scripts/bench_ours.py runs/qwen35-4b/best --data data_ref --permutations 4
|
| 4 |
+
|
| 5 |
+
--permutations K averages each Choice answer over K shuffled option orders (K=1 = as given).
|
| 6 |
+
A long option list is read left to right by a causal backbone, so where the right answer sits
|
| 7 |
+
matters a little; averaging over orders removes that bias. It needs no training and no labels,
|
| 8 |
+
and with the forked state cache the extra orders cost only their own question tokens.
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import collections
|
| 12 |
+
import json
|
| 13 |
+
import random
|
| 14 |
+
import time
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from zico import DecisionModel
|
| 19 |
+
from zico.backbone import pick_device
|
| 20 |
+
from zico.benchmark import BENCH_SETS, format_line, load_rows, summarise
|
| 21 |
+
from zico.data import render_records
|
| 22 |
+
from zico.evaluate import predict
|
| 23 |
+
|
| 24 |
+
p = argparse.ArgumentParser()
|
| 25 |
+
p.add_argument("ckpt")
|
| 26 |
+
p.add_argument("--data", default="data_ref")
|
| 27 |
+
p.add_argument("--n", type=int, default=300)
|
| 28 |
+
p.add_argument("--permutations", type=int, default=1)
|
| 29 |
+
p.add_argument("--out", default=None)
|
| 30 |
+
p.add_argument("--max-len", type=int, default=2048)
|
| 31 |
+
p.add_argument("--errors", default="massive_intent", help="source to print a confusion breakdown for")
|
| 32 |
+
args = p.parse_args()
|
| 33 |
+
|
| 34 |
+
dev = pick_device()
|
| 35 |
+
model, renderer, _ = DecisionModel.load(args.ckpt, device=dev)
|
| 36 |
+
model.autocast_dtype = torch.bfloat16
|
| 37 |
+
rng = random.Random(0)
|
| 38 |
+
|
| 39 |
+
# one record per (row, option order); order[j] = original index of the option shown at position j
|
| 40 |
+
records, orders, owner = [], [], []
|
| 41 |
+
base_rows = []
|
| 42 |
+
for split, source in BENCH_SETS:
|
| 43 |
+
for r in load_rows(args.data, split, source, args.n):
|
| 44 |
+
base_rows.append(r)
|
| 45 |
+
q = r["question"]
|
| 46 |
+
k = args.permutations if q["type"] == "choice" else 1
|
| 47 |
+
crit = q.get("criteria")
|
| 48 |
+
keys = list(crit) if crit is not None else []
|
| 49 |
+
for j in range(k):
|
| 50 |
+
order = list(range(len(keys)))
|
| 51 |
+
if j:
|
| 52 |
+
rng.shuffle(order)
|
| 53 |
+
if q["type"] == "choice":
|
| 54 |
+
new = {keys[i]: crit[keys[i]] for i in order} if isinstance(crit, dict) else [keys[i] for i in order]
|
| 55 |
+
rec = {**r, "sid": f"{r['sid']}#{j}", "question": {**q, "criteria": new}, "label": order.index(r["label"])}
|
| 56 |
+
else:
|
| 57 |
+
rec = r
|
| 58 |
+
records.append(rec)
|
| 59 |
+
orders.append(order)
|
| 60 |
+
owner.append(len(base_rows) - 1)
|
| 61 |
+
|
| 62 |
+
t0 = time.time()
|
| 63 |
+
rows, kept = render_records(records, renderer, args.max_len)
|
| 64 |
+
assert len(kept) == len(records), "rows were dropped by max_len; raise --max-len"
|
| 65 |
+
preds = predict(model, rows, records, renderer.slot_id)
|
| 66 |
+
elapsed = time.time() - t0
|
| 67 |
+
|
| 68 |
+
probs = [None] * len(base_rows)
|
| 69 |
+
for pr, order, o in zip(preds, orders, owner):
|
| 70 |
+
p_orig = torch.zeros(len(pr["p"]))
|
| 71 |
+
if order:
|
| 72 |
+
p_orig[torch.tensor(order)] = pr["p"] # position j showed original option order[j]
|
| 73 |
+
else:
|
| 74 |
+
p_orig = pr["p"]
|
| 75 |
+
probs[o] = p_orig if probs[o] is None else probs[o] + p_orig
|
| 76 |
+
|
| 77 |
+
results = {}
|
| 78 |
+
by_source = collections.defaultdict(list)
|
| 79 |
+
for r, pv in zip(base_rows, probs):
|
| 80 |
+
pv = pv / pv.sum()
|
| 81 |
+
by_source[r["source"]].append((int(pv.argmax()), float(pv.max()), r))
|
| 82 |
+
print(f"{len(records)} forward rows for {len(base_rows)} questions in {elapsed:.0f}s (permutations={args.permutations})")
|
| 83 |
+
for _, source in BENCH_SETS:
|
| 84 |
+
items = by_source[source]
|
| 85 |
+
m = summarise([a for a, _, _ in items], [r["label"] for _, _, r in items], [c for _, c, _ in items], [])
|
| 86 |
+
results[source] = m
|
| 87 |
+
print(source)
|
| 88 |
+
print(format_line(f"ours x{args.permutations}", m))
|
| 89 |
+
|
| 90 |
+
if args.errors in by_source:
|
| 91 |
+
items = by_source[args.errors]
|
| 92 |
+
conf = collections.Counter()
|
| 93 |
+
pos_hits = collections.defaultdict(lambda: [0, 0])
|
| 94 |
+
for pred, _, r in items:
|
| 95 |
+
keys = list(r["question"]["criteria"])
|
| 96 |
+
third = min(2, r["label"] * 3 // len(keys))
|
| 97 |
+
pos_hits[third][0] += pred == r["label"]
|
| 98 |
+
pos_hits[third][1] += 1
|
| 99 |
+
if pred != r["label"]:
|
| 100 |
+
conf[(keys[r["label"]], keys[pred])] += 1
|
| 101 |
+
print(f"\n{args.errors}: accuracy by where the gold option sits in the list: " +
|
| 102 |
+
", ".join(f"{['first', 'middle', 'last'][t]} third {h}/{n} = {h / n:.2f}" for t, (h, n) in sorted(pos_hits.items())))
|
| 103 |
+
print("most common confusions (gold -> predicted):")
|
| 104 |
+
for (g, pd), c in conf.most_common(12):
|
| 105 |
+
print(f" {c:2d} {g} -> {pd}")
|
| 106 |
+
|
| 107 |
+
if args.out:
|
| 108 |
+
with open(args.out, "w") as f:
|
| 109 |
+
json.dump({"permutations": args.permutations, "results": results}, f, indent=2)
|
scripts/build_data.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Download the source datasets and write data/{train,val,test,heldout}.jsonl.
|
| 2 |
+
|
| 3 |
+
python scripts/build_data.py --out data # original 8 sources (what the 0.8B run used)
|
| 4 |
+
python scripts/build_data.py --out data_a --sources all --n-train 15000 --include-ni
|
| 5 |
+
|
| 6 |
+
Held-out sources are never trained on; training rows whose text also appears in a held-out
|
| 7 |
+
set are dropped. With --include-ni, 10% of Super-NaturalInstructions *tasks* are held out
|
| 8 |
+
whole, which measures zero-shot transfer to instructions the model has never seen.
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import collections
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import random
|
| 15 |
+
import re
|
| 16 |
+
import sys
|
| 17 |
+
import traceback
|
| 18 |
+
|
| 19 |
+
from zico.data import HELDOUT_SOURCES, TRAIN_SOURCES, label_context, take, to_records, write_jsonl
|
| 20 |
+
from zico.sources_extra import (
|
| 21 |
+
EXTRA_HELDOUT_SOURCES,
|
| 22 |
+
EXTRA_TRAIN_SOURCES,
|
| 23 |
+
NONE_OK,
|
| 24 |
+
SOURCE_META,
|
| 25 |
+
add_none_option,
|
| 26 |
+
extra_label_context,
|
| 27 |
+
natural_instructions_records,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
p = argparse.ArgumentParser()
|
| 31 |
+
p.add_argument("--out", default="data")
|
| 32 |
+
p.add_argument("--sources", default="core", choices=["core", "all"])
|
| 33 |
+
p.add_argument("--only", default=None, help="comma-separated source names (for testing adapters)")
|
| 34 |
+
p.add_argument("--exclude-nc", action="store_true", help="drop sources with non-commercial licences")
|
| 35 |
+
p.add_argument("--include-ni", action="store_true", help="add Super-NaturalInstructions classification tasks")
|
| 36 |
+
p.add_argument("--ni-scan", type=int, default=2_000_000, help="rows of the NI corpus to scan")
|
| 37 |
+
p.add_argument("--n-train", type=int, default=4000, help="source examples per training dataset")
|
| 38 |
+
p.add_argument("--n-val", type=int, default=300)
|
| 39 |
+
p.add_argument("--n-test", type=int, default=500)
|
| 40 |
+
p.add_argument("--seed", type=int, default=0)
|
| 41 |
+
p.add_argument("--quick", action="store_true", help="stream every source with a small buffer (adapter smoke test, skewed samples)")
|
| 42 |
+
args = p.parse_args()
|
| 43 |
+
|
| 44 |
+
if args.quick:
|
| 45 |
+
_take = take
|
| 46 |
+
|
| 47 |
+
def take(hf_id, cfg, split, n, stream, seed): # noqa: F811
|
| 48 |
+
return _take(hf_id, cfg, split, n, True, seed, buffer_size=1000)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
rng = random.Random(args.seed)
|
| 52 |
+
train_sources, heldout_sources = dict(TRAIN_SOURCES), dict(HELDOUT_SOURCES)
|
| 53 |
+
if args.sources == "all":
|
| 54 |
+
train_sources.update(EXTRA_TRAIN_SOURCES)
|
| 55 |
+
heldout_sources.update(EXTRA_HELDOUT_SOURCES)
|
| 56 |
+
if args.exclude_nc:
|
| 57 |
+
train_sources = {k: v for k, v in train_sources.items() if SOURCE_META.get(k, {}).get("license") != "nc"}
|
| 58 |
+
if args.only:
|
| 59 |
+
keep = set(args.only.split(","))
|
| 60 |
+
train_sources = {k: v for k, v in train_sources.items() if k in keep}
|
| 61 |
+
heldout_sources = {k: v for k, v in heldout_sources.items() if k in keep}
|
| 62 |
+
|
| 63 |
+
ctx = label_context()
|
| 64 |
+
if args.sources == "all":
|
| 65 |
+
ctx.update(extra_label_context())
|
| 66 |
+
splits = collections.defaultdict(list)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def text_key(state) -> str:
|
| 70 |
+
text = " ".join(str(v) for v in state.values()) if isinstance(state, dict) else str(state)
|
| 71 |
+
return re.sub(r"[^a-z0-9]+", " ", text.lower()).strip()[:200]
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
# ------------------------------------------------------------------ held-out first, so training can be de-duplicated against it
|
| 75 |
+
for name, (hf_id, cfg, _, test_split, stream, adapter) in heldout_sources.items():
|
| 76 |
+
try:
|
| 77 |
+
test = take(hf_id, cfg, test_split, args.n_test, stream, args.seed + 1)
|
| 78 |
+
splits["heldout"] += to_records(name, test, adapter, rng, ctx, "heldout")
|
| 79 |
+
print(f"ok {name} (held out)", flush=True)
|
| 80 |
+
except Exception: # noqa: BLE001 - one broken source should not sink the build
|
| 81 |
+
print(f"FAIL {name}\n{traceback.format_exc()}", flush=True)
|
| 82 |
+
heldout_keys = {text_key(r["state"]) for r in splits["heldout"]}
|
| 83 |
+
|
| 84 |
+
for name, (hf_id, cfg, train_split, test_split, stream, adapter) in train_sources.items():
|
| 85 |
+
try:
|
| 86 |
+
# civil_comments drops most non-toxic rows inside its adapter, so over-fetch for it
|
| 87 |
+
mult = 3 if name == "civil_comments" else 1
|
| 88 |
+
pool = take(hf_id, cfg, train_split, (args.n_train + args.n_val) * mult, stream, args.seed)
|
| 89 |
+
n_val = min(args.n_val * mult, len(pool) // 10)
|
| 90 |
+
parts = {"val": to_records(name, pool[:n_val], adapter, rng, ctx, "val"),
|
| 91 |
+
"train": to_records(name, pool[n_val:], adapter, rng, ctx, "train")} # fmt: skip
|
| 92 |
+
if test_split:
|
| 93 |
+
test = take(hf_id, cfg, test_split, args.n_test * mult, stream, args.seed + 1)
|
| 94 |
+
parts["test"] = to_records(name, test, adapter, rng, ctx, "test")
|
| 95 |
+
for split, recs in parts.items():
|
| 96 |
+
if name in NONE_OK:
|
| 97 |
+
recs = [add_none_option(r, rng) for r in recs]
|
| 98 |
+
splits[split] += recs
|
| 99 |
+
print(f"ok {name}: {len(parts['train'])} train rows", flush=True)
|
| 100 |
+
except Exception: # noqa: BLE001
|
| 101 |
+
print(f"FAIL {name}\n{traceback.format_exc()}", flush=True)
|
| 102 |
+
|
| 103 |
+
if args.include_ni:
|
| 104 |
+
try:
|
| 105 |
+
blocked = ("imdb", "_arc_", "pubmedqa", "pubmed_qa", "massive", "askubuntu", "emotion") # overlap with held-out sets
|
| 106 |
+
recs = [r for r in natural_instructions_records(rng, args.ni_scan) if not any(b in r["task"].lower() for b in blocked)]
|
| 107 |
+
tasks = sorted({r["task"] for r in recs})
|
| 108 |
+
rng.shuffle(tasks)
|
| 109 |
+
held = set(tasks[: len(tasks) // 10]) # whole tasks held out; none if fewer than 10 qualified
|
| 110 |
+
for r in recs:
|
| 111 |
+
if r["task"] in held:
|
| 112 |
+
splits["heldout"].append({**r, "source": "ni_unseen_tasks", "sid": r["sid"].replace("train-", "heldout-", 1)})
|
| 113 |
+
else:
|
| 114 |
+
splits["train"].append(r)
|
| 115 |
+
print(f"ok natural_instructions: {len(tasks) - len(held)} train tasks, {len(held)} held-out tasks, {len(recs)} rows", flush=True)
|
| 116 |
+
except Exception: # noqa: BLE001
|
| 117 |
+
print(f"FAIL natural_instructions\n{traceback.format_exc()}", flush=True)
|
| 118 |
+
|
| 119 |
+
stats = {}
|
| 120 |
+
for split, records in splits.items():
|
| 121 |
+
if split != "heldout":
|
| 122 |
+
before = len(records)
|
| 123 |
+
records = [r for r in records if text_key(r["state"]) not in heldout_keys]
|
| 124 |
+
if before != len(records):
|
| 125 |
+
print(f"{split}: dropped {before - len(records)} rows that also appear in a held-out set")
|
| 126 |
+
if split == "train":
|
| 127 |
+
rng.shuffle(records)
|
| 128 |
+
write_jsonl(f"{args.out}/{split}.jsonl", records)
|
| 129 |
+
by = collections.Counter((r["source"], r["question"]["type"]) for r in records)
|
| 130 |
+
stats[split] = {"rows": len(records), "by_source_type": {f"{s}/{t}": n for (s, t), n in sorted(by.items())}}
|
| 131 |
+
print(f"{split:8s} {len(records):7d} rows")
|
| 132 |
+
with open(f"{args.out}/stats.json", "w") as f:
|
| 133 |
+
json.dump(stats, f, indent=2)
|
| 134 |
+
sys.stdout.flush()
|
| 135 |
+
os._exit(0) # streaming dataset readers can leave non-daemon threads behind and hang a normal exit
|
scripts/build_image_data.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the image training set: resized JPEGs + records, mixed with replayed text rows.
|
| 2 |
+
|
| 3 |
+
python scripts/build_image_data.py --out data_img --n-train 5000 --text-replay data_a --text-rows 25000
|
| 4 |
+
|
| 5 |
+
Every image is resized ONCE, here, to its final aspect-preserving size (multiples of 32 px, one
|
| 6 |
+
token per 32x32 px) and its token grid is written into the record. The trainer can then bucket
|
| 7 |
+
rows by length without decoding a single picture and only loads pixels per batch.
|
| 8 |
+
|
| 9 |
+
Text rows are replayed alongside so the text skills the model already has do not fade.
|
| 10 |
+
"""
|
| 11 |
+
import argparse
|
| 12 |
+
import collections
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
import random
|
| 16 |
+
import sys
|
| 17 |
+
import traceback
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
from zico.data import read_jsonl, write_jsonl
|
| 21 |
+
from zico.image_sources import IMAGE_HELDOUT_SOURCES, IMAGE_TRAIN_SOURCES
|
| 22 |
+
from zico.vision import smart_resize
|
| 23 |
+
|
| 24 |
+
p = argparse.ArgumentParser()
|
| 25 |
+
p.add_argument("--out", default="data_img")
|
| 26 |
+
p.add_argument("--n-train", type=int, default=5000, help="images per training source")
|
| 27 |
+
p.add_argument("--doc-mult", type=float, default=2.0, help="documents are the weak spot: take this many times more")
|
| 28 |
+
p.add_argument("--n-val", type=int, default=60)
|
| 29 |
+
p.add_argument("--n-test", type=int, default=150)
|
| 30 |
+
p.add_argument("--n-heldout", type=int, default=300)
|
| 31 |
+
p.add_argument("--text-replay", default=None, help="a text data dir (train/val/test/heldout .jsonl) to mix in")
|
| 32 |
+
p.add_argument("--text-rows", type=int, default=25000)
|
| 33 |
+
p.add_argument("--only", default=None)
|
| 34 |
+
p.add_argument("--quick", action="store_true", help="stream every source (adapter smoke test; samples may be class-skewed)")
|
| 35 |
+
p.add_argument("--min-pixels", type=int, default=256 * 256)
|
| 36 |
+
p.add_argument("--seed", type=int, default=0)
|
| 37 |
+
args = p.parse_args()
|
| 38 |
+
|
| 39 |
+
from datasets import load_dataset # noqa: E402
|
| 40 |
+
from PIL import Image # noqa: E402
|
| 41 |
+
|
| 42 |
+
rng = random.Random(args.seed)
|
| 43 |
+
out = Path(args.out)
|
| 44 |
+
splits = collections.defaultdict(list)
|
| 45 |
+
only = set(args.only.split(",")) if args.only else None
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def take(hf_id, cfg, split, n, stream):
|
| 49 |
+
if args.quick: # adapter smoke test only: the sample is class-skewed (see below)
|
| 50 |
+
ds = load_dataset(hf_id, cfg, split=split, streaming=True)
|
| 51 |
+
return list(ds.shuffle(seed=args.seed, buffer_size=min(4000, max(500, n))).take(n)), ds.features
|
| 52 |
+
if stream:
|
| 53 |
+
return take_uniform_stream(hf_id, cfg, split, n)
|
| 54 |
+
ds = load_dataset(hf_id, cfg, split=split).shuffle(seed=args.seed)
|
| 55 |
+
return [ds[i] for i in range(min(n, len(ds)))], ds.features
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def take_uniform_stream(hf_id, cfg, split, n):
|
| 59 |
+
"""A uniform sample of a large dataset without storing it.
|
| 60 |
+
|
| 61 |
+
`streaming.shuffle(buffer).take(n)` reads only the first few files, and image datasets are usually
|
| 62 |
+
grouped by class -- both across files and inside them. The first image run learned 9 of RVL-CDIP's 16
|
| 63 |
+
document types that way (and 17 of ImageNet-100's classes) and got *worse* on documents. Here every
|
| 64 |
+
file contributes equally, and within a file the shuffle buffer is larger than the file, so the draw is
|
| 65 |
+
uniform. Files are read in parallel threads.
|
| 66 |
+
"""
|
| 67 |
+
import math
|
| 68 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 69 |
+
|
| 70 |
+
ds = load_dataset(hf_id, cfg, split=split, streaming=True)
|
| 71 |
+
k = ds.n_shards
|
| 72 |
+
per = math.ceil(n / k) + 2
|
| 73 |
+
|
| 74 |
+
def one(i):
|
| 75 |
+
part = ds.shard(num_shards=k, index=i) if k > 1 else ds
|
| 76 |
+
return list(part.shuffle(seed=args.seed + i, buffer_size=30_000).take(per))
|
| 77 |
+
|
| 78 |
+
with ThreadPoolExecutor(min(16, k)) as pool:
|
| 79 |
+
rows = [r for part in pool.map(one, range(k)) for r in part]
|
| 80 |
+
random.Random(args.seed).shuffle(rows)
|
| 81 |
+
return rows[:n], ds.features
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def gold_classes(records) -> set:
|
| 85 |
+
"""Distinct correct answers among the Choice questions: a cheap check that a sample covers its classes."""
|
| 86 |
+
return {list(r["question"]["criteria"])[r["label"]] for r in records if r["question"]["type"] == "choice"}
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def label_names(features, rows):
|
| 90 |
+
names = {k: f.names for k, f in features.items() if getattr(f, "names", None)}
|
| 91 |
+
for col in ("masterCategory", "subCategory", "articleType", "baseColour", "gender", "usage"): # string attributes
|
| 92 |
+
if rows and col in rows[0]:
|
| 93 |
+
names[col] = sorted({r[col] for r in rows if r.get(col)})
|
| 94 |
+
return names
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def emit(name, spec, rows, names, split):
|
| 98 |
+
_, _, _, _, _, image_col, adapter, max_pixels, keys = spec
|
| 99 |
+
records = []
|
| 100 |
+
for i, ex in enumerate(rows):
|
| 101 |
+
img = ex[image_col]
|
| 102 |
+
if img is None:
|
| 103 |
+
continue
|
| 104 |
+
img = img.convert("RGB")
|
| 105 |
+
w, h = smart_resize(img.width, img.height, 32, min(args.min_pixels, max_pixels), max_pixels)
|
| 106 |
+
rel = f"images/{name}/{split}-{i:06d}.jpg"
|
| 107 |
+
(out / rel).parent.mkdir(parents=True, exist_ok=True)
|
| 108 |
+
img.resize((w, h), Image.BICUBIC).save(out / rel, quality=92)
|
| 109 |
+
part = {"type": "image", "path": rel}
|
| 110 |
+
state = rng.choice([{rng.choice(keys): part}, part, {"input": {rng.choice(keys): part}}])
|
| 111 |
+
for question, label in adapter(ex, rng, names):
|
| 112 |
+
records.append({"source": name, "sid": f"{split}-{name}-{i}", "state": state, "question": question, "label": label,
|
| 113 |
+
"image_grids": [[h // 32, w // 32]]}) # fmt: skip
|
| 114 |
+
return records
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
for name, spec in IMAGE_HELDOUT_SOURCES.items():
|
| 118 |
+
if only and name not in only:
|
| 119 |
+
continue
|
| 120 |
+
try:
|
| 121 |
+
rows, feats = take(spec[0], spec[1], spec[3], args.n_heldout, spec[4])
|
| 122 |
+
splits["heldout"] += emit(name, spec, rows, label_names(feats, rows), "heldout")
|
| 123 |
+
print(f"ok {name} (held out)", flush=True)
|
| 124 |
+
except Exception: # noqa: BLE001
|
| 125 |
+
print(f"FAIL {name}\n{traceback.format_exc()}", flush=True)
|
| 126 |
+
|
| 127 |
+
for name, spec in IMAGE_TRAIN_SOURCES.items():
|
| 128 |
+
if only and name not in only:
|
| 129 |
+
continue
|
| 130 |
+
try:
|
| 131 |
+
n = int(args.n_train * (args.doc_mult if name == "rvl_cdip" else 1))
|
| 132 |
+
pool, feats = take(spec[0], spec[1], spec[2], n + args.n_val, spec[4])
|
| 133 |
+
names = label_names(feats, pool)
|
| 134 |
+
splits["val"] += emit(name, spec, pool[: args.n_val], names, "val")
|
| 135 |
+
train = emit(name, spec, pool[args.n_val :], names, "train")
|
| 136 |
+
splits["train"] += train
|
| 137 |
+
n_test_classes = 0
|
| 138 |
+
if spec[3]:
|
| 139 |
+
test, _ = take(spec[0], spec[1], spec[3], args.n_test, spec[4])
|
| 140 |
+
test_recs = emit(name, spec, test, names, "test")
|
| 141 |
+
splits["test"] += test_recs
|
| 142 |
+
n_test_classes = len(gold_classes(test_recs))
|
| 143 |
+
n_all = max((len(v) for v in names.values()), default=0)
|
| 144 |
+
covered = len(gold_classes(train))
|
| 145 |
+
flag = " <-- WARNING: skewed sample" if n_all and covered < 0.6 * min(n_all, len(train) / 4) else ""
|
| 146 |
+
print(f"ok {name}: {len(train)} train rows, {covered} distinct answer classes in train (largest label set {n_all}), "
|
| 147 |
+
f"{n_test_classes} in test{flag}", flush=True) # fmt: skip
|
| 148 |
+
except Exception: # noqa: BLE001
|
| 149 |
+
print(f"FAIL {name}\n{traceback.format_exc()}", flush=True)
|
| 150 |
+
|
| 151 |
+
if args.text_replay:
|
| 152 |
+
n_img = len(splits["train"])
|
| 153 |
+
text = read_jsonl(f"{args.text_replay}/train.jsonl")
|
| 154 |
+
splits["train"] += rng.sample(text, min(args.text_rows, len(text)))
|
| 155 |
+
for split, n in (("val", 1500), ("test", 3000)):
|
| 156 |
+
rows = read_jsonl(f"{args.text_replay}/{split}.jsonl")
|
| 157 |
+
splits[split] += rng.sample(rows, min(n, len(rows)))
|
| 158 |
+
splits["heldout"] += read_jsonl(f"{args.text_replay}/heldout.jsonl") # text held-out sets: did anything fade?
|
| 159 |
+
print(f"text replay: {len(splits['train']) - n_img} text rows added to {n_img} image rows")
|
| 160 |
+
|
| 161 |
+
stats = {}
|
| 162 |
+
for split, records in splits.items():
|
| 163 |
+
if split == "train":
|
| 164 |
+
rng.shuffle(records)
|
| 165 |
+
write_jsonl(out / f"{split}.jsonl", records)
|
| 166 |
+
by = collections.Counter(r["source"] for r in records)
|
| 167 |
+
stats[split] = {"rows": len(records), "image_rows": sum("image_grids" in r for r in records), "by_source": dict(sorted(by.items()))}
|
| 168 |
+
print(f"{split:8s} {len(records):7d} rows ({stats[split]['image_rows']} with images)")
|
| 169 |
+
(out / "stats.json").write_text(json.dumps(stats, indent=2))
|
| 170 |
+
sys.stdout.flush()
|
| 171 |
+
os._exit(0) # streaming readers leave threads behind that hang a normal exit
|
scripts/check_kernels.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Numerically compare the fused Gated-DeltaNet kernel (fla) against an independent reference,
|
| 2 |
+
forward AND backward. Run once on any new GPU / Triton combination before training:
|
| 3 |
+
|
| 4 |
+
python scripts/check_kernels.py
|
| 5 |
+
|
| 6 |
+
Motivation: fla refuses to run its backward on Hopper GPUs with Triton 3.4-3.7.0 because that
|
| 7 |
+
combination silently produces wrong gradients. Whatever workaround is installed (newer Triton,
|
| 8 |
+
tilelang backend) should be verified rather than trusted.
|
| 9 |
+
|
| 10 |
+
The reference is a naive token-by-token recurrence in float64 written here. (HF's
|
| 11 |
+
`torch_chunk_gated_delta_rule` is NOT independent: it is decorated to dispatch to fla when fla is
|
| 12 |
+
installed, so comparing against it compares fla with itself.)
|
| 13 |
+
|
| 14 |
+
S_t = exp(g_t) * S_{t-1}
|
| 15 |
+
S_t = S_t + k_t (beta_t * (v_t - S_t^T k_t))^T
|
| 16 |
+
o_t = S_t^T q_t with q, k L2-normalised and q scaled by Dk^-1/2
|
| 17 |
+
"""
|
| 18 |
+
import torch
|
| 19 |
+
import torch.nn.functional as F
|
| 20 |
+
from fla.ops.gated_delta_rule import chunk_gated_delta_rule
|
| 21 |
+
|
| 22 |
+
dev = "cuda"
|
| 23 |
+
B, L, H, DK, DV = 2, 200, 4, 64, 64 # L deliberately not a multiple of the 64-token chunk
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def naive(q, k, v, g, beta):
|
| 27 |
+
q, k = F.normalize(q, dim=-1), F.normalize(k, dim=-1)
|
| 28 |
+
q = q * DK**-0.5
|
| 29 |
+
S = q.new_zeros(B, H, DK, DV)
|
| 30 |
+
outs = []
|
| 31 |
+
for t in range(L):
|
| 32 |
+
S = S * g[:, t].exp()[..., None, None]
|
| 33 |
+
pred = (S * k[:, t][..., None]).sum(-2) # S^T k -> (B, H, DV)
|
| 34 |
+
S = S + k[:, t][..., None] * ((v[:, t] - pred) * beta[:, t][..., None])[..., None, :]
|
| 35 |
+
outs.append((S * q[:, t][..., None]).sum(-2))
|
| 36 |
+
return torch.stack(outs, 1)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def make(dtype):
|
| 40 |
+
gen = torch.Generator(device=dev).manual_seed(1)
|
| 41 |
+
q = torch.randn(B, L, H, DK, device=dev, generator=gen)
|
| 42 |
+
k = torch.randn(B, L, H, DK, device=dev, generator=gen)
|
| 43 |
+
v = torch.randn(B, L, H, DV, device=dev, generator=gen)
|
| 44 |
+
g = -torch.rand(B, L, H, device=dev, generator=gen) * 0.5 # log-space decay, <= 0
|
| 45 |
+
beta = torch.rand(B, L, H, device=dev, generator=gen)
|
| 46 |
+
w = torch.randn(B, L, H, DV, device=dev, generator=gen) # fixed projection: same loss on both paths
|
| 47 |
+
leaves = [t.to(dtype).requires_grad_(True) for t in (q, k, v, g, beta)]
|
| 48 |
+
return leaves, w
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def grads_of(out, leaves, w):
|
| 52 |
+
(out.double() * w.double()).sum().backward()
|
| 53 |
+
return out.double().detach(), [t.grad.double() for t in leaves]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
leaves, w = make(torch.float64)
|
| 57 |
+
ref_out, ref_grads = grads_of(naive(*leaves), leaves, w)
|
| 58 |
+
|
| 59 |
+
ok = True
|
| 60 |
+
rel = lambda a, b: ((a - b).norm() / b.norm().clamp_min(1e-12)).item() # noqa: E731
|
| 61 |
+
for dtype in (torch.float32, torch.bfloat16):
|
| 62 |
+
(q, k, v, g, beta), w = make(dtype)
|
| 63 |
+
try:
|
| 64 |
+
out = chunk_gated_delta_rule(q, k, v, g=g.float() if dtype == torch.bfloat16 else g, beta=beta, use_qk_l2norm_in_kernel=True)
|
| 65 |
+
out = out[0] if isinstance(out, tuple) else out
|
| 66 |
+
out, grads = grads_of(out, (q, k, v, g, beta), w)
|
| 67 |
+
except Exception as e: # noqa: BLE001
|
| 68 |
+
print(f"fla {dtype}: FAILED to run: {type(e).__name__}: {str(e)[:300]}")
|
| 69 |
+
ok = False
|
| 70 |
+
continue
|
| 71 |
+
errs = {"out": rel(out, ref_out), **{n: rel(a, b) for n, a, b in zip(("dq", "dk", "dv", "dg", "dbeta"), grads, ref_grads)}}
|
| 72 |
+
# Triton's tl.dot runs in TF32 on Ampere+ GPUs, so even "fp32" agrees only to ~1e-3. A genuinely
|
| 73 |
+
# wrong backward shows up as O(0.1-1) error on specific gradients, not a uniform small offset
|
| 74 |
+
# shared with the forward output.
|
| 75 |
+
tol = 1e-2 if dtype == torch.float32 else 5e-2
|
| 76 |
+
bad = {n: e for n, e in errs.items() if not e < tol}
|
| 77 |
+
ok &= not bad
|
| 78 |
+
print(f"fla {str(dtype):15s} rel-err vs naive fp64: " + " ".join(f"{n}={e:.1e}" for n, e in errs.items()) + (" <-- MISMATCH" if bad else " ok"))
|
| 79 |
+
print("KERNEL CHECK PASSED" if ok else "KERNEL CHECK FAILED")
|
scripts/check_mlx_parity.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Compare the MLX backend with the PyTorch backend on identical requests, then time both.
|
| 2 |
+
|
| 3 |
+
python scripts/check_mlx_parity.py runs/qwen35-4b-best-bf16 --data data --states 40
|
| 4 |
+
"""
|
| 5 |
+
import argparse
|
| 6 |
+
import collections
|
| 7 |
+
import json
|
| 8 |
+
import statistics
|
| 9 |
+
import time
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from zico import DecisionModel, question_from_dict
|
| 14 |
+
from zico.backbone import pick_device
|
| 15 |
+
from zico.data import read_jsonl
|
| 16 |
+
from zico.mlx_backend import MLXDecisionModel
|
| 17 |
+
|
| 18 |
+
p = argparse.ArgumentParser()
|
| 19 |
+
p.add_argument("ckpt")
|
| 20 |
+
p.add_argument("--data", default="data")
|
| 21 |
+
p.add_argument("--states", type=int, default=40)
|
| 22 |
+
p.add_argument("--skip-torch-timing", action="store_true")
|
| 23 |
+
args = p.parse_args()
|
| 24 |
+
|
| 25 |
+
# requests: real rows grouped by state (so multi-question states are exercised) + the example ticket
|
| 26 |
+
by_state = collections.OrderedDict()
|
| 27 |
+
for split in ("heldout", "test"):
|
| 28 |
+
for r in read_jsonl(f"{args.data}/{split}.jsonl"):
|
| 29 |
+
by_state.setdefault(r["sid"], {"state": r["state"], "questions": {}})["questions"][f"q{len(by_state[r['sid']]['questions'])}"] = r["question"]
|
| 30 |
+
multi = [v for v in by_state.values() if len(v["questions"]) > 1][: args.states // 2]
|
| 31 |
+
single = [v for v in list(by_state.values())[:: max(1, len(by_state) // args.states)] if len(v["questions"]) == 1][: args.states - len(multi)]
|
| 32 |
+
example = json.load(open("examples/systemone_request.json"))
|
| 33 |
+
requests = [{"state": example["state"], "questions": example["questions"]}] + multi + single
|
| 34 |
+
print(f"{len(requests)} requests, {sum(len(r['questions']) for r in requests)} questions")
|
| 35 |
+
|
| 36 |
+
dev = pick_device()
|
| 37 |
+
torch_model, renderer, _ = DecisionModel.load(args.ckpt, device=dev, dtype=torch.bfloat16)
|
| 38 |
+
mlx_model, mlx_renderer, _ = MLXDecisionModel.load(args.ckpt)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def flat(answer):
|
| 42 |
+
a = answer
|
| 43 |
+
if a["type"] == "noul":
|
| 44 |
+
return [1 - a["noul"], a["noul"]]
|
| 45 |
+
return list(a["probabilities"].values())
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def top(answer):
|
| 49 |
+
return answer.get("choice", round(answer.get("noul", answer.get("score", 0))))
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
diffs, agree, total, t_torch, t_mlx = [], 0, 0, [], []
|
| 53 |
+
for i, req in enumerate(requests):
|
| 54 |
+
qs = {k: question_from_dict(v) for k, v in req["questions"].items()}
|
| 55 |
+
t0 = time.perf_counter()
|
| 56 |
+
a = torch_model.answer(renderer, req["state"], qs)
|
| 57 |
+
if dev == "mps":
|
| 58 |
+
torch.mps.synchronize()
|
| 59 |
+
t1 = time.perf_counter()
|
| 60 |
+
b = mlx_model.answer(mlx_renderer, req["state"], qs)
|
| 61 |
+
t2 = time.perf_counter()
|
| 62 |
+
if i: # first request warms both up
|
| 63 |
+
t_torch.append(t1 - t0)
|
| 64 |
+
t_mlx.append(t2 - t1)
|
| 65 |
+
assert a["usage"] == b["usage"], (a["usage"], b["usage"])
|
| 66 |
+
for name in qs:
|
| 67 |
+
pa, pb = flat(a["answers"][name]), flat(b["answers"][name])
|
| 68 |
+
diffs += [abs(x - y) for x, y in zip(pa, pb)]
|
| 69 |
+
same = top(a["answers"][name]) == top(b["answers"][name])
|
| 70 |
+
agree += same
|
| 71 |
+
total += 1
|
| 72 |
+
if not same: # expected only for near-ties, where bf16 rounding decides
|
| 73 |
+
two = lambda p: [round(v, 4) for v in sorted(p, reverse=True)[:2]] # noqa: E731
|
| 74 |
+
print(f" top answer differs on a {a['answers'][name]['type']} question: torch top-2 {two(pa)} vs mlx top-2 {two(pb)} "
|
| 75 |
+
f"(torch says {top(a['answers'][name])!r}, mlx says {top(b['answers'][name])!r})") # fmt: skip
|
| 76 |
+
|
| 77 |
+
print(f"probabilities compared: {len(diffs)} | max |dp| = {max(diffs):.4f} | mean |dp| = {statistics.mean(diffs):.5f} | "
|
| 78 |
+
f"same top answer: {agree}/{total}") # fmt: skip
|
| 79 |
+
print(f"median latency per request PyTorch ({dev}, bf16): {statistics.median(t_torch)*1000:.0f} ms MLX: {statistics.median(t_mlx)*1000:.0f} ms "
|
| 80 |
+
f"-> {statistics.median(t_torch)/statistics.median(t_mlx):.1f}x") # fmt: skip
|
| 81 |
+
ex = mlx_model.answer(mlx_renderer, example["state"], {k: question_from_dict(v) for k, v in example["questions"].items()})
|
| 82 |
+
print("example ticket via MLX:", {k: (v.get("choice") or v.get("noul") or v.get("score")) for k, v in ex["answers"].items()})
|
| 83 |
+
reps = []
|
| 84 |
+
for _ in range(5):
|
| 85 |
+
t0 = time.perf_counter()
|
| 86 |
+
mlx_model.answer(mlx_renderer, example["state"], {k: question_from_dict(v) for k, v in example["questions"].items()})
|
| 87 |
+
reps.append(time.perf_counter() - t0)
|
| 88 |
+
print(f"example ticket (5 questions, {ex['usage']['input_tokens']} tokens) via MLX: best {min(reps)*1000:.0f} ms, median {statistics.median(reps)*1000:.0f} ms")
|
scripts/eval.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Evaluate a checkpoint: accuracy + calibration on in-distribution test data and on
|
| 2 |
+
held-out schemas, post-hoc temperature scaling, and the fork==single invariance / latency check.
|
| 3 |
+
|
| 4 |
+
python scripts/eval.py runs/qwen35-0.8b/best --save-calibrated
|
| 5 |
+
"""
|
| 6 |
+
import argparse
|
| 7 |
+
import json
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from zico import DecisionModel, question_from_dict
|
| 14 |
+
from zico.backbone import pick_device
|
| 15 |
+
from zico.calibration import fit_temperature
|
| 16 |
+
from zico.data import read_jsonl, render_records, subsample
|
| 17 |
+
from zico.evaluate import format_report, predict, report, strip_tables
|
| 18 |
+
from zico.schema import QTYPES
|
| 19 |
+
|
| 20 |
+
p = argparse.ArgumentParser()
|
| 21 |
+
p.add_argument("ckpt")
|
| 22 |
+
p.add_argument("--data", default="data")
|
| 23 |
+
p.add_argument("--max-len", type=int, default=768)
|
| 24 |
+
p.add_argument("--limit", type=int, default=None, help="cap rows per split (quick runs)")
|
| 25 |
+
p.add_argument("--precision", default="bf16", choices=["bf16", "fp32"])
|
| 26 |
+
p.add_argument("--save-calibrated", action="store_true", help="write fitted temperatures into the checkpoint")
|
| 27 |
+
args = p.parse_args()
|
| 28 |
+
|
| 29 |
+
dev = pick_device()
|
| 30 |
+
model, renderer, meta = DecisionModel.load(args.ckpt, device=dev)
|
| 31 |
+
model.autocast_dtype = torch.bfloat16 if args.precision == "bf16" else None
|
| 32 |
+
results = {"ckpt": args.ckpt, "meta": meta}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def run(split):
|
| 36 |
+
recs = subsample(read_jsonl(f"{args.data}/{split}.jsonl"), args.limit)
|
| 37 |
+
image_meta = model.image_pre.meta if model.image_pre is not None else None
|
| 38 |
+
rows, recs = render_records(recs, renderer, args.max_len, image_meta, args.data)
|
| 39 |
+
return predict(model, rows, recs, renderer.slot_id), recs
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def rescale(preds, temps):
|
| 43 |
+
out = []
|
| 44 |
+
for r in preds:
|
| 45 |
+
lg = r["logits"] / temps[r["qtype"]]
|
| 46 |
+
out.append({**r, "p": torch.softmax(lg, -1)})
|
| 47 |
+
return out
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ------------------------------------------------------------ temperature from val
|
| 51 |
+
model.log_temp.zero_()
|
| 52 |
+
val_preds, _ = run("val")
|
| 53 |
+
temps = []
|
| 54 |
+
for qi, name in enumerate(QTYPES):
|
| 55 |
+
sel = [r for r in val_preds if r["qtype"] == qi]
|
| 56 |
+
temps.append(fit_temperature([r["logits"] for r in sel], [r["label"] for r in sel]) if len(sel) >= 50 else 1.0)
|
| 57 |
+
print("fitted temperatures:", {n: round(t, 3) for n, t in zip(QTYPES, temps)})
|
| 58 |
+
results["temperatures"] = dict(zip(QTYPES, temps))
|
| 59 |
+
|
| 60 |
+
# ------------------------------------------------------------ test + held-out
|
| 61 |
+
for split in ("test", "heldout"):
|
| 62 |
+
preds, _ = run(split)
|
| 63 |
+
raw, cal = report(preds), report(rescale(preds, temps))
|
| 64 |
+
print("\n" + format_report(raw, f"{split} (raw)"))
|
| 65 |
+
print("\n" + format_report(cal, f"{split} (temp-scaled)"))
|
| 66 |
+
results[split] = {"raw": strip_tables(raw), "calibrated": strip_tables(cal),
|
| 67 |
+
"reliability_raw": raw["overall"]["reliability"], "reliability_calibrated": cal["overall"]["reliability"]} # fmt: skip
|
| 68 |
+
|
| 69 |
+
# ------------------------------------------------------------ fork invariance + fan-out latency
|
| 70 |
+
recs = [r for r in read_jsonl(f"{args.data}/test.jsonl") if "image_grids" not in r] # a text state, text questions
|
| 71 |
+
if not recs:
|
| 72 |
+
(Path(args.ckpt) / "eval.json").write_text(json.dumps(results, indent=2))
|
| 73 |
+
raise SystemExit("no text rows in the test split: skipping the fork / latency section")
|
| 74 |
+
state = next((r["state"] for r in recs if r["source"] == "yelp"), recs[0]["state"])
|
| 75 |
+
pool = [question_from_dict(r["question"]) for r in recs if r["source"] in ("yelp", "go_emotions", "civil_comments", "ag_news")] \
|
| 76 |
+
or [question_from_dict(r["question"]) for r in recs] # fmt: skip
|
| 77 |
+
seen, questions = set(), []
|
| 78 |
+
for q in pool: # distinct questions only
|
| 79 |
+
key = json.dumps([q.type, q.instructions, q.criteria], sort_keys=True)
|
| 80 |
+
if key not in seen:
|
| 81 |
+
seen.add(key)
|
| 82 |
+
questions.append(q)
|
| 83 |
+
questions = questions[:64]
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def sync():
|
| 87 |
+
if dev == "mps":
|
| 88 |
+
torch.mps.synchronize()
|
| 89 |
+
elif dev == "cuda":
|
| 90 |
+
torch.cuda.synchronize()
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def timed(fn, reps=3):
|
| 94 |
+
best = float("inf")
|
| 95 |
+
for _ in range(reps):
|
| 96 |
+
sync(); t = time.time(); out = fn(); sync() # noqa: E702
|
| 97 |
+
best = min(best, time.time() - t)
|
| 98 |
+
return out, best * 1000
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
model.autocast_dtype = None # compare the two paths in fp32 so differences mean logic, not rounding
|
| 102 |
+
single = [model.question_logits(renderer, state, [q], fork=False)[0][0] for q in questions[:16]]
|
| 103 |
+
forked = model.question_logits(renderer, state, questions[:16], fork=True)[0]
|
| 104 |
+
gap = max((torch.softmax(a, -1) - torch.softmax(b, -1)).abs().max().item() for a, b in zip(single, forked))
|
| 105 |
+
print(f"\nfork vs single-question, max |delta p| over 16 questions (fp32): {gap:.2e}")
|
| 106 |
+
results["fork_invariance_max_dp"] = gap
|
| 107 |
+
|
| 108 |
+
model.autocast_dtype = torch.bfloat16 if args.precision == "bf16" else None
|
| 109 |
+
print(f"fan-out latency on one state ({len(renderer.state(state))} state tokens):")
|
| 110 |
+
results["latency_ms"] = {}
|
| 111 |
+
for n in (1, 4, 16, 64):
|
| 112 |
+
if n > len(questions):
|
| 113 |
+
break
|
| 114 |
+
qs = questions[:n]
|
| 115 |
+
_, t_fork = timed(lambda: model.question_logits(renderer, state, qs, fork=True, max_batch=64))
|
| 116 |
+
_, t_seq = timed(lambda: [model.question_logits(renderer, state, [q], fork=False) for q in qs], reps=1)
|
| 117 |
+
print(f" {n:3d} questions: forked batch {t_fork:7.0f} ms | {n} separate calls {t_seq:7.0f} ms ({t_seq/t_fork:.1f}x)")
|
| 118 |
+
results["latency_ms"][n] = {"fork": t_fork, "separate": t_seq}
|
| 119 |
+
|
| 120 |
+
if args.save_calibrated:
|
| 121 |
+
with torch.no_grad():
|
| 122 |
+
model.log_temp.copy_(torch.tensor(temps).log())
|
| 123 |
+
head = {k: v for k, v in model.state_dict().items() if not k.startswith("backbone.")}
|
| 124 |
+
torch.save(head, Path(args.ckpt) / "head.pt")
|
| 125 |
+
print("saved temperatures into", Path(args.ckpt) / "head.pt")
|
| 126 |
+
(Path(args.ckpt) / "eval.json").write_text(json.dumps(results, indent=2))
|
scripts/eval_images.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Zero-shot image decisions through the production path: base64 image parts inside `state`.
|
| 2 |
+
|
| 3 |
+
python scripts/eval_images.py runs/qwen35-4b-best-bf16 --tasks cifar10,documents --detail auto,high --n 100
|
| 4 |
+
|
| 5 |
+
(scripts/probe_vision.py was the original feasibility probe; this goes through DecisionModel.answer,
|
| 6 |
+
i.e. exactly what the server runs, with aspect-preserving resize and the per-image "detail" hint.)
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import base64
|
| 10 |
+
import io
|
| 11 |
+
import json
|
| 12 |
+
import random
|
| 13 |
+
import time
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
from datasets import load_dataset
|
| 18 |
+
|
| 19 |
+
from zico import Choice, DecisionModel
|
| 20 |
+
from zico.backbone import pick_device
|
| 21 |
+
from zico.calibration import ece
|
| 22 |
+
|
| 23 |
+
p = argparse.ArgumentParser()
|
| 24 |
+
p.add_argument("ckpt")
|
| 25 |
+
p.add_argument("--tasks", default="cifar10,documents")
|
| 26 |
+
p.add_argument("--detail", default="auto", help="comma-separated: auto, low, high")
|
| 27 |
+
p.add_argument("--n", type=int, default=100)
|
| 28 |
+
p.add_argument("--out", default="logs/eval_images.json")
|
| 29 |
+
args = p.parse_args()
|
| 30 |
+
|
| 31 |
+
SPECS = { # name: (dataset, split, image column, label column, instructions, streamed?)
|
| 32 |
+
"cifar10": ("uoft-cs/cifar10", "test", "img", "label", "What does the image show?", False),
|
| 33 |
+
"mnist": ("ylecun/mnist", "test", "image", "label", "Which digit is written in the image?", False),
|
| 34 |
+
"pets": ("timm/oxford-iiit-pet", "test", "image", "label", "Which breed is the animal in the photo?", True),
|
| 35 |
+
"food101": ("ethz/food101", "validation", "image", "label", "Which dish is shown in the photo?", True),
|
| 36 |
+
"documents": ("jordyvl/rvl_cdip_100_examples_per_class", "test", "image", "label", "What type of document is this scan?", False),
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
dev = pick_device()
|
| 40 |
+
model, renderer, _ = DecisionModel.load(args.ckpt, device=dev, dtype=torch.bfloat16 if dev != "cpu" else torch.float32,
|
| 41 |
+
renderer_overrides={"max_state_tokens": 8192}) # serving limit, not the training-time one
|
| 42 |
+
assert model.supports_images, "run scripts/add_vision.py on this checkpoint first"
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def encode(img) -> str:
|
| 46 |
+
buf = io.BytesIO()
|
| 47 |
+
img.convert("RGB").save(buf, format="PNG")
|
| 48 |
+
return base64.b64encode(buf.getvalue()).decode()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
results = {}
|
| 52 |
+
for task in args.tasks.split(","):
|
| 53 |
+
name, split, img_col, lab_col, instructions, stream = SPECS[task]
|
| 54 |
+
if stream:
|
| 55 |
+
ds = load_dataset(name, split=split, streaming=True).shuffle(seed=0, buffer_size=3000)
|
| 56 |
+
rows, names = list(ds.take(args.n)), load_dataset(name, split=split, streaming=True).features[lab_col].names
|
| 57 |
+
else:
|
| 58 |
+
ds = load_dataset(name, split=split).shuffle(seed=0)
|
| 59 |
+
rows, names = [ds[i] for i in range(min(args.n, len(ds)))], ds.features[lab_col].names
|
| 60 |
+
keys = [str(x).replace(" ", "_").lower() for x in names]
|
| 61 |
+
for detail in args.detail.split(","):
|
| 62 |
+
rng = random.Random(0)
|
| 63 |
+
conf, correct, tokens, t0 = [], [], [], time.time()
|
| 64 |
+
for r in rows:
|
| 65 |
+
order = keys[:]
|
| 66 |
+
rng.shuffle(order)
|
| 67 |
+
part = {"type": "image", "data": encode(r[img_col])}
|
| 68 |
+
if detail != "auto":
|
| 69 |
+
part["detail"] = detail
|
| 70 |
+
out = model.answer(renderer, {"image": part}, {"q": Choice(instructions, order)})
|
| 71 |
+
a = out["answers"]["q"]
|
| 72 |
+
conf.append(max(a["probabilities"].values()))
|
| 73 |
+
correct.append(a["choice"] == keys[r[lab_col]])
|
| 74 |
+
tokens.append(out["usage"]["input_tokens"])
|
| 75 |
+
e, _ = ece(torch.tensor(conf), torch.tensor(correct))
|
| 76 |
+
m = {"n": len(rows), "options": len(keys), "acc": sum(correct) / len(correct), "ece": e,
|
| 77 |
+
"mean_tokens": sum(tokens) / len(tokens), "sec_per_image": (time.time() - t0) / len(rows)} # fmt: skip
|
| 78 |
+
results[f"{task}/{detail}"] = m
|
| 79 |
+
print(f"{task:10s} detail={detail:5s} n={m['n']:4d} options={m['options']:3d} acc={m['acc']:.3f} ece={m['ece']:.3f} "
|
| 80 |
+
f"tokens/request={m['mean_tokens']:.0f} {m['sec_per_image']:.2f}s/img", flush=True) # fmt: skip
|
| 81 |
+
Path(args.out).parent.mkdir(parents=True, exist_ok=True)
|
| 82 |
+
Path(args.out).write_text(json.dumps(results, indent=2))
|
scripts/infer.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Answer a System One request (state + typed questions) with a trained checkpoint.
|
| 2 |
+
|
| 3 |
+
python scripts/infer.py runs/qwen35-0.8b/best examples/request.json
|
| 4 |
+
|
| 5 |
+
Request: {"state": <str | object>, "questions": {"name": {"type": "choice|score|noul", "instructions": ..., "criteria": ...}}}
|
| 6 |
+
Response: {"answers": {"name": {...}}, "usage": {...}, "model": ..., "latency_ms": ...}
|
| 7 |
+
"""
|
| 8 |
+
import argparse
|
| 9 |
+
import json
|
| 10 |
+
import sys
|
| 11 |
+
import time
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from zico import DecisionModel, question_from_dict
|
| 16 |
+
from zico.backbone import pick_device
|
| 17 |
+
|
| 18 |
+
p = argparse.ArgumentParser()
|
| 19 |
+
p.add_argument("ckpt")
|
| 20 |
+
p.add_argument("request", nargs="?", default="-", help="path to a JSON request, or - for stdin")
|
| 21 |
+
p.add_argument("--precision", default="bf16", choices=["bf16", "fp32"])
|
| 22 |
+
args = p.parse_args()
|
| 23 |
+
|
| 24 |
+
req = json.load(sys.stdin if args.request == "-" else open(args.request))
|
| 25 |
+
dev = pick_device()
|
| 26 |
+
model, renderer, meta = DecisionModel.load(args.ckpt, device=dev)
|
| 27 |
+
model.autocast_dtype = torch.bfloat16 if args.precision == "bf16" else None
|
| 28 |
+
questions = {name: question_from_dict(q) for name, q in req["questions"].items()}
|
| 29 |
+
|
| 30 |
+
model.answer(renderer, req["state"], questions) # warm-up (kernel compilation)
|
| 31 |
+
t0 = time.time()
|
| 32 |
+
resp = model.answer(renderer, req["state"], questions)
|
| 33 |
+
if dev == "mps":
|
| 34 |
+
torch.mps.synchronize()
|
| 35 |
+
resp["latency_ms"] = round((time.time() - t0) * 1000, 1)
|
| 36 |
+
resp["model"] = f"zico/{meta['base_model']}"
|
| 37 |
+
print(json.dumps(resp, indent=2))
|
scripts/make_soft_targets.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Label rows with a model's own probabilities, to use as soft training targets ("anchor" rows).
|
| 2 |
+
|
| 3 |
+
python scripts/make_soft_targets.py runs/qwen35-4b-best-bf16 --rows data_b/train.jsonl --n 80000 --out data_img3/anchor.jsonl
|
| 4 |
+
|
| 5 |
+
Why: every further full fine-tune we tried made the model more overconfident on tasks it had never
|
| 6 |
+
seen (the second text pass, both image runs). Training the replayed text rows toward the *current*
|
| 7 |
+
model's distribution instead of toward hard labels keeps its text behaviour -- including how unsure it
|
| 8 |
+
is -- where it was, while image rows still learn from hard labels. Soft targets are just another
|
| 9 |
+
distribution for the proper scoring losses, so nothing else in the trainer changes. Rows from tasks
|
| 10 |
+
the teacher never trained on are especially useful: they pin down its zero-shot behaviour.
|
| 11 |
+
"""
|
| 12 |
+
import argparse
|
| 13 |
+
import json
|
| 14 |
+
import random
|
| 15 |
+
|
| 16 |
+
import torch
|
| 17 |
+
|
| 18 |
+
from zico import DecisionModel
|
| 19 |
+
from zico.backbone import pick_device
|
| 20 |
+
from zico.data import read_jsonl, render_records
|
| 21 |
+
from zico.evaluate import predict
|
| 22 |
+
|
| 23 |
+
p = argparse.ArgumentParser()
|
| 24 |
+
p.add_argument("teacher")
|
| 25 |
+
p.add_argument("--rows", required=True, help="jsonl of text records")
|
| 26 |
+
p.add_argument("--n", type=int, default=80000)
|
| 27 |
+
p.add_argument("--out", required=True)
|
| 28 |
+
p.add_argument("--max-len", type=int, default=1024)
|
| 29 |
+
p.add_argument("--seed", type=int, default=0)
|
| 30 |
+
args = p.parse_args()
|
| 31 |
+
|
| 32 |
+
records = [r for r in read_jsonl(args.rows) if "image_grids" not in r]
|
| 33 |
+
random.Random(args.seed).shuffle(records)
|
| 34 |
+
records = records[: args.n]
|
| 35 |
+
|
| 36 |
+
dev = pick_device()
|
| 37 |
+
model, renderer, _ = DecisionModel.load(args.teacher, device=dev)
|
| 38 |
+
model.autocast_dtype = torch.bfloat16 if dev != "cpu" else None
|
| 39 |
+
rows, kept = render_records(records, renderer, args.max_len)
|
| 40 |
+
preds = predict(model, rows, kept, renderer.slot_id, max_tokens=16384, max_rows=256)
|
| 41 |
+
|
| 42 |
+
agree = conf = 0.0
|
| 43 |
+
with open(args.out, "w") as f:
|
| 44 |
+
for rec, pr in zip(kept, preds):
|
| 45 |
+
probs = [round(float(x), 6) for x in pr["p"]]
|
| 46 |
+
agree += int(pr["p"].argmax()) == rec["label"]
|
| 47 |
+
conf += max(probs)
|
| 48 |
+
f.write(json.dumps({**rec, "soft": probs, "anchor": True}, ensure_ascii=False) + "\n")
|
| 49 |
+
n = len(kept)
|
| 50 |
+
print(f"wrote {n} anchor rows to {args.out}: teacher agrees with the gold label on {agree / n:.3f}, mean confidence {conf / n:.3f}")
|
scripts/run_4b.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Runbook for the 4B run. Each stage is restartable; long stages should be launched under nohup/tmux.
|
| 3 |
+
# bash scripts/run_4b.sh data # build the broad dataset (~1 h, CPU + network)
|
| 4 |
+
# bash scripts/run_4b.sh speed # measure tokens/s and memory (~5 min)
|
| 5 |
+
# bash scripts/run_4b.sh train # train; re-running resumes (hours)
|
| 6 |
+
# bash scripts/run_4b.sh eval # test + held-out + calibration
|
| 7 |
+
# Tunables (env): MODEL, DATA, RUN, N_TRAIN, EPOCHS, MAX_TOKENS, ACCUM, LR
|
| 8 |
+
set -euo pipefail
|
| 9 |
+
cd "$(dirname "$0")/.."
|
| 10 |
+
# shellcheck disable=SC1091
|
| 11 |
+
if [ -f /venv/main/bin/activate ]; then source /venv/main/bin/activate; else source .venv/bin/activate; fi
|
| 12 |
+
export TOKENIZERS_PARALLELISM=false HF_HUB_DISABLE_PROGRESS_BARS=1
|
| 13 |
+
|
| 14 |
+
MODEL=${MODEL:-Qwen/Qwen3.5-4B-Base}
|
| 15 |
+
DATA=${DATA:-data_a}
|
| 16 |
+
RUN=${RUN:-runs/qwen35-4b}
|
| 17 |
+
mkdir -p logs
|
| 18 |
+
|
| 19 |
+
case "${1:-}" in
|
| 20 |
+
data)
|
| 21 |
+
python scripts/build_data.py --out "$DATA" --sources all --include-ni \
|
| 22 |
+
--n-train "${N_TRAIN:-15000}" --n-val 300 --n-test 500 ;;
|
| 23 |
+
speed)
|
| 24 |
+
python scripts/spike_backbone.py "$MODEL" --dtype bf16 ;;
|
| 25 |
+
train)
|
| 26 |
+
python scripts/train.py --model "$MODEL" --data "$DATA" --out "$RUN" --resume \
|
| 27 |
+
--grad-checkpointing --max-tokens "${MAX_TOKENS:-16384}" --max-rows 256 --grad-accum "${ACCUM:-4}" \
|
| 28 |
+
--max-len 1024 --max-state-tokens 768 --lr "${LR:-1e-5}" --head-lr 5e-4 \
|
| 29 |
+
--epochs "${EPOCHS:-1}" --eval-every 250 --val-rows 3000 --log-every 10 ;;
|
| 30 |
+
eval)
|
| 31 |
+
python scripts/eval.py "$RUN/best" --data "$DATA" --max-len 1024 --save-calibrated ;;
|
| 32 |
+
*)
|
| 33 |
+
echo "usage: $0 {data|speed|train|eval}"; exit 1 ;;
|
| 34 |
+
esac
|
scripts/run_img.sh
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Runbook for the image fine-tune on a rented CUDA box. Each stage is restartable.
|
| 3 |
+
# bash scripts/run_img.sh vision # package the base ViT into the starting checkpoint (~2 min)
|
| 4 |
+
# bash scripts/run_img.sh data # image datasets + replayed text -> data_img/ (~45 min, CPU + network)
|
| 5 |
+
# bash scripts/run_img.sh speed # 40 optimizer steps without saving: tokens/s and memory (~5 min)
|
| 6 |
+
# bash scripts/run_img.sh train # train; re-running resumes (~1-1.5 h)
|
| 7 |
+
# bash scripts/run_img.sh eval # image + text test/held-out, calibration, text same-row benchmark
|
| 8 |
+
# Needs on the box first: the starting checkpoint (INIT), the text data to replay (TEXT), data_ref/.
|
| 9 |
+
set -euo pipefail
|
| 10 |
+
cd "$(dirname "$0")/.."
|
| 11 |
+
# shellcheck disable=SC1091
|
| 12 |
+
if [ -f /venv/main/bin/activate ]; then source /venv/main/bin/activate; else source .venv/bin/activate; fi
|
| 13 |
+
export TOKENIZERS_PARALLELISM=false HF_HUB_DISABLE_PROGRESS_BARS=1
|
| 14 |
+
|
| 15 |
+
INIT=${INIT:-runs/qwen35-4b-best-bf16}
|
| 16 |
+
TEXT=${TEXT:-data_a}
|
| 17 |
+
DATA=${DATA:-data_img}
|
| 18 |
+
RUN=${RUN:-runs/qwen35-4b-img}
|
| 19 |
+
# lower peak learning rate than the text runs: the model is already good at images zero-shot, and the
|
| 20 |
+
# second text pass showed how quickly calibration degrades when we push
|
| 21 |
+
TRAIN_ARGS="--init $INIT --data $DATA --grad-checkpointing --max-tokens ${MAX_TOKENS:-16384} --max-rows 128 \
|
| 22 |
+
--grad-accum ${ACCUM:-4} --max-len 1024 --lr ${LR:-5e-6} --head-lr 2e-4 --warmup 0.05 --epochs ${EPOCHS:-1} --log-every 10"
|
| 23 |
+
mkdir -p logs
|
| 24 |
+
|
| 25 |
+
case "${1:-}" in
|
| 26 |
+
vision) python scripts/add_vision.py "$INIT" ;;
|
| 27 |
+
data) python scripts/build_image_data.py --out "$DATA" --n-train "${N_TRAIN:-5000}" --text-replay "$TEXT" --text-rows "${TEXT_ROWS:-25000}" ;;
|
| 28 |
+
speed) python scripts/train.py $TRAIN_ARGS --out runs/_speed --train-rows 9000 --val-rows 100 --eval-every 100000 --no-save ;;
|
| 29 |
+
train) python scripts/train.py $TRAIN_ARGS --out "$RUN" --resume --eval-every 150 --val-rows 2500 ;;
|
| 30 |
+
eval)
|
| 31 |
+
python scripts/eval.py "$RUN/best" --data "$DATA" --max-len 1024 --save-calibrated
|
| 32 |
+
[ -d data_ref ] && python -u scripts/bench_ours.py "$RUN/best" --data data_ref --permutations 1 --out logs/bench_img_x1.json ;;
|
| 33 |
+
*) echo "usage: $0 {vision|data|speed|train|eval}"; exit 1 ;;
|
| 34 |
+
esac
|
scripts/serve.sh
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# Run the System-One-compatible server for a zico checkpoint.
|
| 3 |
+
# scripts/serve.sh start | stop | restart | keep-awake | status | log | key-path
|
| 4 |
+
# Config (env): ZICO_CKPT, ZICO_HOST, ZICO_PORT, ZICO_MAX_STATE_TOKENS, ZICO_MODEL_NAME
|
| 5 |
+
# By default it listens on this machine's Tailscale address if there is one (reachable from your
|
| 6 |
+
# other devices, not from the local network or the internet), otherwise on 127.0.0.1.
|
| 7 |
+
set -euo pipefail
|
| 8 |
+
cd "$(dirname "$0")/.."
|
| 9 |
+
if [ -f /venv/main/bin/activate ]; then source /venv/main/bin/activate; else source .venv/bin/activate; fi
|
| 10 |
+
mkdir -p logs
|
| 11 |
+
PIDFILE=logs/server.pid
|
| 12 |
+
LOG=logs/server.log
|
| 13 |
+
|
| 14 |
+
tailscale_ip() {
|
| 15 |
+
for t in tailscale /Applications/Tailscale.app/Contents/MacOS/Tailscale; do
|
| 16 |
+
if command -v "$t" >/dev/null 2>&1 || [ -x "$t" ]; then "$t" ip -4 2>/dev/null | head -1 && return; fi
|
| 17 |
+
done
|
| 18 |
+
}
|
| 19 |
+
# Optional serve.env (KEY=VALUE lines) holds this machine's choices, e.g. which checkpoint is live.
|
| 20 |
+
# Variables already set in the environment win, so a one-off override still works.
|
| 21 |
+
if [ -f serve.env ]; then
|
| 22 |
+
while IFS='=' read -r key value; do
|
| 23 |
+
case "$key" in ''|\#*) continue ;; esac
|
| 24 |
+
if [ -z "${!key:-}" ]; then export "$key=$value"; fi
|
| 25 |
+
done < serve.env
|
| 26 |
+
fi
|
| 27 |
+
export ZICO_CKPT=${ZICO_CKPT:-.}
|
| 28 |
+
export ZICO_PORT=${ZICO_PORT:-8787} # 8080 is commonly taken (llama.cpp's server defaults to it)
|
| 29 |
+
if [ -z "${ZICO_HOST:-}" ]; then ZICO_HOST=$(tailscale_ip || true); fi
|
| 30 |
+
export ZICO_HOST=${ZICO_HOST:-127.0.0.1}
|
| 31 |
+
export TOKENIZERS_PARALLELISM=false HF_HUB_OFFLINE=1
|
| 32 |
+
|
| 33 |
+
running() { [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; }
|
| 34 |
+
|
| 35 |
+
# Keep a Mac awake while the server lives (no effect elsewhere). -s matters as much as -i: a Mac that has
|
| 36 |
+
# already dozed off only surfaces for ~45 s "dark wakes", and those ignore the idle assertion alone.
|
| 37 |
+
awake() { if command -v caffeinate >/dev/null 2>&1; then (nohup caffeinate -i -s "$@" >/dev/null 2>&1 < /dev/null &); fi; }
|
| 38 |
+
|
| 39 |
+
start() {
|
| 40 |
+
if running; then echo "already running (pid $(cat "$PIDFILE"))"; return; fi
|
| 41 |
+
nohup python -m zico.server > "$LOG" 2>&1 < /dev/null &
|
| 42 |
+
echo $! > "$PIDFILE"
|
| 43 |
+
awake -w "$(cat "$PIDFILE")"
|
| 44 |
+
echo "starting on http://$ZICO_HOST:$ZICO_PORT (pid $(cat "$PIDFILE")); follow with: scripts/serve.sh log"
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
case "${1:-}" in
|
| 48 |
+
start) start ;;
|
| 49 |
+
restart)
|
| 50 |
+
# Hold the machine awake across the gap: an idle Mac sleeps within seconds of the old assertion ending.
|
| 51 |
+
awake -t 300
|
| 52 |
+
if running; then kill "$(cat "$PIDFILE")"; rm -f "$PIDFILE"; sleep 2; fi
|
| 53 |
+
start ;;
|
| 54 |
+
keep-awake) # re-assert for a server that is already running (e.g. after the Mac slept anyway)
|
| 55 |
+
if running; then command -v caffeinate >/dev/null 2>&1 && caffeinate -u -t 5 || true; awake -w "$(cat "$PIDFILE")"; echo "asserted for pid $(cat "$PIDFILE")"; else echo "not running"; fi ;;
|
| 56 |
+
stop)
|
| 57 |
+
if running; then kill "$(cat "$PIDFILE")"; rm -f "$PIDFILE"; echo stopped; else echo "not running"; fi ;;
|
| 58 |
+
status)
|
| 59 |
+
if running; then echo "running (pid $(cat "$PIDFILE")) on http://$ZICO_HOST:$ZICO_PORT"; else echo "not running"; fi ;;
|
| 60 |
+
log) tail -n 30 "$LOG" ;;
|
| 61 |
+
key-path) echo "${ZICO_KEY_FILE:-$HOME/.config/zico/api_key}" ;;
|
| 62 |
+
*) echo "usage: $0 {start|stop|restart|keep-awake|status|log|key-path}"; exit 1 ;;
|
| 63 |
+
esac
|
scripts/setup_box.sh
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# One-shot environment setup for a rented CUDA box (Vast.ai PyTorch "devel" template or similar).
|
| 3 |
+
# Usage: bash scripts/setup_box.sh
|
| 4 |
+
# Puts nothing secret on the machine: only public models and datasets are downloaded.
|
| 5 |
+
set -uo pipefail
|
| 6 |
+
cd "$(dirname "$0")/.."
|
| 7 |
+
|
| 8 |
+
echo "=== hardware"
|
| 9 |
+
nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader
|
| 10 |
+
echo "cpus: $(nproc) | ram: $(free -g | awk '/^Mem:/{print $2}') GB | disk free here: $(df -h . | awk 'NR==2{print $4}')"
|
| 11 |
+
|
| 12 |
+
echo "=== python env (reuses the image's CUDA-matched torch if there is one)"
|
| 13 |
+
if [ -f /venv/main/bin/activate ]; then
|
| 14 |
+
# Vast PyTorch images ship torch in /venv/main
|
| 15 |
+
# shellcheck disable=SC1091
|
| 16 |
+
source /venv/main/bin/activate
|
| 17 |
+
else
|
| 18 |
+
python3 -m venv --system-site-packages .venv
|
| 19 |
+
# shellcheck disable=SC1091
|
| 20 |
+
source .venv/bin/activate
|
| 21 |
+
fi
|
| 22 |
+
python -m pip --version >/dev/null 2>&1 || python -m ensurepip -q
|
| 23 |
+
pip install -q --upgrade pip
|
| 24 |
+
python -c "import torch" 2>/dev/null || pip install -q torch
|
| 25 |
+
export MAX_JOBS=${MAX_JOBS:-32}
|
| 26 |
+
pip install -q "transformers>=5.2" datasets peft safetensors numpy pytest bitsandbytes kernels
|
| 27 |
+
pip install -q -e .
|
| 28 |
+
|
| 29 |
+
echo "=== fused kernels for the Gated DeltaNet layers (the slow PyTorch fallback is 4x+ slower)"
|
| 30 |
+
pip install -q flash-linear-attention || echo "WARN: flash-linear-attention did not install"
|
| 31 |
+
pip install -q causal-conv1d --no-build-isolation || echo "WARN: causal-conv1d did not install (conv falls back to PyTorch)"
|
| 32 |
+
# fla's DeltaNet backward is numerically WRONG on Hopper GPUs with Triton 3.4-3.7.0 (fla refuses to run it);
|
| 33 |
+
# the tilelang backend is the fix that leaves PyTorch's pinned Triton alone. Verified below, not assumed.
|
| 34 |
+
pip install -q tilelang || echo "WARN: tilelang did not install"
|
| 35 |
+
pip install -q pillow torchvision fastapi uvicorn httpx || true
|
| 36 |
+
|
| 37 |
+
echo "=== sanity"
|
| 38 |
+
python - <<'PY'
|
| 39 |
+
import torch, transformers
|
| 40 |
+
print("torch", torch.__version__, "| cuda", torch.version.cuda, "| transformers", transformers.__version__)
|
| 41 |
+
assert torch.cuda.is_available(), "CUDA not available"
|
| 42 |
+
print("gpu:", torch.cuda.get_device_name(0), f"{torch.cuda.get_device_properties(0).total_memory/1e9:.0f} GB",
|
| 43 |
+
"| bf16:", torch.cuda.is_bf16_supported())
|
| 44 |
+
for mod in ("bitsandbytes", "fla", "causal_conv1d", "kernels"):
|
| 45 |
+
try:
|
| 46 |
+
__import__(mod); print(f" {mod}: ok")
|
| 47 |
+
except Exception as e: # noqa: BLE001
|
| 48 |
+
print(f" {mod}: MISSING ({type(e).__name__})")
|
| 49 |
+
from transformers import AutoTokenizer
|
| 50 |
+
for m in ("Qwen/Qwen3-0.6B", "Qwen/Qwen3.5-0.8B-Base"):
|
| 51 |
+
AutoTokenizer.from_pretrained(m) # tokenizers only, so the unit tests can run
|
| 52 |
+
PY
|
| 53 |
+
HF_HUB_OFFLINE=1 python -m pytest -q 2>&1 | tail -3
|
| 54 |
+
echo "=== fused-kernel gradients vs an independent float64 reference"
|
| 55 |
+
python scripts/check_kernels.py 2>&1 | grep -E "rel-err|KERNEL CHECK|FAILED"
|
| 56 |
+
echo "=== setup finished"
|
scripts/train.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fine-tune a pretrained decoder into a typed-decision model.
|
| 2 |
+
|
| 3 |
+
python scripts/train.py --model Qwen/Qwen3.5-4B-Base --data data_a --out runs/qwen35-4b --grad-checkpointing
|
| 4 |
+
python scripts/train.py ... --resume # continue an interrupted run from <out>/last
|
| 5 |
+
|
| 6 |
+
Stage 1 (default) is full-feedback training with proper scoring rules. Stage 2 is the
|
| 7 |
+
speculative bandit-feedback "RLCD" objective: continue from a checkpoint with
|
| 8 |
+
--init runs/... --objective bandit
|
| 9 |
+
"""
|
| 10 |
+
import argparse
|
| 11 |
+
import json
|
| 12 |
+
import math
|
| 13 |
+
import random
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import torch
|
| 18 |
+
from transformers import AutoTokenizer
|
| 19 |
+
|
| 20 |
+
from zico import DecisionModel, Renderer
|
| 21 |
+
from zico.backbone import load_text_backbone, pick_device
|
| 22 |
+
from zico.batching import bucketed_batches, collate
|
| 23 |
+
from zico.data import read_jsonl, render_records, subsample
|
| 24 |
+
from zico.evaluate import format_report, predict, report, strip_tables
|
| 25 |
+
from zico.losses import bandit_proper_score_loss, proper_score_loss
|
| 26 |
+
|
| 27 |
+
p = argparse.ArgumentParser()
|
| 28 |
+
p.add_argument("--model", default="Qwen/Qwen3.5-0.8B-Base")
|
| 29 |
+
p.add_argument("--init", default=None, help="start from a saved zico checkpoint instead of --model")
|
| 30 |
+
p.add_argument("--resume", action="store_true", help="continue from <out>/last (weights, optimizer, data position)")
|
| 31 |
+
p.add_argument("--data", default="data")
|
| 32 |
+
p.add_argument("--out", required=True)
|
| 33 |
+
p.add_argument("--epochs", type=float, default=1.0)
|
| 34 |
+
p.add_argument("--max-tokens", type=int, default=4096, help="padded tokens per micro-batch")
|
| 35 |
+
p.add_argument("--max-rows", type=int, default=32)
|
| 36 |
+
p.add_argument("--grad-accum", type=int, default=4)
|
| 37 |
+
p.add_argument("--max-len", type=int, default=768, help="rows longer than this are dropped")
|
| 38 |
+
p.add_argument("--max-state-tokens", type=int, default=512)
|
| 39 |
+
p.add_argument("--lr", type=float, default=2e-5, help="backbone lr (use ~2e-4 with --lora-rank)")
|
| 40 |
+
p.add_argument("--head-lr", type=float, default=1e-3)
|
| 41 |
+
p.add_argument("--weight-decay", type=float, default=0.01)
|
| 42 |
+
p.add_argument("--warmup", type=float, default=0.05)
|
| 43 |
+
p.add_argument("--optim", default="auto", choices=["auto", "adamw", "adamw8bit"], help="auto = 8-bit AdamW on CUDA if available")
|
| 44 |
+
p.add_argument("--lora-rank", type=int, default=0, help="0 = full fine-tuning")
|
| 45 |
+
p.add_argument("--train-embeddings", action="store_true")
|
| 46 |
+
p.add_argument("--grad-checkpointing", action="store_true")
|
| 47 |
+
p.add_argument("--precision", default="bf16", choices=["bf16", "fp32"], help="bf16 = autocast over fp32 weights")
|
| 48 |
+
p.add_argument("--objective", default="supervised", choices=["supervised", "bandit"])
|
| 49 |
+
p.add_argument("--brier-weight", type=float, default=0.5)
|
| 50 |
+
p.add_argument("--rps-weight", type=float, default=1.0)
|
| 51 |
+
p.add_argument("--bandit-eps", type=float, default=0.1)
|
| 52 |
+
p.add_argument("--train-rows", type=int, default=None)
|
| 53 |
+
p.add_argument("--val-rows", type=int, default=1200)
|
| 54 |
+
p.add_argument("--eval-every", type=int, default=200, help="optimizer steps; also writes the resumable <out>/last")
|
| 55 |
+
p.add_argument("--log-every", type=int, default=10)
|
| 56 |
+
p.add_argument("--no-save", action="store_true", help="skip checkpoints (speed / memory probing)")
|
| 57 |
+
p.add_argument("--seed", type=int, default=0)
|
| 58 |
+
args = p.parse_args()
|
| 59 |
+
|
| 60 |
+
torch.manual_seed(args.seed)
|
| 61 |
+
dev = pick_device()
|
| 62 |
+
out = Path(args.out)
|
| 63 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 64 |
+
(out / "args.json").write_text(json.dumps(vars(args), indent=2))
|
| 65 |
+
log_f = open(out / "log.jsonl", "a")
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def log(**kw):
|
| 69 |
+
log_f.write(json.dumps(kw) + "\n")
|
| 70 |
+
log_f.flush()
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# ------------------------------------------------------------------ model
|
| 74 |
+
state_path = out / "last" / "trainer_state.pt"
|
| 75 |
+
resuming = args.resume and state_path.exists()
|
| 76 |
+
if args.resume and not resuming:
|
| 77 |
+
print(f"--resume given but {state_path} not found; starting fresh")
|
| 78 |
+
lora_attached = False
|
| 79 |
+
if resuming or args.init:
|
| 80 |
+
src = out / "last" if resuming else Path(args.init)
|
| 81 |
+
model, renderer, meta = DecisionModel.load(src, merge_lora=not resuming, lora_trainable=resuming)
|
| 82 |
+
lora_attached = resuming and meta["lora"]
|
| 83 |
+
if not resuming:
|
| 84 |
+
with torch.no_grad():
|
| 85 |
+
model.log_temp.zero_() # post-hoc temperatures belong to the old weights; refit after training
|
| 86 |
+
print(f"{'resuming' if resuming else 'starting'} from {src} (base {meta['base_model']})")
|
| 87 |
+
else:
|
| 88 |
+
tok = AutoTokenizer.from_pretrained(args.model)
|
| 89 |
+
renderer = Renderer(tok, max_state_tokens=args.max_state_tokens)
|
| 90 |
+
model = DecisionModel(load_text_backbone(args.model, torch.float32), base_model=args.model)
|
| 91 |
+
|
| 92 |
+
if args.lora_rank and not lora_attached:
|
| 93 |
+
from peft import LoraConfig, get_peft_model
|
| 94 |
+
|
| 95 |
+
model.backbone = get_peft_model(
|
| 96 |
+
model.backbone, LoraConfig(r=args.lora_rank, lora_alpha=2 * args.lora_rank, target_modules="all-linear", lora_dropout=0.0)
|
| 97 |
+
)
|
| 98 |
+
if not args.train_embeddings:
|
| 99 |
+
model.backbone.get_input_embeddings().weight.requires_grad_(False)
|
| 100 |
+
if args.grad_checkpointing:
|
| 101 |
+
model.backbone.gradient_checkpointing_enable()
|
| 102 |
+
model.backbone.enable_input_require_grads()
|
| 103 |
+
model.autocast_dtype = torch.bfloat16 if args.precision == "bf16" else None
|
| 104 |
+
model.to(dev).train()
|
| 105 |
+
if model.image_pre is not None: # frozen ViT: never in the optimizer, runs without gradients, bf16 on GPUs
|
| 106 |
+
model.vision.model.to(device=dev, dtype=torch.bfloat16 if dev != "cpu" and args.precision == "bf16" else torch.float32)
|
| 107 |
+
print("vision tower attached (frozen): image rows are supported")
|
| 108 |
+
|
| 109 |
+
backbone_params = [q for q in model.backbone.parameters() if q.requires_grad]
|
| 110 |
+
head_params = model.head_parameters()
|
| 111 |
+
groups = [
|
| 112 |
+
{"params": backbone_params, "lr": args.lr, "weight_decay": args.weight_decay},
|
| 113 |
+
{"params": head_params, "lr": args.head_lr, "weight_decay": 0.0},
|
| 114 |
+
]
|
| 115 |
+
opt, opt_name = None, "torch AdamW"
|
| 116 |
+
if args.optim == "adamw8bit" or (args.optim == "auto" and dev == "cuda"):
|
| 117 |
+
try:
|
| 118 |
+
import bitsandbytes as bnb
|
| 119 |
+
|
| 120 |
+
opt, opt_name = bnb.optim.AdamW8bit(groups, betas=(0.9, 0.98)), "bitsandbytes AdamW8bit"
|
| 121 |
+
except ImportError:
|
| 122 |
+
print("bitsandbytes not installed; falling back to torch AdamW (needs far more memory)")
|
| 123 |
+
if opt is None:
|
| 124 |
+
opt = torch.optim.AdamW(groups, betas=(0.9, 0.98))
|
| 125 |
+
n_bb, n_head = sum(q.numel() for q in backbone_params), sum(q.numel() for q in head_params)
|
| 126 |
+
print(f"device={dev} precision={args.precision} optimizer={opt_name} trainable: backbone {n_bb/1e6:.1f}M + head {n_head/1e6:.2f}M")
|
| 127 |
+
|
| 128 |
+
# ------------------------------------------------------------------ data
|
| 129 |
+
t0 = time.time()
|
| 130 |
+
train_recs = subsample(read_jsonl(f"{args.data}/train.jsonl"), args.train_rows, args.seed)
|
| 131 |
+
val_recs = subsample(read_jsonl(f"{args.data}/val.jsonl"), args.val_rows, args.seed)
|
| 132 |
+
image_meta = model.image_pre.meta if model.image_pre is not None else None
|
| 133 |
+
train_rows, train_recs = render_records(train_recs, renderer, args.max_len, image_meta, args.data)
|
| 134 |
+
val_rows, val_recs = render_records(val_recs, renderer, args.max_len, image_meta, args.data)
|
| 135 |
+
lengths = [len(r) for r in train_rows]
|
| 136 |
+
n_img = sum(bool(r.image_paths) for r in train_rows)
|
| 137 |
+
if n_img:
|
| 138 |
+
print(f"{n_img} of {len(train_rows)} train rows contain images")
|
| 139 |
+
load_image = model.image_pre.load_prepared if model.image_pre is not None else None
|
| 140 |
+
print(f"rendered {len(train_rows)} train / {len(val_rows)} val rows in {time.time()-t0:.0f}s; "
|
| 141 |
+
f"mean len {sum(lengths)/len(lengths):.0f}, max {max(lengths)}, total {sum(lengths)/1e6:.2f}M tokens/epoch") # fmt: skip
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def epoch_batches(epoch: int):
|
| 145 |
+
"""Deterministic per epoch, so a resumed run sees exactly the batches it has not done yet.
|
| 146 |
+
|
| 147 |
+
Static (rows, length) shapes: see batching.bucketed_batches for why this matters on GPUs.
|
| 148 |
+
"""
|
| 149 |
+
return bucketed_batches(lengths, args.max_tokens, args.max_rows, random.Random(args.seed * 1000 + epoch), drop_last=True)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
batches_per_epoch = len(epoch_batches(0))
|
| 153 |
+
total_steps = max(1, int(batches_per_epoch * args.epochs) // args.grad_accum)
|
| 154 |
+
warmup_steps = max(1, int(args.warmup * total_steps))
|
| 155 |
+
print(f"{batches_per_epoch} micro-batches/epoch -> {total_steps} optimizer steps")
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def prefetch(items, build, workers: int = 12, ahead: int = 24):
|
| 159 |
+
"""Yield (item[0], item[1], build(item)) in order, building up to `ahead` items concurrently."""
|
| 160 |
+
import collections
|
| 161 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 162 |
+
|
| 163 |
+
if not any(r.image_paths for r in train_rows): # text-only data: nothing worth parallelising
|
| 164 |
+
for it in items:
|
| 165 |
+
yield it[0], it[1], build(it)
|
| 166 |
+
return
|
| 167 |
+
with ThreadPoolExecutor(workers) as pool:
|
| 168 |
+
queue, it = collections.deque(), iter(items)
|
| 169 |
+
for item in it:
|
| 170 |
+
queue.append((item, pool.submit(build, item)))
|
| 171 |
+
if len(queue) >= ahead:
|
| 172 |
+
first, fut = queue.popleft()
|
| 173 |
+
yield first[0], first[1], fut.result()
|
| 174 |
+
while queue:
|
| 175 |
+
first, fut = queue.popleft()
|
| 176 |
+
yield first[0], first[1], fut.result()
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def lr_scale(step):
|
| 180 |
+
if step < warmup_steps:
|
| 181 |
+
return (step + 1) / warmup_steps
|
| 182 |
+
t = (step - warmup_steps) / max(1, total_steps - warmup_steps)
|
| 183 |
+
return 0.1 + 0.9 * 0.5 * (1 + math.cos(math.pi * t))
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def run_eval(step):
|
| 187 |
+
preds = predict(model, val_rows, val_recs, renderer.slot_id)
|
| 188 |
+
rep = report(preds)
|
| 189 |
+
print(format_report(rep, f"val @ step {step}"), flush=True)
|
| 190 |
+
log(kind="eval", step=step, **strip_tables(rep))
|
| 191 |
+
return rep["overall"]["nll"]
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def mem() -> str:
|
| 195 |
+
if dev == "cuda":
|
| 196 |
+
return f" mem {torch.cuda.max_memory_allocated()/1e9:.0f}G"
|
| 197 |
+
if dev == "mps":
|
| 198 |
+
return f" mem {torch.mps.driver_allocated_memory()/1e9:.0f}G"
|
| 199 |
+
return ""
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
# ------------------------------------------------------------------ loop
|
| 203 |
+
best_nll, step, epoch, batches_done, tokens_seen = float("inf"), 0, 0, 0, 0
|
| 204 |
+
if resuming:
|
| 205 |
+
st = torch.load(state_path, map_location="cpu", weights_only=False)
|
| 206 |
+
opt.load_state_dict(st["opt"])
|
| 207 |
+
best_nll, step, epoch, batches_done, tokens_seen = st["best_nll"], st["step"], st["epoch"], st["batches_done"], st["tokens_seen"]
|
| 208 |
+
torch.set_rng_state(st["torch_rng"])
|
| 209 |
+
print(f"resumed at step {step} (epoch {epoch}, {batches_done} micro-batches into it), best val nll {best_nll:.4f}")
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def snapshot():
|
| 213 |
+
model.save(out / "last", renderer, {"step": step})
|
| 214 |
+
torch.save({"opt": opt.state_dict(), "best_nll": best_nll, "step": step, "epoch": epoch, "batches_done": batches_done,
|
| 215 |
+
"tokens_seen": tokens_seen, "torch_rng": torch.get_rng_state()}, state_path) # fmt: skip
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
run_loss, run_acc, run_n, micro = 0.0, 0.0, 0, 0
|
| 219 |
+
step_start, tokens_start = step, tokens_seen
|
| 220 |
+
t_start = t_log = time.time()
|
| 221 |
+
tokens_log = 0
|
| 222 |
+
done = step >= total_steps
|
| 223 |
+
while not done:
|
| 224 |
+
todo = [(bi, idx, pad_len) for bi, (idx, pad_len) in enumerate(epoch_batches(epoch)) if bi >= batches_done]
|
| 225 |
+
# JPEG decoding + patchifying happens in background threads, a few batches ahead of the GPU
|
| 226 |
+
for bi, idx, cpu_batch in prefetch(todo, lambda t: collate([train_rows[i] for i in t[1]], renderer.slot_id, t[2], load_image)):
|
| 227 |
+
b = {k: v.to(dev) for k, v in cpu_batch.items()}
|
| 228 |
+
logits = model(**b)
|
| 229 |
+
if args.objective == "supervised":
|
| 230 |
+
loss = proper_score_loss(logits, b["opt_mask"], b["target"], b["qtype"], args.brier_weight, args.rps_weight)
|
| 231 |
+
else:
|
| 232 |
+
loss, _ = bandit_proper_score_loss(logits, b["opt_mask"], b["label"], args.bandit_eps, args.brier_weight)
|
| 233 |
+
(loss.mean() / args.grad_accum).backward()
|
| 234 |
+
with torch.no_grad():
|
| 235 |
+
run_loss += loss.sum().item()
|
| 236 |
+
run_acc += (logits.argmax(-1) == b["label"]).sum().item()
|
| 237 |
+
run_n += len(idx)
|
| 238 |
+
n_tok = int(b["attention_mask"].sum())
|
| 239 |
+
tokens_seen += n_tok
|
| 240 |
+
tokens_log += n_tok
|
| 241 |
+
micro += 1
|
| 242 |
+
batches_done = bi + 1
|
| 243 |
+
if micro % args.grad_accum:
|
| 244 |
+
continue
|
| 245 |
+
|
| 246 |
+
scale = lr_scale(step)
|
| 247 |
+
for g, base in zip(opt.param_groups, (args.lr, args.head_lr)):
|
| 248 |
+
g["lr"] = base * scale
|
| 249 |
+
gnorm = torch.nn.utils.clip_grad_norm_(backbone_params + head_params, 1.0).item()
|
| 250 |
+
opt.step()
|
| 251 |
+
opt.zero_grad(set_to_none=True)
|
| 252 |
+
step += 1
|
| 253 |
+
|
| 254 |
+
if step % args.log_every == 0:
|
| 255 |
+
now = time.time()
|
| 256 |
+
eta = (total_steps - step) * (now - t_start) / max(1, step - step_start)
|
| 257 |
+
print(f"step {step:5d}/{total_steps} loss {run_loss/run_n:.4f} acc {run_acc/run_n:.3f} gnorm {gnorm:.2f} "
|
| 258 |
+
f"lr {args.lr*scale:.2e} {tokens_log/(now-t_log):,.0f} tok/s eta {eta/60:.0f}m{mem()}", flush=True) # fmt: skip
|
| 259 |
+
log(kind="train", step=step, loss=run_loss / run_n, acc=run_acc / run_n, gnorm=gnorm, tokens=tokens_seen)
|
| 260 |
+
run_loss, run_acc, run_n, t_log, tokens_log = 0.0, 0.0, 0, now, 0
|
| 261 |
+
if dev == "mps" and step % 25 == 0:
|
| 262 |
+
torch.mps.empty_cache() # variable shapes make the MPS allocator cache balloon
|
| 263 |
+
if step % args.eval_every == 0 or step == total_steps:
|
| 264 |
+
nll = run_eval(step)
|
| 265 |
+
if nll < best_nll:
|
| 266 |
+
best_nll = nll
|
| 267 |
+
if not args.no_save:
|
| 268 |
+
model.save(out / "best", renderer, {"step": step, "val_nll": nll})
|
| 269 |
+
print(f" saved best (val nll {nll:.4f})", flush=True)
|
| 270 |
+
if not args.no_save:
|
| 271 |
+
snapshot()
|
| 272 |
+
if step >= total_steps:
|
| 273 |
+
done = True
|
| 274 |
+
break
|
| 275 |
+
else:
|
| 276 |
+
epoch, batches_done = epoch + 1, 0
|
| 277 |
+
|
| 278 |
+
trained = tokens_seen - tokens_start
|
| 279 |
+
print(f"done: {step} steps, {trained/1e6:.2f}M tokens this session, {(time.time()-t_start)/60:.1f} min, best val nll {best_nll:.4f}")
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_core.py
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core invariants, checked on tiny randomly-initialised backbones (CPU, fp32, no training).
|
| 2 |
+
|
| 3 |
+
Tokenizers are loaded from the local HF cache; tests skip if a tokenizer is unavailable.
|
| 4 |
+
"""
|
| 5 |
+
import math
|
| 6 |
+
|
| 7 |
+
import pytest
|
| 8 |
+
import torch
|
| 9 |
+
import torch.nn.functional as F
|
| 10 |
+
|
| 11 |
+
from zico import Choice, DecisionModel, Noul, Renderer, Score
|
| 12 |
+
from zico.batching import collate, make_row, token_budget_batches
|
| 13 |
+
from zico.losses import bandit_proper_score_loss, proper_score_loss
|
| 14 |
+
from zico.render import M_OPT
|
| 15 |
+
|
| 16 |
+
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
+
STATE = {"ticket": {"id": 4412, "body": "I was charged twice this month and want my money back.", "plan": "pro"}}
|
| 18 |
+
QUESTIONS = {
|
| 19 |
+
"refund": Noul("The customer is asking for a refund."),
|
| 20 |
+
"team": Choice(
|
| 21 |
+
"Which team should handle this?",
|
| 22 |
+
{"billing": "Charges, invoices, refunds", "technical": "Bugs, outages", "sales": "Upgrades and pricing"},
|
| 23 |
+
),
|
| 24 |
+
"severity": Score("How severe is the issue?", ["Cosmetic", "Degraded but usable", "Blocking"]),
|
| 25 |
+
"lang": Choice("Which language is the message in?", ["english", "spanish", "german", "french", "other"]),
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _tiny(kind):
|
| 30 |
+
from transformers import AutoTokenizer
|
| 31 |
+
|
| 32 |
+
tok_id = {"qwen3": "Qwen/Qwen3-0.6B", "qwen3_5": "Qwen/Qwen3.5-0.8B-Base"}[kind]
|
| 33 |
+
try:
|
| 34 |
+
tok = AutoTokenizer.from_pretrained(tok_id, local_files_only=True)
|
| 35 |
+
except Exception: # noqa: BLE001
|
| 36 |
+
pytest.skip(f"tokenizer {tok_id} not in local cache")
|
| 37 |
+
torch.manual_seed(0)
|
| 38 |
+
if kind == "qwen3":
|
| 39 |
+
from transformers import Qwen3Config, Qwen3Model
|
| 40 |
+
|
| 41 |
+
cfg = Qwen3Config(
|
| 42 |
+
vocab_size=len(tok), hidden_size=64, intermediate_size=128, num_hidden_layers=3,
|
| 43 |
+
num_attention_heads=4, num_key_value_heads=2, head_dim=16, max_position_embeddings=2048,
|
| 44 |
+
)
|
| 45 |
+
backbone = Qwen3Model(cfg)
|
| 46 |
+
else:
|
| 47 |
+
from transformers.models.qwen3_5 import Qwen3_5TextConfig, Qwen3_5TextModel
|
| 48 |
+
|
| 49 |
+
cfg = Qwen3_5TextConfig(
|
| 50 |
+
vocab_size=len(tok), hidden_size=64, intermediate_size=128, num_hidden_layers=4,
|
| 51 |
+
num_attention_heads=4, num_key_value_heads=2, head_dim=16, full_attention_interval=4,
|
| 52 |
+
# 32-dim DeltaNet heads: mlx-lm's Metal kernel cannot be built for smaller ones
|
| 53 |
+
linear_num_key_heads=4, linear_num_value_heads=4, linear_key_head_dim=32, linear_value_head_dim=32,
|
| 54 |
+
max_position_embeddings=2048,
|
| 55 |
+
)
|
| 56 |
+
backbone = Qwen3_5TextModel(cfg)
|
| 57 |
+
model = DecisionModel(backbone, d_head=32, base_model=tok_id).eval()
|
| 58 |
+
with torch.no_grad(): # random head so logits are not trivially equal
|
| 59 |
+
model.unary.weight.normal_(0, 0.5)
|
| 60 |
+
# with fused DeltaNet kernels installed, HF routes to CUDA-only code, so test on the GPU if there is one
|
| 61 |
+
return model.to(DEVICE), Renderer(tok)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@pytest.fixture(scope="module", params=["qwen3", "qwen3_5"])
|
| 65 |
+
def tiny(request):
|
| 66 |
+
return _tiny(request.param)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def test_render_slots_and_shared_prefix(tiny):
|
| 70 |
+
_, r = tiny
|
| 71 |
+
s1, s2 = r.state(STATE), r.state(STATE)
|
| 72 |
+
assert s1 == s2 # the state segment never depends on the question
|
| 73 |
+
for q in QUESTIONS.values():
|
| 74 |
+
rq = r.question(q)
|
| 75 |
+
assert len(rq.opt_pos) == len(q.options()) == len(rq.keys)
|
| 76 |
+
assert all(rq.marker_ids[p] == M_OPT for p in rq.opt_pos)
|
| 77 |
+
assert rq.marker_ids[rq.ans_pos] > M_OPT and rq.ans_pos == len(rq.input_ids) - 1
|
| 78 |
+
assert sum(m >= 0 for m in rq.marker_ids) == len(rq.opt_pos) + 1
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def test_schema_limits():
|
| 82 |
+
with pytest.raises(ValueError):
|
| 83 |
+
Choice("x", ["only-one"])
|
| 84 |
+
with pytest.raises(ValueError):
|
| 85 |
+
Choice("x", [str(i) for i in range(256)])
|
| 86 |
+
with pytest.raises(ValueError):
|
| 87 |
+
Score("x", [str(i) for i in range(11)])
|
| 88 |
+
assert len(Choice("x", [str(i) for i in range(255)]).options()) == 255
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def test_fork_equals_rows_equals_single(tiny):
|
| 92 |
+
"""Batched questions over a forked state cache == each question run alone with the state."""
|
| 93 |
+
model, r = tiny
|
| 94 |
+
qs = list(QUESTIONS.values())
|
| 95 |
+
forked, _, _ = model.question_logits(r, STATE, qs, fork=True)
|
| 96 |
+
rows, _, _ = model.question_logits(r, STATE, qs, fork=False)
|
| 97 |
+
# On CUDA the cached-continuation path runs fla's Triton kernels, whose tl.dot uses TF32: logits agree to
|
| 98 |
+
# ~3e-3 (probabilities to ~2e-4) rather than to float32 rounding. A logic error is orders of magnitude larger.
|
| 99 |
+
atol = 5e-3 if DEVICE == "cuda" else 2e-4
|
| 100 |
+
for i, q in enumerate(qs):
|
| 101 |
+
single, _, _ = model.question_logits(r, STATE, [q], fork=False)
|
| 102 |
+
assert torch.allclose(forked[i], single[0], atol=atol), (i, forked[i], single[0])
|
| 103 |
+
assert torch.allclose(rows[i], single[0], atol=2e-4), (i, rows[i], single[0])
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def test_answers_are_type_safe(tiny):
|
| 107 |
+
model, r = tiny
|
| 108 |
+
out = model.answer(r, STATE, QUESTIONS)["answers"]
|
| 109 |
+
assert set(out) == set(QUESTIONS)
|
| 110 |
+
assert 0.0 <= out["refund"]["noul"] <= 1.0
|
| 111 |
+
team = out["team"]
|
| 112 |
+
assert team["choice"] in QUESTIONS["team"].criteria
|
| 113 |
+
assert set(team["probabilities"]) == set(QUESTIONS["team"].criteria)
|
| 114 |
+
assert math.isclose(sum(team["probabilities"].values()), 1.0, abs_tol=1e-4)
|
| 115 |
+
sev = out["severity"]
|
| 116 |
+
assert 0.0 <= sev["score"] <= 2.0 and set(sev["probabilities"]) == {"0", "1", "2"}
|
| 117 |
+
assert 0.0 <= sev["confidence"] <= 1.0
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_gradients_reach_backbone_and_head(tiny):
|
| 121 |
+
model, r = tiny
|
| 122 |
+
s = r.state(STATE)
|
| 123 |
+
rows = [make_row(s, r.question(q), label=1) for q in QUESTIONS.values()]
|
| 124 |
+
b = {k: v.to(DEVICE) for k, v in collate(rows, r.slot_id).items()}
|
| 125 |
+
loss = proper_score_loss(model(**b), b["opt_mask"], b["target"], b["qtype"], 0.5, 0.5).mean()
|
| 126 |
+
loss.backward()
|
| 127 |
+
assert model.marker_emb.weight.grad.abs().sum() > 0
|
| 128 |
+
assert model.q_proj.weight.grad.abs().sum() > 0
|
| 129 |
+
assert any(p.grad is not None and p.grad.abs().sum() > 0 for p in model.backbone.layers[0].parameters())
|
| 130 |
+
model.zero_grad()
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def test_log_score_is_cross_entropy():
|
| 134 |
+
torch.manual_seed(0)
|
| 135 |
+
logits = torch.randn(6, 5)
|
| 136 |
+
mask = torch.ones(6, 5, dtype=torch.bool)
|
| 137 |
+
label = torch.randint(0, 5, (6,))
|
| 138 |
+
target = F.one_hot(label, 5).float()
|
| 139 |
+
ours = proper_score_loss(logits, mask, target, torch.zeros(6, dtype=torch.long))
|
| 140 |
+
assert torch.allclose(ours, F.cross_entropy(logits, label, reduction="none"), atol=1e-6)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def test_rps_is_ordinal():
|
| 144 |
+
"""Putting mass on a neighbouring level must cost less than putting it on a distant one."""
|
| 145 |
+
mask = torch.ones(2, 5, dtype=torch.bool)
|
| 146 |
+
target = F.one_hot(torch.tensor([0, 0]), 5).float()
|
| 147 |
+
logits = torch.full((2, 5), -20.0)
|
| 148 |
+
logits[0, 1] = 20.0 # near miss
|
| 149 |
+
logits[1, 4] = 20.0 # far miss
|
| 150 |
+
qtype = torch.ones(2, dtype=torch.long) # score
|
| 151 |
+
ce_only = proper_score_loss(logits, mask, target, qtype)
|
| 152 |
+
with_rps = proper_score_loss(logits, mask, target, qtype, rps_weight=1.0)
|
| 153 |
+
rps = with_rps - ce_only
|
| 154 |
+
assert rps[0] < rps[1]
|
| 155 |
+
assert torch.allclose(rps, torch.tensor([0.25, 1.0]), atol=1e-4)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def test_bandit_estimator_is_unbiased():
|
| 159 |
+
"""E[bandit loss] == -log p_y, using only the right/wrong bit of a sampled action."""
|
| 160 |
+
g = torch.Generator().manual_seed(0)
|
| 161 |
+
logits = torch.tensor([[1.0, 0.2, -0.5, 0.0]]).repeat(200_000, 1)
|
| 162 |
+
mask = torch.ones_like(logits, dtype=torch.bool)
|
| 163 |
+
label = torch.full((logits.shape[0],), 2)
|
| 164 |
+
loss, _ = bandit_proper_score_loss(logits, mask, label, eps=0.2, clip=1e9, generator=g)
|
| 165 |
+
expected = -F.log_softmax(logits[0], -1)[2].item()
|
| 166 |
+
assert abs(loss.mean().item() - expected) < 0.03, (loss.mean().item(), expected)
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def test_temperature_scaling_recovers_known_miscalibration():
|
| 170 |
+
"""Labels drawn from softmax(z); an overconfident model reporting 3z must get T ~= 3."""
|
| 171 |
+
from zico.calibration import fit_temperature
|
| 172 |
+
|
| 173 |
+
g = torch.Generator().manual_seed(0)
|
| 174 |
+
z = torch.randn(4000, 6, generator=g) * 1.5
|
| 175 |
+
labels = torch.multinomial(torch.softmax(z, -1), 1, generator=g).squeeze(-1).tolist()
|
| 176 |
+
assert abs(fit_temperature(list(3 * z), labels) - 3.0) < 0.25
|
| 177 |
+
assert abs(fit_temperature(list(z), labels) - 1.0) < 0.1
|
| 178 |
+
# ragged option counts and a hopeless model stay inside the bounds instead of diverging
|
| 179 |
+
ragged = [torch.randn(int(k), generator=g) * 50 for k in torch.randint(2, 9, (300,), generator=g)]
|
| 180 |
+
t = fit_temperature(ragged, [0] * 300)
|
| 181 |
+
assert 0.05 <= t <= 20.0
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def test_token_budget_batches_cover_everything():
|
| 185 |
+
import random
|
| 186 |
+
|
| 187 |
+
lengths = [random.Random(1).randint(10, 400) for _ in range(500)]
|
| 188 |
+
batches = token_budget_batches(lengths, max_tokens=2048, max_rows=16, shuffle_rng=random.Random(0))
|
| 189 |
+
assert sorted(i for b in batches for i in b) == list(range(500))
|
| 190 |
+
assert all(max(lengths[i] for i in b) * len(b) <= 2048 and len(b) <= 16 for b in batches)
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def test_bucketed_batches_use_few_static_shapes():
|
| 194 |
+
import random
|
| 195 |
+
|
| 196 |
+
from zico.batching import bucketed_batches
|
| 197 |
+
|
| 198 |
+
r = random.Random(1)
|
| 199 |
+
n = 60_000
|
| 200 |
+
lengths = [r.randint(10, 1000) for _ in range(n)]
|
| 201 |
+
batches = bucketed_batches(lengths, max_tokens=16384, max_rows=256, shuffle_rng=random.Random(0))
|
| 202 |
+
assert sorted(i for idx, _ in batches for i in idx) == list(range(n)) # nothing lost without drop_last
|
| 203 |
+
assert all(max(lengths[i] for i in idx) <= L and len(idx) * L <= 16384 for idx, L in batches)
|
| 204 |
+
static = bucketed_batches(lengths, 16384, 256, random.Random(0), drop_last=True)
|
| 205 |
+
assert len({(len(idx), L) for idx, L in static}) <= 9 # a handful of shapes, tuned once each
|
| 206 |
+
assert sum(len(idx) for idx, _ in static) > 0.98 * n # only one ragged batch per bucket is dropped
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
def test_save_load_roundtrip(tiny, tmp_path):
|
| 210 |
+
model, r = tiny
|
| 211 |
+
qs = list(QUESTIONS.values())
|
| 212 |
+
before, _, _ = model.question_logits(r, STATE, qs)
|
| 213 |
+
model.save(tmp_path / "ckpt", r)
|
| 214 |
+
loaded, r2, _ = DecisionModel.load(tmp_path / "ckpt", device=DEVICE)
|
| 215 |
+
after, _, _ = loaded.question_logits(r2, STATE, qs)
|
| 216 |
+
for a, b in zip(before, after):
|
| 217 |
+
assert torch.allclose(a, b, atol=1e-5)
|
tests/test_mlx.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MLX backend parity on a tiny random Qwen3.5 (Apple silicon only; skipped elsewhere).
|
| 2 |
+
|
| 3 |
+
Guards the weight conversion (conv layout, the +1 shift on zero-centred norms, key nesting), the
|
| 4 |
+
slot-embedding injection, the cache fork for both layer types, and the head port.
|
| 5 |
+
"""
|
| 6 |
+
import pytest
|
| 7 |
+
|
| 8 |
+
pytest.importorskip("mlx_lm")
|
| 9 |
+
|
| 10 |
+
import torch # noqa: E402
|
| 11 |
+
|
| 12 |
+
from zico.mlx_backend import MLXDecisionModel # noqa: E402
|
| 13 |
+
from tests.test_core import QUESTIONS, STATE, _tiny # noqa: E402
|
| 14 |
+
from tests.test_vision import tiny_mm # noqa: E402, F401 (pytest fixture)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.fixture(scope="module")
|
| 18 |
+
def pair(tmp_path_factory):
|
| 19 |
+
model, renderer = _tiny("qwen3_5")
|
| 20 |
+
with torch.no_grad(): # make every ported parameter matter
|
| 21 |
+
model.readout_norm.weight.normal_(1, 0.2)
|
| 22 |
+
model.readout_norm.bias.normal_(0, 0.2)
|
| 23 |
+
model.log_temp.copy_(torch.tensor([0.1, -0.2, 0.3]))
|
| 24 |
+
path = tmp_path_factory.mktemp("ckpt") / "tiny"
|
| 25 |
+
model.save(path, renderer)
|
| 26 |
+
mlx_model, mlx_renderer, _ = MLXDecisionModel.load(path)
|
| 27 |
+
return model, renderer, mlx_model, mlx_renderer
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def test_mlx_matches_pytorch(pair):
|
| 31 |
+
model, renderer, mlx_model, mlx_renderer = pair
|
| 32 |
+
a = model.answer(renderer, STATE, QUESTIONS)
|
| 33 |
+
b = mlx_model.answer(mlx_renderer, STATE, QUESTIONS)
|
| 34 |
+
assert a["usage"] == b["usage"]
|
| 35 |
+
for name in QUESTIONS:
|
| 36 |
+
x, y = a["answers"][name], b["answers"][name]
|
| 37 |
+
assert x["type"] == y["type"]
|
| 38 |
+
# fp32 on both sides: agreement should be at rounding level. A looser tolerance would have hidden
|
| 39 |
+
# the q/k epsilon mismatch that _reference_qk_norm corrects (it showed up as 6e-2 here).
|
| 40 |
+
if x["type"] == "noul":
|
| 41 |
+
assert abs(x["noul"] - y["noul"]) < 1e-4
|
| 42 |
+
else:
|
| 43 |
+
assert all(abs(x["probabilities"][k] - y["probabilities"][k]) < 1e-4 for k in x["probabilities"])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_mlx_image_path_matches_pytorch(tiny_mm, tmp_path):
|
| 47 |
+
"""Explicit 3-part rope, ViT features scattered into the state, and the fork after an image."""
|
| 48 |
+
from tests.test_vision import QUESTIONS as IMAGE_QUESTIONS
|
| 49 |
+
from tests.test_vision import STATE as IMAGE_STATE
|
| 50 |
+
|
| 51 |
+
_, model, renderer = tiny_mm
|
| 52 |
+
path = tmp_path / "ckpt"
|
| 53 |
+
model.save(path, renderer) # a checkpoint of a vision-enabled model carries its (frozen) ViT along
|
| 54 |
+
assert (path / "vision" / "model.safetensors").exists()
|
| 55 |
+
mlx_model, mlx_renderer, _ = MLXDecisionModel.load(path)
|
| 56 |
+
mlx_model.attach_vision(path / "vision", device="cpu", dtype=torch.float32, min_pixels=32 * 32, max_pixels=128 * 128)
|
| 57 |
+
qs = {f"q{i}": q for i, q in enumerate(IMAGE_QUESTIONS)}
|
| 58 |
+
a = model.answer(renderer, IMAGE_STATE, qs)
|
| 59 |
+
b = mlx_model.answer(mlx_renderer, IMAGE_STATE, qs)
|
| 60 |
+
assert a["usage"] == b["usage"]
|
| 61 |
+
for name in qs:
|
| 62 |
+
x, y = a["answers"][name], b["answers"][name]
|
| 63 |
+
pa = x["probabilities"] if "probabilities" in x else {"true": x["noul"]}
|
| 64 |
+
pb = y["probabilities"] if "probabilities" in y else {"true": y["noul"]}
|
| 65 |
+
assert all(abs(pa[k] - pb[k]) < 1e-4 for k in pa), (name, pa, pb)
|
| 66 |
+
# a different picture under identical token ids must not hit the cached prefix
|
| 67 |
+
other = {**IMAGE_STATE, "second": {"type": "image", "data": __import__("tests.test_vision", fromlist=["png"]).png(64, 128, seed=9)}}
|
| 68 |
+
assert mlx_model.answer(mlx_renderer, other, qs) != b
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_state_prefix_is_reused_and_not_corrupted(pair):
|
| 72 |
+
_, _, mlx_model, mlx_renderer = pair
|
| 73 |
+
mlx_model._prefixes.clear()
|
| 74 |
+
first = mlx_model.answer(mlx_renderer, STATE, QUESTIONS)
|
| 75 |
+
assert len(mlx_model._prefixes) == 1
|
| 76 |
+
again = mlx_model.answer(mlx_renderer, STATE, QUESTIONS) # served from the cached prefix
|
| 77 |
+
assert len(mlx_model._prefixes) == 1 and first == again
|
| 78 |
+
for i in range(mlx_model.prefix_cache_size + 2): # LRU stays bounded
|
| 79 |
+
mlx_model.answer(mlx_renderer, {"n": i}, {"q": QUESTIONS["refund"]})
|
| 80 |
+
assert len(mlx_model._prefixes) == mlx_model.prefix_cache_size
|
tests/test_server.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The HTTP contract, exercised against a tiny random model (no real checkpoint needed)."""
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
import pytest
|
| 5 |
+
|
| 6 |
+
pytest.importorskip("fastapi")
|
| 7 |
+
pytest.importorskip("httpx")
|
| 8 |
+
from fastapi.testclient import TestClient # noqa: E402
|
| 9 |
+
|
| 10 |
+
from zico.server import create_app # noqa: E402
|
| 11 |
+
from tests.test_core import _tiny # noqa: E402
|
| 12 |
+
|
| 13 |
+
KEY = "test-key"
|
| 14 |
+
AUTH = {"Authorization": f"Bearer {KEY}"}
|
| 15 |
+
REQUEST = {
|
| 16 |
+
"model": "zico-latest",
|
| 17 |
+
"state": {"ticket": {"body": "I was charged twice and want my money back."}},
|
| 18 |
+
"questions": {
|
| 19 |
+
"refund": {"type": "noul", "instructions": "The customer asks for a refund."},
|
| 20 |
+
"team": {"type": "choice", "instructions": "Which team?", "criteria": {"billing": "Charges, refunds", "technical": None}},
|
| 21 |
+
"severity": {"type": "score", "instructions": "How severe?", "criteria": ["Cosmetic", "Degraded", "Blocking"]},
|
| 22 |
+
},
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@pytest.fixture(scope="module")
|
| 27 |
+
def client():
|
| 28 |
+
model, renderer = _tiny("qwen3")
|
| 29 |
+
return TestClient(create_app(model, renderer, {KEY}, "tiny-test-model"))
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def test_requires_bearer_key(client):
|
| 33 |
+
assert client.post("/v1/systemone", json=REQUEST).status_code == 401
|
| 34 |
+
assert client.post("/v1/systemone", json=REQUEST, headers={"Authorization": "Bearer wrong"}).status_code == 401
|
| 35 |
+
assert client.get("/healthz").status_code == 200 # health check is open
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_response_matches_the_documented_shape(client):
|
| 39 |
+
r = client.post("/v1/systemone", json=REQUEST, headers=AUTH)
|
| 40 |
+
assert r.status_code == 200
|
| 41 |
+
assert r.headers["x-typesafe-request-id"] == r.headers["x-request-id"]
|
| 42 |
+
body = r.json()
|
| 43 |
+
assert set(body) == {"model", "answers", "usage"}
|
| 44 |
+
assert body["model"] == "tiny-test-model" # always the real model, never the alias that was requested
|
| 45 |
+
assert body["usage"]["output_tokens"] == 0 and body["usage"]["input_tokens"] > 0
|
| 46 |
+
a = body["answers"]
|
| 47 |
+
assert set(a) == set(REQUEST["questions"])
|
| 48 |
+
assert a["refund"]["type"] == "noul" and 0 <= a["refund"]["noul"] <= 1 and set(a["refund"]) == {"type", "noul"}
|
| 49 |
+
team = a["team"]
|
| 50 |
+
assert set(team) == {"type", "choice", "probabilities", "confidence"} and team["choice"] in ("billing", "technical")
|
| 51 |
+
assert set(team["probabilities"]) == {"billing", "technical"} and math.isclose(sum(team["probabilities"].values()), 1, abs_tol=1e-4)
|
| 52 |
+
sev = a["severity"]
|
| 53 |
+
assert set(sev) == {"type", "score", "legend", "probabilities", "confidence"}
|
| 54 |
+
assert sev["legend"] == {"0": "Cosmetic", "1": "Degraded", "2": "Blocking"} and 0 <= sev["score"] <= 2
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
@pytest.mark.parametrize("mutate, status", [
|
| 58 |
+
(lambda b: b.pop("state"), 422),
|
| 59 |
+
(lambda b: b.update(model="not-a-model"), 404),
|
| 60 |
+
(lambda b: b.update(questions={}), 422),
|
| 61 |
+
(lambda b: b["questions"]["team"].update(criteria={"only": "one"}), 422),
|
| 62 |
+
(lambda b: b["questions"]["severity"].update(criteria=[str(i) for i in range(11)]), 422),
|
| 63 |
+
(lambda b: b["questions"]["refund"].update(type="essay"), 422),
|
| 64 |
+
(lambda b: b["questions"]["refund"].update(criteria={"maybe": "x"}), 422),
|
| 65 |
+
]) # fmt: skip
|
| 66 |
+
def test_validation_errors(client, mutate, status):
|
| 67 |
+
import copy
|
| 68 |
+
|
| 69 |
+
body = copy.deepcopy(REQUEST)
|
| 70 |
+
mutate(body)
|
| 71 |
+
r = client.post("/v1/systemone", json=body, headers=AUTH)
|
| 72 |
+
assert r.status_code == status
|
| 73 |
+
detail = r.json()["detail"]
|
| 74 |
+
if status == 422: # published HTTPValidationError shape
|
| 75 |
+
assert set(detail[0]) >= {"loc", "msg", "type"} and detail[0]["loc"][0] == "body"
|
| 76 |
+
else:
|
| 77 |
+
assert isinstance(detail, str)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def test_instructions_are_optional(client):
|
| 81 |
+
body = {**REQUEST, "questions": {"sev": {"type": "score", "criteria": ["Low", "High"]}}}
|
| 82 |
+
assert client.post("/v1/systemone", json=body, headers=AUTH).status_code == 200
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def test_models_listing(client):
|
| 86 |
+
cards = client.get("/v1/models", headers=AUTH).json()["models"]
|
| 87 |
+
assert cards[0]["name"] == "tiny-test-model" and set(cards[0]) == {"name", "description", "release_date"}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def test_test_page_is_public_but_the_api_stays_locked(client):
|
| 91 |
+
for path in ("/", "/ui"):
|
| 92 |
+
r = client.get(path) # no key: the page holds no secrets, the key is typed into it
|
| 93 |
+
assert r.status_code == 200 and r.headers["content-type"].startswith("text/html") and "zico test bench" in r.text
|
| 94 |
+
csp = r.headers["content-security-policy"]
|
| 95 |
+
assert "connect-src 'self'" in csp and "default-src 'none'" in csp and "frame-ancestors 'none'" in csp # it may only talk to this server
|
| 96 |
+
assert client.get("/v1/models").status_code == 401
|
| 97 |
+
assert client.post("/v1/systemone", json=REQUEST).status_code == 401
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def test_test_page_loads_nothing_from_elsewhere():
|
| 101 |
+
from zico.server import UI_FILE
|
| 102 |
+
|
| 103 |
+
html = UI_FILE.read_text(encoding="utf-8")
|
| 104 |
+
assert "innerHTML" not in html # model output and option names only ever go in as text
|
| 105 |
+
for needle in ('src="http', "src='http", 'href="http', "@import", "url(http"):
|
| 106 |
+
assert needle not in html
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def test_test_page_can_be_switched_off():
|
| 110 |
+
model, renderer = _tiny("qwen3")
|
| 111 |
+
off = TestClient(create_app(model, renderer, {KEY}, "tiny-test-model", ui=False))
|
| 112 |
+
assert off.get("/").status_code == 401 and off.get("/", headers=AUTH).status_code == 404
|
| 113 |
+
assert off.get("/healthz").status_code == 200
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def test_extra_model_names_are_configuration():
|
| 117 |
+
model, renderer = _tiny("qwen3")
|
| 118 |
+
app = TestClient(create_app(model, renderer, {KEY}, "tiny-test-model", aliases=("the-name-my-client-sends",)))
|
| 119 |
+
ok = {**REQUEST, "model": "the-name-my-client-sends"}
|
| 120 |
+
assert app.post("/v1/systemone", json=ok, headers=AUTH).status_code == 200
|
| 121 |
+
assert app.post("/v1/systemone", json={**REQUEST, "model": "tiny-test-model"}, headers=AUTH).status_code == 200 # the real name always works
|
| 122 |
+
assert app.post("/v1/systemone", json=REQUEST, headers=AUTH).status_code == 404 # default aliases were replaced
|
tests/test_vision.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Images in the state: parsing, positions, and equivalence with Hugging Face's multimodal forward.
|
| 2 |
+
|
| 3 |
+
Uses a tiny random multimodal Qwen3.5 (vision tower + hybrid language model), CPU / fp32.
|
| 4 |
+
"""
|
| 5 |
+
import base64
|
| 6 |
+
import io
|
| 7 |
+
import json
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
import pytest
|
| 11 |
+
import torch
|
| 12 |
+
|
| 13 |
+
from zico import Choice, DecisionModel, Noul, Renderer
|
| 14 |
+
from zico.vision import ImageError, mrope_positions, render_image_state, smart_resize, split_state
|
| 15 |
+
from tests.test_core import DEVICE
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def png(width=96, height=64, seed=0) -> str:
|
| 19 |
+
from PIL import Image
|
| 20 |
+
|
| 21 |
+
rng = np.random.default_rng(seed)
|
| 22 |
+
buf = io.BytesIO()
|
| 23 |
+
Image.fromarray(rng.integers(0, 255, (height, width, 3), dtype=np.uint8)).save(buf, format="PNG")
|
| 24 |
+
return base64.b64encode(buf.getvalue()).decode()
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# --------------------------------------------------------------------------- parsing
|
| 28 |
+
def test_split_state_finds_images_anywhere_and_in_three_dialects():
|
| 29 |
+
a, b, c = png(seed=1), png(seed=2), png(seed=3)
|
| 30 |
+
state = {
|
| 31 |
+
"listing": {"title": "Blue mug", "photo": {"type": "image", "data": a}},
|
| 32 |
+
"extra": [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b}},
|
| 33 |
+
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{c}"}}],
|
| 34 |
+
"note": {"type": "image"}, # no payload: ordinary data, not an image part
|
| 35 |
+
} # fmt: skip
|
| 36 |
+
segments, images = split_state(state)
|
| 37 |
+
assert [base64.b64encode(i.raw).decode() for i in images] == [a, b, c]
|
| 38 |
+
assert [s for s in segments if isinstance(s, int)] == [0, 1, 2]
|
| 39 |
+
text = "".join(s if isinstance(s, str) else "<IMG>" for s in segments)
|
| 40 |
+
assert text == '{"listing": {"title": "Blue mug", "photo": <IMG>}, "extra": [<IMG>, <IMG>], "note": {"type": "image"}}'
|
| 41 |
+
assert a not in text # the base64 payload never reaches the tokenizer
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_split_state_without_images_and_bad_images():
|
| 45 |
+
assert split_state({"a": 1, "b": ["x", {"type": "text"}]}) == ([], [])
|
| 46 |
+
assert split_state("plain text") == ([], [])
|
| 47 |
+
with pytest.raises(ImageError):
|
| 48 |
+
split_state({"type": "image", "data": "not base64!!"})
|
| 49 |
+
with pytest.raises(ImageError):
|
| 50 |
+
split_state({"type": "image", "url": "https://example.com/cat.png"}) # remote URLs are not fetched
|
| 51 |
+
with pytest.raises(ImageError):
|
| 52 |
+
split_state([{"type": "image", "data": png()}] * 9)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def test_smart_resize_respects_budget_factor_and_aspect():
|
| 56 |
+
for w, h in [(4000, 3000), (640, 480), (20, 20), (1000, 50)]:
|
| 57 |
+
rw, rh = smart_resize(w, h, factor=32, min_pixels=64 * 64, max_pixels=512 * 512)
|
| 58 |
+
assert rw % 32 == 0 and rh % 32 == 0 and rw * rh <= 512 * 512
|
| 59 |
+
assert rw * rh >= 64 * 64 or (w, h) == (1000, 50)
|
| 60 |
+
rw, rh = smart_resize(4000, 3000, 32, 64 * 64, 512 * 512)
|
| 61 |
+
assert abs(rw / rh - 4 / 3) < 0.15
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# --------------------------------------------------------------------------- tiny multimodal model
|
| 65 |
+
@pytest.fixture(scope="module")
|
| 66 |
+
def tiny_mm(tmp_path_factory):
|
| 67 |
+
from safetensors.torch import save_file
|
| 68 |
+
from transformers import AutoImageProcessor, AutoTokenizer
|
| 69 |
+
from transformers.models.qwen3_5 import Qwen3_5Config, Qwen3_5ForConditionalGeneration
|
| 70 |
+
|
| 71 |
+
base = "Qwen/Qwen3.5-0.8B-Base"
|
| 72 |
+
try:
|
| 73 |
+
tok = AutoTokenizer.from_pretrained(base, local_files_only=True)
|
| 74 |
+
image_proc = AutoImageProcessor.from_pretrained(base, local_files_only=True)
|
| 75 |
+
real = __import__("transformers").AutoConfig.from_pretrained(base, local_files_only=True)
|
| 76 |
+
except Exception: # noqa: BLE001
|
| 77 |
+
pytest.skip("base checkpoint files are not in the local cache")
|
| 78 |
+
torch.manual_seed(0)
|
| 79 |
+
cfg = Qwen3_5Config(
|
| 80 |
+
text_config=dict(vocab_size=len(tok), hidden_size=64, intermediate_size=128, num_hidden_layers=4, num_attention_heads=4,
|
| 81 |
+
num_key_value_heads=2, head_dim=16, full_attention_interval=4, linear_num_key_heads=4,
|
| 82 |
+
linear_num_value_heads=4, linear_key_head_dim=32, linear_value_head_dim=32, max_position_embeddings=2048),
|
| 83 |
+
vision_config=dict(depth=2, hidden_size=32, intermediate_size=64, num_heads=2, patch_size=16, spatial_merge_size=2,
|
| 84 |
+
temporal_patch_size=2, in_channels=3, out_hidden_size=64, num_position_embeddings=256),
|
| 85 |
+
image_token_id=real.image_token_id, video_token_id=real.video_token_id,
|
| 86 |
+
vision_start_token_id=real.vision_start_token_id, vision_end_token_id=real.vision_end_token_id,
|
| 87 |
+
) # fmt: skip
|
| 88 |
+
full = Qwen3_5ForConditionalGeneration(cfg).eval().to(DEVICE)
|
| 89 |
+
vdir = tmp_path_factory.mktemp("vision")
|
| 90 |
+
save_file({k: v.contiguous().cpu() for k, v in full.model.visual.state_dict().items()}, vdir / "model.safetensors")
|
| 91 |
+
(vdir / "config.json").write_text(json.dumps({k: v for k, v in cfg.vision_config.to_dict().items() if k not in ("transformers_version", "architectures")}))
|
| 92 |
+
image_proc.save_pretrained(vdir)
|
| 93 |
+
(vdir / "zico_vision.json").write_text(json.dumps({
|
| 94 |
+
"base_model": base, "image_token_id": cfg.image_token_id, "vision_start_token_id": cfg.vision_start_token_id,
|
| 95 |
+
"vision_end_token_id": cfg.vision_end_token_id, "patch_size": 16, "spatial_merge_size": 2})) # fmt: skip
|
| 96 |
+
model = DecisionModel(full.model.language_model, d_head=32, base_model=base).eval().to(DEVICE)
|
| 97 |
+
with torch.no_grad():
|
| 98 |
+
model.unary.weight.normal_(0, 0.5)
|
| 99 |
+
model.attach_vision(vdir, min_pixels=32 * 32, max_pixels=128 * 128)
|
| 100 |
+
return full, model, Renderer(tok)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
STATE = {"listing": {"title": "Blue mug", "photo": {"type": "image", "data": png(96, 64)}, "price": 12}, "second": {"type": "image", "data": png(64, 128, seed=5)}}
|
| 104 |
+
QUESTIONS = [Noul("The photo shows a mug."), Choice("What is in the photo?", {"mug": "A cup", "shoe": "Footwear", "lamp": None}),
|
| 105 |
+
Choice("Pick a colour.", ["red", "green", "blue", "black"])] # fmt: skip
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def test_positions_match_hugging_face(tiny_mm):
|
| 109 |
+
full, model, renderer = tiny_mm
|
| 110 |
+
st = render_image_state(renderer, STATE, model.image_pre)
|
| 111 |
+
assert [(i.rows, i.cols) for i in st.images] == [(2, 3), (4, 2)] # 96x64 and 64x128 at 32 px per token
|
| 112 |
+
ids = torch.tensor([st.input_ids], device=DEVICE)
|
| 113 |
+
grids = torch.tensor(np.stack([i.grid_thw for i in st.images]), device=DEVICE)
|
| 114 |
+
is_image = (ids == model.image_pre.meta["image_token_id"]).int()
|
| 115 |
+
ref, _ = full.model.get_rope_index(ids, is_image, image_grid_thw=grids, attention_mask=torch.ones_like(ids))
|
| 116 |
+
assert torch.equal(ref[:3, 0].cpu(), torch.from_numpy(st.positions))
|
| 117 |
+
assert st.next_pos == int(ref.max()) + 1 < len(st.input_ids) # positions advance by grid size, not token count
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_image_state_matches_hugging_face_forward(tiny_mm):
|
| 121 |
+
"""Our embedding scatter + positions reproduce HF's own multimodal forward pass."""
|
| 122 |
+
full, model, renderer = tiny_mm
|
| 123 |
+
st = render_image_state(renderer, STATE, model.image_pre)
|
| 124 |
+
ids = torch.tensor([st.input_ids], device=DEVICE)
|
| 125 |
+
px = torch.from_numpy(np.concatenate([i.pixel_values for i in st.images])).to(DEVICE)
|
| 126 |
+
grids = torch.tensor(np.stack([i.grid_thw for i in st.images]), device=DEVICE)
|
| 127 |
+
with torch.no_grad():
|
| 128 |
+
ref = full.model(input_ids=ids, pixel_values=px, image_grid_thw=grids, attention_mask=torch.ones_like(ids),
|
| 129 |
+
mm_token_type_ids=(ids == model.image_pre.meta["image_token_id"]).int()).last_hidden_state # fmt: skip
|
| 130 |
+
emb, pos = model._image_state_embeddings(st, torch.device(DEVICE))
|
| 131 |
+
ours = model.backbone(inputs_embeds=emb, position_ids=pos, attention_mask=torch.ones_like(ids), use_cache=False).last_hidden_state
|
| 132 |
+
assert torch.allclose(ours, ref, atol=1e-4), (ours - ref).abs().max()
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def test_fork_equals_single_row_with_images(tiny_mm):
|
| 136 |
+
_, model, renderer = tiny_mm
|
| 137 |
+
forked, _, n1 = model.question_logits(renderer, STATE, QUESTIONS, fork=True)
|
| 138 |
+
rows, _, n2 = model.question_logits(renderer, STATE, QUESTIONS, fork=False)
|
| 139 |
+
assert n1 == n2
|
| 140 |
+
for a, b in zip(forked, rows):
|
| 141 |
+
assert torch.allclose(a, b, atol=2e-4), (a, b)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
def test_answers_depend_on_the_image_and_text_states_are_untouched(tiny_mm):
|
| 145 |
+
_, model, renderer = tiny_mm
|
| 146 |
+
qs = {"q": QUESTIONS[1]}
|
| 147 |
+
a = model.answer(renderer, {"photo": {"type": "image", "data": png(seed=1)}}, qs)
|
| 148 |
+
b = model.answer(renderer, {"photo": {"type": "image", "data": png(seed=2)}}, qs)
|
| 149 |
+
assert a["answers"]["q"]["probabilities"] != b["answers"]["q"]["probabilities"]
|
| 150 |
+
assert a["usage"]["input_tokens"] < 200 # image tokens are counted; the base64 payload is not
|
| 151 |
+
text_state = {"note": "no pictures here"}
|
| 152 |
+
before = model.answer(renderer, text_state, qs)
|
| 153 |
+
model.vision, keep = None, model.vision # a text-only model must give the identical answer
|
| 154 |
+
model.image_pre, keep_pre = None, model.image_pre
|
| 155 |
+
try:
|
| 156 |
+
assert model.answer(renderer, text_state, qs) == before
|
| 157 |
+
finally:
|
| 158 |
+
model.vision, model.image_pre = keep, keep_pre
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def test_detail_hint_changes_the_token_budget(tiny_mm):
|
| 162 |
+
_, model, renderer = tiny_mm
|
| 163 |
+
big = png(512, 384)
|
| 164 |
+
n = lambda part: render_image_state(renderer, {"img": part}, model.image_pre).images[0].n_tokens # noqa: E731
|
| 165 |
+
default, low, high = n({"type": "image", "data": big}), n({"type": "image", "data": big, "detail": "low"}), \
|
| 166 |
+
n({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{big}", "detail": "high"}}) # fmt: skip
|
| 167 |
+
assert default <= 16 < low <= 64 < high <= 1024 # fixture budget 128^2, "low" 256^2, "high" 1024^2 (capped by the image itself)
|
| 168 |
+
with pytest.raises(ImageError):
|
| 169 |
+
split_state({"type": "image", "data": big, "detail": "ultra"})
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def test_server_accepts_and_rejects_images(tiny_mm):
|
| 173 |
+
pytest.importorskip("fastapi")
|
| 174 |
+
from fastapi.testclient import TestClient
|
| 175 |
+
|
| 176 |
+
from zico.server import create_app
|
| 177 |
+
from tests.test_core import _tiny
|
| 178 |
+
|
| 179 |
+
_, model, renderer = tiny_mm
|
| 180 |
+
client = TestClient(create_app(model, renderer, {"k"}, "tiny-vision"))
|
| 181 |
+
auth = {"Authorization": "Bearer k"}
|
| 182 |
+
body = {"model": "zico-latest", "state": {"photo": {"type": "image", "data": png()}},
|
| 183 |
+
"questions": {"what": {"type": "choice", "instructions": "What is shown?", "criteria": {"mug": None, "shoe": None}}}} # fmt: skip
|
| 184 |
+
r = client.post("/v1/systemone", json=body, headers=auth)
|
| 185 |
+
assert r.status_code == 200 and r.json()["answers"]["what"]["choice"] in ("mug", "shoe")
|
| 186 |
+
bad = {**body, "state": {"photo": {"type": "image", "data": "%%%"}}}
|
| 187 |
+
r = client.post("/v1/systemone", json=bad, headers=auth)
|
| 188 |
+
assert r.status_code == 422 and r.json()["detail"][0]["type"] == "invalid_image"
|
| 189 |
+
undecodable = {**body, "state": {"photo": {"type": "image", "data": base64.b64encode(b"not an image").decode()}}}
|
| 190 |
+
assert client.post("/v1/systemone", json=undecodable, headers=auth).status_code == 422
|
| 191 |
+
text_model, text_renderer = _tiny("qwen3")
|
| 192 |
+
text_client = TestClient(create_app(text_model, text_renderer, {"k"}, "tiny-text"))
|
| 193 |
+
r = text_client.post("/v1/systemone", json=body, headers=auth)
|
| 194 |
+
assert r.status_code == 422 and r.json()["detail"][0]["type"] == "images_not_supported"
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def test_training_batch_with_images_matches_inference(tiny_mm, tmp_path):
|
| 198 |
+
"""Image rows + a text row in one padded batch == the single-row inference path; the ViT gets no gradient."""
|
| 199 |
+
from PIL import Image
|
| 200 |
+
|
| 201 |
+
from zico.batching import collate
|
| 202 |
+
from zico.data import render_records
|
| 203 |
+
from zico.losses import proper_score_loss
|
| 204 |
+
|
| 205 |
+
_, model, renderer = tiny_mm
|
| 206 |
+
rng = np.random.default_rng(3)
|
| 207 |
+
files = {"a.png": (96, 64), "b.png": (64, 128)} # already multiples of 32 and inside the fixture's budget
|
| 208 |
+
for name, (w, h) in files.items():
|
| 209 |
+
Image.fromarray(rng.integers(0, 255, (h, w, 3), dtype=np.uint8)).save(tmp_path / name)
|
| 210 |
+
q0, q1, q2 = ({"type": "noul", "instructions": "The photo shows a mug."},
|
| 211 |
+
{"type": "choice", "instructions": "What is in the photo?", "criteria": {"mug": "A cup", "shoe": None, "lamp": None}},
|
| 212 |
+
{"type": "choice", "instructions": "Pick a colour.", "criteria": ["red", "green", "blue", "black"]}) # fmt: skip
|
| 213 |
+
records = [
|
| 214 |
+
{"source": "t", "sid": "s0", "state": {"listing": {"title": "Blue mug", "photo": {"type": "image", "path": "a.png"}}}, "question": q1, "label": 0, "image_grids": [[2, 3]]},
|
| 215 |
+
{"source": "t", "sid": "s0", "state": {"listing": {"title": "Blue mug", "photo": {"type": "image", "path": "a.png"}}}, "question": q0, "label": 1, "image_grids": [[2, 3]]},
|
| 216 |
+
{"source": "t", "sid": "s1", "state": {"note": "no picture here"}, "question": q2, "label": 2},
|
| 217 |
+
{"source": "t", "sid": "s2", "state": [{"type": "image", "path": "b.png"}, {"type": "image", "path": "a.png"}], "question": q2, "label": 3, "image_grids": [[4, 2], [2, 3]]},
|
| 218 |
+
] # fmt: skip
|
| 219 |
+
rows, kept = render_records(records, renderer, None, model.image_pre.meta, tmp_path)
|
| 220 |
+
assert len(rows) == 4 and [len(r.image_paths) for r in rows] == [1, 1, 0, 2]
|
| 221 |
+
batch = {k: v.to(DEVICE) for k, v in collate(rows, renderer.slot_id, 256, model.image_pre.load_prepared).items()}
|
| 222 |
+
assert batch["position_ids"].shape == (3, 4, 256) and batch["image_grid_thw"].shape == (4, 3)
|
| 223 |
+
logits = model(**batch)
|
| 224 |
+
|
| 225 |
+
def b64(name):
|
| 226 |
+
return base64.b64encode((tmp_path / name).read_bytes()).decode()
|
| 227 |
+
|
| 228 |
+
api_states = [{"listing": {"title": "Blue mug", "photo": {"type": "image", "data": b64("a.png")}}}] * 2 + \
|
| 229 |
+
[{"note": "no picture here"}, [{"type": "image", "data": b64("b.png")}, {"type": "image", "data": b64("a.png")}]] # fmt: skip
|
| 230 |
+
from zico import question_from_dict
|
| 231 |
+
|
| 232 |
+
for i, (state, rec) in enumerate(zip(api_states, records)):
|
| 233 |
+
ref, _, _ = model.question_logits(renderer, state, [question_from_dict(rec["question"])], fork=False)
|
| 234 |
+
assert torch.allclose(logits[i, : len(ref[0])], ref[0], atol=2e-4), (i, logits[i], ref[0])
|
| 235 |
+
|
| 236 |
+
model.zero_grad()
|
| 237 |
+
proper_score_loss(logits, batch["opt_mask"], batch["target"], batch["qtype"], 0.5, 1.0).mean().backward()
|
| 238 |
+
assert any(p.grad is not None and p.grad.abs().sum() > 0 for p in model.backbone.layers[0].parameters())
|
| 239 |
+
assert all(p.grad is None for p in model.vision.model.parameters()) # the tower is never trained
|
| 240 |
+
model.zero_grad()
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
def test_mrope_layout_arithmetic():
|
| 244 |
+
pos, nxt = mrope_positions([("text", 2), ("image", (2, 3)), ("text", 1)])
|
| 245 |
+
assert pos[:, :2].tolist() == [[0, 1]] * 3
|
| 246 |
+
assert pos[0, 2:8].tolist() == [2] * 6 # time is constant over an image
|
| 247 |
+
assert pos[1, 2:8].tolist() == [2, 2, 2, 3, 3, 3] and pos[2, 2:8].tolist() == [2, 3, 4, 2, 3, 4]
|
| 248 |
+
assert pos[:, 8].tolist() == [5, 5, 5] and nxt == 6 # continues from max(rows, cols), not from 6 image tokens
|
tokenizer/chat_template.jinja
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set image_count = namespace(value=0) %}
|
| 2 |
+
{%- set video_count = namespace(value=0) %}
|
| 3 |
+
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
| 4 |
+
{%- if content is string %}
|
| 5 |
+
{{- content }}
|
| 6 |
+
{%- elif content is iterable and content is not mapping %}
|
| 7 |
+
{%- for item in content %}
|
| 8 |
+
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
| 9 |
+
{%- if is_system_content %}
|
| 10 |
+
{{- raise_exception('System message cannot contain images.') }}
|
| 11 |
+
{%- endif %}
|
| 12 |
+
{%- if do_vision_count %}
|
| 13 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{%- if add_vision_id %}
|
| 16 |
+
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
| 17 |
+
{%- endif %}
|
| 18 |
+
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
| 19 |
+
{%- elif 'video' in item or item.type == 'video' %}
|
| 20 |
+
{%- if is_system_content %}
|
| 21 |
+
{{- raise_exception('System message cannot contain videos.') }}
|
| 22 |
+
{%- endif %}
|
| 23 |
+
{%- if do_vision_count %}
|
| 24 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 25 |
+
{%- endif %}
|
| 26 |
+
{%- if add_vision_id %}
|
| 27 |
+
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
| 28 |
+
{%- endif %}
|
| 29 |
+
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
| 30 |
+
{%- elif 'text' in item %}
|
| 31 |
+
{{- item.text }}
|
| 32 |
+
{%- else %}
|
| 33 |
+
{{- raise_exception('Unexpected item type in content.') }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endfor %}
|
| 36 |
+
{%- elif content is none or content is undefined %}
|
| 37 |
+
{{- '' }}
|
| 38 |
+
{%- else %}
|
| 39 |
+
{{- raise_exception('Unexpected content type.') }}
|
| 40 |
+
{%- endif %}
|
| 41 |
+
{%- endmacro %}
|
| 42 |
+
{%- if not messages %}
|
| 43 |
+
{{- raise_exception('No messages provided.') }}
|
| 44 |
+
{%- endif %}
|
| 45 |
+
{%- if tools and tools is iterable and tools is not mapping %}
|
| 46 |
+
{{- '<|im_start|>system\n' }}
|
| 47 |
+
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
| 48 |
+
{%- for tool in tools %}
|
| 49 |
+
{{- "\n" }}
|
| 50 |
+
{{- tool | tojson }}
|
| 51 |
+
{%- endfor %}
|
| 52 |
+
{{- "\n</tools>" }}
|
| 53 |
+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
| 54 |
+
{%- if messages[0].role == 'system' %}
|
| 55 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 56 |
+
{%- if content %}
|
| 57 |
+
{{- '\n\n' + content }}
|
| 58 |
+
{%- endif %}
|
| 59 |
+
{%- endif %}
|
| 60 |
+
{{- '<|im_end|>\n' }}
|
| 61 |
+
{%- else %}
|
| 62 |
+
{%- if messages[0].role == 'system' %}
|
| 63 |
+
{%- set content = render_content(messages[0].content, false, true)|trim %}
|
| 64 |
+
{{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
|
| 65 |
+
{%- endif %}
|
| 66 |
+
{%- endif %}
|
| 67 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 68 |
+
{%- for message in messages[::-1] %}
|
| 69 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 70 |
+
{%- if ns.multi_step_tool and message.role == "user" %}
|
| 71 |
+
{%- set content = render_content(message.content, false)|trim %}
|
| 72 |
+
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
| 73 |
+
{%- set ns.multi_step_tool = false %}
|
| 74 |
+
{%- set ns.last_query_index = index %}
|
| 75 |
+
{%- endif %}
|
| 76 |
+
{%- endif %}
|
| 77 |
+
{%- endfor %}
|
| 78 |
+
{%- if ns.multi_step_tool %}
|
| 79 |
+
{{- raise_exception('No user query found in messages.') }}
|
| 80 |
+
{%- endif %}
|
| 81 |
+
{%- for message in messages %}
|
| 82 |
+
{%- set content = render_content(message.content, true)|trim %}
|
| 83 |
+
{%- if message.role == "system" %}
|
| 84 |
+
{%- if not loop.first %}
|
| 85 |
+
{{- raise_exception('System message must be at the beginning.') }}
|
| 86 |
+
{%- endif %}
|
| 87 |
+
{%- elif message.role == "user" %}
|
| 88 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 89 |
+
{%- elif message.role == "assistant" %}
|
| 90 |
+
{%- set reasoning_content = '' %}
|
| 91 |
+
{%- if message.reasoning_content is string %}
|
| 92 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 93 |
+
{%- else %}
|
| 94 |
+
{%- if '</think>' in content %}
|
| 95 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 96 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 97 |
+
{%- endif %}
|
| 98 |
+
{%- endif %}
|
| 99 |
+
{%- set reasoning_content = reasoning_content|trim %}
|
| 100 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 101 |
+
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
| 102 |
+
{%- else %}
|
| 103 |
+
{{- '<|im_start|>' + message.role + '\n' + content }}
|
| 104 |
+
{%- endif %}
|
| 105 |
+
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
| 106 |
+
{%- for tool_call in message.tool_calls %}
|
| 107 |
+
{%- if tool_call.function is defined %}
|
| 108 |
+
{%- set tool_call = tool_call.function %}
|
| 109 |
+
{%- endif %}
|
| 110 |
+
{%- if loop.first %}
|
| 111 |
+
{%- if content|trim %}
|
| 112 |
+
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 113 |
+
{%- else %}
|
| 114 |
+
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 115 |
+
{%- endif %}
|
| 116 |
+
{%- else %}
|
| 117 |
+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 118 |
+
{%- endif %}
|
| 119 |
+
{%- if tool_call.arguments is defined %}
|
| 120 |
+
{%- for args_name, args_value in tool_call.arguments|items %}
|
| 121 |
+
{{- '<parameter=' + args_name + '>\n' }}
|
| 122 |
+
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
| 123 |
+
{{- args_value }}
|
| 124 |
+
{{- '\n</parameter>\n' }}
|
| 125 |
+
{%- endfor %}
|
| 126 |
+
{%- endif %}
|
| 127 |
+
{{- '</function>\n</tool_call>' }}
|
| 128 |
+
{%- endfor %}
|
| 129 |
+
{%- endif %}
|
| 130 |
+
{{- '<|im_end|>\n' }}
|
| 131 |
+
{%- elif message.role == "tool" %}
|
| 132 |
+
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
| 133 |
+
{{- '<|im_start|>user' }}
|
| 134 |
+
{%- endif %}
|
| 135 |
+
{{- '\n<tool_response>\n' }}
|
| 136 |
+
{{- content }}
|
| 137 |
+
{{- '\n</tool_response>' }}
|
| 138 |
+
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
| 139 |
+
{{- '<|im_end|>\n' }}
|
| 140 |
+
{%- elif loop.last %}
|
| 141 |
+
{{- '<|im_end|>\n' }}
|
| 142 |
+
{%- endif %}
|
| 143 |
+
{%- else %}
|
| 144 |
+
{{- raise_exception('Unexpected message role.') }}
|
| 145 |
+
{%- endif %}
|
| 146 |
+
{%- endfor %}
|
| 147 |
+
{%- if add_generation_prompt %}
|
| 148 |
+
{{- '<|im_start|>assistant\n' }}
|
| 149 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 150 |
+
{{- '<think>\n\n</think>\n\n' }}
|
| 151 |
+
{%- else %}
|
| 152 |
+
{{- '<think>\n' }}
|
| 153 |
+
{%- endif %}
|
| 154 |
+
{%- endif %}
|
tokenizer/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
|
| 3 |
+
size 19989325
|
tokenizer/tokenizer_config.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"audio_bos_token": "<|audio_start|>",
|
| 4 |
+
"audio_eos_token": "<|audio_end|>",
|
| 5 |
+
"audio_token": "<|audio_pad|>",
|
| 6 |
+
"backend": "tokenizers",
|
| 7 |
+
"bos_token": null,
|
| 8 |
+
"clean_up_tokenization_spaces": false,
|
| 9 |
+
"eos_token": "<|endoftext|>",
|
| 10 |
+
"errors": "replace",
|
| 11 |
+
"image_token": "<|image_pad|>",
|
| 12 |
+
"is_local": false,
|
| 13 |
+
"local_files_only": false,
|
| 14 |
+
"model_max_length": 262144,
|
| 15 |
+
"model_specific_special_tokens": {
|
| 16 |
+
"audio_bos_token": "<|audio_start|>",
|
| 17 |
+
"audio_eos_token": "<|audio_end|>",
|
| 18 |
+
"audio_token": "<|audio_pad|>",
|
| 19 |
+
"image_token": "<|image_pad|>",
|
| 20 |
+
"video_token": "<|video_pad|>",
|
| 21 |
+
"vision_bos_token": "<|vision_start|>",
|
| 22 |
+
"vision_eos_token": "<|vision_end|>"
|
| 23 |
+
},
|
| 24 |
+
"pad_token": "<|endoftext|>",
|
| 25 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 26 |
+
"split_special_tokens": false,
|
| 27 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 28 |
+
"unk_token": null,
|
| 29 |
+
"video_token": "<|video_pad|>",
|
| 30 |
+
"vision_bos_token": "<|vision_start|>",
|
| 31 |
+
"vision_eos_token": "<|vision_end|>"
|
| 32 |
+
}
|
vision/config.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"output_hidden_states": false,
|
| 3 |
+
"return_dict": true,
|
| 4 |
+
"dtype": null,
|
| 5 |
+
"chunk_size_feed_forward": 0,
|
| 6 |
+
"is_encoder_decoder": false,
|
| 7 |
+
"id2label": {
|
| 8 |
+
"0": "LABEL_0",
|
| 9 |
+
"1": "LABEL_1"
|
| 10 |
+
},
|
| 11 |
+
"label2id": {
|
| 12 |
+
"LABEL_0": 0,
|
| 13 |
+
"LABEL_1": 1
|
| 14 |
+
},
|
| 15 |
+
"problem_type": null,
|
| 16 |
+
"depth": 24,
|
| 17 |
+
"hidden_size": 1024,
|
| 18 |
+
"hidden_act": "gelu_pytorch_tanh",
|
| 19 |
+
"intermediate_size": 4096,
|
| 20 |
+
"num_heads": 16,
|
| 21 |
+
"in_channels": 3,
|
| 22 |
+
"patch_size": 16,
|
| 23 |
+
"spatial_merge_size": 2,
|
| 24 |
+
"temporal_patch_size": 2,
|
| 25 |
+
"out_hidden_size": 2560,
|
| 26 |
+
"num_position_embeddings": 2304,
|
| 27 |
+
"initializer_range": 0.02,
|
| 28 |
+
"rope_parameters": {
|
| 29 |
+
"rope_theta": 10000.0,
|
| 30 |
+
"rope_type": "axial"
|
| 31 |
+
},
|
| 32 |
+
"_name_or_path": "",
|
| 33 |
+
"deepstack_visual_indexes": [],
|
| 34 |
+
"model_type": "qwen3_5_vision",
|
| 35 |
+
"output_attentions": false
|
| 36 |
+
}
|
vision/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ed70be60931ea6143cf3c1b31cd21783b6e9127b01c6c74e27733ece44cea2b2
|
| 3 |
+
size 667057584
|
vision/preprocessor_config.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_convert_rgb": true,
|
| 3 |
+
"do_normalize": true,
|
| 4 |
+
"do_rescale": true,
|
| 5 |
+
"do_resize": true,
|
| 6 |
+
"image_mean": [
|
| 7 |
+
0.5,
|
| 8 |
+
0.5,
|
| 9 |
+
0.5
|
| 10 |
+
],
|
| 11 |
+
"image_processor_type": "Qwen2VLImageProcessor",
|
| 12 |
+
"image_std": [
|
| 13 |
+
0.5,
|
| 14 |
+
0.5,
|
| 15 |
+
0.5
|
| 16 |
+
],
|
| 17 |
+
"merge_size": 2,
|
| 18 |
+
"patch_size": 16,
|
| 19 |
+
"resample": 3,
|
| 20 |
+
"rescale_factor": 0.00392156862745098,
|
| 21 |
+
"size": {
|
| 22 |
+
"longest_edge": 16777216,
|
| 23 |
+
"shortest_edge": 65536
|
| 24 |
+
},
|
| 25 |
+
"temporal_patch_size": 2
|
| 26 |
+
}
|
vision/zico_vision.json
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_model": "Qwen/Qwen3.5-4B-Base",
|
| 3 |
+
"image_token_id": 248056,
|
| 4 |
+
"vision_start_token_id": 248053,
|
| 5 |
+
"vision_end_token_id": 248054,
|
| 6 |
+
"patch_size": 16,
|
| 7 |
+
"spatial_merge_size": 2
|
| 8 |
+
}
|
zico.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_model": "Qwen/Qwen3.5-4B-Base",
|
| 3 |
+
"d_head": 256,
|
| 4 |
+
"lora": false,
|
| 5 |
+
"renderer": {
|
| 6 |
+
"max_state_tokens": 768,
|
| 7 |
+
"max_instruction_tokens": 256,
|
| 8 |
+
"max_option_tokens": 64
|
| 9 |
+
},
|
| 10 |
+
"step": 734,
|
| 11 |
+
"val_nll": 0.27321311831474304
|
| 12 |
+
}
|
zico/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .render import Renderer
|
| 2 |
+
from .schema import Choice, Noul, Score, question_from_dict, question_to_dict
|
| 3 |
+
|
| 4 |
+
__all__ = ["DecisionModel", "Renderer", "Choice", "Score", "Noul", "question_from_dict", "question_to_dict"]
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def __getattr__(name):
|
| 8 |
+
# Lazy, so the MLX serving backend can use the package without importing PyTorch.
|
| 9 |
+
if name == "DecisionModel":
|
| 10 |
+
from .model import DecisionModel
|
| 11 |
+
|
| 12 |
+
return DecisionModel
|
| 13 |
+
raise AttributeError(name)
|
zico/answers.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Turn per-question probability vectors into the typed answer objects (framework-free)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def format_answers(names: list[str], probs: list[list[float]], rendered: list, questions: dict, n_tokens: int) -> dict:
|
| 8 |
+
"""rendered[i] is the RenderedQuestion for names[i]; probs[i] its distribution over rendered[i].keys."""
|
| 9 |
+
answers = {}
|
| 10 |
+
for name, p, rq in zip(names, probs, rendered):
|
| 11 |
+
by_key = {k: round(v, 6) for k, v in zip(rq.keys, p)}
|
| 12 |
+
if rq.qtype == "noul":
|
| 13 |
+
answers[name] = {"type": "noul", "noul": by_key["true"]}
|
| 14 |
+
continue
|
| 15 |
+
entropy = -sum(v * math.log(max(v, 1e-12)) for v in p)
|
| 16 |
+
conf = round(1.0 - entropy / math.log(len(p)), 6)
|
| 17 |
+
if rq.qtype == "choice":
|
| 18 |
+
best = max(range(len(p)), key=p.__getitem__)
|
| 19 |
+
answers[name] = {"type": "choice", "choice": rq.keys[best], "probabilities": by_key, "confidence": conf}
|
| 20 |
+
else:
|
| 21 |
+
score = sum(i * v for i, v in enumerate(p))
|
| 22 |
+
legend = dict(questions[name].options())
|
| 23 |
+
answers[name] = {"type": "score", "score": round(score, 4), "legend": legend, "probabilities": by_key, "confidence": conf}
|
| 24 |
+
return {"answers": answers, "usage": {"input_tokens": n_tokens, "output_tokens": 0}}
|
zico/backbone.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backbone loading and prefix-cache forking.
|
| 2 |
+
|
| 3 |
+
Any HF causal decoder stack works: pure softmax attention (Qwen3, Ministral, ...) or
|
| 4 |
+
hybrids with recurrent layers (Qwen3.5 Gated DeltaNet). We never edit attention masks,
|
| 5 |
+
so nothing here depends on the token-mixing layer type.
|
| 6 |
+
"""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import copy
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import AutoConfig, AutoModel, AutoModelForImageTextToText
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def pick_device() -> str:
|
| 16 |
+
if torch.cuda.is_available():
|
| 17 |
+
return "cuda"
|
| 18 |
+
if torch.backends.mps.is_available():
|
| 19 |
+
return "mps"
|
| 20 |
+
return "cpu"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def load_text_backbone(model_id: str, dtype=torch.float32):
|
| 24 |
+
"""Return the text decoder stack only (no LM head, no vision tower)."""
|
| 25 |
+
cfg = AutoConfig.from_pretrained(model_id)
|
| 26 |
+
if hasattr(cfg, "text_config") and hasattr(cfg, "vision_config"):
|
| 27 |
+
full = AutoModelForImageTextToText.from_pretrained(model_id, dtype=dtype)
|
| 28 |
+
return full.model.language_model
|
| 29 |
+
return AutoModel.from_pretrained(model_id, dtype=dtype)
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def fork_cache(cache, n: int):
|
| 33 |
+
"""Copy a batch-1 prefix cache into a batch-n cache.
|
| 34 |
+
|
| 35 |
+
Handles attention KV tensors as well as the {state_idx: tensor} dicts that hybrid
|
| 36 |
+
layers use for conv / recurrent state (HF's batch_repeat_interleave does not).
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def rep(v):
|
| 40 |
+
if torch.is_tensor(v) and v.dim() > 0 and v.shape[0] == 1:
|
| 41 |
+
return v.repeat_interleave(n, dim=0)
|
| 42 |
+
return v
|
| 43 |
+
|
| 44 |
+
forked = copy.deepcopy(cache)
|
| 45 |
+
for layer in forked.layers:
|
| 46 |
+
for name, val in list(vars(layer).items()):
|
| 47 |
+
if torch.is_tensor(val):
|
| 48 |
+
setattr(layer, name, rep(val))
|
| 49 |
+
elif isinstance(val, dict):
|
| 50 |
+
setattr(layer, name, {k: rep(v) for k, v in val.items()})
|
| 51 |
+
elif isinstance(val, list):
|
| 52 |
+
setattr(layer, name, [rep(v) for v in val])
|
| 53 |
+
return forked
|
zico/batching.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Rows and collation. One row = one (state, question) pair, right-padded.
|
| 2 |
+
|
| 3 |
+
Right padding is deliberate: padded positions come after every readout slot, so with a
|
| 4 |
+
causal backbone (attention or recurrent) they cannot influence any value we read.
|
| 5 |
+
"""
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from .render import RenderedQuestion
|
| 13 |
+
from .schema import QTYPE_INDEX
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass
|
| 17 |
+
class Row:
|
| 18 |
+
input_ids: list[int]
|
| 19 |
+
marker_ids: list[int]
|
| 20 |
+
opt_pos: list[int] # absolute positions
|
| 21 |
+
ans_pos: int
|
| 22 |
+
qtype: int
|
| 23 |
+
label: int = -1
|
| 24 |
+
soft: list[float] | None = None # optional soft target over options
|
| 25 |
+
# image rows only: 3-part rotary positions [3, len] and the prepared image files, in order of appearance
|
| 26 |
+
positions: object = None
|
| 27 |
+
image_paths: tuple = ()
|
| 28 |
+
|
| 29 |
+
def __len__(self):
|
| 30 |
+
return len(self.input_ids)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def make_row(state_ids: list[int], rq: RenderedQuestion, label: int = -1, soft=None) -> Row:
|
| 34 |
+
s = len(state_ids)
|
| 35 |
+
return Row(
|
| 36 |
+
input_ids=state_ids + rq.input_ids,
|
| 37 |
+
marker_ids=[-1] * s + rq.marker_ids,
|
| 38 |
+
opt_pos=[s + p for p in rq.opt_pos],
|
| 39 |
+
ans_pos=s + rq.ans_pos,
|
| 40 |
+
qtype=QTYPE_INDEX[rq.qtype],
|
| 41 |
+
label=label,
|
| 42 |
+
soft=soft,
|
| 43 |
+
)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def make_image_row(st, rq: RenderedQuestion, label: int = -1, soft=None) -> Row:
|
| 47 |
+
"""st: vision.RenderedImageState laid out from recorded grids (its `images` are ImageParts with paths)."""
|
| 48 |
+
import numpy as np
|
| 49 |
+
|
| 50 |
+
s = len(st.input_ids)
|
| 51 |
+
q_pos = st.next_pos + np.arange(len(rq.input_ids)) # text after an image continues from the grid size
|
| 52 |
+
return Row(
|
| 53 |
+
input_ids=st.input_ids + rq.input_ids,
|
| 54 |
+
marker_ids=[-1] * s + rq.marker_ids,
|
| 55 |
+
opt_pos=[s + p for p in rq.opt_pos],
|
| 56 |
+
ans_pos=s + rq.ans_pos,
|
| 57 |
+
qtype=QTYPE_INDEX[rq.qtype],
|
| 58 |
+
label=label,
|
| 59 |
+
soft=soft,
|
| 60 |
+
positions=np.concatenate([st.positions, np.stack([q_pos] * 3)], axis=1),
|
| 61 |
+
image_paths=tuple(p.path for p in st.images),
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def collate(rows: list[Row], pad_id: int = 0, pad_len: int | None = None, load_image=None) -> dict[str, torch.Tensor]:
|
| 66 |
+
"""load_image(path) -> vision.EncodedImage is required when any row carries images."""
|
| 67 |
+
batch = _collate_text(rows, pad_id, pad_len)
|
| 68 |
+
if not any(r.image_paths for r in rows):
|
| 69 |
+
return batch
|
| 70 |
+
import numpy as np
|
| 71 |
+
|
| 72 |
+
B, L = batch["input_ids"].shape
|
| 73 |
+
pos = torch.arange(L)[None, None].repeat(3, B, 1) # text rows (and padding): all three parts advance together
|
| 74 |
+
pixels, grids = [], []
|
| 75 |
+
for i, r in enumerate(rows):
|
| 76 |
+
if r.positions is not None:
|
| 77 |
+
pos[:, i, : len(r)] = torch.from_numpy(r.positions)
|
| 78 |
+
for path in r.image_paths: # row order == the order of <|image_pad|> runs in the flattened batch
|
| 79 |
+
img = load_image(path)
|
| 80 |
+
pixels.append(img.pixel_values)
|
| 81 |
+
grids.append(img.grid_thw)
|
| 82 |
+
batch["position_ids"] = pos
|
| 83 |
+
batch["pixel_values"] = torch.from_numpy(np.concatenate(pixels))
|
| 84 |
+
batch["image_grid_thw"] = torch.from_numpy(np.stack(grids))
|
| 85 |
+
return batch
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _collate_text(rows: list[Row], pad_id: int = 0, pad_len: int | None = None) -> dict[str, torch.Tensor]:
|
| 89 |
+
B = len(rows)
|
| 90 |
+
L = max(pad_len or 0, max(len(r) for r in rows))
|
| 91 |
+
K = max(len(r.opt_pos) for r in rows)
|
| 92 |
+
input_ids = torch.full((B, L), pad_id, dtype=torch.long)
|
| 93 |
+
marker_ids = torch.full((B, L), -1, dtype=torch.long)
|
| 94 |
+
attention_mask = torch.zeros((B, L), dtype=torch.long)
|
| 95 |
+
opt_pos = torch.zeros((B, K), dtype=torch.long)
|
| 96 |
+
opt_mask = torch.zeros((B, K), dtype=torch.bool)
|
| 97 |
+
target = torch.zeros((B, K), dtype=torch.float32)
|
| 98 |
+
for i, r in enumerate(rows):
|
| 99 |
+
n, k = len(r), len(r.opt_pos)
|
| 100 |
+
input_ids[i, :n] = torch.tensor(r.input_ids)
|
| 101 |
+
marker_ids[i, :n] = torch.tensor(r.marker_ids)
|
| 102 |
+
attention_mask[i, :n] = 1
|
| 103 |
+
opt_pos[i, :k] = torch.tensor(r.opt_pos)
|
| 104 |
+
opt_mask[i, :k] = True
|
| 105 |
+
if r.soft is not None:
|
| 106 |
+
target[i, :k] = torch.tensor(r.soft, dtype=torch.float32)
|
| 107 |
+
elif r.label >= 0:
|
| 108 |
+
target[i, r.label] = 1.0
|
| 109 |
+
return {
|
| 110 |
+
"input_ids": input_ids,
|
| 111 |
+
"marker_ids": marker_ids,
|
| 112 |
+
"attention_mask": attention_mask,
|
| 113 |
+
"opt_pos": opt_pos,
|
| 114 |
+
"opt_mask": opt_mask,
|
| 115 |
+
"ans_pos": torch.tensor([r.ans_pos for r in rows], dtype=torch.long),
|
| 116 |
+
"qtype": torch.tensor([r.qtype for r in rows], dtype=torch.long),
|
| 117 |
+
"label": torch.tensor([r.label for r in rows], dtype=torch.long),
|
| 118 |
+
"target": target, # one-hot or soft distribution over options
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
LENGTH_BUCKETS = (64, 96, 128, 192, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 6144, 8192)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def bucket_len(n: int) -> int:
|
| 126 |
+
for b in LENGTH_BUCKETS:
|
| 127 |
+
if n <= b:
|
| 128 |
+
return b
|
| 129 |
+
return -(-n // 2048) * 2048
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def bucketed_batches(lengths: list[int], max_tokens: int, max_rows: int, shuffle_rng=None, drop_last=False):
|
| 133 |
+
"""Batches with a small, fixed set of (rows, padded length) shapes -> list of (indices, padded length).
|
| 134 |
+
|
| 135 |
+
Fused GPU kernels (Triton autotune, fla) re-tune for every new tensor shape, which costs seconds.
|
| 136 |
+
Free-form token-budget batching makes nearly every batch a new shape and leaves the GPU idle
|
| 137 |
+
most of the time; a dozen static shapes are tuned once and then run at full speed. The price
|
| 138 |
+
is some padding up to the bucket boundary. `drop_last` skips the one ragged batch per bucket.
|
| 139 |
+
"""
|
| 140 |
+
by_bucket: dict[int, list[int]] = {}
|
| 141 |
+
for i, n in enumerate(lengths):
|
| 142 |
+
by_bucket.setdefault(bucket_len(n), []).append(i)
|
| 143 |
+
batches = []
|
| 144 |
+
for L, idx in sorted(by_bucket.items()):
|
| 145 |
+
if shuffle_rng is not None:
|
| 146 |
+
shuffle_rng.shuffle(idx)
|
| 147 |
+
B = max(1, min(max_rows, max_tokens // L))
|
| 148 |
+
for s in range(0, len(idx), B):
|
| 149 |
+
chunk = idx[s : s + B]
|
| 150 |
+
if len(chunk) == B or not drop_last:
|
| 151 |
+
batches.append((chunk, L))
|
| 152 |
+
if shuffle_rng is not None:
|
| 153 |
+
shuffle_rng.shuffle(batches)
|
| 154 |
+
return batches
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def token_budget_batches(lengths: list[int], max_tokens: int, max_rows: int, shuffle_rng=None) -> list[list[int]]:
|
| 158 |
+
"""Group row indices into batches whose padded size stays under max_tokens.
|
| 159 |
+
|
| 160 |
+
Rows are bucketed by length (so little compute is wasted on padding) and the batch
|
| 161 |
+
order is shuffled; within-bucket membership is reshuffled each call via the rng.
|
| 162 |
+
"""
|
| 163 |
+
idx = list(range(len(lengths)))
|
| 164 |
+
if shuffle_rng is not None:
|
| 165 |
+
shuffle_rng.shuffle(idx)
|
| 166 |
+
chunk = max_rows * 50
|
| 167 |
+
idx = [j for c in range(0, len(idx), chunk) for j in sorted(idx[c : c + chunk], key=lengths.__getitem__)]
|
| 168 |
+
else:
|
| 169 |
+
idx.sort(key=lengths.__getitem__)
|
| 170 |
+
batches, cur, cur_max = [], [], 0
|
| 171 |
+
for j in idx:
|
| 172 |
+
new_max = max(cur_max, lengths[j])
|
| 173 |
+
if cur and (new_max * (len(cur) + 1) > max_tokens or len(cur) >= max_rows):
|
| 174 |
+
batches.append(cur)
|
| 175 |
+
cur, new_max = [], lengths[j]
|
| 176 |
+
cur.append(j)
|
| 177 |
+
cur_max = new_max
|
| 178 |
+
if cur:
|
| 179 |
+
batches.append(cur)
|
| 180 |
+
if shuffle_rng is not None:
|
| 181 |
+
shuffle_rng.shuffle(batches)
|
| 182 |
+
return batches
|
zico/benchmark.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared pieces for the head-to-head benchmarks (same rows, same scoring for every system)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import statistics
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from .calibration import ece
|
| 9 |
+
from .data import read_jsonl
|
| 10 |
+
|
| 11 |
+
# (split, source): heldout = zero-shot for every system; test = in-distribution for zico checkpoints
|
| 12 |
+
BENCH_SETS = [("heldout", "massive_intent"), ("heldout", "emotion"), ("heldout", "imdb"), ("heldout", "arc_easy"),
|
| 13 |
+
("test", "ag_news"), ("test", "banking77")] # fmt: skip
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def load_rows(data_dir: str, split: str, source: str, n: int) -> list[dict]:
|
| 17 |
+
return [r for r in read_jsonl(f"{data_dir}/{split}.jsonl") if r["source"] == source][:n]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def auroc(conf, correct) -> float:
|
| 21 |
+
"""P(confidence of a correct answer > confidence of a wrong one); 0.5 = uninformative."""
|
| 22 |
+
pos = [c for c, ok in zip(conf, correct) if ok]
|
| 23 |
+
neg = [c for c, ok in zip(conf, correct) if not ok]
|
| 24 |
+
if not pos or not neg:
|
| 25 |
+
return float("nan")
|
| 26 |
+
return sum((a > b) + 0.5 * (a == b) for a in pos for b in neg) / (len(pos) * len(neg))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def summarise(preds, labels, confs, times) -> dict:
|
| 30 |
+
"""preds may contain None (abstention / error), which is scored as wrong."""
|
| 31 |
+
answered = [p is not None for p in preds]
|
| 32 |
+
correct = [p == y for p, y in zip(preds, labels)]
|
| 33 |
+
conf = [c if c is not None else 0.0 for c in confs]
|
| 34 |
+
n_ans = sum(answered)
|
| 35 |
+
top = sorted(range(len(conf)), key=lambda i: -conf[i])[: max(1, len(conf) // 2)]
|
| 36 |
+
return {
|
| 37 |
+
"n": len(preds),
|
| 38 |
+
"acc": sum(correct) / len(correct),
|
| 39 |
+
"abstain": 1 - n_ans / len(preds),
|
| 40 |
+
"acc_answered": sum(c for c, a in zip(correct, answered) if a) / max(1, n_ans),
|
| 41 |
+
"ece": ece(torch.tensor(conf), torch.tensor(correct))[0],
|
| 42 |
+
"auroc": auroc(conf, correct),
|
| 43 |
+
"acc_top50_conf": sum(correct[i] for i in top) / len(top),
|
| 44 |
+
"median_ms": statistics.median(times) * 1000 if times else float("nan"),
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def format_line(name: str, m: dict) -> str:
|
| 49 |
+
return (f" {name:13s} acc {m['acc']:.3f} | abstain {m['abstain']:.3f} | acc(answered) {m['acc_answered']:.3f} | "
|
| 50 |
+
f"ece {m['ece']:.3f} | auroc {m['auroc']:.3f} | acc@top50%conf {m['acc_top50_conf']:.3f} | "
|
| 51 |
+
f"median {m['median_ms']:.0f} ms") # fmt: skip
|
zico/calibration.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Calibration metrics and post-hoc temperature scaling."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import math
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from .schema import QTYPE_INDEX, QTYPES
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def ece(conf: torch.Tensor, correct: torch.Tensor, n_bins: int = 15):
|
| 12 |
+
"""Expected calibration error of the top-label confidence, plus the reliability table."""
|
| 13 |
+
edges = torch.linspace(0, 1, n_bins + 1)
|
| 14 |
+
total, table = 0.0, []
|
| 15 |
+
for lo, hi in zip(edges[:-1], edges[1:]):
|
| 16 |
+
m = (conf > lo) & (conf <= hi)
|
| 17 |
+
if m.any():
|
| 18 |
+
c, a, w = conf[m].mean().item(), correct[m].float().mean().item(), m.float().mean().item()
|
| 19 |
+
total += w * abs(c - a)
|
| 20 |
+
table.append({"lo": round(lo.item(), 3), "hi": round(hi.item(), 3), "conf": c, "acc": a, "frac": w})
|
| 21 |
+
return total, table
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def metrics(records: list[dict]) -> dict:
|
| 25 |
+
"""records: {"p": 1-D tensor over options, "label": int, "qtype": int, "target": optional soft tensor}."""
|
| 26 |
+
if not records:
|
| 27 |
+
return {}
|
| 28 |
+
conf = torch.tensor([r["p"].max().item() for r in records])
|
| 29 |
+
pred = torch.tensor([int(r["p"].argmax()) for r in records])
|
| 30 |
+
label = torch.tensor([r["label"] for r in records])
|
| 31 |
+
correct = pred == label
|
| 32 |
+
nll = torch.tensor([-math.log(max(r["p"][r["label"]].item(), 1e-12)) for r in records])
|
| 33 |
+
brier = torch.tensor(
|
| 34 |
+
[((r["p"] - torch.nn.functional.one_hot(torch.tensor(r["label"]), len(r["p"]))) ** 2).sum().item() for r in records]
|
| 35 |
+
)
|
| 36 |
+
e, table = ece(conf, correct)
|
| 37 |
+
out = {
|
| 38 |
+
"n": len(records),
|
| 39 |
+
"acc": correct.float().mean().item(),
|
| 40 |
+
"nll": nll.mean().item(),
|
| 41 |
+
"brier": brier.mean().item(),
|
| 42 |
+
"ece": e,
|
| 43 |
+
"mean_conf": conf.mean().item(),
|
| 44 |
+
"reliability": table,
|
| 45 |
+
}
|
| 46 |
+
score_rows = [r for r in records if r["qtype"] == QTYPE_INDEX["score"]]
|
| 47 |
+
if score_rows:
|
| 48 |
+
exp = torch.tensor([(r["p"] * torch.arange(len(r["p"]))).sum().item() for r in score_rows])
|
| 49 |
+
gold = torch.tensor([float(r["label"]) for r in score_rows])
|
| 50 |
+
out["score_mae"] = (exp - gold).abs().mean().item()
|
| 51 |
+
return out
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def metrics_by(records: list[dict], key: str) -> dict:
|
| 55 |
+
groups: dict = {}
|
| 56 |
+
for r in records:
|
| 57 |
+
groups.setdefault(r[key], []).append(r)
|
| 58 |
+
return {(QTYPES[k] if key == "qtype" else k): metrics(v) for k, v in sorted(groups.items(), key=lambda kv: str(kv[0]))}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def fit_temperature(logits: list[torch.Tensor], labels: list[int], lo: float = 0.05, hi: float = 20.0) -> float:
|
| 62 |
+
"""Single scalar T in [lo, hi] minimising NLL of softmax(logits / T).
|
| 63 |
+
|
| 64 |
+
Accuracy is unchanged by construction. The problem is one-dimensional, so we use a
|
| 65 |
+
bounded grid + golden-section search rather than a gradient method that can diverge.
|
| 66 |
+
"""
|
| 67 |
+
k = max(len(l) for l in logits)
|
| 68 |
+
padded = torch.full((len(logits), k), -1e9)
|
| 69 |
+
for i, l in enumerate(logits):
|
| 70 |
+
padded[i, : len(l)] = l.detach().float().cpu()
|
| 71 |
+
y = torch.tensor(labels)
|
| 72 |
+
|
| 73 |
+
def nll(log_t: float) -> float:
|
| 74 |
+
return torch.nn.functional.cross_entropy(padded / math.exp(log_t), y).item()
|
| 75 |
+
|
| 76 |
+
grid = torch.linspace(math.log(lo), math.log(hi), 61).tolist()
|
| 77 |
+
best = min(range(len(grid)), key=lambda i: nll(grid[i]))
|
| 78 |
+
a, b = grid[max(best - 1, 0)], grid[min(best + 1, len(grid) - 1)]
|
| 79 |
+
phi = (math.sqrt(5) - 1) / 2
|
| 80 |
+
for _ in range(40):
|
| 81 |
+
c, d = b - phi * (b - a), a + phi * (b - a)
|
| 82 |
+
if nll(c) < nll(d):
|
| 83 |
+
b = d
|
| 84 |
+
else:
|
| 85 |
+
a = c
|
| 86 |
+
return min(max(math.exp((a + b) / 2), lo), hi)
|
zico/data.py
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public datasets recast as (state, typed question, label) records.
|
| 2 |
+
|
| 3 |
+
The aim is a model that *reads the schema* rather than memorising label sets, so records
|
| 4 |
+
vary what a fixed classifier would hold constant: option order, option subsets, presence
|
| 5 |
+
of descriptions, instruction wording, and whether the state is a string or a JSON object.
|
| 6 |
+
|
| 7 |
+
Record format (one JSON object per line):
|
| 8 |
+
{"source", "sid", "state", "question": {"type", "instructions", "criteria"}, "label", "soft"?}
|
| 9 |
+
"""
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
import json
|
| 13 |
+
import random
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from .batching import Row, make_row
|
| 17 |
+
from .render import Renderer
|
| 18 |
+
from .schema import question_from_dict
|
| 19 |
+
|
| 20 |
+
GO_EMOTIONS = [
|
| 21 |
+
"admiration", "amusement", "anger", "annoyance", "approval", "caring", "confusion", "curiosity", "desire",
|
| 22 |
+
"disappointment", "disapproval", "disgust", "embarrassment", "excitement", "fear", "gratitude", "grief", "joy",
|
| 23 |
+
"love", "nervousness", "optimism", "pride", "realization", "relief", "remorse", "sadness", "surprise", "neutral",
|
| 24 |
+
] # fmt: skip
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _q(qtype, instructions, criteria=None):
|
| 28 |
+
q = {"type": qtype, "instructions": instructions}
|
| 29 |
+
if criteria is not None:
|
| 30 |
+
q["criteria"] = criteria
|
| 31 |
+
return q
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _shuffled_choice(rng, instructions, options: list[tuple[str, str]], gold_key: str, describe=True):
|
| 35 |
+
"""options: [(key, description)]. Returns (question, label index) with a random option order."""
|
| 36 |
+
options = options[:]
|
| 37 |
+
rng.shuffle(options)
|
| 38 |
+
keys = [k for k, _ in options]
|
| 39 |
+
criteria = {k: d for k, d in options} if describe and any(d for _, d in options) else keys
|
| 40 |
+
return _q("choice", instructions, criteria), keys.index(gold_key)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _wrap(rng, field, text):
|
| 44 |
+
"""States arrive as plain strings or as JSON objects; train on both."""
|
| 45 |
+
return {field: text} if rng.random() < 0.6 else text
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
# --------------------------------------------------------------------------- training sources
|
| 49 |
+
def boolq(ex, rng, ctx):
|
| 50 |
+
question = ex["question"].strip().rstrip("?")
|
| 51 |
+
question = question[0].upper() + question[1:] + "?"
|
| 52 |
+
yield _wrap(rng, "passage", ex["passage"]), _q("noul", question), int(ex["answer"]), None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
_AG = [
|
| 56 |
+
("world", "International news, politics, conflicts and diplomacy"),
|
| 57 |
+
("sports", "Sports events, teams, athletes and results"),
|
| 58 |
+
("business", "Companies, markets, the economy and finance"),
|
| 59 |
+
("science_technology", "Science, technology, computing, the internet and gadgets"),
|
| 60 |
+
]
|
| 61 |
+
_AG_T = ["Which news category does this article belong to?", "What is the main topic of the article?",
|
| 62 |
+
"Route this article to the right news desk."] # fmt: skip
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def ag_news(ex, rng, ctx):
|
| 66 |
+
q, y = _shuffled_choice(rng, rng.choice(_AG_T), _AG, _AG[ex["label"]][0], describe=rng.random() < 0.7)
|
| 67 |
+
yield _wrap(rng, "article", ex["text"]), q, y, None
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
_SAT5 = ["Very dissatisfied: a bad experience, would warn others", "Dissatisfied: more negatives than positives",
|
| 71 |
+
"Mixed: an average or so-so experience", "Satisfied: a good experience with minor issues",
|
| 72 |
+
"Delighted: an excellent experience, highly recommended"] # fmt: skip
|
| 73 |
+
_SAT3 = ["Negative overall", "Mixed or neutral overall", "Positive overall"]
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def yelp(ex, rng, ctx):
|
| 77 |
+
stars, state = ex["label"], _wrap(rng, "review", ex["text"])
|
| 78 |
+
if rng.random() < 0.6:
|
| 79 |
+
yield state, _q("score", "How satisfied is the reviewer with the business?", _SAT5), stars, None
|
| 80 |
+
else:
|
| 81 |
+
yield state, _q("score", "What is the overall sentiment of the review?", _SAT3), (0, 0, 1, 2, 2)[stars], None
|
| 82 |
+
if stars != 2 and rng.random() < 0.5:
|
| 83 |
+
if rng.random() < 0.5:
|
| 84 |
+
yield state, _q("noul", "The reviewer would recommend this business."), int(stars >= 3), None
|
| 85 |
+
else:
|
| 86 |
+
yield state, _q("noul", "The reviewer had a negative experience."), int(stars <= 1), None
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def go_emotions(ex, rng, ctx):
|
| 90 |
+
gold = [GO_EMOTIONS[i] for i in ex["labels"]]
|
| 91 |
+
state = _wrap(rng, "comment", ex["text"])
|
| 92 |
+
if len(gold) == 1:
|
| 93 |
+
k = rng.randint(4, 10)
|
| 94 |
+
distract = rng.sample([e for e in GO_EMOTIONS if e != gold[0]], k - 1)
|
| 95 |
+
q, y = _shuffled_choice(rng, "Which emotion does the comment primarily express?",
|
| 96 |
+
[(e, "") for e in [gold[0]] + distract], gold[0]) # fmt: skip
|
| 97 |
+
yield state, q, y, None
|
| 98 |
+
non_neutral = [e for e in gold if e != "neutral"]
|
| 99 |
+
if non_neutral and rng.random() < 0.5:
|
| 100 |
+
yield state, _q("noul", f"The author expresses {rng.choice(non_neutral)}."), 1, None
|
| 101 |
+
else:
|
| 102 |
+
neg = rng.choice([e for e in GO_EMOTIONS if e not in gold and e != "neutral"])
|
| 103 |
+
yield state, _q("noul", f"The author expresses {neg}."), 0, None
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
_INTENT_T = ["Which intent best matches the customer's message?", "What is the customer trying to do?",
|
| 107 |
+
"Classify the support request."] # fmt: skip
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def banking77(ex, rng, ctx):
|
| 111 |
+
names = ctx["banking77_labels"]
|
| 112 |
+
gold = ex["label_text"]
|
| 113 |
+
k = rng.choice([16, 32, len(names), len(names)]) # favour the full 77-way list
|
| 114 |
+
keys = [gold] + rng.sample([n for n in names if n != gold], k - 1)
|
| 115 |
+
q, y = _shuffled_choice(rng, rng.choice(_INTENT_T), [(n, "") for n in keys], gold)
|
| 116 |
+
yield _wrap(rng, "message", ex["text"]), q, y, None
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
_NLI = [("entailment", "The hypothesis must be true if the premise is true"),
|
| 120 |
+
("neutral", "The hypothesis may or may not be true given the premise"),
|
| 121 |
+
("contradiction", "The hypothesis cannot be true if the premise is true")] # fmt: skip
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def mnli(ex, rng, ctx):
|
| 125 |
+
state = {"premise": ex["premise"], "hypothesis": ex["hypothesis"]}
|
| 126 |
+
if rng.random() < 0.7:
|
| 127 |
+
q, y = _shuffled_choice(rng, "How does the hypothesis relate to the premise?", _NLI, _NLI[ex["label"]][0],
|
| 128 |
+
describe=rng.random() < 0.7) # fmt: skip
|
| 129 |
+
yield state, q, y, None
|
| 130 |
+
else:
|
| 131 |
+
yield state, _q("noul", "The hypothesis necessarily follows from the premise."), int(ex["label"] == 0), None
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def race(ex, rng, ctx):
|
| 135 |
+
opts = list(ex["options"])
|
| 136 |
+
gold = opts["ABCD".index(ex["answer"])]
|
| 137 |
+
rng.shuffle(opts)
|
| 138 |
+
criteria = {"ABCD"[i]: o for i, o in enumerate(opts)}
|
| 139 |
+
yield {"passage": ex["article"]}, _q("choice", ex["question"], criteria), opts.index(gold), None
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
_TOX = {
|
| 143 |
+
"toxicity": "This comment is toxic: rude, disrespectful, or likely to make someone leave a discussion.",
|
| 144 |
+
"insult": "This comment insults a person or group.",
|
| 145 |
+
"obscene": "This comment contains obscene or vulgar language.",
|
| 146 |
+
"threat": "This comment contains a threat.",
|
| 147 |
+
"identity_attack": "This comment attacks someone because of their identity.",
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def civil_comments(ex, rng, ctx):
|
| 152 |
+
"""Targets are annotator fractions: a natural source of *soft*, calibrated labels."""
|
| 153 |
+
if ex["toxicity"] == 0 and rng.random() > 0.25:
|
| 154 |
+
return
|
| 155 |
+
state = _wrap(rng, "comment", ex["text"])
|
| 156 |
+
attrs = ["toxicity"] + ([rng.choice(list(_TOX)[1:])] if rng.random() < 0.5 else [])
|
| 157 |
+
for a in attrs:
|
| 158 |
+
t = float(ex[a])
|
| 159 |
+
yield state, _q("noul", _TOX[a]), int(t >= 0.5), [1 - t, t]
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
# --------------------------------------------------------------------------- held-out sources
|
| 163 |
+
def emotion(ex, rng, ctx):
|
| 164 |
+
names = ["sadness", "joy", "love", "anger", "fear", "surprise"]
|
| 165 |
+
q, y = _shuffled_choice(rng, "Which emotion does the author express?", [(n, "") for n in names], names[ex["label"]])
|
| 166 |
+
yield ex["text"], q, y, None
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def imdb(ex, rng, ctx):
|
| 170 |
+
yield {"review": ex["text"]}, _q("noul", "The reviewer liked the movie."), int(ex["label"]), None
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def massive_intent(ex, rng, ctx):
|
| 174 |
+
names = ctx["massive_labels"]
|
| 175 |
+
q, y = _shuffled_choice(rng, "Which intent does the voice assistant request express?",
|
| 176 |
+
[(n, "") for n in names], ex["label_text"]) # fmt: skip
|
| 177 |
+
yield {"utterance": ex["text"]}, q, y, None
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def arc_easy(ex, rng, ctx):
|
| 181 |
+
texts, labels = ex["choices"]["text"], ex["choices"]["label"]
|
| 182 |
+
if ex["answerKey"] not in labels or len(texts) < 2:
|
| 183 |
+
return
|
| 184 |
+
criteria = {"ABCDE"[i]: t for i, t in enumerate(texts)}
|
| 185 |
+
yield {"question": ex["question"]}, _q("choice", "Select the correct answer to the question.", criteria), \
|
| 186 |
+
labels.index(ex["answerKey"]), None # fmt: skip
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# name -> (hf id, config, train split, test split, stream?, adapter)
|
| 190 |
+
TRAIN_SOURCES = {
|
| 191 |
+
"boolq": ("google/boolq", None, "train", "validation", False, boolq),
|
| 192 |
+
"ag_news": ("fancyzhx/ag_news", None, "train", "test", False, ag_news),
|
| 193 |
+
"yelp": ("Yelp/yelp_review_full", None, "train", "test", True, yelp),
|
| 194 |
+
"go_emotions": ("google-research-datasets/go_emotions", "simplified", "train", "test", False, go_emotions),
|
| 195 |
+
"banking77": ("mteb/banking77", None, "train", "test", False, banking77),
|
| 196 |
+
"mnli": ("nyu-mll/glue", "mnli", "train", "validation_matched", False, mnli),
|
| 197 |
+
"race": ("ehovy/race", "all", "train", "test", False, race),
|
| 198 |
+
"civil_comments": ("google/civil_comments", None, "train", "test", True, civil_comments),
|
| 199 |
+
}
|
| 200 |
+
HELDOUT_SOURCES = {
|
| 201 |
+
"emotion": ("dair-ai/emotion", "split", None, "test", False, emotion),
|
| 202 |
+
"imdb": ("stanfordnlp/imdb", "plain_text", None, "test", False, imdb),
|
| 203 |
+
"massive_intent": ("mteb/amazon_massive_intent", "en", None, "test", False, massive_intent),
|
| 204 |
+
"arc_easy": ("allenai/ai2_arc", "ARC-Easy", None, "test", False, arc_easy),
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def take(hf_id, config, split, n, stream, seed, buffer_size=20_000):
|
| 209 |
+
from datasets import load_dataset
|
| 210 |
+
|
| 211 |
+
if stream:
|
| 212 |
+
ds = load_dataset(hf_id, config, split=split, streaming=True).shuffle(seed=seed, buffer_size=buffer_size)
|
| 213 |
+
return list(ds.take(n))
|
| 214 |
+
ds = load_dataset(hf_id, config, split=split).shuffle(seed=seed)
|
| 215 |
+
return [ds[i] for i in range(min(n, len(ds)))]
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
def label_context() -> dict:
|
| 219 |
+
from datasets import load_dataset
|
| 220 |
+
|
| 221 |
+
return {
|
| 222 |
+
"banking77_labels": sorted(set(load_dataset("mteb/banking77", split="train")["label_text"])),
|
| 223 |
+
"massive_labels": sorted(set(load_dataset("mteb/amazon_massive_intent", "en", split="train")["label_text"])),
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def to_records(name, examples, adapter, rng, ctx, prefix):
|
| 228 |
+
out = []
|
| 229 |
+
for i, ex in enumerate(examples):
|
| 230 |
+
for state, question, label, soft in adapter(ex, rng, ctx):
|
| 231 |
+
rec = {"source": name, "sid": f"{prefix}-{name}-{i}", "state": state, "question": question, "label": label}
|
| 232 |
+
if soft is not None:
|
| 233 |
+
rec["soft"] = soft
|
| 234 |
+
out.append(rec)
|
| 235 |
+
return out
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
# --------------------------------------------------------------------------- loading
|
| 239 |
+
def read_jsonl(path) -> list[dict]:
|
| 240 |
+
with open(path) as f:
|
| 241 |
+
return [json.loads(line) for line in f if line.strip()]
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def write_jsonl(path, records):
|
| 245 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 246 |
+
with open(path, "w") as f:
|
| 247 |
+
for r in records:
|
| 248 |
+
f.write(json.dumps(r, ensure_ascii=False) + "\n")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def render_records(records: list[dict], renderer: Renderer, max_len: int | None = None, image_meta=None,
|
| 252 |
+
image_root=None) -> tuple[list[Row], list[dict]]: # fmt: skip
|
| 253 |
+
"""Tokenise records into rows; drops rows longer than max_len (returns the kept records too).
|
| 254 |
+
|
| 255 |
+
Image records carry "image_grids" (token grid per image, recorded when the data was built), so their
|
| 256 |
+
length and positions are known without decoding any picture; pixels are loaded per batch by collate().
|
| 257 |
+
image_meta = the vision tower's zico_vision.json; image_root = directory the image paths are relative to.
|
| 258 |
+
"""
|
| 259 |
+
from .batching import make_image_row
|
| 260 |
+
from .vision import ImageError, render_image_state
|
| 261 |
+
|
| 262 |
+
rows, kept, state_cache = [], [], {}
|
| 263 |
+
for rec in records:
|
| 264 |
+
grids = rec.get("image_grids")
|
| 265 |
+
if grids:
|
| 266 |
+
if image_meta is None:
|
| 267 |
+
raise ValueError("the data contains images but the checkpoint has no vision tower (scripts/add_vision.py)")
|
| 268 |
+
if rec["sid"] not in state_cache:
|
| 269 |
+
try:
|
| 270 |
+
st = render_image_state(renderer, rec["state"], image_meta, grids=grids)
|
| 271 |
+
except ImageError:
|
| 272 |
+
st = None # state longer than the renderer's limit
|
| 273 |
+
if st is not None and image_root is not None:
|
| 274 |
+
for part in st.images:
|
| 275 |
+
part.path = str(Path(image_root) / part.path)
|
| 276 |
+
state_cache[rec["sid"]] = st
|
| 277 |
+
if state_cache[rec["sid"]] is None:
|
| 278 |
+
continue
|
| 279 |
+
row = make_image_row(state_cache[rec["sid"]], renderer.question(question_from_dict(rec["question"])),
|
| 280 |
+
label=rec["label"], soft=rec.get("soft")) # fmt: skip
|
| 281 |
+
if max_len is None or len(row) <= max_len:
|
| 282 |
+
rows.append(row)
|
| 283 |
+
kept.append(rec)
|
| 284 |
+
continue
|
| 285 |
+
if rec["sid"] not in state_cache:
|
| 286 |
+
state_cache[rec["sid"]] = renderer.state(rec["state"])
|
| 287 |
+
row = make_row(state_cache[rec["sid"]], renderer.question(question_from_dict(rec["question"])),
|
| 288 |
+
label=rec["label"], soft=rec.get("soft")) # fmt: skip
|
| 289 |
+
if max_len is None or len(row) <= max_len:
|
| 290 |
+
rows.append(row)
|
| 291 |
+
kept.append(rec)
|
| 292 |
+
return rows, kept
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def subsample(records, n, seed=0):
|
| 296 |
+
if n is None or len(records) <= n:
|
| 297 |
+
return records
|
| 298 |
+
return random.Random(seed).sample(records, n)
|
zico/evaluate.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Batched prediction over rendered rows (the training-time, row-per-question path)."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from .batching import Row, bucketed_batches, collate
|
| 7 |
+
from .calibration import metrics, metrics_by
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@torch.no_grad()
|
| 11 |
+
def predict(model, rows: list[Row], records: list[dict], pad_id: int, max_tokens=8192, max_rows=64) -> list[dict]:
|
| 12 |
+
dev = next(model.parameters()).device
|
| 13 |
+
was_training = model.training
|
| 14 |
+
model.eval()
|
| 15 |
+
load_image = model.image_pre.load_prepared if getattr(model, "image_pre", None) is not None else None
|
| 16 |
+
out: list[dict | None] = [None] * len(rows)
|
| 17 |
+
for idx, pad_len in bucketed_batches([len(r) for r in rows], max_tokens, max_rows):
|
| 18 |
+
b = collate([rows[i] for i in idx], pad_id, pad_len, load_image)
|
| 19 |
+
logits = model(**{k: v.to(dev) for k, v in b.items()}).float().cpu()
|
| 20 |
+
for j, i in enumerate(idx):
|
| 21 |
+
k = len(rows[i].opt_pos)
|
| 22 |
+
lg = logits[j, :k]
|
| 23 |
+
out[i] = {"logits": lg, "p": torch.softmax(lg, -1), "label": rows[i].label, "qtype": rows[i].qtype,
|
| 24 |
+
"source": records[i]["source"]} # fmt: skip
|
| 25 |
+
model.train(was_training)
|
| 26 |
+
return out
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def report(preds: list[dict]) -> dict:
|
| 30 |
+
return {"overall": metrics(preds), "by_type": metrics_by(preds, "qtype"), "by_source": metrics_by(preds, "source")}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def format_report(rep: dict, title: str = "") -> str:
|
| 34 |
+
lines = [f"{title:<22s} {'n':>6s} {'acc':>7s} {'nll':>7s} {'brier':>7s} {'ece':>7s} {'conf':>7s}"]
|
| 35 |
+
|
| 36 |
+
def row(name, m):
|
| 37 |
+
return f"{name:<22s} {m['n']:6d} {m['acc']:7.3f} {m['nll']:7.3f} {m['brier']:7.3f} {m['ece']:7.3f} {m['mean_conf']:7.3f}"
|
| 38 |
+
|
| 39 |
+
lines.append(row("overall", rep["overall"]))
|
| 40 |
+
lines += [row(f" type:{k}", m) for k, m in rep["by_type"].items()]
|
| 41 |
+
lines += [row(f" {k}", m) for k, m in rep["by_source"].items()]
|
| 42 |
+
return "\n".join(lines)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def strip_tables(rep: dict) -> dict:
|
| 46 |
+
"""Drop reliability tables for compact logging."""
|
| 47 |
+
if "reliability" in rep:
|
| 48 |
+
return {k: v for k, v in rep.items() if k != "reliability"}
|
| 49 |
+
return {k: strip_tables(v) if isinstance(v, dict) else v for k, v in rep.items()}
|
zico/image_sources.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Public image datasets recast as typed questions about an image in the state.
|
| 2 |
+
|
| 3 |
+
Same philosophy as data.py: vary what a fixed classifier would hold constant (option subsets and
|
| 4 |
+
order, instruction wording, where the image sits in the state) so the model reads the schema.
|
| 5 |
+
Held-out sources are never trained on, as with text.
|
| 6 |
+
|
| 7 |
+
An adapter yields (question, label index) pairs for one example; the builder supplies the state.
|
| 8 |
+
"""
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
|
| 13 |
+
from .data import _q, _shuffled_choice
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _clean(name) -> str:
|
| 17 |
+
name = re.sub(r"^\d+\.", "", str(name)).split(",")[0] # "001.Black_footed_Albatross" / "bonnet, poke bonnet"
|
| 18 |
+
return re.sub(r"[\s_]+", " ", name).strip()
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _article(name: str) -> str:
|
| 22 |
+
return ("an " if name[:1].lower() in "aeiou" else "a ") + name
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def classify(label_col: str, instructions: list[str], sizes: list[int], statement: str | None = "The image shows {a}."):
|
| 26 |
+
"""Single-label classification -> a Choice over a sampled option subset, sometimes plus a yes/no statement."""
|
| 27 |
+
|
| 28 |
+
def adapter(ex, rng, names):
|
| 29 |
+
names = [_clean(n) for n in names[label_col]]
|
| 30 |
+
gold = names[ex[label_col]]
|
| 31 |
+
others = sorted(set(names) - {gold})
|
| 32 |
+
k = min(len(others) + 1, rng.choice(sizes))
|
| 33 |
+
opts = [gold] + rng.sample(others, k - 1)
|
| 34 |
+
yield _shuffled_choice(rng, rng.choice(instructions), [(o, "") for o in opts], gold)
|
| 35 |
+
if statement and rng.random() < 0.35:
|
| 36 |
+
truth = rng.random() < 0.5
|
| 37 |
+
shown = gold if truth else rng.choice(others)
|
| 38 |
+
yield _q("noul", statement.format(a=_article(shown), x=shown)), int(truth)
|
| 39 |
+
|
| 40 |
+
return adapter
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def cifar100(ex, rng, names):
|
| 44 |
+
yield from classify("fine_label", ["What does the image show?", "Which object is pictured?"], [10, 20, 100])(ex, rng, names)
|
| 45 |
+
if rng.random() < 0.5:
|
| 46 |
+
yield from classify("coarse_label", ["Which broad category does the pictured thing belong to?"], [20], None)(ex, rng, names)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def aircraft(ex, rng, names):
|
| 50 |
+
yield from classify("label", ["Which aircraft model variant is this?"], [8, 16, 32], "The aircraft is {a}.")(ex, rng, names)
|
| 51 |
+
which = rng.choice(["family", "manufacturer"])
|
| 52 |
+
ask = "Which aircraft family is this?" if which == "family" else "Which company built this aircraft?"
|
| 53 |
+
yield from classify(which, [ask], [8, 16, 30], None)(ex, rng, names)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
_FASHION = {"masterCategory": "Which top-level category does the product belong to?", "subCategory": "Which sub-category is the product?",
|
| 57 |
+
"articleType": "What type of article is this product?", "baseColour": "What is the main colour of the product?",
|
| 58 |
+
"gender": "Who is the product intended for?", "usage": "What occasion is the product meant for?"} # fmt: skip
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def fashion(ex, rng, names):
|
| 62 |
+
"""Several attributes per product photo: natural multi-question supervision."""
|
| 63 |
+
for col in rng.sample(list(_FASHION), 2):
|
| 64 |
+
gold, pool = ex.get(col), names.get(col) or []
|
| 65 |
+
if not gold or gold not in pool or len(pool) < 3:
|
| 66 |
+
continue
|
| 67 |
+
others = [p for p in pool if p != gold]
|
| 68 |
+
opts = [gold] + rng.sample(others, min(len(others), rng.choice([3, 7, 15, 31])))
|
| 69 |
+
yield _shuffled_choice(rng, _FASHION[col], [(_clean(o), "") for o in opts], _clean(gold))
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def pope(ex, rng, names):
|
| 73 |
+
yield _q("noul", ex["question"]), int(str(ex["answer"]).strip().lower() == "yes")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
_DOC = ["What type of document is this scan?", "Classify the scanned document.", "Which kind of document is shown?"]
|
| 77 |
+
|
| 78 |
+
# name: (hf id, config, train split, test split, stream?, image column, adapter, max pixels, state keys)
|
| 79 |
+
IMAGE_TRAIN_SOURCES = {
|
| 80 |
+
"rvl_cdip": ("chainyo/rvl-cdip", None, "train", "test", True, "image", classify("label", _DOC, [6, 16, 16], "This is {a}."), 640 * 640, ["document", "scan", "page"]),
|
| 81 |
+
"imagenet100": ("clane9/imagenet-100", None, "train", "validation", True, "image", classify("label", ["What does the image show?", "What is the main subject of the photo?"], [8, 16, 32, 100]), 448 * 448, ["image", "photo"]),
|
| 82 |
+
"cifar100": ("uoft-cs/cifar100", "cifar100", "train", "test", False, "img", cifar100, 256 * 256, ["image", "picture"]),
|
| 83 |
+
"stanford_cars": ("tanganke/stanford_cars", None, "train", "test", False, "image", classify("label", ["Which car model is this?", "Identify the make, model and year of the car."], [8, 16, 32, 64], "The car is {a}."), 512 * 512, ["photo", "image"]),
|
| 84 |
+
"cub_birds": ("bentrevett/caltech-ucsd-birds-200-2011", None, "train", "test", False, "image", classify("label", ["Which bird species is this?"], [8, 16, 32, 64], "The bird is {a}."), 512 * 512, ["photo", "image"]),
|
| 85 |
+
"aircraft": ("Multimodal-Fatima/FGVC_Aircraft_train", None, "train", None, False, "image", aircraft, 512 * 512, ["photo", "image"]),
|
| 86 |
+
"dtd": ("tanganke/dtd", None, "train", "test", False, "image", classify("label", ["Which texture does the surface have?"], [8, 16, 47], "The texture is {x}."), 448 * 448, ["image", "surface"]),
|
| 87 |
+
"eurosat": ("tanganke/eurosat", None, "train", "test", False, "image", classify("label", ["What kind of land use does this satellite image show?"], [5, 10], "The satellite image shows {x}."), 256 * 256, ["satellite_image", "image"]),
|
| 88 |
+
"gtsrb": ("tanganke/gtsrb", None, "train", "test", False, "image", classify("label", ["Which traffic sign is this?"], [8, 16, 43], None), 256 * 256, ["image", "sign"]),
|
| 89 |
+
"resisc45": ("tanganke/resisc45", None, "train", "test", False, "image", classify("label", ["What does this aerial image show?"], [8, 16, 45], "The aerial image shows {a}."), 384 * 384, ["aerial_image", "image"]),
|
| 90 |
+
"sun397": ("tanganke/sun397", None, "train", "test", True, "image", classify("label", ["What kind of place is this?", "Which scene category fits the photo?"], [8, 16, 32, 64], "The photo was taken in {a}."), 448 * 448, ["photo", "image"]),
|
| 91 |
+
"fashion": ("ashraq/fashion-product-images-small", None, "train", None, False, "image", fashion, 256 * 256, ["product_photo", "image"]),
|
| 92 |
+
} # fmt: skip
|
| 93 |
+
IMAGE_HELDOUT_SOURCES = {
|
| 94 |
+
"pets": ("timm/oxford-iiit-pet", None, None, "test", True, "image", classify("label", ["Which breed is the animal in the photo?"], [37], None), 448 * 448, ["photo"]),
|
| 95 |
+
"food101": ("ethz/food101", None, None, "validation", True, "image", classify("label", ["Which dish is shown in the photo?"], [101], None), 448 * 448, ["photo"]),
|
| 96 |
+
"cifar10": ("uoft-cs/cifar10", None, None, "test", False, "img", classify("label", ["What does the image show?"], [10], None), 256 * 256, ["image"]),
|
| 97 |
+
"mnist": ("ylecun/mnist", None, None, "test", False, "image", classify("label", ["Which digit is written in the image?"], [10], None), 256 * 256, ["image"]),
|
| 98 |
+
"pope": ("lmms-lab/POPE", "Full", None, "random", True, "image", pope, 448 * 448, ["image"]),
|
| 99 |
+
} # fmt: skip
|
zico/losses.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Training objectives: strictly proper scoring rules.
|
| 2 |
+
|
| 3 |
+
Our reading of "Reinforcement Learning for Calibrated Decisions": the reward is a strictly
|
| 4 |
+
proper scoring rule S(p, y) of the predicted distribution p against the realised outcome y.
|
| 5 |
+
A proper rule is maximised in expectation only by the true conditional distribution, so
|
| 6 |
+
optimising it targets calibration and accuracy together.
|
| 7 |
+
|
| 8 |
+
* Full feedback (the label y is observed). The model outputs the whole distribution, so
|
| 9 |
+
the expected reward and its gradient are available in closed form -- no sampling, no
|
| 10 |
+
policy-gradient variance. With the log score this *is* cross-entropy; Brier and the
|
| 11 |
+
ranked probability score (ordinal, for Score questions) are the other two we use.
|
| 12 |
+
Soft targets (annotator fractions, teacher vote shares) slot in unchanged.
|
| 13 |
+
|
| 14 |
+
* Bandit feedback (the model commits to an answer and only learns "right" / "wrong",
|
| 15 |
+
e.g. a verifier, a downstream outcome, a game). Then RL is actually needed. We use an
|
| 16 |
+
importance-weighted estimator that is unbiased for the same proper score:
|
| 17 |
+
a ~ mu, c = 1[a == y] E[ c * log p_a / mu_a ] = log p_y
|
| 18 |
+
E[ sum p^2 - 2 c p_a / mu_a ] = Brier(p, y) - 1
|
| 19 |
+
so the calibrated distribution remains the optimum although y is never revealed.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
|
| 23 |
+
import torch
|
| 24 |
+
import torch.nn.functional as F
|
| 25 |
+
|
| 26 |
+
from .schema import QTYPE_INDEX
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def log_probs(logits: torch.Tensor) -> torch.Tensor:
|
| 30 |
+
"""Masked options already carry a large negative logit (model.NEG)."""
|
| 31 |
+
return F.log_softmax(logits.float(), dim=-1)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def proper_score_loss(logits, opt_mask, target, qtype, brier_weight=0.0, rps_weight=0.0):
|
| 35 |
+
"""Per-row loss. `target` is one-hot or a soft distribution over the row's options."""
|
| 36 |
+
logp = log_probs(logits)
|
| 37 |
+
p = logp.exp()
|
| 38 |
+
mask = opt_mask.float()
|
| 39 |
+
loss = -(target * logp * mask).sum(-1)
|
| 40 |
+
if brier_weight:
|
| 41 |
+
loss = loss + brier_weight * (((p - target) ** 2) * mask).sum(-1)
|
| 42 |
+
if rps_weight:
|
| 43 |
+
is_score = (qtype == QTYPE_INDEX["score"]).float()
|
| 44 |
+
k = mask.sum(-1).clamp(min=2)
|
| 45 |
+
cdf_gap = (torch.cumsum(p * mask, -1) - torch.cumsum(target * mask, -1)) ** 2
|
| 46 |
+
loss = loss + rps_weight * is_score * (cdf_gap * mask).sum(-1) / (k - 1)
|
| 47 |
+
return loss
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def bandit_proper_score_loss(logits, opt_mask, label, eps=0.1, brier_weight=0.0, clip=50.0, generator=None):
|
| 51 |
+
"""Proper-score objective from correctness-only feedback (see module docstring).
|
| 52 |
+
|
| 53 |
+
Returns (per-row loss, per-row correctness of the sampled action).
|
| 54 |
+
"""
|
| 55 |
+
logp = log_probs(logits)
|
| 56 |
+
p = logp.exp()
|
| 57 |
+
mask = opt_mask.float()
|
| 58 |
+
uniform = mask / mask.sum(-1, keepdim=True)
|
| 59 |
+
mu = (1 - eps) * p.detach() * mask + eps * uniform
|
| 60 |
+
mu = mu / mu.sum(-1, keepdim=True)
|
| 61 |
+
a = torch.multinomial(mu, 1, generator=generator)
|
| 62 |
+
correct = (a.squeeze(-1) == label).float()
|
| 63 |
+
w = (correct / mu.gather(1, a).squeeze(-1)).clamp(max=clip) # clipping trades a little bias for variance
|
| 64 |
+
loss = -w * logp.gather(1, a).squeeze(-1)
|
| 65 |
+
if brier_weight:
|
| 66 |
+
loss = loss + brier_weight * (((p**2) * mask).sum(-1) - 2 * w * p.gather(1, a).squeeze(-1))
|
| 67 |
+
return loss, correct
|