Initial release: JoyCaption Beta One + JoyTag batch captioning WebUI
Browse files- .gitattributes +3 -35
- .gitignore +5 -0
- LICENSE +202 -0
- app.py +571 -0
- install_linux.sh +11 -0
- install_windows.bat +12 -0
- joytag_models.py +1163 -0
- requirements.txt +11 -0
- start_linux.sh +4 -0
- start_windows.bat +7 -0
- start_windows_lowvram.bat +7 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,3 @@
|
|
| 1 |
-
*
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz 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
|
|
|
|
| 1 |
+
* text=auto
|
| 2 |
+
*.bat text eol=crlf
|
| 3 |
+
*.sh text eol=lf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
venv/
|
| 2 |
+
models/
|
| 3 |
+
__pycache__/
|
| 4 |
+
*.pyc
|
| 5 |
+
.claude/
|
LICENSE
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.
|
app.py
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
JoyCaption Batch WebUI — local Gradio UI for JoyCaption Beta One + JoyTag,
|
| 3 |
+
with batch dataset captioning (writes .txt files next to images, kohya-style).
|
| 4 |
+
|
| 5 |
+
Models by fancyfeast (fpgaminer): https://github.com/fpgaminer/joycaption (Apache-2.0)
|
| 6 |
+
UI based on the HF spaces fancyfeast/joy-caption-beta-one and MegaTronX/JoyCaptionBetaOne.
|
| 7 |
+
|
| 8 |
+
Defaults target a 24GB GPU (bf16 + SDPA + TF32); run with --nf4 for 4-bit
|
| 9 |
+
(~7GB VRAM) if you need to keep a diffusion UI like ComfyUI loaded alongside.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import gc
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from threading import Thread
|
| 17 |
+
from typing import Generator
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
import gradio as gr
|
| 21 |
+
from PIL import Image
|
| 22 |
+
from transformers import LlavaForConditionalGeneration, TextIteratorStreamer, AutoProcessor
|
| 23 |
+
|
| 24 |
+
parser = argparse.ArgumentParser()
|
| 25 |
+
parser.add_argument("--nf4", action="store_true", help="Load model in 4-bit NF4 (~7GB VRAM instead of ~18GB)")
|
| 26 |
+
parser.add_argument("--port", type=int, default=7861, help="Port (default 7861, avoids ComfyUI's 8188/7860)")
|
| 27 |
+
parser.add_argument("--listen", action="store_true", help="Listen on 0.0.0.0 (LAN access)")
|
| 28 |
+
parser.add_argument("--cache-dir", type=str, default=None, help="Model download/cache dir (default: ./models next to this file)")
|
| 29 |
+
parser.add_argument("--no-browser", action="store_true", help="Don't auto-open a browser tab on launch")
|
| 30 |
+
args = parser.parse_args()
|
| 31 |
+
|
| 32 |
+
ON_SPACES = os.environ.get("SPACE_ID") is not None
|
| 33 |
+
DEVICE = "cuda:0" if torch.cuda.is_available() else "cpu"
|
| 34 |
+
|
| 35 |
+
MODEL_PATH = "fancyfeast/llama-joycaption-beta-one-hf-llava"
|
| 36 |
+
CACHE_DIR = args.cache_dir or str(Path(__file__).parent / "models")
|
| 37 |
+
|
| 38 |
+
# Ampere+ optimizations
|
| 39 |
+
torch.backends.cuda.matmul.allow_tf32 = True
|
| 40 |
+
torch.backends.cudnn.allow_tf32 = True
|
| 41 |
+
|
| 42 |
+
TITLE = """<h1 style="text-align:center;">JoyCaption Batch WebUI</h1>
|
| 43 |
+
<p style="text-align:center;">JoyCaption Beta One + JoyTag — single-image and batch dataset captioning</p>
|
| 44 |
+
<hr>"""
|
| 45 |
+
|
| 46 |
+
CAPTION_TYPE_MAP = {
|
| 47 |
+
"Descriptive": [
|
| 48 |
+
"Write a detailed description for this image.",
|
| 49 |
+
"Write a detailed description for this image in {word_count} words or less.",
|
| 50 |
+
"Write a {length} detailed description for this image.",
|
| 51 |
+
],
|
| 52 |
+
"Descriptive (Casual)": [
|
| 53 |
+
"Write a descriptive caption for this image in a casual tone.",
|
| 54 |
+
"Write a descriptive caption for this image in a casual tone within {word_count} words.",
|
| 55 |
+
"Write a {length} descriptive caption for this image in a casual tone.",
|
| 56 |
+
],
|
| 57 |
+
"Straightforward": [
|
| 58 |
+
"Write a straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
|
| 59 |
+
"Write a straightforward caption for this image within {word_count} words. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
|
| 60 |
+
"Write a {length} straightforward caption for this image. Begin with the main subject and medium. Mention pivotal elements—people, objects, scenery—using confident, definite language. Focus on concrete details like color, shape, texture, and spatial relationships. Show how elements interact. Omit mood and speculative wording. If text is present, quote it exactly. Note any watermarks, signatures, or compression artifacts. Never mention what's absent, resolution, or unobservable details. Vary your sentence structure and keep the description concise, without starting with \"This image is…\" or similar phrasing.",
|
| 61 |
+
],
|
| 62 |
+
"Stable Diffusion Prompt": [
|
| 63 |
+
"Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
|
| 64 |
+
"Output a stable diffusion prompt that is indistinguishable from a real stable diffusion prompt. {word_count} words or less.",
|
| 65 |
+
"Output a {length} stable diffusion prompt that is indistinguishable from a real stable diffusion prompt.",
|
| 66 |
+
],
|
| 67 |
+
"MidJourney": [
|
| 68 |
+
"Write a MidJourney prompt for this image.",
|
| 69 |
+
"Write a MidJourney prompt for this image within {word_count} words.",
|
| 70 |
+
"Write a {length} MidJourney prompt for this image.",
|
| 71 |
+
],
|
| 72 |
+
"Danbooru tag list": [
|
| 73 |
+
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text.",
|
| 74 |
+
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {word_count} words or less.",
|
| 75 |
+
"Generate only comma-separated Danbooru tags (lowercase_underscores). Strict order: `artist:`, `copyright:`, `character:`, `meta:`, then general tags. Include counts (1girl), appearance, clothing, accessories, pose, expression, actions, background. Use precise Danbooru syntax. No extra text. {length} length.",
|
| 76 |
+
],
|
| 77 |
+
"e621 tag list": [
|
| 78 |
+
"Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
|
| 79 |
+
"Write a comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags. Keep it under {word_count} words.",
|
| 80 |
+
"Write a {length} comma-separated list of e621 tags in alphabetical order for this image. Start with the artist, copyright, character, species, meta, and lore tags (if any), prefixed by 'artist:', 'copyright:', 'character:', 'species:', 'meta:', and 'lore:'. Then all the general tags.",
|
| 81 |
+
],
|
| 82 |
+
"Rule34 tag list": [
|
| 83 |
+
"Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
|
| 84 |
+
"Write a comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags. Keep it under {word_count} words.",
|
| 85 |
+
"Write a {length} comma-separated list of rule34 tags in alphabetical order for this image. Start with the artist, copyright, character, and meta tags (if any), prefixed by 'artist:', 'copyright:', 'character:', and 'meta:'. Then all the general tags.",
|
| 86 |
+
],
|
| 87 |
+
"Booru-like tag list": [
|
| 88 |
+
"Write a list of Booru-like tags for this image.",
|
| 89 |
+
"Write a list of Booru-like tags for this image within {word_count} words.",
|
| 90 |
+
"Write a {length} list of Booru-like tags for this image.",
|
| 91 |
+
],
|
| 92 |
+
"Art Critic": [
|
| 93 |
+
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc.",
|
| 94 |
+
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it within {word_count} words.",
|
| 95 |
+
"Analyze this image like an art critic would with information about its composition, style, symbolism, the use of color, light, any artistic movement it might belong to, etc. Keep it {length}.",
|
| 96 |
+
],
|
| 97 |
+
"Product Listing": [
|
| 98 |
+
"Write a caption for this image as though it were a product listing.",
|
| 99 |
+
"Write a caption for this image as though it were a product listing. Keep it under {word_count} words.",
|
| 100 |
+
"Write a {length} caption for this image as though it were a product listing.",
|
| 101 |
+
],
|
| 102 |
+
"Social Media Post": [
|
| 103 |
+
"Write a caption for this image as if it were being used for a social media post.",
|
| 104 |
+
"Write a caption for this image as if it were being used for a social media post. Limit the caption to {word_count} words.",
|
| 105 |
+
"Write a {length} caption for this image as if it were being used for a social media post.",
|
| 106 |
+
],
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
NAME_OPTION = "If there is a person/character in the image you must refer to them as {name}."
|
| 110 |
+
|
| 111 |
+
EXTRA_OPTIONS_LIST = [
|
| 112 |
+
"Do NOT include information about people/characters that cannot be changed (like ethnicity, gender, etc), but do still include changeable attributes (like hair style).",
|
| 113 |
+
"Include information about lighting.",
|
| 114 |
+
"Include information about camera angle.",
|
| 115 |
+
"Include information about whether there is a watermark or not.",
|
| 116 |
+
"Include information about whether there are JPEG artifacts or not.",
|
| 117 |
+
"If it is a photo you MUST include information about what camera was likely used and details such as aperture, shutter speed, ISO, etc.",
|
| 118 |
+
"Do NOT include anything sexual; keep it PG.",
|
| 119 |
+
"Do NOT mention the image's resolution.",
|
| 120 |
+
"You MUST include information about the subjective aesthetic quality of the image from low to very high.",
|
| 121 |
+
"Include information on the image's composition style, such as leading lines, rule of thirds, or symmetry.",
|
| 122 |
+
"Do NOT mention any text that is in the image.",
|
| 123 |
+
"Specify the depth of field and whether the background is in focus or blurred.",
|
| 124 |
+
"If applicable, mention the likely use of artificial or natural lighting sources.",
|
| 125 |
+
"Do NOT use any ambiguous language.",
|
| 126 |
+
"Include whether the image is sfw, suggestive, or nsfw.",
|
| 127 |
+
"ONLY describe the most important elements of the image.",
|
| 128 |
+
]
|
| 129 |
+
|
| 130 |
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tif", ".tiff"}
|
| 131 |
+
|
| 132 |
+
processor = None
|
| 133 |
+
model = None
|
| 134 |
+
|
| 135 |
+
JOYTAG_REPO = "fancyfeast/joytag"
|
| 136 |
+
joytag_model = None
|
| 137 |
+
joytag_tags = None
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def load_model() -> str:
|
| 141 |
+
global processor, model
|
| 142 |
+
if model is not None:
|
| 143 |
+
return "✅ Model is already loaded."
|
| 144 |
+
print("Loading processor...")
|
| 145 |
+
processor = AutoProcessor.from_pretrained(MODEL_PATH, cache_dir=CACHE_DIR)
|
| 146 |
+
print("Loading model..." + (" (4-bit NF4)" if args.nf4 else " (bf16)"))
|
| 147 |
+
load_kwargs = dict(
|
| 148 |
+
torch_dtype=torch.bfloat16,
|
| 149 |
+
attn_implementation="sdpa",
|
| 150 |
+
cache_dir=CACHE_DIR,
|
| 151 |
+
)
|
| 152 |
+
if args.nf4:
|
| 153 |
+
from transformers import BitsAndBytesConfig
|
| 154 |
+
load_kwargs["quantization_config"] = BitsAndBytesConfig(
|
| 155 |
+
load_in_4bit=True,
|
| 156 |
+
bnb_4bit_quant_type="nf4",
|
| 157 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 158 |
+
bnb_4bit_use_double_quant=True,
|
| 159 |
+
)
|
| 160 |
+
load_kwargs["device_map"] = DEVICE
|
| 161 |
+
else:
|
| 162 |
+
load_kwargs["device_map"] = DEVICE
|
| 163 |
+
model = LlavaForConditionalGeneration.from_pretrained(MODEL_PATH, **load_kwargs)
|
| 164 |
+
model.eval()
|
| 165 |
+
print("Model loaded and ready!")
|
| 166 |
+
return "✅ Model loaded and ready!" + (" (4-bit NF4)" if args.nf4 else " (bf16)")
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def unload_model() -> str:
|
| 170 |
+
global processor, model
|
| 171 |
+
if model is None:
|
| 172 |
+
return "⚠️ Model is not currently loaded."
|
| 173 |
+
del model
|
| 174 |
+
del processor
|
| 175 |
+
model = None
|
| 176 |
+
processor = None
|
| 177 |
+
gc.collect()
|
| 178 |
+
torch.cuda.empty_cache()
|
| 179 |
+
return "✅ Model unloaded from VRAM."
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def load_joytag() -> str:
|
| 183 |
+
global joytag_model, joytag_tags
|
| 184 |
+
if joytag_model is not None:
|
| 185 |
+
return "✅ JoyTag is already loaded."
|
| 186 |
+
import huggingface_hub
|
| 187 |
+
from joytag_models import VisionModel
|
| 188 |
+
print("Loading JoyTag...")
|
| 189 |
+
path = huggingface_hub.snapshot_download(JOYTAG_REPO, cache_dir=CACHE_DIR)
|
| 190 |
+
joytag_model = VisionModel.load_model(path, device="cuda" if torch.cuda.is_available() else "cpu")
|
| 191 |
+
joytag_model.eval()
|
| 192 |
+
joytag_tags = [line.strip() for line in (Path(path) / "top_tags.txt").read_text(encoding="utf-8").splitlines() if line.strip()]
|
| 193 |
+
print("JoyTag loaded!")
|
| 194 |
+
return "✅ JoyTag loaded (~0.4GB VRAM)."
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
def unload_joytag() -> str:
|
| 198 |
+
global joytag_model, joytag_tags
|
| 199 |
+
if joytag_model is None:
|
| 200 |
+
return "⚠️ JoyTag is not currently loaded."
|
| 201 |
+
joytag_model = None
|
| 202 |
+
joytag_tags = None
|
| 203 |
+
gc.collect()
|
| 204 |
+
torch.cuda.empty_cache()
|
| 205 |
+
return "✅ JoyTag unloaded from VRAM."
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def prepare_joytag_image(image: Image.Image, target_size: int) -> torch.Tensor:
|
| 209 |
+
import torchvision.transforms.functional as TVF
|
| 210 |
+
# Pad to square on white, resize, CLIP-normalize (mirrors the official space)
|
| 211 |
+
max_dim = max(image.size)
|
| 212 |
+
padded = Image.new("RGB", (max_dim, max_dim), (255, 255, 255))
|
| 213 |
+
padded.paste(image, ((max_dim - image.size[0]) // 2, (max_dim - image.size[1]) // 2))
|
| 214 |
+
if max_dim != target_size:
|
| 215 |
+
padded = padded.resize((target_size, target_size), Image.BICUBIC)
|
| 216 |
+
tensor = TVF.pil_to_tensor(padded) / 255.0
|
| 217 |
+
return TVF.normalize(tensor, mean=[0.48145466, 0.4578275, 0.40821073], std=[0.26862954, 0.26130258, 0.27577711])
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
@torch.no_grad()
|
| 221 |
+
def joytag_predict(image: Image.Image, threshold: float) -> tuple[list[tuple[str, float]], dict[str, float]]:
|
| 222 |
+
"""Returns (tags over threshold sorted by confidence, all scores)."""
|
| 223 |
+
if joytag_model is None:
|
| 224 |
+
load_joytag()
|
| 225 |
+
device = next(joytag_model.parameters()).device
|
| 226 |
+
tensor = prepare_joytag_image(image.convert("RGB"), joytag_model.image_size).unsqueeze(0).to(device)
|
| 227 |
+
with torch.autocast(device.type, enabled=True):
|
| 228 |
+
preds = joytag_model({"image": tensor})
|
| 229 |
+
tag_preds = preds["tags"].sigmoid().float().cpu()[0]
|
| 230 |
+
scores = {joytag_tags[i]: tag_preds[i].item() for i in range(len(joytag_tags))}
|
| 231 |
+
predicted = sorted(((t, s) for t, s in scores.items() if s > threshold), key=lambda x: -x[1])
|
| 232 |
+
return predicted, scores
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def format_joytag_tags(predicted: list[tuple[str, float]], spaces: bool) -> str:
|
| 236 |
+
tags = [t for t, _ in predicted]
|
| 237 |
+
if spaces:
|
| 238 |
+
tags = [t.replace("_", " ") for t in tags]
|
| 239 |
+
return ", ".join(tags)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
def joytag_single(input_image: Image.Image, threshold: float, spaces: bool):
|
| 243 |
+
if input_image is None:
|
| 244 |
+
return "Please upload an image first.", ""
|
| 245 |
+
predicted, scores = joytag_predict(input_image, threshold)
|
| 246 |
+
top = sorted(scores.items(), key=lambda x: -x[1])[:50]
|
| 247 |
+
score_text = "\n".join(f"{s:.3f} {t}" for t, s in top)
|
| 248 |
+
return format_joytag_tags(predicted, spaces), score_text
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def joytag_batch(
|
| 252 |
+
folder: str,
|
| 253 |
+
threshold: float,
|
| 254 |
+
spaces: bool,
|
| 255 |
+
skip_existing: bool,
|
| 256 |
+
prefix: str,
|
| 257 |
+
progress=gr.Progress(),
|
| 258 |
+
) -> Generator[str, None, None]:
|
| 259 |
+
folder_path = Path(folder.strip().strip('"'))
|
| 260 |
+
if not folder_path.is_dir():
|
| 261 |
+
yield f"❌ Not a folder: {folder_path}"
|
| 262 |
+
return
|
| 263 |
+
if joytag_model is None:
|
| 264 |
+
yield "Loading JoyTag..."
|
| 265 |
+
load_joytag()
|
| 266 |
+
|
| 267 |
+
images = sorted(p for p in folder_path.iterdir() if p.suffix.lower() in IMAGE_EXTS)
|
| 268 |
+
if not images:
|
| 269 |
+
yield f"❌ No images found in {folder_path}"
|
| 270 |
+
return
|
| 271 |
+
|
| 272 |
+
log = []
|
| 273 |
+
done = skipped = failed = 0
|
| 274 |
+
for i, img_path in enumerate(progress.tqdm(images, desc="Tagging")):
|
| 275 |
+
txt_path = img_path.with_suffix(".txt")
|
| 276 |
+
if skip_existing and txt_path.exists():
|
| 277 |
+
skipped += 1
|
| 278 |
+
continue
|
| 279 |
+
try:
|
| 280 |
+
predicted, _ = joytag_predict(Image.open(img_path), threshold)
|
| 281 |
+
tag_string = format_joytag_tags(predicted, spaces)
|
| 282 |
+
if prefix.strip():
|
| 283 |
+
tag_string = prefix.strip() + " " + tag_string
|
| 284 |
+
txt_path.write_text(tag_string, encoding="utf-8")
|
| 285 |
+
done += 1
|
| 286 |
+
log.append(f"[{i + 1}/{len(images)}] {img_path.name} ✓ ({len(predicted)} tags)")
|
| 287 |
+
except Exception as e:
|
| 288 |
+
failed += 1
|
| 289 |
+
log.append(f"[{i + 1}/{len(images)}] {img_path.name} ❌ {e}")
|
| 290 |
+
yield "\n".join(log[-30:]) + f"\n\n— {done} tagged, {skipped} skipped, {failed} failed —"
|
| 291 |
+
|
| 292 |
+
yield "\n".join(log[-30:]) + f"\n\n✅ Finished: {done} tagged, {skipped} skipped, {failed} failed ({len(images)} images total)"
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
def build_prompt(caption_type: str, caption_length: str | int, extra_options: list[str], name_input: str) -> str:
|
| 296 |
+
if caption_length == "any":
|
| 297 |
+
map_idx = 0
|
| 298 |
+
elif isinstance(caption_length, str) and caption_length.isdigit():
|
| 299 |
+
map_idx = 1 # numeric word count template
|
| 300 |
+
else:
|
| 301 |
+
map_idx = 2 # length descriptor template
|
| 302 |
+
|
| 303 |
+
prompt = CAPTION_TYPE_MAP[caption_type][map_idx]
|
| 304 |
+
|
| 305 |
+
if extra_options:
|
| 306 |
+
prompt += " " + " ".join(extra_options)
|
| 307 |
+
|
| 308 |
+
return prompt.format(name=name_input, length=caption_length, word_count=caption_length)
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
def toggle_name_box(extra_options: list[str]):
|
| 312 |
+
return gr.update(visible=NAME_OPTION in extra_options)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
def prepare_inputs(input_image: Image.Image, prompt: str):
|
| 316 |
+
convo = [
|
| 317 |
+
{"role": "system", "content": "You are a helpful image captioner."},
|
| 318 |
+
{"role": "user", "content": prompt},
|
| 319 |
+
]
|
| 320 |
+
convo_string = processor.apply_chat_template(convo, tokenize=False, add_generation_prompt=True)
|
| 321 |
+
inputs = processor(text=[convo_string], images=[input_image.convert("RGB")], return_tensors="pt").to(model.device)
|
| 322 |
+
inputs["pixel_values"] = inputs["pixel_values"].to(torch.bfloat16)
|
| 323 |
+
return inputs
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
def generation_kwargs(temperature: float, top_p: float, max_new_tokens: int) -> dict:
|
| 327 |
+
return dict(
|
| 328 |
+
max_new_tokens=int(max_new_tokens),
|
| 329 |
+
do_sample=temperature > 0,
|
| 330 |
+
suppress_tokens=None,
|
| 331 |
+
use_cache=True,
|
| 332 |
+
temperature=temperature if temperature > 0 else None,
|
| 333 |
+
top_k=None,
|
| 334 |
+
top_p=top_p if temperature > 0 else None,
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
@torch.no_grad()
|
| 339 |
+
def chat_joycaption(
|
| 340 |
+
input_image: Image.Image,
|
| 341 |
+
prompt: str,
|
| 342 |
+
temperature: float,
|
| 343 |
+
top_p: float,
|
| 344 |
+
max_new_tokens: int,
|
| 345 |
+
) -> Generator[str, None, None]:
|
| 346 |
+
if input_image is None:
|
| 347 |
+
yield "Please upload an image first."
|
| 348 |
+
return
|
| 349 |
+
if model is None:
|
| 350 |
+
load_model()
|
| 351 |
+
|
| 352 |
+
torch.cuda.empty_cache()
|
| 353 |
+
inputs = prepare_inputs(input_image, prompt)
|
| 354 |
+
|
| 355 |
+
streamer = TextIteratorStreamer(processor.tokenizer, skip_prompt=True, skip_special_tokens=True)
|
| 356 |
+
gen_kwargs = dict(**inputs, streamer=streamer, **generation_kwargs(temperature, top_p, max_new_tokens))
|
| 357 |
+
|
| 358 |
+
thread = Thread(target=model.generate, kwargs=gen_kwargs)
|
| 359 |
+
thread.start()
|
| 360 |
+
|
| 361 |
+
output = ""
|
| 362 |
+
for new_text in streamer:
|
| 363 |
+
output += new_text
|
| 364 |
+
yield output.strip()
|
| 365 |
+
|
| 366 |
+
thread.join()
|
| 367 |
+
|
| 368 |
+
|
| 369 |
+
@torch.no_grad()
|
| 370 |
+
def caption_single(input_image: Image.Image, prompt: str, temperature: float, top_p: float, max_new_tokens: int) -> str:
|
| 371 |
+
inputs = prepare_inputs(input_image, prompt)
|
| 372 |
+
generate_ids = model.generate(**inputs, **generation_kwargs(temperature, top_p, max_new_tokens))[0]
|
| 373 |
+
generate_ids = generate_ids[inputs["input_ids"].shape[1]:]
|
| 374 |
+
return processor.tokenizer.decode(generate_ids, skip_special_tokens=True).strip()
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def batch_caption(
|
| 378 |
+
folder: str,
|
| 379 |
+
prompt: str,
|
| 380 |
+
temperature: float,
|
| 381 |
+
top_p: float,
|
| 382 |
+
max_new_tokens: int,
|
| 383 |
+
skip_existing: bool,
|
| 384 |
+
prefix: str,
|
| 385 |
+
progress=gr.Progress(),
|
| 386 |
+
) -> Generator[str, None, None]:
|
| 387 |
+
folder_path = Path(folder.strip().strip('"'))
|
| 388 |
+
if not folder_path.is_dir():
|
| 389 |
+
yield f"❌ Not a folder: {folder_path}"
|
| 390 |
+
return
|
| 391 |
+
if model is None:
|
| 392 |
+
yield "Loading model..."
|
| 393 |
+
load_model()
|
| 394 |
+
|
| 395 |
+
images = sorted(p for p in folder_path.iterdir() if p.suffix.lower() in IMAGE_EXTS)
|
| 396 |
+
if not images:
|
| 397 |
+
yield f"❌ No images found in {folder_path}"
|
| 398 |
+
return
|
| 399 |
+
|
| 400 |
+
log = []
|
| 401 |
+
done = skipped = failed = 0
|
| 402 |
+
for i, img_path in enumerate(progress.tqdm(images, desc="Captioning")):
|
| 403 |
+
txt_path = img_path.with_suffix(".txt")
|
| 404 |
+
if skip_existing and txt_path.exists():
|
| 405 |
+
skipped += 1
|
| 406 |
+
continue
|
| 407 |
+
try:
|
| 408 |
+
image = Image.open(img_path)
|
| 409 |
+
caption = caption_single(image, prompt, temperature, top_p, max_new_tokens)
|
| 410 |
+
if prefix.strip():
|
| 411 |
+
caption = prefix.strip() + " " + caption
|
| 412 |
+
txt_path.write_text(caption, encoding="utf-8")
|
| 413 |
+
done += 1
|
| 414 |
+
log.append(f"[{i + 1}/{len(images)}] {img_path.name} ✓")
|
| 415 |
+
except Exception as e:
|
| 416 |
+
failed += 1
|
| 417 |
+
log.append(f"[{i + 1}/{len(images)}] {img_path.name} ❌ {e}")
|
| 418 |
+
yield "\n".join(log[-30:]) + f"\n\n— {done} captioned, {skipped} skipped, {failed} failed —"
|
| 419 |
+
|
| 420 |
+
yield "\n".join(log[-30:]) + f"\n\n✅ Finished: {done} captioned, {skipped} skipped, {failed} failed ({len(images)} images total)"
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
# ─── Gradio UI ───────────────────────────────────────────────────────────────
|
| 424 |
+
|
| 425 |
+
# gradio 6.12's tab bar moves tabs that don't fit into an "overflow" dropdown, and
|
| 426 |
+
# selecting a tab from that dropdown hangs the page in a layout loop. Letting the tab
|
| 427 |
+
# bar wrap keeps scrollWidth <= clientWidth so the overflow logic never engages.
|
| 428 |
+
TAB_OVERFLOW_FIX_CSS = """
|
| 429 |
+
.tab-container { flex-wrap: wrap !important; }
|
| 430 |
+
"""
|
| 431 |
+
|
| 432 |
+
with gr.Blocks(title="JoyCaption Batch WebUI", css=TAB_OVERFLOW_FIX_CSS) as demo:
|
| 433 |
+
gr.HTML(TITLE)
|
| 434 |
+
|
| 435 |
+
with gr.Row():
|
| 436 |
+
load_btn = gr.Button("⚡ Load Model", variant="primary")
|
| 437 |
+
unload_btn = gr.Button("🗑️ Unload Model from VRAM", variant="stop")
|
| 438 |
+
model_status = gr.Textbox(label="Model Status", value="Model loads automatically on first caption.", interactive=False)
|
| 439 |
+
load_btn.click(load_model, outputs=model_status)
|
| 440 |
+
unload_btn.click(unload_model, outputs=model_status)
|
| 441 |
+
|
| 442 |
+
# Shared prompt controls
|
| 443 |
+
with gr.Row():
|
| 444 |
+
with gr.Column():
|
| 445 |
+
caption_type = gr.Dropdown(
|
| 446 |
+
choices=list(CAPTION_TYPE_MAP.keys()),
|
| 447 |
+
label="Caption Type",
|
| 448 |
+
value="Descriptive",
|
| 449 |
+
)
|
| 450 |
+
caption_length = gr.Dropdown(
|
| 451 |
+
choices=["any", "very short", "short", "medium-length", "long", "very long"]
|
| 452 |
+
+ [str(i) for i in range(20, 261, 10)],
|
| 453 |
+
label="Caption Length",
|
| 454 |
+
value="long",
|
| 455 |
+
)
|
| 456 |
+
with gr.Accordion("Extra Options", open=False):
|
| 457 |
+
extra_options = gr.CheckboxGroup(
|
| 458 |
+
choices=[NAME_OPTION] + EXTRA_OPTIONS_LIST,
|
| 459 |
+
label="Extra Options",
|
| 460 |
+
)
|
| 461 |
+
name_input = gr.Textbox(
|
| 462 |
+
label="Person/Character Name (if applicable)",
|
| 463 |
+
visible=False,
|
| 464 |
+
)
|
| 465 |
+
with gr.Accordion("Generation Settings", open=False):
|
| 466 |
+
temperature_slider = gr.Slider(minimum=0.0, maximum=2.0, value=0.6, step=0.05, label="Temperature")
|
| 467 |
+
top_p_slider = gr.Slider(minimum=0.0, maximum=1.0, value=0.9, step=0.01, label="Top-p")
|
| 468 |
+
max_tokens_slider = gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max New Tokens")
|
| 469 |
+
|
| 470 |
+
with gr.Column():
|
| 471 |
+
# Initial value set here directly: a demo.load() handler for this hangs the
|
| 472 |
+
# page when switching to a third tab (gradio 6.12 bug, see repro notes).
|
| 473 |
+
prompt_box = gr.Textbox(
|
| 474 |
+
lines=4, label="Prompt", interactive=True,
|
| 475 |
+
value=build_prompt("Descriptive", "long", [], ""),
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
extra_options.change(toggle_name_box, inputs=extra_options, outputs=name_input)
|
| 479 |
+
for ctrl in (caption_type, caption_length, extra_options, name_input):
|
| 480 |
+
ctrl.change(
|
| 481 |
+
build_prompt,
|
| 482 |
+
inputs=[caption_type, caption_length, extra_options, name_input],
|
| 483 |
+
outputs=prompt_box,
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
with gr.Tab("Single Image"):
|
| 487 |
+
with gr.Row():
|
| 488 |
+
with gr.Column():
|
| 489 |
+
input_image = gr.Image(type="pil", label="Input Image")
|
| 490 |
+
run_button = gr.Button("Caption", variant="primary")
|
| 491 |
+
with gr.Column():
|
| 492 |
+
output_caption = gr.Textbox(label="Caption", lines=10)
|
| 493 |
+
copy_btn = gr.Button("📋 Copy to Clipboard")
|
| 494 |
+
copy_btn.click(None, output_caption, None, js="(text) => navigator.clipboard.writeText(text)")
|
| 495 |
+
|
| 496 |
+
run_button.click(
|
| 497 |
+
chat_joycaption,
|
| 498 |
+
inputs=[input_image, prompt_box, temperature_slider, top_p_slider, max_tokens_slider],
|
| 499 |
+
outputs=output_caption,
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
with gr.Tab("Batch Folder"):
|
| 503 |
+
gr.Markdown("Dataset captioning: captions every image in a folder and writes a `.txt` with the same name next to each image (kohya/diffusion-pipe style).")
|
| 504 |
+
folder_box = gr.Textbox(label="Folder Path", placeholder=r"C:\path\to\my_dataset")
|
| 505 |
+
with gr.Row():
|
| 506 |
+
skip_existing_cb = gr.Checkbox(value=True, label="Skip images that already have a .txt")
|
| 507 |
+
prefix_box = gr.Textbox(label="Caption Prefix (optional, e.g. trigger word)", placeholder="myTriggerWord,")
|
| 508 |
+
batch_button = gr.Button("Caption Folder", variant="primary")
|
| 509 |
+
batch_log = gr.Textbox(label="Progress", lines=15)
|
| 510 |
+
|
| 511 |
+
batch_button.click(
|
| 512 |
+
batch_caption,
|
| 513 |
+
inputs=[folder_box, prompt_box, temperature_slider, top_p_slider, max_tokens_slider, skip_existing_cb, prefix_box],
|
| 514 |
+
outputs=batch_log,
|
| 515 |
+
)
|
| 516 |
+
|
| 517 |
+
with gr.Tab("JoyTag"):
|
| 518 |
+
gr.Markdown(
|
| 519 |
+
"[JoyTag](https://huggingface.co/fancyfeast/joytag) is a small ViT Danbooru tagger by the same author — "
|
| 520 |
+
"near-instant per image, ~0.4GB VRAM, loads independently of the JoyCaption model. "
|
| 521 |
+
"It loads automatically on first use."
|
| 522 |
+
)
|
| 523 |
+
with gr.Row():
|
| 524 |
+
joytag_load_btn = gr.Button("⚡ Load JoyTag")
|
| 525 |
+
joytag_unload_btn = gr.Button("🗑️ Unload JoyTag", variant="stop")
|
| 526 |
+
joytag_status = gr.Textbox(label="JoyTag Status", value="JoyTag loads automatically on first use.", interactive=False)
|
| 527 |
+
joytag_load_btn.click(load_joytag, outputs=joytag_status)
|
| 528 |
+
joytag_unload_btn.click(unload_joytag, outputs=joytag_status)
|
| 529 |
+
|
| 530 |
+
with gr.Row():
|
| 531 |
+
joytag_threshold = gr.Slider(minimum=0.05, maximum=0.95, value=0.4, step=0.05, label="Tag Threshold (lower = more tags)")
|
| 532 |
+
joytag_spaces_cb = gr.Checkbox(value=False, label="Replace underscores with spaces")
|
| 533 |
+
|
| 534 |
+
with gr.Row():
|
| 535 |
+
with gr.Column():
|
| 536 |
+
joytag_image = gr.Image(type="pil", label="Input Image")
|
| 537 |
+
joytag_run_btn = gr.Button("Tag", variant="primary")
|
| 538 |
+
with gr.Column():
|
| 539 |
+
joytag_output = gr.Textbox(label="Tags", lines=6)
|
| 540 |
+
# NOTE: gr.Label hangs the page on tab switch in gradio 6.12 — use a textbox
|
| 541 |
+
joytag_scores = gr.Textbox(label="Tag Confidences (top 50)", lines=10)
|
| 542 |
+
joytag_run_btn.click(
|
| 543 |
+
joytag_single,
|
| 544 |
+
inputs=[joytag_image, joytag_threshold, joytag_spaces_cb],
|
| 545 |
+
outputs=[joytag_output, joytag_scores],
|
| 546 |
+
)
|
| 547 |
+
|
| 548 |
+
with gr.Accordion("Batch Folder (dataset tagging)", open=False):
|
| 549 |
+
gr.Markdown("Tags every image in a folder and writes a `.txt` with the same name next to each image.")
|
| 550 |
+
joytag_folder_box = gr.Textbox(label="Folder Path", placeholder=r"C:\path\to\my_dataset")
|
| 551 |
+
with gr.Row():
|
| 552 |
+
joytag_skip_cb = gr.Checkbox(value=True, label="Skip images that already have a .txt")
|
| 553 |
+
joytag_prefix_box = gr.Textbox(label="Tag Prefix (optional, e.g. trigger word)", placeholder="myTriggerWord,")
|
| 554 |
+
joytag_batch_btn = gr.Button("Tag Folder", variant="primary")
|
| 555 |
+
joytag_batch_log = gr.Textbox(label="Progress", lines=15)
|
| 556 |
+
joytag_batch_btn.click(
|
| 557 |
+
joytag_batch,
|
| 558 |
+
inputs=[joytag_folder_box, joytag_threshold, joytag_spaces_cb, joytag_skip_cb, joytag_prefix_box],
|
| 559 |
+
outputs=joytag_batch_log,
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
if __name__ == "__main__":
|
| 563 |
+
if ON_SPACES:
|
| 564 |
+
# On HF Spaces let gradio pick up the Spaces defaults (0.0.0.0:7860).
|
| 565 |
+
demo.launch()
|
| 566 |
+
else:
|
| 567 |
+
demo.launch(
|
| 568 |
+
server_name="0.0.0.0" if args.listen else "127.0.0.1",
|
| 569 |
+
server_port=args.port,
|
| 570 |
+
inbrowser=not args.no_browser,
|
| 571 |
+
)
|
install_linux.sh
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
# One-time setup: creates a venv and installs dependencies (CUDA 12.4 wheels).
|
| 3 |
+
set -e
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
python3 -m venv venv
|
| 6 |
+
source venv/bin/activate
|
| 7 |
+
python -m pip install --upgrade pip
|
| 8 |
+
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
|
| 9 |
+
pip install -r requirements.txt
|
| 10 |
+
echo
|
| 11 |
+
echo "Done. Run: ./start_linux.sh (add --nf4 for ~7GB VRAM mode)"
|
install_windows.bat
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM One-time setup: creates a venv and installs dependencies (CUDA 12.4 wheels).
|
| 3 |
+
cd /d %~dp0
|
| 4 |
+
where python >nul 2>nul || (echo Python not found on PATH. Install Python 3.10+ first. & pause & exit /b 1)
|
| 5 |
+
python -m venv venv
|
| 6 |
+
call venv\Scripts\activate
|
| 7 |
+
python -m pip install --upgrade pip
|
| 8 |
+
pip install torch==2.6.0 torchvision==0.21.0 --index-url https://download.pytorch.org/whl/cu124
|
| 9 |
+
pip install -r requirements.txt
|
| 10 |
+
echo.
|
| 11 |
+
echo Done. Use start_windows.bat (24GB GPU) or start_windows_lowvram.bat (8GB+ GPU).
|
| 12 |
+
pause
|
joytag_models.py
ADDED
|
@@ -0,0 +1,1163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# JoyTag model code by fancyfeast (fpgaminer), Apache-2.0.
|
| 2 |
+
# Source: Models.py from https://huggingface.co/spaces/fancyfeast/joytag
|
| 3 |
+
# Project: https://github.com/fpgaminer/joytag
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Optional
|
| 8 |
+
import torch
|
| 9 |
+
import torch.backends.cuda
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
import torch.nn.functional as F
|
| 12 |
+
import torchvision
|
| 13 |
+
|
| 14 |
+
from transformers.activations import QuickGELUActivation
|
| 15 |
+
import math
|
| 16 |
+
from einops.layers.torch import Rearrange
|
| 17 |
+
import einops
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
MODEL_CONFIGS = {
|
| 21 |
+
# Custom models trained from scratch
|
| 22 |
+
# "Standard" definitions:
|
| 23 |
+
# name | layers | width | heads
|
| 24 |
+
# B | 12 | 768 | 12
|
| 25 |
+
# L | 24 | 1024 | 16
|
| 26 |
+
# H | 32 | 1280 | 16
|
| 27 |
+
# G | 48 | 1664 | 16
|
| 28 |
+
# e | 56 | 1792 | 16
|
| 29 |
+
# 22 | 48 | 6144 | 48
|
| 30 |
+
|
| 31 |
+
# B/16, 224, PaLM, GELU
|
| 32 |
+
'CustomTest6': {
|
| 33 |
+
'class': 'CLIPLikeModel',
|
| 34 |
+
'embedding_dim': 768,
|
| 35 |
+
'num_attention_heads': 12,
|
| 36 |
+
'activation_cls': nn.GELU,
|
| 37 |
+
'num_channels': 3,
|
| 38 |
+
'patch_size': 16,
|
| 39 |
+
'use_palm_alt': True,
|
| 40 |
+
'num_layers': 12,
|
| 41 |
+
'use_mha_alt': False,
|
| 42 |
+
'good_dropout': False,
|
| 43 |
+
},
|
| 44 |
+
|
| 45 |
+
# GAP head + Sinusoidal positional embeddings + 448 image size
|
| 46 |
+
'CustomTest18': {
|
| 47 |
+
'class': 'CLIPLikeModel',
|
| 48 |
+
'embedding_dim': 768,
|
| 49 |
+
'num_attention_heads': 12,
|
| 50 |
+
'activation_cls': nn.GELU,
|
| 51 |
+
'num_channels': 3,
|
| 52 |
+
'patch_size': 16,
|
| 53 |
+
'use_palm_alt': True,
|
| 54 |
+
'num_layers': 12,
|
| 55 |
+
'use_mha_alt': False,
|
| 56 |
+
'good_dropout': False,
|
| 57 |
+
'use_gap_head': True,
|
| 58 |
+
'sine_positional_embeddings': True,
|
| 59 |
+
},
|
| 60 |
+
|
| 61 |
+
# SW Model + B/16 + ASL + 448 image size
|
| 62 |
+
# cutout_max_pct = 0
|
| 63 |
+
# mixup_alpha = 0.8
|
| 64 |
+
# noise_level = 2
|
| 65 |
+
# random_resize_method = true
|
| 66 |
+
# total_labels = 6549
|
| 67 |
+
'SWModel1': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': False},
|
| 68 |
+
|
| 69 |
+
# Sinusoidal positional embeddings
|
| 70 |
+
'SWModel2': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 71 |
+
|
| 72 |
+
# Sinusoidal positional embeddings + 224 image size + L/14
|
| 73 |
+
'SWModel3': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 74 |
+
|
| 75 |
+
# Sinusoidal positional embeddings + 224 image size + G/14
|
| 76 |
+
'SWModel4': {'class': 'ViT', 'num_blocks': 48, 'patch_size': 14, 'd_model': 1664, 'mlp_dim': 1664*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 77 |
+
|
| 78 |
+
# Sinusoidal positional embeddings + focal loss
|
| 79 |
+
'SWModel5': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 80 |
+
|
| 81 |
+
'SWModel6': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 82 |
+
|
| 83 |
+
'SWModel7': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 84 |
+
'SWModel8': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 85 |
+
'SWModel9': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 86 |
+
'SWModel10': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 87 |
+
'SWModel11': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0, 'use_sine': True},
|
| 88 |
+
|
| 89 |
+
# Trying head_mean_after
|
| 90 |
+
'SWModel12': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'head_mean_after': True},
|
| 91 |
+
|
| 92 |
+
# Fat boy
|
| 93 |
+
'SWModel13': {'class': 'ViT', 'num_blocks': 6, 'patch_size': 16, 'd_model': 1536, 'mlp_dim': 1536*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True},
|
| 94 |
+
|
| 95 |
+
# L/14
|
| 96 |
+
'SWModel14': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 97 |
+
'SWModel15': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-5, 'use_sine': True},
|
| 98 |
+
'SWModel16': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.10, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 99 |
+
'SWModel16f': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.10, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 100 |
+
'SWModel22': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 14, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.20, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 101 |
+
'SWModel25': {'class': 'ViT', 'num_blocks': 24, 'patch_size': 16, 'd_model': 1024, 'mlp_dim': 1024*4, 'num_heads': 16, 'stochdepth_rate': 0.15, 'layerscale_init': 1e-1, 'use_sine': True, 'cnn_stem': 'conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=1024;ln;relu;conv:c=1024,s=1,k=1,p=0'},
|
| 102 |
+
|
| 103 |
+
# CNN stem
|
| 104 |
+
'SWModel18': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;bn;relu;conv:c=128;bn;relu;conv:c=256;bn;relu;conv:c=512;bn;relu;conv:c=768,s=1,k=1'},
|
| 105 |
+
'SWModel19': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;bn;relu;conv:c=128;bn;relu;conv:c=128,s=1;bn;relu;conv:c=256;bn;relu;conv:c=256,s=1;bn;relu;conv:c=512;bn;relu;conv:c=768,s=1,k=1,p=0'},
|
| 106 |
+
'SWModel20': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
|
| 107 |
+
'SWModel21': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;gelu;conv:c=128;ln;gelu;conv:c=256;ln;gelu;conv:c=512;ln;gelu;conv:c=768,s=1,k=1,p=0'},
|
| 108 |
+
'SWModel23': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
|
| 109 |
+
'SWModel24': {'class': 'ViT', 'num_blocks': 12, 'patch_size': 16, 'd_model': 768, 'mlp_dim': 768*4, 'num_heads': 12, 'stochdepth_rate': 0.05, 'use_sine': True, 'cnn_stem': 'conv:c=64;ln;relu;conv:c=128;ln;relu;conv:c=256;ln;relu;conv:c=512;ln;relu;conv:c=768,s=1,k=1,p=0'},
|
| 110 |
+
|
| 111 |
+
# H/14
|
| 112 |
+
'SWModel17': {'class': 'ViT', 'num_blocks': 32, 'patch_size': 14, 'd_model': 1280, 'mlp_dim': 1280*4, 'num_heads': 16, 'stochdepth_rate': 0.05, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 113 |
+
'SWModel26': {'class': 'ViT', 'num_blocks': 32, 'patch_size': 14, 'd_model': 1280, 'mlp_dim': 1280*4, 'num_heads': 16, 'stochdepth_rate': 0.15, 'layerscale_init': 1e-1, 'use_sine': True},
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
class VisionModel(nn.Module):
|
| 118 |
+
image_size: int
|
| 119 |
+
n_tags: int
|
| 120 |
+
|
| 121 |
+
def __init__(self, image_size: int, n_tags: int):
|
| 122 |
+
super().__init__()
|
| 123 |
+
|
| 124 |
+
self.image_size = image_size
|
| 125 |
+
self.n_tags = n_tags
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
def load_model(path: Path | str, device: str | None = None) -> 'VisionModel':
|
| 129 |
+
"""
|
| 130 |
+
Load a model from a directory.
|
| 131 |
+
:param path: The directory containing the model.
|
| 132 |
+
:return: The model, the image size, and the number of tags.
|
| 133 |
+
"""
|
| 134 |
+
with open(Path(path) / 'config.json', 'r') as f:
|
| 135 |
+
config = json.load(f)
|
| 136 |
+
|
| 137 |
+
if (Path(path) / 'model.safetensors').exists():
|
| 138 |
+
from safetensors.torch import load_file
|
| 139 |
+
resume = load_file(Path(path) / 'model.safetensors', device='cpu')
|
| 140 |
+
else:
|
| 141 |
+
resume = torch.load(Path(path) / 'model.pt', map_location=torch.device('cpu'))['model']
|
| 142 |
+
|
| 143 |
+
model_classes = VisionModel.__subclasses__()
|
| 144 |
+
model_cls = next(cls for cls in model_classes if cls.__name__ == config['class'])
|
| 145 |
+
|
| 146 |
+
model = model_cls(**{k: v for k, v in config.items() if k != 'class'})
|
| 147 |
+
model.load(resume)
|
| 148 |
+
if device is not None:
|
| 149 |
+
model = model.to(device)
|
| 150 |
+
|
| 151 |
+
return model
|
| 152 |
+
|
| 153 |
+
@staticmethod
|
| 154 |
+
def from_config(config: dict) -> 'VisionModel':
|
| 155 |
+
model_classes = VisionModel.__subclasses__()
|
| 156 |
+
model_cls = next(cls for cls in model_classes if cls.__name__ == config['class'])
|
| 157 |
+
return model_cls(**{k: v for k, v in config.items() if k != 'class'})
|
| 158 |
+
|
| 159 |
+
def get_optimized_parameters(self, lr: float):
|
| 160 |
+
raise NotImplementedError
|
| 161 |
+
|
| 162 |
+
def save(self):
|
| 163 |
+
raise NotImplementedError
|
| 164 |
+
|
| 165 |
+
def load(self, state_dict):
|
| 166 |
+
raise NotImplementedError
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def basic_calculate_loss(preds: dict[str, torch.Tensor], batch: dict, pos_weight: torch.Tensor | None, loss_type: str):
|
| 170 |
+
def asl_helper(preds, target):
|
| 171 |
+
p = F.softmax(preds, dim=1)
|
| 172 |
+
xs_pos = p.clamp(min=1e-6)
|
| 173 |
+
xs_neg = (1 - p).clamp(min=1e-6)
|
| 174 |
+
|
| 175 |
+
los_pos = torch.log(torch.gather(xs_pos, 1, target.unsqueeze(1))).sum()
|
| 176 |
+
los_neg = torch.log(xs_neg)
|
| 177 |
+
los_neg = los_neg.sum() - torch.gather(los_neg, 1, target.unsqueeze(1)).sum()
|
| 178 |
+
loss = los_pos + los_neg
|
| 179 |
+
|
| 180 |
+
return -loss
|
| 181 |
+
|
| 182 |
+
if loss_type == "ce":
|
| 183 |
+
loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'])
|
| 184 |
+
elif loss_type == "weighted":
|
| 185 |
+
loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight)
|
| 186 |
+
elif loss_type == "focal":
|
| 187 |
+
gamma = 2
|
| 188 |
+
p = torch.sigmoid(preds['tags'])
|
| 189 |
+
ce_loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], reduction='none')
|
| 190 |
+
p_t = p * batch['tags'] + (1 - p) * (1 - batch['tags'])
|
| 191 |
+
loss = ce_loss * ((1 - p_t) ** gamma)
|
| 192 |
+
loss = loss.mean()
|
| 193 |
+
elif loss_type == "focal2":
|
| 194 |
+
gamma = 2
|
| 195 |
+
p = torch.sigmoid(preds['tags'])
|
| 196 |
+
ce_loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], reduction='none')
|
| 197 |
+
p_t = p * batch['tags'] + (1 - p) * (1 - batch['tags'])
|
| 198 |
+
loss = ce_loss * ((1 - p_t) ** gamma) * 256
|
| 199 |
+
loss = loss.mean()
|
| 200 |
+
elif loss_type == "asl":
|
| 201 |
+
p = torch.sigmoid(preds['tags'])
|
| 202 |
+
xs_pos = p
|
| 203 |
+
xs_neg = 1 - p
|
| 204 |
+
|
| 205 |
+
los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
|
| 206 |
+
los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
|
| 207 |
+
loss = los_pos + los_neg
|
| 208 |
+
loss = -loss.sum()
|
| 209 |
+
|
| 210 |
+
# Rating
|
| 211 |
+
loss = loss + asl_helper(preds['rating'], batch['rating'])
|
| 212 |
+
|
| 213 |
+
# Score
|
| 214 |
+
loss = loss + asl_helper(preds['score'], batch['score'])
|
| 215 |
+
elif loss_type == "asl2":
|
| 216 |
+
p = torch.sigmoid(preds['tags'])
|
| 217 |
+
xs_pos = p
|
| 218 |
+
xs_neg = 1 - p
|
| 219 |
+
|
| 220 |
+
los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
|
| 221 |
+
los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
|
| 222 |
+
loss = -los_pos - los_neg
|
| 223 |
+
loss = loss.sum()
|
| 224 |
+
elif loss_type == "asl3":
|
| 225 |
+
p = torch.sigmoid(preds['tags'])
|
| 226 |
+
xs_pos = p
|
| 227 |
+
xs_neg = 1 - p
|
| 228 |
+
|
| 229 |
+
los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
|
| 230 |
+
los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
|
| 231 |
+
loss = -los_pos - los_neg
|
| 232 |
+
loss = loss.mean()
|
| 233 |
+
elif loss_type == "asl4":
|
| 234 |
+
p = torch.sigmoid(preds['tags'])
|
| 235 |
+
xs_pos = p
|
| 236 |
+
xs_neg = 1 - p
|
| 237 |
+
|
| 238 |
+
los_pos = batch['tags'] * torch.log(xs_pos.clamp(min=1e-6))
|
| 239 |
+
los_neg = (1 - batch['tags']) * torch.log(xs_neg.clamp(min=1e-6))
|
| 240 |
+
loss = -los_pos - los_neg
|
| 241 |
+
loss = loss.mean() * 128
|
| 242 |
+
elif loss_type == "asl5":
|
| 243 |
+
loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 128
|
| 244 |
+
elif loss_type == "asl6":
|
| 245 |
+
loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 256
|
| 246 |
+
elif loss_type == "asl7":
|
| 247 |
+
loss = F.binary_cross_entropy_with_logits(preds['tags'], batch['tags'], pos_weight=pos_weight) * 2
|
| 248 |
+
else:
|
| 249 |
+
raise ValueError(f"Invalid loss type: {loss_type}")
|
| 250 |
+
|
| 251 |
+
return loss
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
class CLIPMlp(nn.Module):
|
| 255 |
+
def __init__(self, hidden_size: int, intermediate_size: int, activation_cls):
|
| 256 |
+
super().__init__()
|
| 257 |
+
self.activation_fn = activation_cls()
|
| 258 |
+
self.fc1 = nn.Linear(hidden_size, intermediate_size)
|
| 259 |
+
self.fc2 = nn.Linear(intermediate_size, hidden_size)
|
| 260 |
+
|
| 261 |
+
def forward(self, hidden_states: torch.Tensor):
|
| 262 |
+
hidden_states = self.fc1(hidden_states)
|
| 263 |
+
hidden_states = self.activation_fn(hidden_states)
|
| 264 |
+
hidden_states = self.fc2(hidden_states)
|
| 265 |
+
return hidden_states
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
class FastCLIPAttention2(nn.Module):
|
| 269 |
+
"""Fast Attention module for CLIP-like. This is NOT a drop-in replacement for CLIPAttention, since it adds additional flexibility. Mainly uses xformers."""
|
| 270 |
+
def __init__(self, hidden_size: int, out_dim: int, num_attention_heads: int, out_seq_len: Optional[int] = None, norm_qk: bool = False):
|
| 271 |
+
super().__init__()
|
| 272 |
+
self.out_seq_len = out_seq_len
|
| 273 |
+
self.embed_dim = hidden_size
|
| 274 |
+
self.out_dim = out_dim
|
| 275 |
+
self.norm_qk = norm_qk
|
| 276 |
+
self.num_heads = num_attention_heads
|
| 277 |
+
self.head_dim = hidden_size // num_attention_heads
|
| 278 |
+
assert self.head_dim * num_attention_heads == self.embed_dim, "embed_dim must be divisible by num_attention_heads"
|
| 279 |
+
|
| 280 |
+
self.q_proj = nn.Linear(self.embed_dim, self.embed_dim)
|
| 281 |
+
self.kv_proj = nn.Linear(self.embed_dim, self.embed_dim * 2)
|
| 282 |
+
self.out_proj = nn.Linear(self.embed_dim, self.out_dim)
|
| 283 |
+
|
| 284 |
+
if self.norm_qk:
|
| 285 |
+
self.query_norm = nn.LayerNorm(self.embed_dim)
|
| 286 |
+
self.key_norm = nn.LayerNorm(self.embed_dim)
|
| 287 |
+
|
| 288 |
+
#def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):
|
| 289 |
+
# return tensor.view(bsz, seq_len, self.num_heads, self.head_dim).contiguous()
|
| 290 |
+
|
| 291 |
+
def forward(self, query_states: torch.Tensor, kv_states: torch.Tensor) -> torch.Tensor:
|
| 292 |
+
bsz, src_len, embed_dim = kv_states.size()
|
| 293 |
+
if self.out_seq_len is not None:
|
| 294 |
+
tgt_len = self.out_seq_len
|
| 295 |
+
else:
|
| 296 |
+
tgt_len = src_len
|
| 297 |
+
|
| 298 |
+
kv_states = self.kv_proj(kv_states) # (bsz, src_len, embed_dim * 2)
|
| 299 |
+
q_states = self.q_proj(query_states[:, :tgt_len]) # (bsz, tgt_len, embed_dim)
|
| 300 |
+
|
| 301 |
+
# NOTE: It is not clear if LayerNorm should be applied to the embed_dim, or to the head_dim
|
| 302 |
+
if self.norm_qk:
|
| 303 |
+
q_states = self.query_norm(q_states).type(q_states.dtype)
|
| 304 |
+
k_states = self.key_norm(kv_states[:, :, :embed_dim]).type(kv_states.dtype)
|
| 305 |
+
v_states = kv_states[:, :, embed_dim:]
|
| 306 |
+
else:
|
| 307 |
+
k_states = kv_states[:, :, :embed_dim]
|
| 308 |
+
v_states = kv_states[:, :, embed_dim:]
|
| 309 |
+
|
| 310 |
+
q_states = q_states.view(bsz, tgt_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, tgt_len, head_dim)
|
| 311 |
+
k_states = k_states.view(bsz, src_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, src_len, head_dim)
|
| 312 |
+
v_states = v_states.view(bsz, src_len, self.num_heads, self.head_dim).transpose(1, 2) # (bsz, num_heads, src_len, head_dim)
|
| 313 |
+
|
| 314 |
+
# Performs scale of query_states, attention, and softmax
|
| 315 |
+
with torch.backends.cuda.sdp_kernel(enable_math=False):
|
| 316 |
+
x = F.scaled_dot_product_attention(q_states, k_states, v_states) # (bsz, num_heads, tgt_len, head_dim)
|
| 317 |
+
x = x.transpose(1, 2).contiguous().view(bsz, tgt_len, embed_dim) # (bsz, tgt_len, embed_dim)
|
| 318 |
+
|
| 319 |
+
# Projection
|
| 320 |
+
x = self.out_proj(x) # (bsz, tgt_len, out_dim)
|
| 321 |
+
|
| 322 |
+
return x
|
| 323 |
+
|
| 324 |
+
|
| 325 |
+
class SkipInit(nn.Module):
|
| 326 |
+
def __init__(self, hidden_size: int, channel_wise: bool, init_scale: float):
|
| 327 |
+
super().__init__()
|
| 328 |
+
self.hidden_size = hidden_size
|
| 329 |
+
self.channel_wise = channel_wise
|
| 330 |
+
self.init_scale = init_scale
|
| 331 |
+
|
| 332 |
+
if self.channel_wise:
|
| 333 |
+
self.scale = nn.Parameter(torch.ones(hidden_size) * init_scale)
|
| 334 |
+
else:
|
| 335 |
+
self.scale = nn.Parameter(torch.tensor(init_scale))
|
| 336 |
+
|
| 337 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 338 |
+
return x * self.scale
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
class FastCLIPEncoderLayer(nn.Module):
|
| 342 |
+
def __init__(
|
| 343 |
+
self,
|
| 344 |
+
hidden_size: int,
|
| 345 |
+
num_attention_heads: int,
|
| 346 |
+
out_seq_len: Optional[int],
|
| 347 |
+
activation_cls = QuickGELUActivation,
|
| 348 |
+
use_palm_alt: bool = False,
|
| 349 |
+
norm_qk: bool = False,
|
| 350 |
+
skip_init: Optional[float] = None,
|
| 351 |
+
stochastic_depth: Optional[float] = None,
|
| 352 |
+
):
|
| 353 |
+
super().__init__()
|
| 354 |
+
|
| 355 |
+
self.use_palm_alt = use_palm_alt
|
| 356 |
+
self.stochastic_depth = stochastic_depth
|
| 357 |
+
|
| 358 |
+
self.self_attn = FastCLIPAttention2(
|
| 359 |
+
hidden_size=hidden_size,
|
| 360 |
+
out_dim=hidden_size,
|
| 361 |
+
num_attention_heads=num_attention_heads,
|
| 362 |
+
out_seq_len=out_seq_len,
|
| 363 |
+
norm_qk=norm_qk,
|
| 364 |
+
)
|
| 365 |
+
self.mlp = CLIPMlp(hidden_size, 4 * hidden_size, activation_cls)
|
| 366 |
+
self.layer_norm1 = nn.LayerNorm(hidden_size)
|
| 367 |
+
if not use_palm_alt:
|
| 368 |
+
self.layer_norm2 = nn.LayerNorm(hidden_size)
|
| 369 |
+
|
| 370 |
+
if skip_init is not None:
|
| 371 |
+
self.attn_skip_init = SkipInit(hidden_size, channel_wise=True, init_scale=skip_init)
|
| 372 |
+
self.mlp_skip_init = SkipInit(hidden_size, channel_wise=True, init_scale=skip_init)
|
| 373 |
+
else:
|
| 374 |
+
self.attn_skip_init = nn.Identity()
|
| 375 |
+
self.mlp_skip_init = nn.Identity()
|
| 376 |
+
|
| 377 |
+
def forward(self, hidden_states: torch.Tensor):
|
| 378 |
+
residual = hidden_states
|
| 379 |
+
hidden_states = self.layer_norm1(hidden_states)
|
| 380 |
+
|
| 381 |
+
if not self.use_palm_alt:
|
| 382 |
+
hidden_states = self.self_attn(query_states=hidden_states, kv_states=hidden_states)
|
| 383 |
+
hidden_states = self.attn_skip_init(hidden_states)
|
| 384 |
+
hidden_states = hidden_states + residual[:, :hidden_states.size(1)]
|
| 385 |
+
|
| 386 |
+
residual = hidden_states
|
| 387 |
+
hidden_states = self.layer_norm2(hidden_states)
|
| 388 |
+
hidden_states = self.mlp(hidden_states)
|
| 389 |
+
hidden_states = self.mlp_skip_init(hidden_states)
|
| 390 |
+
hidden_states = hidden_states + residual
|
| 391 |
+
else:
|
| 392 |
+
# An alternative implementation inspired by the PALM paper
|
| 393 |
+
# By performing the attention and MLP in parallel it's possible to fuse the linear projections of the attention and MLP layers
|
| 394 |
+
# We don't do that here yet, but that supposedly improves efficiency without hurting performance
|
| 395 |
+
attn = self.self_attn(query_states=hidden_states, kv_states=hidden_states)
|
| 396 |
+
attn = self.attn_skip_init(attn)
|
| 397 |
+
mlp = self.mlp(hidden_states[:, :attn.size(1)])
|
| 398 |
+
mlp = self.mlp_skip_init(mlp)
|
| 399 |
+
|
| 400 |
+
if self.stochastic_depth is not None:
|
| 401 |
+
attn = torchvision.ops.stochastic_depth(attn, self.stochastic_depth, mode='row', training=self.training)
|
| 402 |
+
mlp = torchvision.ops.stochastic_depth(mlp, self.stochastic_depth, mode='row', training=self.training)
|
| 403 |
+
|
| 404 |
+
hidden_states = residual[:, :attn.size(1)] + attn + mlp
|
| 405 |
+
|
| 406 |
+
return hidden_states
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def sinusoidal_position_embedding(width: int, height: int, depth: int, dtype, device, temperature = 10000):
|
| 410 |
+
"""
|
| 411 |
+
Sinusoidal position embedding. Returns a flat tensor of shape (h * w, d).
|
| 412 |
+
"""
|
| 413 |
+
assert depth % 4 == 0, "Embedding dimension must be divisible by 4."
|
| 414 |
+
|
| 415 |
+
y, x = torch.meshgrid(torch.arange(height, device=device), torch.arange(width, device=device), indexing="ij")
|
| 416 |
+
omega = torch.arange(depth // 4, device=device) / (depth // 4 - 1)
|
| 417 |
+
omega = 1. / (temperature ** omega)
|
| 418 |
+
|
| 419 |
+
y = y.flatten()[:, None] * omega[None, :]
|
| 420 |
+
x = x.flatten()[:, None] * omega[None, :]
|
| 421 |
+
embedding = torch.cat([x.sin(), x.cos(), y.sin(), y.cos()], dim=1)
|
| 422 |
+
|
| 423 |
+
return embedding.type(dtype)
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
class CLIPEmbeddingLayer(nn.Module):
|
| 427 |
+
def __init__(self, hidden_size: int, num_channels: int, image_size: int, patch_size: int, patch_dropout: float = 0.0, good_dropout: bool = False, dpn: bool = False, sine_positional_embeddings: bool = False):
|
| 428 |
+
super().__init__()
|
| 429 |
+
|
| 430 |
+
assert image_size % patch_size == 0, "Image dimensions must be divisible by the patch size."
|
| 431 |
+
|
| 432 |
+
seq_len = (image_size // patch_size) ** 2
|
| 433 |
+
self.patch_dropout = patch_dropout
|
| 434 |
+
self.hidden_size = hidden_size
|
| 435 |
+
self.good_dropout = good_dropout
|
| 436 |
+
self.dpn = dpn
|
| 437 |
+
self.sine_positional_embeddings = sine_positional_embeddings
|
| 438 |
+
self.patch_size = patch_size
|
| 439 |
+
|
| 440 |
+
self.patch_embeddings = nn.Conv2d(
|
| 441 |
+
in_channels=num_channels,
|
| 442 |
+
out_channels=hidden_size,
|
| 443 |
+
kernel_size=patch_size,
|
| 444 |
+
stride=patch_size,
|
| 445 |
+
bias=False,
|
| 446 |
+
)
|
| 447 |
+
if not self.sine_positional_embeddings:
|
| 448 |
+
self.positional_embeddings = nn.Embedding(seq_len, hidden_size)
|
| 449 |
+
self.register_buffer("position_ids", torch.arange(seq_len))
|
| 450 |
+
|
| 451 |
+
if self.dpn:
|
| 452 |
+
self.to_patch_embeddings = nn.Sequential(
|
| 453 |
+
Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=patch_size, p2=patch_size),
|
| 454 |
+
nn.LayerNorm(3 * patch_size * patch_size),
|
| 455 |
+
nn.Linear(3 * patch_size * patch_size, hidden_size),
|
| 456 |
+
nn.LayerNorm(hidden_size),
|
| 457 |
+
)
|
| 458 |
+
else:
|
| 459 |
+
self.to_patch_embeddings = nn.Conv2d(
|
| 460 |
+
in_channels=num_channels,
|
| 461 |
+
out_channels=hidden_size,
|
| 462 |
+
kernel_size=patch_size,
|
| 463 |
+
stride=patch_size,
|
| 464 |
+
bias=False,
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
def forward(self, pixel_values: torch.FloatTensor) -> torch.Tensor:
|
| 468 |
+
B, C, H, W = pixel_values.shape
|
| 469 |
+
assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
|
| 470 |
+
assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
|
| 471 |
+
|
| 472 |
+
if self.dpn:
|
| 473 |
+
patches = self.to_patch_embeddings(pixel_values)
|
| 474 |
+
else:
|
| 475 |
+
patches = self.to_patch_embeddings(pixel_values)
|
| 476 |
+
patches = patches.flatten(2).transpose(1, 2)
|
| 477 |
+
|
| 478 |
+
seq_len = patches.shape[1]
|
| 479 |
+
patch_dropout = int(math.ceil((1.0 - self.patch_dropout) * seq_len))
|
| 480 |
+
|
| 481 |
+
if self.sine_positional_embeddings:
|
| 482 |
+
position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.hidden_size, pixel_values.dtype, pixel_values.device)
|
| 483 |
+
else:
|
| 484 |
+
position_embeddings = self.positional_embeddings(self.position_ids)
|
| 485 |
+
|
| 486 |
+
if patch_dropout == seq_len or not self.training:
|
| 487 |
+
embeddings = patches + position_embeddings
|
| 488 |
+
elif self.good_dropout:
|
| 489 |
+
# Pick random patches to drop out
|
| 490 |
+
# The "good_dropout" variant uses random permutations for each batch item, but is slightly slower and involves more code
|
| 491 |
+
|
| 492 |
+
# The below method is a nice trick to generate a batch of random permutations.
|
| 493 |
+
# Torch (as of 1.13) doesn't have a built-in function to do this, and a for loop of torch.randperm is slow.
|
| 494 |
+
# Based on some benchmarks I measured the generation of the mask and the fetching to be only 50% slower than the non-"good_dropout" variant.
|
| 495 |
+
# And the time taken here is only a fraction of the time spent performing the embedding convolution.
|
| 496 |
+
# Generate a matrix of random numbers between 0 and 1 of shape (B, seq_len)
|
| 497 |
+
patch_mask = torch.rand(B, seq_len, device=patches.device)
|
| 498 |
+
# For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
|
| 499 |
+
patch_mask = torch.argsort(patch_mask, dim=1)
|
| 500 |
+
# Truncate
|
| 501 |
+
patch_mask = patch_mask[:, :patch_dropout]
|
| 502 |
+
|
| 503 |
+
embeddings = patches.gather(1, patch_mask.unsqueeze(-1).expand(-1, -1, self.hidden_size)) + position_embeddings[patch_mask]
|
| 504 |
+
else:
|
| 505 |
+
# The non-"good_dropout" variant uses a single random permutation for all batch items, but is faster and uses less code
|
| 506 |
+
indices = torch.randperm(seq_len, device=pixel_values.device)[:patch_dropout]
|
| 507 |
+
embeddings = patches[:, indices, :] + position_embeddings[indices.expand(1, -1)]
|
| 508 |
+
|
| 509 |
+
return embeddings
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
class MHAPoolingHead(nn.Module):
|
| 513 |
+
def __init__(self, hidden_size: int, num_attention_heads: int, activation_cls, out_dim: int, alt_style: bool, norm_qk: bool):
|
| 514 |
+
super().__init__()
|
| 515 |
+
|
| 516 |
+
self.out_dim = out_dim if not alt_style else hidden_size
|
| 517 |
+
|
| 518 |
+
self.probe = nn.Parameter(torch.randn(hidden_size))
|
| 519 |
+
|
| 520 |
+
self.mlp = CLIPMlp(hidden_size, 4 * hidden_size, activation_cls)
|
| 521 |
+
self.layer_norm = nn.LayerNorm(hidden_size)
|
| 522 |
+
self.pooling_head = nn.Linear(hidden_size, 1)
|
| 523 |
+
|
| 524 |
+
self.self_attn = FastCLIPAttention2(
|
| 525 |
+
hidden_size=hidden_size,
|
| 526 |
+
out_dim=self.out_dim,
|
| 527 |
+
num_attention_heads=num_attention_heads,
|
| 528 |
+
out_seq_len=1,
|
| 529 |
+
norm_qk=norm_qk,
|
| 530 |
+
)
|
| 531 |
+
self.mlp = CLIPMlp(self.out_dim, 4 * self.out_dim, activation_cls)
|
| 532 |
+
self.layer_norm1 = nn.LayerNorm(hidden_size)
|
| 533 |
+
self.layer_norm2 = nn.LayerNorm(self.out_dim)
|
| 534 |
+
|
| 535 |
+
if alt_style:
|
| 536 |
+
self.final_proj = nn.Linear(hidden_size, out_dim)
|
| 537 |
+
else:
|
| 538 |
+
self.final_proj = nn.Identity()
|
| 539 |
+
|
| 540 |
+
def forward(self, hidden_states: torch.Tensor):
|
| 541 |
+
hidden_states = self.layer_norm1(hidden_states)
|
| 542 |
+
query_states = self.probe.unsqueeze(0).unsqueeze(0).expand(hidden_states.size(0), 1, -1)
|
| 543 |
+
|
| 544 |
+
hidden_states = self.self_attn(query_states=query_states, kv_states=hidden_states)
|
| 545 |
+
# We don't use a residual connection here because the out_dim is different from the hidden_size
|
| 546 |
+
|
| 547 |
+
residual = hidden_states
|
| 548 |
+
hidden_states = self.layer_norm2(hidden_states)
|
| 549 |
+
hidden_states = self.mlp(hidden_states)
|
| 550 |
+
hidden_states = hidden_states + residual
|
| 551 |
+
hidden_states = self.final_proj(hidden_states)
|
| 552 |
+
|
| 553 |
+
return hidden_states.squeeze(1)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
class GAPHead(nn.Module):
|
| 557 |
+
def __init__(self, hidden_size: int, out_dim: int):
|
| 558 |
+
super().__init__()
|
| 559 |
+
|
| 560 |
+
self.norm = nn.LayerNorm(hidden_size)
|
| 561 |
+
self.proj = nn.Linear(hidden_size, out_dim)
|
| 562 |
+
|
| 563 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 564 |
+
x = x.mean(dim=1)
|
| 565 |
+
x = self.norm(x)
|
| 566 |
+
x = self.proj(x)
|
| 567 |
+
return x
|
| 568 |
+
|
| 569 |
+
|
| 570 |
+
class CLIPLikeModel(VisionModel):
|
| 571 |
+
def __init__(
|
| 572 |
+
self,
|
| 573 |
+
n_tags: int,
|
| 574 |
+
embedding_dim: int,
|
| 575 |
+
num_attention_heads: int,
|
| 576 |
+
activation_cls,
|
| 577 |
+
num_channels: int,
|
| 578 |
+
image_size: int,
|
| 579 |
+
patch_size: int,
|
| 580 |
+
patch_dropout: float,
|
| 581 |
+
use_palm_alt: bool,
|
| 582 |
+
num_layers: int,
|
| 583 |
+
use_mha_alt: bool,
|
| 584 |
+
loss_type: str,
|
| 585 |
+
good_dropout: bool=False,
|
| 586 |
+
dpn: bool=False,
|
| 587 |
+
sine_positional_embeddings: bool=False,
|
| 588 |
+
norm_qk: bool = False,
|
| 589 |
+
no_wd_bias: bool = False,
|
| 590 |
+
use_gap_head: bool = False,
|
| 591 |
+
skip_init: Optional[float] = None,
|
| 592 |
+
stochastic_depth: Optional[float] = None,
|
| 593 |
+
):
|
| 594 |
+
super().__init__(image_size, n_tags)
|
| 595 |
+
|
| 596 |
+
out_dim = n_tags
|
| 597 |
+
self.n_tags = n_tags
|
| 598 |
+
self.loss_type = loss_type
|
| 599 |
+
self.no_wd_bias = no_wd_bias
|
| 600 |
+
|
| 601 |
+
stochastic_depth_space = torch.linspace(0, stochastic_depth, num_layers) if stochastic_depth is not None else None
|
| 602 |
+
|
| 603 |
+
self.embedding_layer = CLIPEmbeddingLayer(embedding_dim, num_channels, image_size, patch_size, patch_dropout, good_dropout, dpn, sine_positional_embeddings)
|
| 604 |
+
self.pre_layer_norm = nn.LayerNorm(embedding_dim)
|
| 605 |
+
self.encoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
|
| 606 |
+
hidden_size=embedding_dim,
|
| 607 |
+
num_attention_heads=num_attention_heads,
|
| 608 |
+
out_seq_len=None,
|
| 609 |
+
activation_cls=activation_cls,
|
| 610 |
+
use_palm_alt=use_palm_alt,
|
| 611 |
+
norm_qk=norm_qk,
|
| 612 |
+
skip_init=skip_init,
|
| 613 |
+
stochastic_depth=stochastic_depth_space[i].item() if stochastic_depth_space is not None else None,
|
| 614 |
+
) for i in range(num_layers)])
|
| 615 |
+
|
| 616 |
+
if use_gap_head:
|
| 617 |
+
self.pooling_head = GAPHead(embedding_dim, out_dim)
|
| 618 |
+
else:
|
| 619 |
+
self.pooling_head = MHAPoolingHead(embedding_dim, num_attention_heads, activation_cls, out_dim, use_mha_alt, norm_qk=norm_qk)
|
| 620 |
+
|
| 621 |
+
def forward(self, batch):
|
| 622 |
+
hidden_states = self.embedding_layer(batch['image'])
|
| 623 |
+
hidden_states = self.pre_layer_norm(hidden_states)
|
| 624 |
+
|
| 625 |
+
for layer in self.encoder_layers:
|
| 626 |
+
hidden_states = layer(hidden_states)
|
| 627 |
+
|
| 628 |
+
preds = self.pooling_head(hidden_states)
|
| 629 |
+
|
| 630 |
+
result = {
|
| 631 |
+
'tags': preds,
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
return result
|
| 635 |
+
|
| 636 |
+
def calculate_loss(self, preds, batch, pos_weight):
|
| 637 |
+
return basic_calculate_loss(preds, batch, pos_weight, self.loss_type)
|
| 638 |
+
|
| 639 |
+
def get_optimized_parameters(self, lr: float):
|
| 640 |
+
if self.no_wd_bias:
|
| 641 |
+
return self.get_optimized_parameters_no_wd_bias()
|
| 642 |
+
else:
|
| 643 |
+
return self.parameters()
|
| 644 |
+
|
| 645 |
+
def get_optimized_parameters_no_wd_bias(self):
|
| 646 |
+
decay = []
|
| 647 |
+
no_decay = []
|
| 648 |
+
|
| 649 |
+
for name, param in self.named_parameters():
|
| 650 |
+
if not param.requires_grad:
|
| 651 |
+
continue
|
| 652 |
+
|
| 653 |
+
if len(param.shape) == 1 or name.endswith(".bias"):
|
| 654 |
+
no_decay.append(param)
|
| 655 |
+
print(f'No decay: {name}')
|
| 656 |
+
else:
|
| 657 |
+
decay.append(param)
|
| 658 |
+
|
| 659 |
+
return [
|
| 660 |
+
{'params': decay},
|
| 661 |
+
{'params': no_decay, 'weight_decay': 0.},
|
| 662 |
+
]
|
| 663 |
+
|
| 664 |
+
def save(self):
|
| 665 |
+
return self.state_dict()
|
| 666 |
+
|
| 667 |
+
def load(self, state_dict):
|
| 668 |
+
self.load_state_dict(state_dict)
|
| 669 |
+
|
| 670 |
+
|
| 671 |
+
class MaskedAutoEncoderViT(nn.Module):
|
| 672 |
+
def __init__(
|
| 673 |
+
self,
|
| 674 |
+
n_tags: int,
|
| 675 |
+
|
| 676 |
+
embedding_dim: int,
|
| 677 |
+
num_attention_heads: int,
|
| 678 |
+
activation_cls,
|
| 679 |
+
num_channels: int,
|
| 680 |
+
image_size: int,
|
| 681 |
+
patch_size: int,
|
| 682 |
+
num_layers: int,
|
| 683 |
+
loss_type: str,
|
| 684 |
+
sine_positional_embeddings: bool=False,
|
| 685 |
+
|
| 686 |
+
decoder_embedding_dim: int = 512,
|
| 687 |
+
decoder_num_attention_heads: int = 8,
|
| 688 |
+
decoder_num_layers: int = 6,
|
| 689 |
+
decoder_force_projection: bool = False,
|
| 690 |
+
|
| 691 |
+
masking_ratio: float = 0.75,
|
| 692 |
+
mae_loss_weight: float = 1.0,
|
| 693 |
+
mae_normalize_targets: bool = False,
|
| 694 |
+
mae_post_norm: bool = False,
|
| 695 |
+
):
|
| 696 |
+
super().__init__()
|
| 697 |
+
|
| 698 |
+
self.n_tags = n_tags
|
| 699 |
+
self.seq_len = (image_size // patch_size) ** 2
|
| 700 |
+
self.embedding_dim = embedding_dim
|
| 701 |
+
self.decoder_embedding_dim = decoder_embedding_dim
|
| 702 |
+
self.sine_positional_embeddings = sine_positional_embeddings
|
| 703 |
+
self.image_size = image_size
|
| 704 |
+
self.patch_size = patch_size
|
| 705 |
+
self.masking_ratio = masking_ratio
|
| 706 |
+
self.loss_type = loss_type
|
| 707 |
+
self.mae_loss_weight = mae_loss_weight
|
| 708 |
+
self.mae_normalize_targets = mae_normalize_targets
|
| 709 |
+
|
| 710 |
+
if not self.sine_positional_embeddings:
|
| 711 |
+
self.positional_embeddings = nn.Embedding(self.seq_len, embedding_dim)
|
| 712 |
+
self.decoder_positional_embeddings = nn.Embedding(self.seq_len, decoder_embedding_dim)
|
| 713 |
+
self.register_buffer("position_ids", torch.arange(self.seq_len))
|
| 714 |
+
|
| 715 |
+
self.to_patches = Rearrange('b c (h p1) (w p2) -> b (h w) (p1 p2 c)', p1=patch_size, p2=patch_size)
|
| 716 |
+
self.patch_embedder = nn.Linear(num_channels * patch_size * patch_size, embedding_dim)
|
| 717 |
+
|
| 718 |
+
# Encoder
|
| 719 |
+
self.pre_layer_norm = nn.LayerNorm(embedding_dim)
|
| 720 |
+
self.encoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
|
| 721 |
+
hidden_size=embedding_dim,
|
| 722 |
+
num_attention_heads=num_attention_heads,
|
| 723 |
+
out_seq_len=None,
|
| 724 |
+
activation_cls=activation_cls,
|
| 725 |
+
use_palm_alt=True,
|
| 726 |
+
norm_qk=False,
|
| 727 |
+
skip_init=None,
|
| 728 |
+
) for _ in range(num_layers)])
|
| 729 |
+
|
| 730 |
+
# Head for classification
|
| 731 |
+
self.pooling_head = GAPHead(embedding_dim, n_tags)
|
| 732 |
+
|
| 733 |
+
# Decoder
|
| 734 |
+
if embedding_dim != decoder_embedding_dim or decoder_force_projection:
|
| 735 |
+
self.encoder_to_decoder_proj = nn.Linear(embedding_dim, decoder_embedding_dim)
|
| 736 |
+
else:
|
| 737 |
+
self.encoder_to_decoder_proj = nn.Identity()
|
| 738 |
+
self.decoder_pre_layer_norm = nn.LayerNorm(decoder_embedding_dim)
|
| 739 |
+
self.decoder_layers = nn.ModuleList([FastCLIPEncoderLayer(
|
| 740 |
+
hidden_size=decoder_embedding_dim,
|
| 741 |
+
num_attention_heads=decoder_num_attention_heads,
|
| 742 |
+
out_seq_len=None,
|
| 743 |
+
activation_cls=activation_cls,
|
| 744 |
+
use_palm_alt=True,
|
| 745 |
+
norm_qk=False,
|
| 746 |
+
skip_init=None,
|
| 747 |
+
) for _ in range(decoder_num_layers)])
|
| 748 |
+
|
| 749 |
+
if mae_post_norm:
|
| 750 |
+
self.decoder_to_pixel_values = nn.Sequential(
|
| 751 |
+
nn.LayerNorm(decoder_embedding_dim),
|
| 752 |
+
nn.Linear(decoder_embedding_dim, num_channels * patch_size * patch_size)
|
| 753 |
+
)
|
| 754 |
+
else:
|
| 755 |
+
self.decoder_to_pixel_values = nn.Linear(decoder_embedding_dim, num_channels * patch_size * patch_size)
|
| 756 |
+
self.mask_token = nn.Parameter(torch.zeros(decoder_embedding_dim))
|
| 757 |
+
torch.nn.init.normal_(self.mask_token, std=0.02)
|
| 758 |
+
|
| 759 |
+
def forward(self, batch):
|
| 760 |
+
pixel_values = batch['image']
|
| 761 |
+
device = pixel_values.device
|
| 762 |
+
B, C, H, W = pixel_values.shape
|
| 763 |
+
assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
|
| 764 |
+
assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
|
| 765 |
+
|
| 766 |
+
# Convert image to patches (B, seq_len, C * patch_size * patch_size)
|
| 767 |
+
patches = self.to_patches(pixel_values)
|
| 768 |
+
seq_len = patches.shape[1]
|
| 769 |
+
num_masked = int(self.masking_ratio * seq_len)
|
| 770 |
+
|
| 771 |
+
# For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
|
| 772 |
+
# From this we can get the masked and unmasked indices
|
| 773 |
+
patch_mask = torch.rand(B, seq_len, device=device)
|
| 774 |
+
patch_mask = torch.argsort(patch_mask, dim=1)
|
| 775 |
+
masked_indices, unmasked_indices = patch_mask[:, :num_masked], patch_mask[:, num_masked:]
|
| 776 |
+
batch_range = torch.arange(B, device=device)[:, None]
|
| 777 |
+
|
| 778 |
+
# Masked and unmasked patches
|
| 779 |
+
unmasked_patches = patches[batch_range, unmasked_indices]
|
| 780 |
+
masked_patches = patches[batch_range, masked_indices]
|
| 781 |
+
|
| 782 |
+
# Embed unmasked patches for the encoder (B, seq_len, embedding_dim)
|
| 783 |
+
tokens = self.patch_embedder(unmasked_patches)
|
| 784 |
+
|
| 785 |
+
if self.sine_positional_embeddings:
|
| 786 |
+
position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.embedding_dim, pixel_values.dtype, device)
|
| 787 |
+
decoder_position_embeddings = sinusoidal_position_embedding(W // self.patch_size, H // self.patch_size, self.decoder_embedding_dim, pixel_values.dtype, device)
|
| 788 |
+
else:
|
| 789 |
+
position_embeddings = self.positional_embeddings(self.position_ids)
|
| 790 |
+
decoder_position_embeddings = self.decoder_positional_embeddings(self.position_ids)
|
| 791 |
+
|
| 792 |
+
# Add position embeddings
|
| 793 |
+
tokens = tokens + position_embeddings[unmasked_indices]
|
| 794 |
+
|
| 795 |
+
# Run the encoder
|
| 796 |
+
encoded_tokens = self.pre_layer_norm(tokens)
|
| 797 |
+
|
| 798 |
+
for layer in self.encoder_layers:
|
| 799 |
+
encoded_tokens = layer(encoded_tokens)
|
| 800 |
+
|
| 801 |
+
# Label predictions
|
| 802 |
+
if self.training:
|
| 803 |
+
preds = self.pooling_head(encoded_tokens)
|
| 804 |
+
else:
|
| 805 |
+
# During inference, classify using the entire image
|
| 806 |
+
# But we'll do the usual for the MAE part, just so we can see how MAE is performing during validation
|
| 807 |
+
tokens = self.patch_embedder(patches)
|
| 808 |
+
tokens = tokens + position_embeddings
|
| 809 |
+
tokens = self.pre_layer_norm(tokens)
|
| 810 |
+
for layer in self.encoder_layers:
|
| 811 |
+
tokens = layer(tokens)
|
| 812 |
+
preds = self.pooling_head(tokens)
|
| 813 |
+
|
| 814 |
+
# Projection for the decoder and position embeddings
|
| 815 |
+
decoder_tokens = self.encoder_to_decoder_proj(encoded_tokens)
|
| 816 |
+
decoder_tokens = decoder_tokens + decoder_position_embeddings[unmasked_indices]
|
| 817 |
+
|
| 818 |
+
# Fill in the masked patches
|
| 819 |
+
mask_tokens = einops.repeat(self.mask_token, 'd -> b n d', b = B, n = num_masked)
|
| 820 |
+
mask_tokens = mask_tokens + decoder_position_embeddings[masked_indices]
|
| 821 |
+
decoder_tokens = torch.cat([decoder_tokens, mask_tokens], dim=1)
|
| 822 |
+
|
| 823 |
+
# Run the decoder
|
| 824 |
+
decoded_tokens = self.decoder_pre_layer_norm(decoder_tokens)
|
| 825 |
+
|
| 826 |
+
for layer in self.decoder_layers:
|
| 827 |
+
decoded_tokens = layer(decoded_tokens)
|
| 828 |
+
|
| 829 |
+
# Only predict the masked patches
|
| 830 |
+
# All the masked patches are at the end of the sequence
|
| 831 |
+
decoded_tokens = decoded_tokens[:, -num_masked:]
|
| 832 |
+
pred_pixel_values = self.decoder_to_pixel_values(decoded_tokens)
|
| 833 |
+
|
| 834 |
+
# Calculate the mae loss
|
| 835 |
+
if self.mae_normalize_targets:
|
| 836 |
+
# Normalize each patch by its mean and variance. The ViCHA paper says this provides better results
|
| 837 |
+
means = masked_patches.mean(dim=-1, keepdim=True)
|
| 838 |
+
vars = masked_patches.var(dim=-1, keepdim=True)
|
| 839 |
+
target = (masked_patches - means) / (vars + 1e-6)**0.5
|
| 840 |
+
mae_loss = F.mse_loss(pred_pixel_values, target)
|
| 841 |
+
else:
|
| 842 |
+
mae_loss = F.mse_loss(pred_pixel_values, masked_patches)
|
| 843 |
+
mae_loss = mae_loss * self.mae_loss_weight
|
| 844 |
+
|
| 845 |
+
return {
|
| 846 |
+
'tags': preds,
|
| 847 |
+
'mae_loss': mae_loss,
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
def calculate_loss(self, preds, batch, pos_weight):
|
| 851 |
+
return basic_calculate_loss(preds, batch, pos_weight, self.loss_type) + preds['mae_loss']
|
| 852 |
+
|
| 853 |
+
def get_optimized_parameters(self, lr: float):
|
| 854 |
+
return self.parameters()
|
| 855 |
+
|
| 856 |
+
def save(self):
|
| 857 |
+
return self.state_dict()
|
| 858 |
+
|
| 859 |
+
def load(self, state_dict):
|
| 860 |
+
self.load_state_dict(state_dict)
|
| 861 |
+
|
| 862 |
+
|
| 863 |
+
class StochDepth(nn.Module):
|
| 864 |
+
def __init__(self, drop_rate: float, scale_by_keep: bool = False):
|
| 865 |
+
super().__init__()
|
| 866 |
+
self.drop_rate = drop_rate
|
| 867 |
+
self.scale_by_keep = scale_by_keep
|
| 868 |
+
|
| 869 |
+
def forward(self, x):
|
| 870 |
+
if not self.training:
|
| 871 |
+
return x
|
| 872 |
+
|
| 873 |
+
batch_size = x.shape[0]
|
| 874 |
+
r = torch.rand((batch_size, 1, 1), device=x.device)
|
| 875 |
+
keep_prob = 1 - self.drop_rate
|
| 876 |
+
binary_tensor = torch.floor(keep_prob + r)
|
| 877 |
+
if self.scale_by_keep:
|
| 878 |
+
x = x / keep_prob
|
| 879 |
+
|
| 880 |
+
return x * binary_tensor
|
| 881 |
+
|
| 882 |
+
|
| 883 |
+
class SkipInitChannelwise(nn.Module):
|
| 884 |
+
def __init__(self, channels, init_val=1e-6):
|
| 885 |
+
super().__init__()
|
| 886 |
+
self.channels = channels
|
| 887 |
+
self.init_val = init_val
|
| 888 |
+
self.skip = nn.Parameter(torch.ones(channels) * init_val)
|
| 889 |
+
|
| 890 |
+
def forward(self, x):
|
| 891 |
+
return x * self.skip
|
| 892 |
+
|
| 893 |
+
|
| 894 |
+
class PosEmbedding(nn.Module):
|
| 895 |
+
def __init__(self, d_model: int, max_len: int, use_sine: bool, patch_size: int):
|
| 896 |
+
super().__init__()
|
| 897 |
+
self.d_model = d_model
|
| 898 |
+
self.max_len = max_len
|
| 899 |
+
self.use_sine = use_sine
|
| 900 |
+
self.patch_size = patch_size
|
| 901 |
+
|
| 902 |
+
if not self.use_sine:
|
| 903 |
+
self.embedding = nn.Embedding(max_len, d_model)
|
| 904 |
+
nn.init.trunc_normal_(self.embedding.weight, std=0.02)
|
| 905 |
+
self.register_buffer("position_ids", torch.arange(max_len))
|
| 906 |
+
|
| 907 |
+
def forward(self, x, width: int, height: int):
|
| 908 |
+
if self.use_sine:
|
| 909 |
+
position_embeddings = sinusoidal_position_embedding(width // self.patch_size, height // self.patch_size, self.d_model, x.dtype, x.device)
|
| 910 |
+
else:
|
| 911 |
+
position_embeddings = self.embedding(self.position_ids)
|
| 912 |
+
|
| 913 |
+
return x + position_embeddings
|
| 914 |
+
|
| 915 |
+
|
| 916 |
+
class MLPBlock(nn.Module):
|
| 917 |
+
def __init__(self, d_model: int, d_ff: int, stochdepth_rate: float):
|
| 918 |
+
super().__init__()
|
| 919 |
+
self.linear1 = nn.Linear(d_model, d_ff)
|
| 920 |
+
self.linear2 = nn.Linear(d_ff, d_model)
|
| 921 |
+
self.activation = nn.GELU()
|
| 922 |
+
if stochdepth_rate > 0:
|
| 923 |
+
self.stochdepth = StochDepth(stochdepth_rate, scale_by_keep=True)
|
| 924 |
+
else:
|
| 925 |
+
self.stochdepth = None
|
| 926 |
+
|
| 927 |
+
def forward(self, x):
|
| 928 |
+
x = self.linear1(x)
|
| 929 |
+
x = self.activation(x)
|
| 930 |
+
if self.stochdepth is not None:
|
| 931 |
+
x = self.stochdepth(x)
|
| 932 |
+
x = self.linear2(x)
|
| 933 |
+
return x
|
| 934 |
+
|
| 935 |
+
|
| 936 |
+
class ViTBlock(nn.Module):
|
| 937 |
+
def __init__(self, num_heads: int, d_model: int, d_ff: int, layerscale_init: float, stochdepth_rate: float):
|
| 938 |
+
super().__init__()
|
| 939 |
+
self.num_heads = num_heads
|
| 940 |
+
self.d_model = d_model
|
| 941 |
+
|
| 942 |
+
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
|
| 943 |
+
|
| 944 |
+
# MHA
|
| 945 |
+
self.norm1 = nn.LayerNorm(d_model)
|
| 946 |
+
self.qkv_proj = nn.Linear(d_model, d_model * 3)
|
| 947 |
+
self.out_proj = nn.Linear(d_model, d_model)
|
| 948 |
+
self.skip_init1 = SkipInitChannelwise(channels=d_model, init_val=layerscale_init)
|
| 949 |
+
self.stochdepth1 = StochDepth(stochdepth_rate, scale_by_keep=True) if stochdepth_rate > 0 else None
|
| 950 |
+
|
| 951 |
+
# MLP
|
| 952 |
+
self.norm2 = nn.LayerNorm(d_model)
|
| 953 |
+
self.mlp = MLPBlock(d_model, d_ff, stochdepth_rate)
|
| 954 |
+
self.skip_init2 = SkipInitChannelwise(channels=d_model, init_val=layerscale_init)
|
| 955 |
+
self.stochdepth2 = StochDepth(stochdepth_rate, scale_by_keep=True) if stochdepth_rate > 0 else None
|
| 956 |
+
|
| 957 |
+
def forward(self, x):
|
| 958 |
+
bsz, src_len, embed_dim = x.shape
|
| 959 |
+
|
| 960 |
+
out = x
|
| 961 |
+
out = self.norm1(out)
|
| 962 |
+
|
| 963 |
+
# MHA
|
| 964 |
+
qkv_states = self.qkv_proj(out).split(self.d_model, dim=-1)
|
| 965 |
+
q_states = qkv_states[0].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
|
| 966 |
+
k_states = qkv_states[1].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
|
| 967 |
+
v_states = qkv_states[2].view(bsz, src_len, self.num_heads, embed_dim // self.num_heads).transpose(1, 2) # (bsz, num_heads, src_len, embed_dim // num_heads)
|
| 968 |
+
|
| 969 |
+
with torch.backends.cuda.sdp_kernel(enable_math=False):
|
| 970 |
+
out = F.scaled_dot_product_attention(q_states, k_states, v_states) # (bsz, num_heads, tgt_len, head_dim)
|
| 971 |
+
out = out.transpose(1, 2).contiguous().view(bsz, src_len, embed_dim) # (bsz, tgt_len, embed_dim)
|
| 972 |
+
|
| 973 |
+
out = self.out_proj(out)
|
| 974 |
+
|
| 975 |
+
out = self.skip_init1(out)
|
| 976 |
+
if self.stochdepth1 is not None:
|
| 977 |
+
out = self.stochdepth1(out)
|
| 978 |
+
x = out + x
|
| 979 |
+
|
| 980 |
+
out = self.norm2(x)
|
| 981 |
+
out = self.mlp(out)
|
| 982 |
+
out = self.skip_init2(out)
|
| 983 |
+
if self.stochdepth2 is not None:
|
| 984 |
+
out = self.stochdepth2(out)
|
| 985 |
+
|
| 986 |
+
out = out + x
|
| 987 |
+
|
| 988 |
+
return out
|
| 989 |
+
|
| 990 |
+
|
| 991 |
+
def CaiT_LayerScale_init(network_depth):
|
| 992 |
+
if network_depth <= 18:
|
| 993 |
+
return 1e-1
|
| 994 |
+
elif network_depth <= 24:
|
| 995 |
+
return 1e-5
|
| 996 |
+
else:
|
| 997 |
+
return 1e-6
|
| 998 |
+
|
| 999 |
+
|
| 1000 |
+
class CNNLayerNorm(nn.Module):
|
| 1001 |
+
def __init__(self, d_model: int):
|
| 1002 |
+
super().__init__()
|
| 1003 |
+
self.norm = nn.LayerNorm(d_model)
|
| 1004 |
+
|
| 1005 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 1006 |
+
x = x.transpose(1, 3)
|
| 1007 |
+
x = self.norm(x)
|
| 1008 |
+
x = x.transpose(1, 3)
|
| 1009 |
+
return x
|
| 1010 |
+
|
| 1011 |
+
|
| 1012 |
+
class CNNStem(nn.Module):
|
| 1013 |
+
def __init__(self, config: str):
|
| 1014 |
+
super().__init__()
|
| 1015 |
+
self.config = config
|
| 1016 |
+
|
| 1017 |
+
layers = []
|
| 1018 |
+
channels = 3
|
| 1019 |
+
|
| 1020 |
+
for line in config.split(";"):
|
| 1021 |
+
ty, line = line.split(":") if ":" in line else (line, "")
|
| 1022 |
+
options = line.split(",")
|
| 1023 |
+
options = [o.split("=") for o in options] if line else []
|
| 1024 |
+
options = {k: v for k, v in options}
|
| 1025 |
+
|
| 1026 |
+
if ty == 'conv':
|
| 1027 |
+
layers.append(nn.Conv2d(
|
| 1028 |
+
in_channels=channels,
|
| 1029 |
+
out_channels=int(options['c']),
|
| 1030 |
+
kernel_size=int(options['k'] if 'k' in options else 3),
|
| 1031 |
+
stride=int(options['s'] if 's' in options else 2),
|
| 1032 |
+
bias=True,
|
| 1033 |
+
padding=int(options['p'] if 'p' in options else 1),
|
| 1034 |
+
))
|
| 1035 |
+
channels = int(options['c'])
|
| 1036 |
+
elif ty == 'bn':
|
| 1037 |
+
layers.append(nn.BatchNorm2d(channels))
|
| 1038 |
+
elif ty == 'ln':
|
| 1039 |
+
layers.append(CNNLayerNorm(channels))
|
| 1040 |
+
elif ty == 'relu':
|
| 1041 |
+
layers.append(nn.ReLU())
|
| 1042 |
+
elif ty == 'gelu':
|
| 1043 |
+
layers.append(nn.GELU())
|
| 1044 |
+
|
| 1045 |
+
self.conv = nn.Sequential(*layers)
|
| 1046 |
+
|
| 1047 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 1048 |
+
return self.conv(x)
|
| 1049 |
+
|
| 1050 |
+
|
| 1051 |
+
class ViT(VisionModel):
|
| 1052 |
+
def __init__(self,
|
| 1053 |
+
n_tags: int,
|
| 1054 |
+
image_size: int,
|
| 1055 |
+
num_blocks: int,
|
| 1056 |
+
patch_size: int,
|
| 1057 |
+
d_model: int,
|
| 1058 |
+
mlp_dim: int,
|
| 1059 |
+
num_heads: int,
|
| 1060 |
+
stochdepth_rate: float,
|
| 1061 |
+
use_sine: bool,
|
| 1062 |
+
loss_type: str,
|
| 1063 |
+
layerscale_init: Optional[float] = None,
|
| 1064 |
+
head_mean_after: bool = False,
|
| 1065 |
+
cnn_stem: str | None = None,
|
| 1066 |
+
patch_dropout: float = 0.0,
|
| 1067 |
+
):
|
| 1068 |
+
super().__init__(image_size, n_tags)
|
| 1069 |
+
|
| 1070 |
+
#assert image_size % patch_size == 0, "image_size must be divisible by patch_size"
|
| 1071 |
+
assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
|
| 1072 |
+
|
| 1073 |
+
out_dim = n_tags
|
| 1074 |
+
self.n_tags = n_tags
|
| 1075 |
+
self.loss_type = loss_type
|
| 1076 |
+
self.patch_size = patch_size
|
| 1077 |
+
self.head_mean_after = head_mean_after
|
| 1078 |
+
self.patch_dropout = patch_dropout
|
| 1079 |
+
|
| 1080 |
+
layerscale_init = CaiT_LayerScale_init(num_blocks) if layerscale_init is None else layerscale_init
|
| 1081 |
+
self.patch_embeddings = nn.Conv2d(
|
| 1082 |
+
in_channels=3,
|
| 1083 |
+
out_channels=d_model,
|
| 1084 |
+
kernel_size=patch_size,
|
| 1085 |
+
stride=patch_size,
|
| 1086 |
+
bias=True,
|
| 1087 |
+
) if cnn_stem is None else CNNStem(cnn_stem)
|
| 1088 |
+
self.pos_embedding = PosEmbedding(d_model, (image_size // patch_size) ** 2, use_sine=use_sine, patch_size=patch_size)
|
| 1089 |
+
|
| 1090 |
+
self.blocks = nn.ModuleList([
|
| 1091 |
+
ViTBlock(num_heads, d_model, mlp_dim, layerscale_init, stochdepth_rate)
|
| 1092 |
+
for _ in range(num_blocks)
|
| 1093 |
+
])
|
| 1094 |
+
|
| 1095 |
+
self.norm = nn.LayerNorm(d_model)
|
| 1096 |
+
self.head = nn.Linear(d_model, out_dim)
|
| 1097 |
+
|
| 1098 |
+
def forward(self, batch, return_embeddings=False, return_loss: bool = False, pos_weight = None):
|
| 1099 |
+
B, C, H, W = batch['image'].shape
|
| 1100 |
+
assert H % self.patch_size == 0, f"Input image height ({H}) needs to be divisible by the patch size ({self.patch_size})."
|
| 1101 |
+
assert W % self.patch_size == 0, f"Input image width ({W}) needs to be divisible by the patch size ({self.patch_size})."
|
| 1102 |
+
|
| 1103 |
+
x = self.patch_embeddings(batch['image']) # (bsz, d_model, patch_num, patch_num)
|
| 1104 |
+
x = x.flatten(2).transpose(1, 2) # (bsz, patch_num ** 2, d_model)
|
| 1105 |
+
x = self.pos_embedding(x, W, H) # (bsz, patch_num ** 2, d_model)
|
| 1106 |
+
|
| 1107 |
+
# Patch dropout
|
| 1108 |
+
seq_len = x.shape[1]
|
| 1109 |
+
patch_dropout = int(math.ceil((1.0 - self.patch_dropout) * seq_len))
|
| 1110 |
+
|
| 1111 |
+
if patch_dropout != seq_len:
|
| 1112 |
+
# Generate a matrix of random numbers between 0 and 1 of shape (B, seq_len)
|
| 1113 |
+
patch_mask = torch.rand(B, seq_len, device=x.device)
|
| 1114 |
+
# For each batch tensor, use argsort to convert the random numbers into a permutation of the patch indices
|
| 1115 |
+
patch_mask = torch.argsort(patch_mask, dim=1)
|
| 1116 |
+
# Truncate
|
| 1117 |
+
patch_mask = patch_mask[:, :patch_dropout]
|
| 1118 |
+
|
| 1119 |
+
x = x.gather(1, patch_mask.unsqueeze(-1).expand(-1, -1, x.shape[-1]))
|
| 1120 |
+
|
| 1121 |
+
#indices = torch.randperm(seq_len, device=x.device)[:patch_dropout]
|
| 1122 |
+
#x = x[:, indices, :]
|
| 1123 |
+
|
| 1124 |
+
# Transformer
|
| 1125 |
+
for block in self.blocks:
|
| 1126 |
+
x = block(x)
|
| 1127 |
+
|
| 1128 |
+
# Head
|
| 1129 |
+
result = {}
|
| 1130 |
+
|
| 1131 |
+
x = self.norm(x)
|
| 1132 |
+
if self.head_mean_after:
|
| 1133 |
+
x = self.head(x)
|
| 1134 |
+
x = x.mean(dim=1)
|
| 1135 |
+
else:
|
| 1136 |
+
x = x.mean(dim=1)
|
| 1137 |
+
if return_embeddings:
|
| 1138 |
+
result['embeddings'] = x
|
| 1139 |
+
x = self.head(x)
|
| 1140 |
+
|
| 1141 |
+
result['tags'] = x
|
| 1142 |
+
|
| 1143 |
+
if return_loss:
|
| 1144 |
+
result['loss'] = self.calculate_loss(result, batch, pos_weight)
|
| 1145 |
+
|
| 1146 |
+
return result
|
| 1147 |
+
|
| 1148 |
+
def calculate_loss(self, preds, batch, pos_weight):
|
| 1149 |
+
return basic_calculate_loss(preds, batch, pos_weight, self.loss_type)
|
| 1150 |
+
|
| 1151 |
+
def get_optimized_parameters(self, lr: float):
|
| 1152 |
+
return self.parameters()
|
| 1153 |
+
|
| 1154 |
+
def save(self):
|
| 1155 |
+
return self.state_dict()
|
| 1156 |
+
|
| 1157 |
+
def load(self, state_dict):
|
| 1158 |
+
if 'head.weight' in state_dict and 'head.bias' in state_dict and state_dict['head.weight'].shape[0] == (self.n_tags + 9):
|
| 1159 |
+
# Support old models which included 3 rating and 6 score dimensions
|
| 1160 |
+
state_dict['head.weight'] = state_dict['head.weight'][:self.n_tags]
|
| 1161 |
+
state_dict['head.bias'] = state_dict['head.bias'][:self.n_tags]
|
| 1162 |
+
|
| 1163 |
+
self.load_state_dict(state_dict)
|
requirements.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# IMPORTANT: gradio is pinned because 6.14.0 has a rendering regression that hides
|
| 2 |
+
# the Caption Type / Caption Length dropdowns and breaks prompt updates. Do NOT upgrade.
|
| 3 |
+
gradio==6.12.0
|
| 4 |
+
transformers==4.46.3
|
| 5 |
+
accelerate
|
| 6 |
+
torch==2.6.0
|
| 7 |
+
torchvision==0.21.0
|
| 8 |
+
bitsandbytes==0.49.2; sys_platform != "darwin"
|
| 9 |
+
pillow
|
| 10 |
+
einops # required by joytag_models.py (JoyTag tab)
|
| 11 |
+
huggingface_hub
|
start_linux.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
cd "$(dirname "$0")"
|
| 3 |
+
source venv/bin/activate
|
| 4 |
+
python app.py "$@"
|
start_windows.bat
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM Full-quality bf16 mode (~18GB VRAM)
|
| 3 |
+
cd /d %~dp0
|
| 4 |
+
call venv\Scripts\activate
|
| 5 |
+
set HF_HUB_DISABLE_SYMLINKS_WARNING=1
|
| 6 |
+
python app.py %*
|
| 7 |
+
pause
|
start_windows_lowvram.bat
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@echo off
|
| 2 |
+
REM 4-bit NF4 mode (~7GB VRAM) - use this on smaller GPUs or when ComfyUI is also running
|
| 3 |
+
cd /d %~dp0
|
| 4 |
+
call venv\Scripts\activate
|
| 5 |
+
set HF_HUB_DISABLE_SYMLINKS_WARNING=1
|
| 6 |
+
python app.py --nf4 %*
|
| 7 |
+
pause
|