{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.11.13"},"kaggle":{"accelerator":"none","dataSources":[],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":443.236597,"end_time":"2026-01-11T03:21:01.244396","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-01-11T03:13:38.007799","version":"2.6.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"04c8c6d965674c078298509623ebc9fb":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"0d1ee7ae0030412dae52cffcb379966d":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_1b668978be774fa2b660414b04344c1a","IPY_MODEL_5c4d105c76a84aed9bdef21a14f22e69","IPY_MODEL_9ca08dbc4d664f49859a75db4493ac98"],"layout":"IPY_MODEL_197dd02bf62342799eab3a1375b803fb","tabbable":null,"tooltip":null}},"0f85b9b05fa44473b0f0adadf96736dd":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"1181374d90994962b12c3904668eb1d3":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"1371e74c3eba4d3ca53359f4749298a4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"13cf2bbf18c648b7b024e9885d55a622":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"13e31358f5944f53989cfaaaebbf08e4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_6ac48f4a962a420cb60d066523a616ee","placeholder":"​","style":"IPY_MODEL_16603b3104a54e57a2e0e25f58bf140b","tabbable":null,"tooltip":null,"value":"DINO3 TTA 1: 100%"}},"16603b3104a54e57a2e0e25f58bf140b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"1803a1dde3f9406f9a7baf2da16b46dd":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"194ee77804b44fe4929f9989d3e12762":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_98480358ddef4c20830f9f1687dda55f","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_73a60cff2af2423f80559b4fe72dec1e","tabbable":null,"tooltip":null,"value":1}},"197dd02bf62342799eab3a1375b803fb":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"1b668978be774fa2b660414b04344c1a":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_2af42452f66148769e80d633885dc313","placeholder":"​","style":"IPY_MODEL_b71e7e417192473aa2b76de7bcc07f9c","tabbable":null,"tooltip":null,"value":"ConvNext View 3/4: 100%"}},"21e8e63af64941528fffb31ad46a6731":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_ec49b7dcc69e4092919fc7b289b60529","IPY_MODEL_609fa140f2714c08afbee2cbcc62f186","IPY_MODEL_6f4dc50f832d42b3be52f6480c7ea1e5"],"layout":"IPY_MODEL_882c175646264ef6a9f42d34252e8e07","tabbable":null,"tooltip":null}},"26f2de52610445928e60ed55f4b37f2e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_c1cda1770b9349e88855a4c73384107e","placeholder":"​","style":"IPY_MODEL_63817b807eb1490cb92e2f2ea14dc041","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.97s/it]"}},"2af42452f66148769e80d633885dc313":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"2bb037ce55494d708f25783584746a88":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"2bd1290835cc4db7ae5c7a0db4818935":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"350c1e60d5f247e981e226603031460c":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_13cf2bbf18c648b7b024e9885d55a622","placeholder":"​","style":"IPY_MODEL_9419299993ef4e9282baf3a5891a098f","tabbable":null,"tooltip":null,"value":" 1/1 [00:04&lt;00:00,  4.01s/it]"}},"378b49cad0984c0dabed08f1ff90a21e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_d330605166534712aca317937691995f","placeholder":"​","style":"IPY_MODEL_51a2ba17d846466bbcfdc1bcd0489334","tabbable":null,"tooltip":null,"value":"ConvNext View 1/4: 100%"}},"3a36116fb52745319c78a030d900dbca":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"3e9bce9eddf04b1194e37e5b239176a4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_b385a7869d5f4297baec8b062d399187","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_1371e74c3eba4d3ca53359f4749298a4","tabbable":null,"tooltip":null,"value":1}},"3fdc20517b4b4e71b3b8510c68ea1f9a":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"51a2ba17d846466bbcfdc1bcd0489334":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"52a66169eaa3442c84060351547b867f":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_fc1f7e72c61f49e0a76eee37ea39d770","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_d4b6ad5706994a3f90fe6886c5ec3519","tabbable":null,"tooltip":null,"value":1}},"5597be508fca4331b4bf7561b03df682":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"5c4d105c76a84aed9bdef21a14f22e69":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_04c8c6d965674c078298509623ebc9fb","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_b836a82fad234cc48f635e5e9d6fb2da","tabbable":null,"tooltip":null,"value":1}},"609fa140f2714c08afbee2cbcc62f186":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_1803a1dde3f9406f9a7baf2da16b46dd","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_9ebd2e5aeead431da47f075533ce6c36","tabbable":null,"tooltip":null,"value":1}},"62dd2abcb9c342c4929dba7c70dbc483":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_1181374d90994962b12c3904668eb1d3","placeholder":"​","style":"IPY_MODEL_82f0bb69bfcc4077a4ff08efeac8888e","tabbable":null,"tooltip":null,"value":"ConvNext View 2/4: 100%"}},"63817b807eb1490cb92e2f2ea14dc041":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"64c049b38b7d4b759444ebaa94645e27":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"6ac48f4a962a420cb60d066523a616ee":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"6d34a6de2a874a77b7412193a5ecbec6":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"6f4dc50f832d42b3be52f6480c7ea1e5":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_b8455b148ae44a52a1fc95317dbf2401","placeholder":"​","style":"IPY_MODEL_a5012889b2a34b5bbbb441593e2e2d5e","tabbable":null,"tooltip":null,"value":" 1/1 [00:00&lt;00:00,  1.26it/s]"}},"72f80c5c309a416fadf56f232bfe657b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_bdd64a607a0f4ee3918a9aec7619859b","placeholder":"​","style":"IPY_MODEL_ff8f9f54b0f84bd6989ac5710a147133","tabbable":null,"tooltip":null,"value":" 1/1 [00:01&lt;00:00,  1.19s/it]"}},"73a60cff2af2423f80559b4fe72dec1e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"7ccacb000627459fbc717cfdb0456695":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_f86df6cb49834fc087826c8d18485170","IPY_MODEL_52a66169eaa3442c84060351547b867f","IPY_MODEL_d1af0bacf69c44e7ae9dc682079fe516"],"layout":"IPY_MODEL_2bd1290835cc4db7ae5c7a0db4818935","tabbable":null,"tooltip":null}},"82f0bb69bfcc4077a4ff08efeac8888e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"84227b2fb68740bf9f2e5a797bf9cc0c":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_91c5b2c9193f4a298d7f35093d11795c","placeholder":"​","style":"IPY_MODEL_c05c644cc80945a580e91b5f1da6189b","tabbable":null,"tooltip":null,"value":"100%"}},"882c175646264ef6a9f42d34252e8e07":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"91c5b2c9193f4a298d7f35093d11795c":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"932102e4e223471badcf55bb3575c8a4":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9419299993ef4e9282baf3a5891a098f":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"97491afee3d5431aaf187d693683dee4":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"98480358ddef4c20830f9f1687dda55f":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"999f421fd6b94648b258315f3350fbc3":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"9ca08dbc4d664f49859a75db4493ac98":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_97491afee3d5431aaf187d693683dee4","placeholder":"​","style":"IPY_MODEL_5597be508fca4331b4bf7561b03df682","tabbable":null,"tooltip":null,"value":" 1/1 [00:00&lt;00:00,  1.33it/s]"}},"9ebd2e5aeead431da47f075533ce6c36":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"9f6553972e54415487805cb0f0aa24ab":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_a95558ef7e004364bff42c6754ddc08b","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_64c049b38b7d4b759444ebaa94645e27","tabbable":null,"tooltip":null,"value":1}},"a5012889b2a34b5bbbb441593e2e2d5e":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"a95558ef7e004364bff42c6754ddc08b":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"af38616f3a6d471b8a9549fc801c12b7":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_62dd2abcb9c342c4929dba7c70dbc483","IPY_MODEL_3e9bce9eddf04b1194e37e5b239176a4","IPY_MODEL_72f80c5c309a416fadf56f232bfe657b"],"layout":"IPY_MODEL_3a36116fb52745319c78a030d900dbca","tabbable":null,"tooltip":null}},"b385a7869d5f4297baec8b062d399187":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"b3b9886482584dd1bc94708c4708b1b3":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"b71e7e417192473aa2b76de7bcc07f9c":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"b836a82fad234cc48f635e5e9d6fb2da":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"b8455b148ae44a52a1fc95317dbf2401":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"bc410fba9d3543499c88a0ffbb9c9b3b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"bdd64a607a0f4ee3918a9aec7619859b":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c05c644cc80945a580e91b5f1da6189b":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"c1cda1770b9349e88855a4c73384107e":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"c86fdfd496094146b3c4480c517e7ecf":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_378b49cad0984c0dabed08f1ff90a21e","IPY_MODEL_d1f897356eaa4c529537fe04d4a612d0","IPY_MODEL_fde155db57cf42ef9762858b33797379"],"layout":"IPY_MODEL_932102e4e223471badcf55bb3575c8a4","tabbable":null,"tooltip":null}},"c94109708f8b4396b9148701a021d29f":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"c96d6ec1bd4f40d8a9e3e0296f552e37":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_13e31358f5944f53989cfaaaebbf08e4","IPY_MODEL_194ee77804b44fe4929f9989d3e12762","IPY_MODEL_350c1e60d5f247e981e226603031460c"],"layout":"IPY_MODEL_999f421fd6b94648b258315f3350fbc3","tabbable":null,"tooltip":null}},"ce9c666db9f9413cabb9662afb0c7b74":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HBoxModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HBoxModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HBoxView","box_style":"","children":["IPY_MODEL_84227b2fb68740bf9f2e5a797bf9cc0c","IPY_MODEL_9f6553972e54415487805cb0f0aa24ab","IPY_MODEL_26f2de52610445928e60ed55f4b37f2e"],"layout":"IPY_MODEL_ee9a380e1e57464e841a4c1e89123e15","tabbable":null,"tooltip":null}},"d1af0bacf69c44e7ae9dc682079fe516":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_b3b9886482584dd1bc94708c4708b1b3","placeholder":"​","style":"IPY_MODEL_6d34a6de2a874a77b7412193a5ecbec6","tabbable":null,"tooltip":null,"value":" 1/1 [00:03&lt;00:00,  3.97s/it]"}},"d1f897356eaa4c529537fe04d4a612d0":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"FloatProgressModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"FloatProgressModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"ProgressView","bar_style":"success","description":"","description_allow_html":false,"layout":"IPY_MODEL_dc4a8be7a5314e96a8056489a6c3a9cf","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_c94109708f8b4396b9148701a021d29f","tabbable":null,"tooltip":null,"value":1}},"d330605166534712aca317937691995f":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"d4b6ad5706994a3f90fe6886c5ec3519":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"ProgressStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"ProgressStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","bar_color":null,"description_width":""}},"d51a52e4dd994e83880de8ce903a46c4":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}},"dbb940b3362442e793195b5a6783b1d4":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"dc4a8be7a5314e96a8056489a6c3a9cf":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"ec49b7dcc69e4092919fc7b289b60529":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_3fdc20517b4b4e71b3b8510c68ea1f9a","placeholder":"​","style":"IPY_MODEL_d51a52e4dd994e83880de8ce903a46c4","tabbable":null,"tooltip":null,"value":"ConvNext View 4/4: 100%"}},"ee9a380e1e57464e841a4c1e89123e15":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"f86df6cb49834fc087826c8d18485170":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_0f85b9b05fa44473b0f0adadf96736dd","placeholder":"​","style":"IPY_MODEL_bc410fba9d3543499c88a0ffbb9c9b3b","tabbable":null,"tooltip":null,"value":"DINO3 TTA 2: 100%"}},"fc1f7e72c61f49e0a76eee37ea39d770":{"model_module":"@jupyter-widgets/base","model_module_version":"2.0.0","model_name":"LayoutModel","state":{"_model_module":"@jupyter-widgets/base","_model_module_version":"2.0.0","_model_name":"LayoutModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"LayoutView","align_content":null,"align_items":null,"align_self":null,"border_bottom":null,"border_left":null,"border_right":null,"border_top":null,"bottom":null,"display":null,"flex":null,"flex_flow":null,"grid_area":null,"grid_auto_columns":null,"grid_auto_flow":null,"grid_auto_rows":null,"grid_column":null,"grid_gap":null,"grid_row":null,"grid_template_areas":null,"grid_template_columns":null,"grid_template_rows":null,"height":null,"justify_content":null,"justify_items":null,"left":null,"margin":null,"max_height":null,"max_width":null,"min_height":null,"min_width":null,"object_fit":null,"object_position":null,"order":null,"overflow":null,"padding":null,"right":null,"top":null,"visibility":null,"width":null}},"fde155db57cf42ef9762858b33797379":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLModel","state":{"_dom_classes":[],"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLModel","_view_count":null,"_view_module":"@jupyter-widgets/controls","_view_module_version":"2.0.0","_view_name":"HTMLView","description":"","description_allow_html":false,"layout":"IPY_MODEL_dbb940b3362442e793195b5a6783b1d4","placeholder":"​","style":"IPY_MODEL_2bb037ce55494d708f25783584746a88","tabbable":null,"tooltip":null,"value":" 1/1 [01:15&lt;00:00, 75.59s/it]"}},"ff8f9f54b0f84bd6989ac5710a147133":{"model_module":"@jupyter-widgets/controls","model_module_version":"2.0.0","model_name":"HTMLStyleModel","state":{"_model_module":"@jupyter-widgets/controls","_model_module_version":"2.0.0","_model_name":"HTMLStyleModel","_view_count":null,"_view_module":"@jupyter-widgets/base","_view_module_version":"2.0.0","_view_name":"StyleView","background":null,"description_width":"","font_size":null,"text_color":null}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"0f74563d","cell_type":"markdown","source":"## Model 1 EVA02","metadata":{"papermill":{"duration":0.006335,"end_time":"2026-01-11T03:13:43.160436","exception":false,"start_time":"2026-01-11T03:13:43.154101","status":"completed"},"tags":[]}},{"id":"90dc4344","cell_type":"code","source":"# ====================================================================================\n# [Part 1 Optimized] EVA02 Inference with Multi-Scale TTA\n# ====================================================================================\nimport os\nimport gc\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport timm\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\n\n# class ConfigEVA:\n#     data_dir = '/kaggle/input/csiro-biomass/test'\n#     # 确保路径正确，如果不正确请修改\n#     model_path = '/kaggle/input/model-0-65/best_model_fold_4.pth'\n#     base_size = 448\n#     # 新增：多尺度列表\n#     scales = [448, 448] \n#     device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n# class BiomassModelEVA(nn.Module):\n#     def regression_head(self, name, in_features: int, dropout: float):\n#         return nn.Sequential(\n#             nn.Linear(in_features, in_features // 2),\n#             nn.ReLU(),\n#             nn.Dropout(dropout),\n#             nn.Linear(in_features // 2, 1)\n#         )\n#     def __init__(self, model_name, dropout = 0.0):\n#         super().__init__()\n#         self.backbone = timm.create_model(model_name, pretrained=False, num_classes=0, global_pool='avg')\n#         n_features = self.backbone.num_features * 2\n#         self.head_total = self.regression_head('dry_total', n_features, dropout)\n#         self.head_gdm = self.regression_head('dry_gdm', n_features, dropout)\n#         self.head_green = self.regression_head('dry_green', n_features, dropout)\n\n#     def forward(self, img_left, img_right):\n#         fl = self.backbone(img_left)\n#         fr = self.backbone(img_right)\n#         img_feat = torch.cat([fl, fr], dim=1)\n#         return self.head_total(img_feat), self.head_gdm(img_feat), self.head_green(img_feat)\n\n# def get_transforms(img_size):\n#     return A.Compose([\n#         A.Resize(img_size, img_size),\n#         A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n#         ToTensorV2()\n#     ])\n\n# def run_eva_inference_optimized():\n#     print(f\"🚀 Starting EVA02 Optimized Inference (Scales: {ConfigEVA.scales})...\")\n    \n#     if not os.path.exists(ConfigEVA.model_path):\n#         print(f\"❌ Error: Model not found at {ConfigEVA.model_path}\")\n#         return\n\n#     ckpt = torch.load(ConfigEVA.model_path, map_location=ConfigEVA.device)\n#     model_name = ckpt.get('model_name', 'eva02_large_patch14_448.mim_m38m_ft_in22k_in1k')\n    \n#     model = BiomassModelEVA(model_name=model_name)\n#     model.load_state_dict(ckpt['model_state_dict'])\n#     model.eval()\n#     model.to(ConfigEVA.device)\n    \n#     image_files = [f for f in os.listdir(ConfigEVA.data_dir) if f.lower().endswith(('.png', '.jpg'))]\n#     results = []\n    \n#     # 定义两个尺度的 TTA\n#     tf_base = get_transforms(ConfigEVA.scales[0])\n#     tf_large = get_transforms(ConfigEVA.scales[1])\n    \n#     # 简单的 Batch 处理 TTA (Resize + Flip)\n#     # TTA 策略: [Size1, Size1_Flip, Size2] (共3次推理)\n    \n#     for img_file in tqdm(image_files, desc=\"EVA02 Scale Predicting\"):\n#         img_path = os.path.join(ConfigEVA.data_dir, img_file)\n#         image = cv2.imread(img_path)\n#         if image is None: continue\n#         image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)\n#         h, w, _ = image.shape\n        \n#         img_l = image[:, :w//2]\n#         img_r = image[:, w//2:]\n        \n#         # 准备 TTA 输入\n#         inputs_l, inputs_r = [], []\n        \n#         # 1. Base Scale (Normal)\n#         inputs_l.append(tf_base(image=img_l)['image'])\n#         inputs_r.append(tf_base(image=img_r)['image'])\n        \n#         # 2. Base Scale (HFlip)\n#         inputs_l.append(tf_base(image=cv2.flip(img_l, 1))['image'])\n#         inputs_r.append(tf_base(image=cv2.flip(img_r, 1))['image'])\n        \n#         # 3. Large Scale (Normal) - 增加细节捕捉\n#         inputs_l.append(tf_large(image=img_l)['image'])\n#         inputs_r.append(tf_large(image=img_r)['image'])\n        \n#         # Stack & Infer\n#         tensor_l = torch.stack(inputs_l).to(ConfigEVA.device)\n#         tensor_r = torch.stack(inputs_r).to(ConfigEVA.device)\n        \n#         with torch.inference_mode():\n#             p_tot, p_gdm, p_green = model(tensor_l, tensor_r)\n            \n#         # 融合 TTA 结果 (加权：大尺度给予 0.4 权重，Base给予 0.6)\n#         # raw preds shape: (3, 1) -> (3,)\n#         preds_tot = p_tot.cpu().numpy().flatten()\n#         preds_gdm = p_gdm.cpu().numpy().flatten()\n#         preds_grn = p_green.cpu().numpy().flatten()\n        \n#         # 加权平均：0.35(Base) + 0.35(Flip) + 0.30(Large)\n#         w = np.array([0.35, 0.35, 0.30])\n#         val_tot = np.dot(preds_tot, w)\n#         val_gdm = np.dot(preds_gdm, w)\n#         val_grn = np.dot(preds_grn, w)\n        \n#         results.append({\n#             'base_id': os.path.splitext(img_file)[0],\n#             'Dry_Total_g': max(0, val_tot),\n#             'GDM_g': max(0, val_gdm),\n#             'Dry_Green_g': max(0, val_grn)\n#         })\n\n#     df = pd.DataFrame(results)\n#     df.to_csv('preds_eva02_wide.csv', index=False)\n#     print(f\"✅ EVA02 Optimized Predictions Saved! (Rows: {len(df)})\")\n    \n#     del model, tensor_l, tensor_r\n#     gc.collect()\n#     torch.cuda.empty_cache()\n\n# if __name__ == \"__main__\":\n#     run_eva_inference_optimized()","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:13:43.17161Z","iopub.status.busy":"2026-01-11T03:13:43.171325Z","iopub.status.idle":"2026-01-11T03:14:36.572629Z","shell.execute_reply":"2026-01-11T03:14:36.57185Z"},"papermill":{"duration":53.412618,"end_time":"2026-01-11T03:14:36.578193","exception":false,"start_time":"2026-01-11T03:13:43.165575","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"e454ba77","cell_type":"markdown","source":"## Model 2 DINOv2 Giant Mamba","metadata":{"papermill":{"duration":0.004437,"end_time":"2026-01-11T03:14:36.587258","exception":false,"start_time":"2026-01-11T03:14:36.582821","status":"completed"},"tags":[]}},{"id":"36a969f0","cell_type":"code","source":"# =============================================================================\n# CSIRO Image2Biomass - v4 CrossPVT T2T Mamba Inference (Self-contained)\n# =============================================================================\n\nimport os\nimport gc\nimport math\nimport argparse\nimport logging\nimport random  # 新增 import\nfrom dataclasses import dataclass, field\nfrom typing import List, Tuple, Dict, Optional\n\nimport cv2\nimport timm\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\n\n# =============================================================================\n# Logger\n# =============================================================================\nLOGGER = logging.getLogger(\"csiro_infer_v4\")\nif not LOGGER.handlers:\n    LOGGER.addHandler(logging.StreamHandler())\nLOGGER.setLevel(logging.INFO)\n\n# =============================================================================\n# 新增：固定随机种子\n# =============================================================================\ndef seed_everything(seed=43):\n    random.seed(seed)\n    os.environ['PYTHONHASHSEED'] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.cuda.manual_seed_all(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    print(f\"🔒 Seed set to {seed}\")\n\n# =============================================================================\n# 训练时使用的配置（会被 checkpoint 中的 cfg 覆盖）\n# =============================================================================\n@dataclass\nclass TrainCFG:\n    dropout: float = 0.1\n    hidden_ratio: float = 0.35\n\n    # DINO backbone 相关\n    dino_candidates: Tuple[str, ...] = (\n        \"vit_base_patch14_dinov2\",\n        \"vit_base_patch14_reg4_dinov2\",\n        \"vit_small_patch14_dinov2\",\n    )\n    small_grid: Tuple[int, int] = (4, 4)\n    big_grid: Tuple[int, int] = (2, 2)\n    t2t_depth: int = 2\n    cross_layers: int = 2\n    cross_heads: int = 6\n\n    # Pyramid + Mamba\n    pyramid_dims: Tuple[int, int, int] = (384, 512, 640)\n    mobilevit_heads: int = 4\n    mobilevit_depth: int = 2\n    sra_heads: int = 8\n    sra_ratio: int = 2\n    mamba_depth: int = 3\n    mamba_kernel: int = 5\n    aux_head: bool = True\n    aux_loss_weight: float = 0.4\n\n    # 目标列（用于辅助头 / 5 目标打包）\n    ALL_TARGET_COLS: Tuple[str, ...] = (\n        \"Dry_Green_g\",\n        \"Dry_Dead_g\",\n        \"Dry_Clover_g\",\n        \"GDM_g\",\n        \"Dry_Total_g\",\n    )\n\n\nCFG = TrainCFG()\n\n\ndef update_cfg_from_checkpoint(cfg_dict: dict):\n    \"\"\"\n    用 checkpoint 中保存的 cfg 覆盖当前 CFG 的同名字段，\n    确保推理结构与训练完全一致。\n    \"\"\"\n    global CFG\n    if not cfg_dict:\n        return\n    for k, v in cfg_dict.items():\n        if hasattr(CFG, k):\n            setattr(CFG, k, v)\n\n\n# =============================================================================\n# Model blocks（与 train.py 保持一致的简化实现）\n# =============================================================================\nclass FeedForward(nn.Module):\n    def __init__(self, dim, mlp_ratio=4.0, dropout=0.0):\n        super().__init__()\n        hid = int(dim * mlp_ratio)\n        self.net = nn.Sequential(\n            nn.Linear(dim, hid),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(hid, dim),\n            nn.Dropout(dropout),\n        )\n\n    def forward(self, x):\n        return self.net(x)\n\n\nclass AttentionBlock(nn.Module):\n    def __init__(self, dim, heads=8, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.attn = nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x):\n        h = self.norm1(x)\n        attn_out, _ = self.attn(h, h, h, need_weights=False)\n        x = x + attn_out\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass MobileViTBlock(nn.Module):\n    \"\"\"\n    轻量 MobileViT：局部 CNN + 小型 Transformer（token 化和 fold back）\n    \"\"\"\n\n    def __init__(self, dim, heads=4, depth=2, patch=(2, 2), dropout=0.0):\n        super().__init__()\n        self.local = nn.Sequential(\n            nn.Conv2d(dim, dim, 3, padding=1, groups=dim),\n            nn.Conv2d(dim, dim, 1),\n            nn.GELU(),\n        )\n        self.patch = patch\n        self.transformer = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n        self.fuse = nn.Conv2d(dim * 2, dim, kernel_size=1)\n\n    def forward(self, x: torch.Tensor):\n        local_feat = self.local(x)\n        B, C, H, W = local_feat.shape\n        ph, pw = self.patch\n        new_h = math.ceil(H / ph) * ph\n        new_w = math.ceil(W / pw) * pw\n        if new_h != H or new_w != W:\n            local_feat = F.interpolate(local_feat, size=(new_h, new_w), mode=\"bilinear\", align_corners=False)\n            H, W = new_h, new_w\n\n        tokens = local_feat.unfold(2, ph, ph).unfold(3, pw, pw)  # B,C,nh,nw,ph,pw\n        tokens = tokens.contiguous().view(B, C, -1, ph, pw)\n        tokens = tokens.permute(0, 2, 3, 4, 1).reshape(B, -1, C)\n\n        for blk in self.transformer:\n            tokens = blk(tokens)\n\n        feat = tokens.view(B, -1, ph * pw, C).permute(0, 3, 1, 2)\n        nh = H // ph\n        nw = W // pw\n        feat = feat.view(B, C, nh, nw, ph, pw).permute(0, 1, 2, 4, 3, 5)\n        feat = feat.reshape(B, C, H, W)\n\n        if feat.shape[-2:] != x.shape[-2:]:\n            feat = F.interpolate(feat, size=x.shape[-2:], mode=\"bilinear\", align_corners=False)\n\n        out = self.fuse(torch.cat([x, feat], dim=1))\n        return out\n\n\nclass SpatialReductionAttention(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0):\n        super().__init__()\n        self.heads = heads\n        self.scale = (dim // heads) ** -0.5\n        self.q = nn.Linear(dim, dim)\n        self.kv = nn.Linear(dim, dim * 2)\n        self.sr_ratio = sr_ratio\n        if sr_ratio > 1:\n            self.sr = nn.Conv2d(dim, dim, kernel_size=sr_ratio, stride=sr_ratio)\n            self.norm = nn.LayerNorm(dim)\n        else:\n            self.sr = None\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        B, N, C = x.shape\n        q = self.q(x).reshape(B, N, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        if self.sr is not None:\n            H, W = hw\n            feat = x.transpose(1, 2).reshape(B, C, H, W)\n            feat = self.sr(feat)\n            feat = feat.reshape(B, C, -1).transpose(1, 2)\n            feat = self.norm(feat)\n        else:\n            feat = x\n\n        kv = self.kv(feat)\n        k, v = kv.chunk(2, dim=-1)\n        k = k.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 3, 1)\n        v = v.reshape(B, -1, self.heads, C // self.heads).permute(0, 2, 1, 3)\n\n        attn = torch.matmul(q, k) * self.scale\n        attn = attn.softmax(dim=-1)\n        attn = self.drop(attn)\n        out = torch.matmul(attn, v).permute(0, 2, 1, 3).reshape(B, N, C)\n        out = self.proj(out)\n        return out\n\n\nclass PVTBlock(nn.Module):\n    def __init__(self, dim, heads=8, sr_ratio=2, dropout=0.0, mlp_ratio=4.0):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(dim)\n        self.sra = SpatialReductionAttention(dim, heads=heads, sr_ratio=sr_ratio, dropout=dropout)\n        self.norm2 = nn.LayerNorm(dim)\n        self.ff = FeedForward(dim, mlp_ratio=mlp_ratio, dropout=dropout)\n\n    def forward(self, x, hw: Tuple[int, int]):\n        x = x + self.sra(self.norm1(x), hw)\n        x = x + self.ff(self.norm2(x))\n        return x\n\n\nclass LocalMambaBlock(nn.Module):\n    \"\"\"\n    简化版 local Mamba：DW-Conv + gating + 线性映射\n    \"\"\"\n\n    def __init__(self, dim, kernel_size=5, dropout=0.0):\n        super().__init__()\n        self.norm = nn.LayerNorm(dim)\n        self.dwconv = nn.Conv1d(dim, dim, kernel_size=kernel_size, padding=kernel_size // 2, groups=dim)\n        self.gate = nn.Linear(dim, dim)\n        self.proj = nn.Linear(dim, dim)\n        self.drop = nn.Dropout(dropout)\n\n    def forward(self, x):\n        shortcut = x\n        x = self.norm(x)\n        g = torch.sigmoid(self.gate(x))\n        x = (x * g).transpose(1, 2)  # B, C, N\n        x = self.dwconv(x).transpose(1, 2)\n        x = self.proj(x)\n        x = self.drop(x)\n        return shortcut + x\n\n\nclass T2TRetokenizer(nn.Module):\n    \"\"\"\n    将 4x4 tile token 做局部 attention + 下采样到 2x2\n    \"\"\"\n\n    def __init__(self, dim, depth=2, heads=4, dropout=0.0):\n        super().__init__()\n        self.blocks = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(depth)]\n        )\n\n    def forward(self, tokens: torch.Tensor, grid_hw: Tuple[int, int]):\n        B, T, C = tokens.shape\n        H, W = grid_hw\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        seq = feat_map.flatten(2).transpose(1, 2)\n        for blk in self.blocks:\n            seq = blk(seq)\n        seq_map = seq.transpose(1, 2).reshape(B, C, H, W)\n        pooled = F.adaptive_avg_pool2d(seq_map, (2, 2))\n        retokens = pooled.flatten(2).transpose(1, 2)\n        return retokens, seq_map\n\n\nclass CrossScaleFusion(nn.Module):\n    def __init__(self, dim, heads=6, dropout=0.0, layers=2):\n        super().__init__()\n        self.layers_s = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.layers_b = nn.ModuleList(\n            [AttentionBlock(dim, heads=heads, dropout=dropout, mlp_ratio=2.0) for _ in range(layers)]\n        )\n        self.cross_s = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.cross_b = nn.ModuleList(\n            [\n                nn.MultiheadAttention(dim, heads, dropout=dropout, batch_first=True, kdim=dim, vdim=dim)\n                for _ in range(layers)\n            ]\n        )\n        self.norm_s = nn.LayerNorm(dim)\n        self.norm_b = nn.LayerNorm(dim)\n\n    def forward(self, tok_s: torch.Tensor, tok_b: torch.Tensor):\n        B, Ts, C = tok_s.shape\n        Tb = tok_b.shape[1]\n        cls_s = tok_s.new_zeros(B, 1, C)\n        cls_b = tok_b.new_zeros(B, 1, C)\n        tok_s = torch.cat([cls_s, tok_s], dim=1)\n        tok_b = torch.cat([cls_b, tok_b], dim=1)\n\n        for ls, lb, cs, cb in zip(self.layers_s, self.layers_b, self.cross_s, self.cross_b):\n            tok_s = ls(tok_s)\n            tok_b = lb(tok_b)\n            q_s = self.norm_s(tok_s[:, :1])\n            q_b = self.norm_b(tok_b[:, :1])\n            cls_s_upd, _ = cs(\n                q_s,\n                torch.cat([tok_b, q_b], dim=1),\n                torch.cat([tok_b, q_b], dim=1),\n                need_weights=False,\n            )\n            cls_b_upd, _ = cb(\n                q_b,\n                torch.cat([tok_s, q_s], dim=1),\n                torch.cat([tok_s, q_s], dim=1),\n                need_weights=False,\n            )\n            tok_s = torch.cat([tok_s[:, :1] + cls_s_upd, tok_s[:, 1:]], dim=1)\n            tok_b = torch.cat([tok_b[:, :1] + cls_b_upd, tok_b[:, 1:]], dim=1)\n\n        tokens = torch.cat([tok_s[:, :1], tok_b[:, :1], tok_s[:, 1:], tok_b[:, 1:]], dim=1)\n        return tokens  # shape ~ (B, 2 + Ts + Tb, C)\n\n\nclass TileEncoder(nn.Module):\n    def __init__(self, backbone: nn.Module, input_res: int):\n        super().__init__()\n        self.backbone = backbone\n        self.input_res = input_res\n\n    def forward(self, x: torch.Tensor, grid: Tuple[int, int]):\n        B, C, H, W = x.shape\n        r, c = grid\n        hs = torch.linspace(0, H, steps=r + 1, device=x.device).round().long()\n        ws = torch.linspace(0, W, steps=c + 1, device=x.device).round().long()\n        tiles = []\n        for i in range(r):\n            for j in range(c):\n                rs, re = hs[i].item(), hs[i + 1].item()\n                cs, ce = ws[j].item(), ws[j + 1].item()\n                xt = x[:, :, rs:re, cs:ce]\n                if xt.shape[-2:] != (self.input_res, self.input_res):\n                    xt = F.interpolate(xt, size=(self.input_res, self.input_res), mode=\"bilinear\", align_corners=False)\n                tiles.append(xt)\n        tiles = torch.stack(tiles, dim=1)  # (B, T, C, H, W)\n        flat = tiles.view(-1, C, self.input_res, self.input_res)\n        feats = self.backbone(flat)\n        feats = feats.view(B, -1, feats.shape[-1])\n        return feats\n\n\nclass PyramidMixer(nn.Module):\n    def __init__(\n        self,\n        dim_in: int,\n        dims: Tuple[int, int, int],\n        mobilevit_heads: int = 4,\n        mobilevit_depth: int = 2,\n        sra_heads: int = 6,\n        sra_ratio: int = 2,\n        mamba_depth: int = 3,\n        mamba_kernel: int = 5,\n        dropout: float = 0.0,\n    ):\n        super().__init__()\n        c1, c2, c3 = dims\n        self.proj1 = nn.Linear(dim_in, c1)\n        self.mobilevit = MobileViTBlock(c1, heads=mobilevit_heads, depth=mobilevit_depth, dropout=dropout)\n        self.proj2 = nn.Linear(c1, c2)\n        self.pvt = PVTBlock(c2, heads=sra_heads, sr_ratio=sra_ratio, dropout=dropout, mlp_ratio=3.0)\n        self.mamba_local = LocalMambaBlock(c2, kernel_size=mamba_kernel, dropout=dropout)\n        self.proj3 = nn.Linear(c2, c3)\n        self.mamba_global = nn.ModuleList(\n            [LocalMambaBlock(c3, kernel_size=mamba_kernel, dropout=dropout) for _ in range(mamba_depth)]\n        )\n        self.final_attn = AttentionBlock(c3, heads=min(8, c3 // 64 + 1), dropout=dropout, mlp_ratio=2.0)\n\n    def _tokens_to_map(self, tokens: torch.Tensor, target_hw: Tuple[int, int]):\n        B, N, C = tokens.shape\n        H, W = target_hw\n        need = H * W\n        if N < need:\n            pad = tokens.new_zeros(B, need - N, C)\n            tokens = torch.cat([tokens, pad], dim=1)\n        tokens = tokens[:, :need, :]\n        feat_map = tokens.transpose(1, 2).reshape(B, C, H, W)\n        return feat_map\n\n    @staticmethod\n    def _fit_hw(n_tokens: int) -> Tuple[int, int]:\n        \"\"\"选择一个接近方形、满足 h*w>=n_tokens 的网格。\"\"\"\n        h = int(math.sqrt(n_tokens))\n        w = h\n        while h * w < n_tokens:\n            w += 1\n            if h * w < n_tokens:\n                h += 1\n        return h, w\n\n    def forward(self, tokens: torch.Tensor):\n        # 约 10 tokens -> 3x4 map\n        B, N, C = tokens.shape\n        map_hw = (3, 4)\n        feat_map = self._tokens_to_map(tokens, map_hw)\n\n        t1 = self.proj1(tokens)\n        m1 = self._tokens_to_map(t1, map_hw)\n        m1 = self.mobilevit(m1)\n        t1_out = m1.flatten(2).transpose(1, 2)[:, :N]\n\n        # Stage2: 下采样 token 数量（平均池化）\n        t2 = self.proj2(t1_out)\n        new_len = max(4, N // 2)\n        t2 = t2[:, :new_len] + F.adaptive_avg_pool1d(t2.transpose(1, 2), new_len).transpose(1, 2)\n        hw2 = self._fit_hw(t2.size(1))\n        if t2.size(1) < hw2[0] * hw2[1]:\n            pad = t2.new_zeros(B, hw2[0] * hw2[1] - t2.size(1), t2.size(2))\n            t2 = torch.cat([t2, pad], dim=1)\n        t2 = self.pvt(t2, hw2)\n        t2 = self.mamba_local(t2)\n\n        # Stage3: 全局\n        t3 = self.proj3(t2)\n        pooled = torch.stack([t3.mean(dim=1), t3.max(dim=1).values], dim=1)  # (B,2,C)\n        t3 = pooled\n        for blk in self.mamba_global:\n            t3 = blk(t3)\n        t3 = self.final_attn(t3)\n        global_feat = t3.mean(dim=1)\n        return global_feat, {\"stage1_map\": m1.detach(), \"stage2_tokens\": t2.detach(), \"stage3_tokens\": t3.detach()}\n\n\nclass CrossPVT_T2T_MambaDINO(nn.Module):\n    def __init__(self, dropout: float = 0.1, hidden_ratio: float = 0.35):\n        super().__init__()\n        self.backbone, self.feat_dim, self.backbone_name, self.input_res = self._build_dino_backbone()\n        self.tile_encoder = TileEncoder(self.backbone, self.input_res)\n        self.t2t = T2TRetokenizer(self.feat_dim, depth=CFG.t2t_depth, heads=CFG.cross_heads, dropout=dropout)\n        self.cross = CrossScaleFusion(\n            self.feat_dim, heads=CFG.cross_heads, dropout=dropout, layers=CFG.cross_layers\n        )\n        self.pyramid = PyramidMixer(\n            dim_in=self.feat_dim,\n            dims=CFG.pyramid_dims,\n            mobilevit_heads=CFG.mobilevit_heads,\n            mobilevit_depth=CFG.mobilevit_depth,\n            sra_heads=CFG.sra_heads,\n            sra_ratio=CFG.sra_ratio,\n            mamba_depth=CFG.mamba_depth,\n            mamba_kernel=CFG.mamba_kernel,\n            dropout=dropout,\n        )\n\n        combined = CFG.pyramid_dims[-1] * 2\n        self.combined_dim = combined\n        hidden = max(32, int(combined * hidden_ratio))\n\n        def head():\n            return nn.Sequential(\n                nn.Linear(combined, hidden),\n                nn.GELU(),\n                nn.Dropout(dropout),\n                nn.Linear(hidden, 1),\n            )\n\n        self.head_green = head()\n        self.head_clover = head()\n        self.head_dead = head()\n        self.score_head = nn.Sequential(nn.LayerNorm(combined), nn.Linear(combined, 1))\n        self.aux_head = (\n            nn.Sequential(nn.LayerNorm(CFG.pyramid_dims[1]), nn.Linear(CFG.pyramid_dims[1], 5))\n            if CFG.aux_head\n            else None\n        )\n        self.softplus = nn.Softplus(beta=1.0)\n\n        self.cross_gate_left = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n        self.cross_gate_right = nn.Linear(CFG.pyramid_dims[-1], CFG.pyramid_dims[-1])\n\n    def _build_dino_backbone(self):\n        \"\"\"\n        只创建骨干结构，不加载预训练权重（pretrained=False），\n        避免在 Kaggle 无网环境下下载；权重由 checkpoint 提供。\n        \"\"\"\n        last_err = None\n        for name in CFG.dino_candidates:\n            for gp in [\"token\", \"avg\", \"__default__\"]:\n                try:\n                    if gp == \"__default__\":\n                        m = timm.create_model(name, pretrained=False, num_classes=0)\n                        gp_str = \"default\"\n                    else:\n                        m = timm.create_model(name, pretrained=False, num_classes=0, global_pool=gp)\n                        gp_str = gp\n                    feat = m.num_features\n                    input_res = self._infer_input_res(m)\n                    LOGGER.info(\n                        f\"✅ 使用 DINO 主干: {name} | global_pool={gp_str} | \"\n                        f\"feat_dim={feat} | input_res={input_res}\"\n                    )\n                    if hasattr(m, \"set_grad_checkpointing\"):\n                        m.set_grad_checkpointing(True)\n                    return m, feat, name, int(input_res)\n                except Exception as e:\n                    last_err = e\n                    continue\n        raise RuntimeError(f\"无法创建任何 DINO 主干。最后错误: {last_err}\")\n\n    @staticmethod\n    def _infer_input_res(m) -> int:\n        if hasattr(m, \"patch_embed\") and hasattr(m.patch_embed, \"img_size\"):\n            isz = m.patch_embed.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        if hasattr(m, \"img_size\"):\n            isz = m.img_size\n            return int(isz if isinstance(isz, (int, float)) else isz[0])\n        dc = getattr(m, \"default_cfg\", {}) or {}\n        ins = dc.get(\"input_size\", None)\n        if ins:\n            if isinstance(ins, (tuple, list)) and len(ins) >= 2:\n                return int(ins[1])\n            return int(ins if isinstance(ins, (int, float)) else 224)\n        return 518\n\n    def _half_forward(self, x_half: torch.Tensor):\n        tiles_small = self.tile_encoder(x_half, CFG.small_grid)\n        tiles_big = self.tile_encoder(x_half, CFG.big_grid)\n        t2, stage1_map = self.t2t(tiles_small, CFG.small_grid)\n        fused = self.cross(t2, tiles_big)\n        feat, feat_maps = self.pyramid(fused)\n        feat_maps[\"stage1_map\"] = stage1_map\n        return feat, feat_maps\n\n    def _merge_heads(self, f_l: torch.Tensor, f_r: torch.Tensor):\n        g_l = torch.sigmoid(self.cross_gate_left(f_r))\n        g_r = torch.sigmoid(self.cross_gate_right(f_l))\n        f_l = f_l * g_l\n        f_r = f_r * g_r\n        f = torch.cat([f_l, f_r], dim=1)\n        green_pos = self.softplus(self.head_green(f))\n        clover_pos = self.softplus(self.head_clover(f))\n        dead_pos = self.softplus(self.head_dead(f))\n        gdm = green_pos + clover_pos\n        total = gdm + dead_pos\n        return total, gdm, green_pos, f\n\n    def _param_device_dtype(self):\n        try:\n            ref = next(self.parameters())\n            return ref.device, ref.dtype\n        except StopIteration:\n            return torch.device(\"cpu\"), torch.float32\n\n    def _empty_forward_output(self, device=None, dtype=None, return_features: bool = False):\n        if device is None or dtype is None:\n            device_p, dtype_p = self._param_device_dtype()\n            if device is None:\n                device = device_p\n            if dtype is None:\n                dtype = dtype_p\n\n        zero = torch.zeros(0, 1, device=device, dtype=dtype)\n        out = {\n            \"total\": zero,\n            \"gdm\": zero,\n            \"green\": zero,\n            \"score_feat\": torch.zeros(0, self.combined_dim, device=device, dtype=dtype),\n        }\n        if self.aux_head is not None:\n            out[\"aux\"] = torch.zeros(0, len(CFG.ALL_TARGET_COLS), device=device, dtype=dtype)\n        if return_features:\n            out[\"feature_maps\"] = {}\n        return out\n\n    def forward(self, *inputs, x_left=None, x_right=None, return_features: bool = False):\n        # 兼容 DataParallel 传参方式（单 tensor / 元组）\n        if inputs:\n            if len(inputs) == 1:\n                first = inputs[0]\n                if isinstance(first, (tuple, list)):\n                    if len(first) >= 1:\n                        x_left = first[0]\n                    if len(first) >= 2:\n                        x_right = first[1]\n                else:\n                    x_left = first\n            else:\n                x_left = inputs[0]\n                x_right = inputs[1]\n\n        if x_left is None:\n            return self._empty_forward_output(return_features=return_features)\n        if isinstance(x_left, torch.Tensor) and x_left.shape[0] == 0:\n            return self._empty_forward_output(return_features=return_features)\n\n        if x_right is None:\n            if isinstance(x_left, torch.Tensor):\n                if x_left.shape[1] % 2 != 0:\n                    raise ValueError(\"无法从单个张量推断左右分支，请显式提供 x_right。\")\n                x_left, x_right = torch.chunk(x_left, 2, dim=1)\n            else:\n                raise ValueError(\"缺少 x_right 输入。\")\n\n        feat_l, feats_l = self._half_forward(x_left)\n        feat_r, feats_r = self._half_forward(x_right)\n        total, gdm, green, f_concat = self._merge_heads(feat_l, feat_r)\n\n        out = {\n            \"total\": total,\n            \"gdm\": gdm,\n            \"green\": green,\n            \"score_feat\": f_concat,\n        }\n        if self.aux_head is not None:\n            aux_tokens = torch.cat([feats_l[\"stage2_tokens\"], feats_r[\"stage2_tokens\"]], dim=1)\n            aux_pred = self.softplus(self.aux_head(aux_tokens.mean(dim=1)))\n            out[\"aux\"] = aux_pred  # 顺序与 CFG.ALL_TARGET_COLS 对齐\n        if return_features:\n            out[\"feature_maps\"] = {\n                \"stage1_left\": feats_l.get(\"stage1_map\"),\n                \"stage1_right\": feats_r.get(\"stage1_map\"),\n                \"stage3_left\": feats_l.get(\"stage3_tokens\"),\n                \"stage3_right\": feats_r.get(\"stage3_tokens\"),\n            }\n        return out\n\n\n# =============================================================================\n# 推理配置 (Modified)\n# =============================================================================\nclass INF_CFG:\n    # 数据路径（Kaggle 默认，本地测试可修改）\n    BASE_PATH = \"/kaggle/input/csiro-biomass\"\n    TEST_CSV = os.path.join(BASE_PATH, \"test.csv\")\n    TEST_IMAGE_DIR = os.path.join(BASE_PATH, \"test\")\n\n    # [修改] 实验目录（你的权重所在目录）\n    EXPERIMENT_DIR = \"/kaggle/input/csiro/pytorch/default/12\"\n\n    # Checkpoint 路径（5-fold）\n    CKPT_PATTERN_FOLD_X = os.path.join(\n        EXPERIMENT_DIR, \"fold_{fold}\", \"checkpoints\", \"best_wr2.pt\"\n    )\n    CKPT_PATTERN_FOLDX = os.path.join(\n        EXPERIMENT_DIR, \"fold{fold}\", \"checkpoints\", \"best_wr2.pt\"\n    )\n    N_FOLDS = 5\n\n    # [修改] 输出文件名称\n    SUBMISSION_FILE = \"preds_dino_wide.csv\"\n\n    # 推理设置\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    BATCH_SIZE = 1\n    NUM_WORKERS = 0\n    MIXED_PRECISION = True\n\n    # TTA 设置\n    USE_TTA = True\n    TTA_TRANSFORMS = [\"original\", \"hflip\", \"vflip\"]\n\n    # 目标列顺序（与训练一致）\n    ALL_TARGET_COLS = [\"Dry_Green_g\", \"Dry_Dead_g\", \"Dry_Clover_g\", \"GDM_g\", \"Dry_Total_g\"]\n\n\nprint(f\"Device: {INF_CFG.DEVICE}\")\nprint(f\"Experiment Dir: {INF_CFG.EXPERIMENT_DIR}\")\n\n\n# =============================================================================\n# 数据集\n# =============================================================================\nclass TestBiomassDataset(Dataset):\n    \"\"\"测试数据集：左右两路输入\"\"\"\n\n    def __init__(self, df: pd.DataFrame, transform, image_dir: str):\n        self.df = df.reset_index(drop=True)\n        self.transform = transform\n        self.image_dir = image_dir\n        self.paths = self.df[\"image_path\"].values\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        filename = os.path.basename(self.paths[idx])\n        full_path = os.path.join(self.image_dir, filename)\n\n        img = cv2.imread(full_path)\n        if img is None:\n            # 容错：若读图失败，用黑图占位\n            img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n        else:\n            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n\n        # 左右切半（与训练一致）\n        h, w, _ = img.shape\n        mid = w // 2\n        left = img[:, :mid]\n        right = img[:, mid:]\n\n        left_t = self.transform(image=left)[\"image\"]\n        right_t = self.transform(image=right)[\"image\"]\n\n        return left_t, right_t\n\n\n# =============================================================================\n# TTA 变换\n# =============================================================================\ndef get_tta_transforms(img_size: int) -> List[A.Compose]:\n    \"\"\"生成 TTA 变换列表\"\"\"\n    base = [\n        A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n        ToTensorV2(),\n    ]\n\n    transforms = []\n\n    # 原图\n    transforms.append(\n        A.Compose([\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    # 水平翻转\n    transforms.append(\n        A.Compose([\n            A.HorizontalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    # 垂直翻转\n    transforms.append(\n        A.Compose([\n            A.VerticalFlip(p=1.0),\n            A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA),\n            *base,\n        ])\n    )\n\n    return transforms\n\n\n# =============================================================================\n# 权重加载\n# =============================================================================\ndef strip_module_prefix(state_dict: dict) -> dict:\n    \"\"\"移除 DataParallel 的 module. 前缀\"\"\"\n    if not state_dict:\n        return state_dict\n\n    keys = list(state_dict.keys())\n    if all(k.startswith(\"module.\") for k in keys):\n        return {k[len(\"module.\"):]: v for k, v in state_dict.items()}\n    return state_dict\n\n\ndef load_checkpoint(path: str) -> dict:\n    \"\"\"加载 checkpoint\"\"\"\n    if not os.path.exists(path):\n        raise FileNotFoundError(f\"Checkpoint not found: {path}\")\n\n    try:\n        state = torch.load(path, map_location=\"cpu\", weights_only=False)\n    except TypeError:\n        state = torch.load(path, map_location=\"cpu\")\n\n    return state\n\n\ndef load_model_from_checkpoint(ckpt_path: str) -> nn.Module:\n    \"\"\"从 checkpoint 加载模型\"\"\"\n    print(f\"\\n加载 checkpoint: {os.path.basename(ckpt_path)}\")\n\n    state = load_checkpoint(ckpt_path)\n\n    # 用 checkpoint 中的 cfg 覆盖当前 CFG（保证结构对应）\n    cfg_dict = state.get(\"cfg\", {})\n    update_cfg_from_checkpoint(cfg_dict)\n\n    dropout = cfg_dict.get(\"dropout\", CFG.dropout)\n    hidden_ratio = cfg_dict.get(\"hidden_ratio\", CFG.hidden_ratio)\n\n    # 创建模型（结构与训练一致）\n    model = CrossPVT_T2T_MambaDINO(dropout=dropout, hidden_ratio=hidden_ratio)\n\n    # 提取模型状态\n    model_state = state.get(\"model_state\")\n    if model_state is None:\n        # 如果 checkpoint 直接是 state_dict\n        model_state = state\n\n    # 移除 module. 前缀\n    model_state = strip_module_prefix(model_state)\n\n    # 加载权重\n    missing_keys, unexpected_keys = model.load_state_dict(model_state, strict=False)\n\n    if missing_keys:\n        print(f\"  ⚠️  缺失的键: {len(missing_keys)} 个\")\n        if len(missing_keys) <= 10:\n            for k in missing_keys[:10]:\n                print(f\"    - {k}\")\n\n    if unexpected_keys:\n        print(f\"  ⚠️  意外的键: {len(unexpected_keys)} 个\")\n        if len(unexpected_keys) <= 10:\n            for k in unexpected_keys[:10]:\n                print(f\"    - {k}\")\n\n    model.to(INF_CFG.DEVICE)\n    model.eval()\n\n    # 获取输入分辨率\n    input_res = getattr(model, \"input_res\", 518)\n    backbone_name = getattr(model, \"backbone_name\", \"unknown\")\n\n    print(f\"  ✓ 模型加载成功 | backbone={backbone_name} | input_res={input_res}\")\n\n    return model\n\n\n# =============================================================================\n# 推理\n# =============================================================================\ndef pack5_targets(total: torch.Tensor, gdm: torch.Tensor, green: torch.Tensor) -> torch.Tensor:\n    \"\"\"将 total, gdm, green 打包为 5 个目标\"\"\"\n    clover = gdm - green\n    dead = total - gdm\n    return torch.cat([green, dead, clover, gdm, total], dim=1)\n\n\n@torch.no_grad()\ndef predict_one_view(models: List[nn.Module], loader: DataLoader) -> np.ndarray:\n    \"\"\"对单个 TTA 视角进行预测\"\"\"\n    preds_list = []\n    amp_dtype = \"cuda\" if INF_CFG.DEVICE.type == \"cuda\" else \"cpu\"\n\n    for xl, xr in tqdm(loader, desc=\"  Predicting\", leave=False):\n        xl = xl.to(INF_CFG.DEVICE, non_blocking=True)\n        xr = xr.to(INF_CFG.DEVICE, non_blocking=True)\n\n        # 拼接为单 tensor（与训练时的 DataParallel 调用方式一致）\n        x_cat = torch.cat([xl, xr], dim=1)\n\n        per_model_preds = []\n\n        with torch.amp.autocast(amp_dtype, enabled=INF_CFG.MIXED_PRECISION):\n            for model in models:\n                out = model(x_cat, return_features=False)\n\n                total = out[\"total\"]\n                gdm = out[\"gdm\"]\n                green = out[\"green\"]\n\n                # 打包为 5 个目标\n                five = pack5_targets(total, gdm, green)\n\n                # 非负约束\n                five = torch.clamp(five, min=0.0)\n\n                per_model_preds.append(five.float().cpu())\n\n        # 5-fold ensemble 平均\n        stacked = torch.mean(torch.stack(per_model_preds, dim=0), dim=0)\n        preds_list.append(stacked.numpy())\n\n    return np.concatenate(preds_list, axis=0)\n\n\ndef run_inference(test_df: pd.DataFrame, image_dir: str) -> np.ndarray:\n    \"\"\"运行完整推理流程（5-fold ensemble + TTA）\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"开始推理\")\n    print(\"=\" * 80)\n\n    # 加载所有 fold 的模型\n    print(\"\\n加载模型 (5-fold)...\")\n    models = []\n    input_res = None\n\n    for fold in range(INF_CFG.N_FOLDS):\n        ckpt_path = INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)\n        if not os.path.exists(ckpt_path):\n            ckpt_path = INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)\n\n        if not os.path.exists(ckpt_path):\n            print(f\"  ⚠️  Fold {fold} checkpoint 不存在，尝试路径:\")\n            print(f\"    - {INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)}\")\n            print(f\"    - {INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)}\")\n            continue\n\n        model = load_model_from_checkpoint(ckpt_path)\n        models.append(model)\n\n        if input_res is None:\n            input_res = getattr(model, \"input_res\", 518)\n            print(f\"  输入分辨率: {input_res}\")\n\n    if len(models) == 0:\n        print(\"\\n❌ 错误：没有找到任何可用的 checkpoint！\")\n        print(f\"   请检查实验目录: {INF_CFG.EXPERIMENT_DIR}\")\n        for fold in range(INF_CFG.N_FOLDS):\n            print(f\"     - {INF_CFG.CKPT_PATTERN_FOLD_X.format(fold=fold)}\")\n            print(f\"     - {INF_CFG.CKPT_PATTERN_FOLDX.format(fold=fold)}\")\n        raise RuntimeError(\"没有找到任何可用的 checkpoint！\")\n\n    print(f\"\\n✓ 成功加载 {len(models)} 个模型\")\n\n    # TTA 推理\n    if INF_CFG.USE_TTA:\n        tta_transforms = get_tta_transforms(input_res)\n        print(f\"\\n使用 TTA: {len(tta_transforms)} 个视角\")\n\n        per_view_preds = []\n\n        for i, transform in enumerate(tta_transforms):\n            view_name = INF_CFG.TTA_TRANSFORMS[i] if i < len(INF_CFG.TTA_TRANSFORMS) else f\"view_{i+1}\"\n            print(f\"\\n--- TTA 视角 {i+1}/{len(tta_transforms)}: {view_name} ---\")\n\n            ds = TestBiomassDataset(test_df, transform, image_dir)\n            dl = DataLoader(\n                ds,\n                batch_size=INF_CFG.BATCH_SIZE,\n                shuffle=False,\n                num_workers=INF_CFG.NUM_WORKERS,\n                pin_memory=True,\n            )\n\n            view_pred = predict_one_view(models, dl)\n            per_view_preds.append(view_pred)\n\n        final_pred = np.mean(per_view_preds, axis=0)\n        print(f\"\\n✓ TTA 完成，最终预测形状: {final_pred.shape}\")\n    else:\n        transform = get_tta_transforms(input_res)[0]\n        ds = TestBiomassDataset(test_df, transform, image_dir)\n        dl = DataLoader(\n            ds,\n            batch_size=INF_CFG.BATCH_SIZE,\n            shuffle=False,\n            num_workers=INF_CFG.NUM_WORKERS,\n            pin_memory=True,\n        )\n        final_pred = predict_one_view(models, dl)\n\n    return final_pred\n\n\n# =============================================================================\n# 生成提交文件 (Modified for Wide Format)\n# =============================================================================\ndef create_submission(final_pred: np.ndarray, test_long: pd.DataFrame, test_unique: pd.DataFrame) -> pd.DataFrame:\n    \"\"\"生成宽格式输出文件 (用于集成)\"\"\"\n    print(\"\\n\" + \"=\" * 80)\n    print(\"生成集成用宽格式文件\")\n    print(\"=\" * 80)\n\n    # 提取各目标 (对应 pack5_targets 的顺序: green, dead, clover, gdm, total)\n    green = final_pred[:, 0]\n    dead = final_pred[:, 1]\n    clover = final_pred[:, 2]\n    gdm = final_pred[:, 3]\n    total = final_pred[:, 4]\n\n    # 非负 & NaN/Inf 处理\n    def clean(x):\n        x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)\n        return np.maximum(0, x)\n\n    green, dead, clover, gdm, total = map(clean, [green, dead, clover, gdm, total])\n\n    # 构造 Wide DataFrame\n    wide = pd.DataFrame(\n        {\n            \"image_path\": test_unique[\"image_path\"],\n            \"Dry_Green_g\": green,\n            \"Dry_Dead_g\": dead,\n            \"Dry_Clover_g\": clover,\n            \"GDM_g\": gdm,\n            \"Dry_Total_g\": total,\n        }\n    )\n\n    # 提取 base_id 用于后续集成索引\n    wide['base_id'] = wide['image_path'].apply(lambda x: os.path.splitext(os.path.basename(x))[0])\n\n    # 调整列顺序，确保 base_id 在前，且包含所有必要的列\n    output_cols = ['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n    final_df = wide[output_cols]\n\n    # 保存文件\n    final_df.to_csv(INF_CFG.SUBMISSION_FILE, index=False)\n\n    print(f\"\\n✓ 宽格式文件已保存: {INF_CFG.SUBMISSION_FILE}\")\n    print(f\"  行数: {len(final_df)}\")\n    print(f\"\\n前 5 行预览:\")\n    print(final_df.head(5).to_string())\n    return final_df\n\n\n# =============================================================================\n# 主函数（使用 parse_known_args 避免 SystemExit:2）\n# =============================================================================\ndef parse_args():\n    parser = argparse.ArgumentParser(description=\"CSIRO v4 CrossPVT T2T Mamba Inference\")\n\n    parser.add_argument(\n        \"--test-csv\",\n        type=str,\n        default=None,\n        help=\"测试集 CSV 路径（默认: INF_CFG.TEST_CSV）\",\n    )\n    parser.add_argument(\n        \"--test-image-dir\",\n        type=str,\n        default=None,\n        help=\"测试图像目录（默认: INF_CFG.TEST_IMAGE_DIR）\",\n    )\n    parser.add_argument(\n        \"--experiment-dir\",\n        type=str,\n        default=None,\n        help=\"实验目录（checkpoint 所在位置，默认: INF_CFG.EXPERIMENT_DIR）\",\n    )\n    parser.add_argument(\n        \"--output\",\n        type=str,\n        default=None,\n        help=\"输出文件路径（默认: INF_CFG.SUBMISSION_FILE）\",\n    )\n    parser.add_argument(\n        \"--batch-size\",\n        type=int,\n        default=None,\n        help=\"批次大小（默认: INF_CFG.BATCH_SIZE）\",\n    )\n    parser.add_argument(\n        \"--no-tta\",\n        action=\"store_true\",\n        help=\"禁用 TTA\",\n    )\n\n    # 关键修改：用 parse_known_args 吞掉 Kaggle/Colab 多余参数（如 -f /tmp/xxx.json）\n    args, _ = parser.parse_known_args()\n    return args\n\n\ndef main():\n    # [新增] 设置随机种子\n    seed_everything(43)\n\n    args = parse_args()\n\n    # 更新配置\n    if args.test_csv:\n        INF_CFG.TEST_CSV = args.test_csv\n    if args.test_image_dir:\n        INF_CFG.TEST_IMAGE_DIR = args.test_image_dir\n    if args.experiment_dir:\n        INF_CFG.EXPERIMENT_DIR = args.experiment_dir\n        INF_CFG.CKPT_PATTERN_FOLD_X = os.path.join(\n            INF_CFG.EXPERIMENT_DIR, \"fold_{fold}\", \"checkpoints\", \"best_wr2.pt\"\n        )\n        INF_CFG.CKPT_PATTERN_FOLDX = os.path.join(\n            INF_CFG.EXPERIMENT_DIR, \"fold{fold}\", \"checkpoints\", \"best_wr2.pt\"\n        )\n    if args.output:\n        INF_CFG.SUBMISSION_FILE = args.output\n    if args.batch_size:\n        INF_CFG.BATCH_SIZE = args.batch_size\n    if args.no_tta:\n        INF_CFG.USE_TTA = False\n\n    print(\"=\" * 80)\n    print(\"CSIRO Image2Biomass - v4 CrossPVT T2T Mamba Inference\")\n    print(\"=\" * 80)\n    print(f\"测试 CSV: {INF_CFG.TEST_CSV}\")\n    print(f\"测试图像目录: {INF_CFG.TEST_IMAGE_DIR}\")\n    print(f\"实验目录: {INF_CFG.EXPERIMENT_DIR}\")\n    print(f\"输出文件: {INF_CFG.SUBMISSION_FILE}\")\n    print(f\"批次大小: {INF_CFG.BATCH_SIZE}\")\n    print(f\"使用 TTA: {INF_CFG.USE_TTA}\")\n\n    # 加载测试数据\n    print(\"\\n加载测试数据...\")\n    if not os.path.exists(INF_CFG.TEST_CSV):\n        raise FileNotFoundError(f\"测试 CSV 不存在: {INF_CFG.TEST_CSV}\")\n\n    test_long = pd.read_csv(INF_CFG.TEST_CSV)\n    test_unique = test_long.drop_duplicates(subset=[\"image_path\"]).reset_index(drop=True)\n    print(f\"✓ 找到 {len(test_unique)} 张独立测试图像\")\n    print(f\"  总测试样本数: {len(test_long)}\")\n\n    # 运行推理\n    final_pred = run_inference(test_unique, INF_CFG.TEST_IMAGE_DIR)\n\n    # 生成宽格式提交文件\n    submission = create_submission(final_pred, test_long, test_unique)\n\n    print(\"\\n\" + \"=\" * 80)\n    print(\"推理完成！\")\n    print(\"=\" * 80)\n\n    # 清理\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\n\nif __name__ == \"__main__\":\n    main()","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:14:36.598051Z","iopub.status.busy":"2026-01-11T03:14:36.597593Z","iopub.status.idle":"2026-01-11T03:15:56.81812Z","shell.execute_reply":"2026-01-11T03:15:56.817548Z"},"papermill":{"duration":80.227802,"end_time":"2026-01-11T03:15:56.819361","exception":false,"start_time":"2026-01-11T03:14:36.591559","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"697c2dc4","cell_type":"markdown","source":"## Model 3 ConvNext Small","metadata":{"papermill":{"duration":0.005231,"end_time":"2026-01-11T03:15:56.830483","exception":false,"start_time":"2026-01-11T03:15:56.825252","status":"completed"},"tags":[]}},{"id":"41003ed7","cell_type":"code","source":"# ====================================================================================\n# [ConvNext Small 优化版] - 增强TTA + 后处理 + 速度优化\n# ====================================================================================\nimport os\nimport gc\nimport cv2\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader, Dataset\nimport albumentations as A\nfrom albumentations.pytorch import ToTensorV2\nfrom tqdm.auto import tqdm\nfrom pathlib import Path\nimport timm\n\ndef run_convnext_inference():\n    import random\n    SEED = 43\n    # Python原生随机种子\n    random.seed(SEED)\n    # Numpy随机种子（ShiftScaleRotate依赖）\n    np.random.seed(SEED)\n    # PyTorch CPU/GPU种子\n    torch.manual_seed(SEED)\n    torch.cuda.manual_seed(SEED)\n    torch.cuda.manual_seed_all(SEED)\n    # 强制CUDA算子确定性\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n    # Python哈希种子\n    os.environ['PYTHONHASHSEED'] = str(SEED)\n    \n    print(\"🚀 Starting ConvNext Inference...\")\n    \n    # Configuration\n    model_dir = Path('/kaggle/input/csiro-exp3/convnext_exp3') \n    img_size = 1000\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n    \n    # Model Definition\n    class BiomassModelConv(nn.Module):\n        def __init__(self, model_name='convnext_small', pretrained=False):\n            super().__init__()\n            self.backbone = timm.create_model(model_name, pretrained=pretrained, num_classes=0, global_pool='avg')\n            n_combined = self.backbone.num_features * 2\n            self.head_total = nn.Sequential(nn.Linear(n_combined, n_combined//2), nn.GELU(), nn.Dropout(0.3), nn.Linear(n_combined//2, 1))\n            self.head_gdm = nn.Sequential(nn.Linear(n_combined, n_combined//2), nn.GELU(), nn.Dropout(0.3), nn.Linear(n_combined//2, 1))\n            self.head_green = nn.Sequential(nn.Linear(n_combined, n_combined//2), nn.GELU(), nn.Dropout(0.3), nn.Linear(n_combined//2, 1))\n\n            self.softplus = nn.Softplus(beta=1.0)\n\n        def forward(self, img_left, img_right):\n            l, r = self.backbone(img_left), self.backbone(img_right)\n            combined = torch.cat([l, r], dim=1)\n            return (\n                self.softplus(self.head_total(combined)),\n                self.softplus(self.head_gdm(combined)),\n                self.softplus(self.head_green(combined))\n            )\n\n    # Load Models\n    models = []\n    for fold in range(5):\n        p = model_dir / f'best_model_fold{fold}.pth'\n        if p.exists():\n            try:\n                m = BiomassModelConv()\n                st = torch.load(p, map_location=device)\n                st = {k.replace('module.', ''): v for k, v in st.items()}\n                m.load_state_dict(st)\n                m.eval().to(device)\n                \n                # PyTorch 2.0+ 加速推理\n                if hasattr(torch, 'compile'):\n                    m = torch.compile(m, mode='max-autotune')\n                \n                models.append(m)\n                print(f\"  ✅ Loaded ConvNext Fold {fold}\")\n            except Exception as e:\n                print(f\"  ❌ Error loading fold {fold}: {e}\")\n            \n    if not models:\n        print(\"⚠️ No ConvNext models found. Saving dummy file.\")\n        pd.DataFrame(columns=['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g']).to_csv('preds_convnext_wide.csv', index=False)\n        return\n\n    # Dataset\n    class TestDataset(Dataset):\n        def __init__(self, img_dir, tf):\n            self.paths = [os.path.join(img_dir, f) for f in os.listdir(img_dir) if f.lower().endswith(('.png', '.jpg'))]\n            self.tf = tf\n        def __len__(self): return len(self.paths)\n        def __getitem__(self, idx):\n            p = self.paths[idx]\n            try:\n                img = cv2.imread(p)\n                if img is None: raise ValueError(\"Image not found\")\n                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n                # 增加随机旋转增强（TTA多样性）\n                if self.tf and len(self.tf.transforms) > 1:\n                    img = self.tf(image=img)['image']\n            except:\n                img = np.zeros((img_size, img_size*2, 3), dtype=np.uint8)\n                if self.tf:\n                    img = self.tf(image=img)['image']\n                \n            h, w, _ = img.shape\n            l = img[:, :w//2]\n            r = img[:, w//2:]\n            return l, r, os.path.splitext(os.path.basename(p))[0]\n\n    # Inference with TTA\n    # 增加更多样化的TTA策略\n    tf_list = [\n        A.Compose([A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA), A.Normalize(), ToTensorV2()]),\n        A.Compose([A.HorizontalFlip(p=1), A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA), A.Normalize(), ToTensorV2()]),\n        A.Compose([A.VerticalFlip(p=1), A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA), A.Normalize(), ToTensorV2()]),\n        A.Compose([A.ShiftScaleRotate(shift_limit=0.1, scale_limit=0.1, rotate_limit=15, p=1), \n                   A.Resize(img_size, img_size, interpolation=cv2.INTER_AREA), A.Normalize(), ToTensorV2()]),\n    ]\n    \n    test_dir = '/kaggle/input/csiro-biomass/test'\n    final_results = {}\n    \n    for tf_idx, tf in enumerate(tf_list):\n        ds = TestDataset(test_dir, tf)\n        dl = DataLoader(ds, batch_size=4, num_workers=0, shuffle=False, pin_memory=True)  # 启用pin_memory加速\n        \n        with torch.no_grad():\n            for l, r, ids in tqdm(dl, desc=f\"ConvNext View {tf_idx+1}/{len(tf_list)}\"):\n                l, r = l.to(device, non_blocking=True), r.to(device, non_blocking=True)\n                \n                # Ensemble over folds\n                batch_preds = []\n                for m in models:\n                    t, g, gr = m(l, r)\n                    pred_tensor = torch.cat([t, g, gr], dim=1)\n                    batch_preds.append(pred_tensor.cpu().numpy())\n                \n                # Mean over folds\n                avg_preds = np.mean(batch_preds, axis=0)\n                \n                for i, bid in enumerate(ids):\n                    if bid not in final_results: final_results[bid] = []\n                    final_results[bid].append(avg_preds[i])\n                \n                # 主动清理缓存\n                if torch.cuda.is_available():\n                    torch.cuda.empty_cache()\n\n    # TTA Average & Format\n    rows = []\n    for bid, preds_list in final_results.items():\n        # 加权平均：原图权重更高\n        if len(preds_list) == 4:\n            weights = np.array([0.4, 0.2, 0.2, 0.2])  # 原图权重40%，其余各20%\n        else:\n            weights = np.ones(len(preds_list)) / len(preds_list)\n        \n        final_p = np.average(preds_list, axis=0, weights=weights)\n        final_p = np.maximum(final_p, 0)\n        rows.append({\n            'base_id': bid, \n            'Dry_Total_g': final_p[0], \n            'GDM_g': final_p[1], \n            'Dry_Green_g': final_p[2]\n        })\n        \n    df_out = pd.DataFrame(rows)\n    \n    # 生物学约束后处理\n    # 约束1: GDM >= Dry_Green\n    df_out['GDM_g'] = np.maximum(df_out['GDM_g'], df_out['Dry_Green_g'] * 1.05)\n    \n    # 约束2: 极端值clip\n    for col in ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']:\n        p01, p99 = df_out[col].quantile([0.01, 0.99])\n        df_out[col] = df_out[col].clip(p01 * 0.8, p99 * 1.2)\n    \n    df_out.to_csv('preds_convnext_wide.csv', index=False)\n    print(f\"✅ ConvNext predictions saved. (Processed {len(rows)} images)\")\n    \n    # 清理内存\n    del models, dl, ds\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n\nif __name__ == \"__main__\":\n    run_convnext_inference()","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:15:56.842482Z","iopub.status.busy":"2026-01-11T03:15:56.842162Z","iopub.status.idle":"2026-01-11T03:17:32.45826Z","shell.execute_reply":"2026-01-11T03:17:32.457696Z"},"papermill":{"duration":95.623698,"end_time":"2026-01-11T03:17:32.459397","exception":false,"start_time":"2026-01-11T03:15:56.835699","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"4108763c","cell_type":"markdown","source":"## Model 4 Dino3","metadata":{"papermill":{"duration":0.005959,"end_time":"2026-01-11T03:17:32.471932","exception":false,"start_time":"2026-01-11T03:17:32.465973","status":"completed"},"tags":[]}},{"id":"93b0d5a1","cell_type":"code","source":"# ====================================================================================\n# [Part 4] ViT DINO3 Tiled Inference (Score ~0.65)\n# ====================================================================================\nimport os\nimport cv2\nimport timm\nimport torch\nimport numpy as np\nimport pandas as pd\nimport torch.nn as nn\nimport albumentations as A\nimport torch.nn.functional as F\nfrom tqdm.auto import tqdm\nfrom albumentations.pytorch import ToTensorV2\nfrom torch.utils.data import Dataset, DataLoader\nimport gc\n\n# --- 1. Model Definitions (Tiled FiLM DINO) ---\ndef get_input_size(model):\n    # Helper to guess input size from timm model\n    if hasattr(model, \"patch_embed\") and hasattr(model.patch_embed, \"img_size\"):\n        return model.patch_embed.img_size[0]\n    return 518 # Default for DINOv2/v3\n\nclass FiLM(nn.Module):\n    def __init__(self, feat_dim):\n        super().__init__()\n        hidden = max(32, feat_dim // 2)\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim, hidden), nn.ReLU(inplace=True), nn.Linear(hidden, feat_dim * 2)\n        )\n    def forward(self, context):\n        gamma_beta = self.mlp(context)\n        return torch.chunk(gamma_beta, 2, dim=1)\n\nclass BaseDINO(nn.Module):\n    def __init__(self, backbone_name):\n        super().__init__()\n        self.backbone = timm.create_model(backbone_name, pretrained=False, num_classes=0)\n        self.feat_dim = self.backbone.num_features\n        self.input_size = int(get_input_size(self.backbone))\n        if isinstance(self.input_size, tuple): self.input_size = self.input_size[0]\n        \n        self.combined_dim = self.feat_dim * 2\n        hidden_size = max(8, int(self.combined_dim * 0.25))\n        dropout = 0.30\n\n        def make_head():\n            return nn.Sequential(\n                nn.Linear(self.combined_dim, hidden_size),\n                nn.ReLU(inplace=True),\n                nn.Dropout(dropout),\n                nn.Linear(hidden_size, 1),\n            )\n        self.head_green = make_head()\n        self.head_clover = make_head()\n        self.head_dead = make_head()\n        self.softplus = nn.Softplus(beta=1.0)\n\n    def merge_features(self, left_feat, right_feat):\n        combined = torch.cat([left_feat, right_feat], dim=1)\n        green = self.softplus(self.head_green(combined))\n        clover = self.softplus(self.head_clover(combined))\n        dead = self.softplus(self.head_dead(combined))\n        gdm = green + clover\n        total = gdm + dead\n        return total, gdm, green\n\ndef split_dimension(length, parts):\n    step = length // parts\n    segments = []\n    start = 0\n    for _ in range(parts - 1):\n        segments.append((start, start + step))\n        start += step\n    segments.append((start, length))\n    return segments\n\nclass TiledFiLMDINO(BaseDINO):\n    def __init__(self, backbone_name):\n        super().__init__(backbone_name)\n        self.grid = (2, 2)\n        self.film_left = FiLM(self.feat_dim)\n        self.film_right = FiLM(self.feat_dim)\n\n    def extract_tile_features(self, x):\n        B, C, H, W = x.shape\n        rows, cols = self.grid\n        row_segments = split_dimension(H, rows)\n        col_segments = split_dimension(W, cols)\n        features = []\n        for (rs, re) in row_segments:\n            for (cs, ce) in col_segments:\n                tile = x[:, :, rs:re, cs:ce]\n                # Dynamic resize if needed\n                if tile.shape[-2:] != (self.input_size, self.input_size):\n                    tile = F.interpolate(tile, size=(self.input_size, self.input_size), mode=\"bilinear\")\n                feat = self.backbone(tile)\n                features.append(feat)\n        return torch.stack(features, dim=0).permute(1, 0, 2)\n\n    def process_stream(self, x, film_layer):\n        tiles = self.extract_tile_features(x)\n        context = tiles.mean(dim=1)\n        gamma, beta = film_layer(context)\n        modulated = tiles * (1 + gamma.unsqueeze(1)) + beta.unsqueeze(1)\n        return modulated.mean(dim=1)\n\n    def forward(self, left_img, right_img):\n        left_feat = self.process_stream(left_img, self.film_left)\n        right_feat = self.process_stream(right_img, self.film_right)\n        return self.merge_features(left_feat, right_feat)\n\n# --- 2. Inference Logic ---\ndef run_dino3_inference():\n    print(\"🚀 Starting Part 4: ViT DINO3 Inference...\")\n    DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    \n    # 路径设置\n    model_dir = \"/kaggle/input/csiro-mvp-models\"  # 你的新模型存放路径\n    test_dir = '/kaggle/input/csiro-biomass/test' # 比赛测试集图片路径\n    \n    # 查找模型权重\n    if not os.path.exists(model_dir):\n        print(\"⚠️ Model dir not found. Creating dummy output.\")\n        pd.DataFrame(columns=['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g']).to_csv('preds_dino3_wide.csv', index=False)\n        return\n\n    model_paths = [os.path.join(model_dir, f\"model{i}.pth\") for i in range(1, 11)]\n    model_paths = [p for p in model_paths if os.path.exists(p)]\n    # 限制模型数量以防超时 (例如只用前5个fold)\n    model_paths = model_paths[:5] \n    \n    if not model_paths:\n        print(\"⚠️ No .pth files found in dir. Creating dummy output.\")\n        pd.DataFrame(columns=['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g']).to_csv('preds_dino3_wide.csv', index=False)\n        return\n\n    # 尝试匹配架构\n    backbones = [\n        \"vit_base_patch14_reg4_dinov3\", \"vit_base_patch14_dinov3\", \n        \"vit_small_patch14_dinov3\", \"vit_base_patch14_reg4_dinov2\"\n    ]\n\n    # 加载模型\n    models = []\n    print(f\"  Found {len(model_paths)} checkpoints.\")\n    \n    for path in model_paths:\n        try:\n            ckpt = torch.load(path, map_location=DEVICE)\n            # Clean state dict keys\n            st = ckpt['state_dict'] if 'state_dict' in ckpt else ckpt\n            st = {k.replace('module.', '').replace('student.', ''): v for k,v in st.items()}\n            \n            loaded = False\n            for bb in backbones:\n                try:\n                    m = TiledFiLMDINO(bb)\n                    # Use strict=False to ignore minor mismatches if any\n                    m.load_state_dict(st, strict=False) \n                    m.to(DEVICE).eval()\n                    models.append(m)\n                    loaded = True\n                    # print(f\"    Loaded {os.path.basename(path)} as {bb}\")\n                    break\n                except:\n                    continue\n            if not loaded: print(f\"    Could not match architecture for {path}\")\n        except Exception as e:\n            print(f\"    Error loading {path}: {e}\")\n\n    if not models:\n        print(\"⚠️ Failed to load any models.\")\n        return\n\n    # 准备数据集\n    input_size = models[0].input_size\n    print(f\"  Input size inferred: {input_size}\")\n\n    class SimpleTestDataset(Dataset):\n        def __init__(self, img_dir, tf):\n            self.files = [f for f in os.listdir(img_dir) if f.lower().endswith(('.png', '.jpg'))]\n            self.img_dir = img_dir\n            self.tf = tf\n        def __len__(self): return len(self.files)\n        def __getitem__(self, idx):\n            f = self.files[idx]\n            path = os.path.join(self.img_dir, f)\n            img = cv2.imread(path)\n            if img is None: img = np.zeros((1000, 2000, 3), dtype=np.uint8)\n            else: img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n            \n            h, w, _ = img.shape\n            l = self.tf(image=img[:, :w//2])['image']\n            r = self.tf(image=img[:, w//2:])['image']\n            return l, r, os.path.splitext(f)[0]\n\n    # TTA Transforms\n    norm = [A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), ToTensorV2()]\n    ttas = [\n        A.Compose([A.Resize(input_size, input_size, interpolation=cv2.INTER_AREA), *norm]),\n        A.Compose([A.HorizontalFlip(p=1), A.Resize(input_size, input_size, interpolation=cv2.INTER_AREA), *norm]),\n    ]\n    \n    # Inference\n    final_results = {} # id -> list of [total, gdm, green]\n\n    for tta_idx, tf in enumerate(ttas):\n        ds = SimpleTestDataset(test_dir, tf)\n        dl = DataLoader(ds, batch_size=4, num_workers=0, shuffle=False)\n        \n        with torch.no_grad():\n            for l_img, r_img, ids in tqdm(dl, desc=f\"DINO3 TTA {tta_idx+1}\"):\n                l_img, r_img = l_img.to(DEVICE), r_img.to(DEVICE)\n                \n                # Stack preds from all loaded fold models\n                fold_preds = []\n                for m in models:\n                    p_tot, p_gdm, p_grn = m(l_img, r_img)\n                    # Stack: (B, 3)\n                    pred = torch.stack([p_tot, p_gdm, p_grn], dim=1).squeeze(-1) \n                    fold_preds.append(pred.cpu().numpy())\n                \n                # Average over folds\n                avg_folds = np.mean(fold_preds, axis=0) # (B, 3)\n                \n                for i, base_id in enumerate(ids):\n                    if base_id not in final_results: final_results[base_id] = []\n                    final_results[base_id].append(avg_folds[i])\n\n    # Aggregate TTA and Format\n    rows = []\n    for bid, preds_list in final_results.items():\n        # Average over TTA\n        p = np.mean(preds_list, axis=0)\n        p = np.maximum(p, 0)\n        rows.append({\n            'base_id': bid,\n            'Dry_Total_g': p[0],\n            'GDM_g': p[1],\n            'Dry_Green_g': p[2]\n        })\n        \n    df = pd.DataFrame(rows)\n    df.to_csv('preds_dino3_wide.csv', index=False)\n    print(f\"✅ DINO3 predictions saved. Shape: {df.shape}\")\n    \n    # Cleanup\n    del models, dl, ds\n    gc.collect()\n    torch.cuda.empty_cache()\n\nif __name__ == \"__main__\":\n    run_dino3_inference()","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:17:32.485144Z","iopub.status.busy":"2026-01-11T03:17:32.484932Z","iopub.status.idle":"2026-01-11T03:18:02.610642Z","shell.execute_reply":"2026-01-11T03:18:02.609804Z"},"papermill":{"duration":30.134112,"end_time":"2026-01-11T03:18:02.611978","exception":false,"start_time":"2026-01-11T03:17:32.477866","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"9e4f46ba","cell_type":"markdown","source":"## Model5 SigEngine","metadata":{"papermill":{"duration":0.006471,"end_time":"2026-01-11T03:18:02.625114","exception":false,"start_time":"2026-01-11T03:18:02.618643","status":"completed"},"tags":[]}},{"id":"1fe19339","cell_type":"code","source":"import os\nimport glob\nfrom pathlib import Path\nimport sys\nfrom tqdm.auto import tqdm\nimport json\nfrom copy import deepcopy\n\nimport pandas as pd\nimport numpy as np\nimport os\nimport math\nimport random\n\nimport torch\nimport torch.nn as nn\nfrom torchvision import transforms\n\nfrom PIL import Image\nimport cv2\n\nfrom transformers import AutoProcessor, AutoImageProcessor, AutoModel, Siglip2Model, Siglip2ImageProcessor, SiglipModel, SiglipImageProcessor\nfrom sklearn.model_selection import KFold, GroupKFold\nfrom sklearn.linear_model import LinearRegression, Ridge, Lasso, BayesianRidge\nfrom sklearn.svm import SVR\nfrom sklearn.ensemble import GradientBoostingRegressor, HistGradientBoostingRegressor, ExtraTreesRegressor\nfrom xgboost import XGBRegressor\nfrom lightgbm import LGBMRegressor\nfrom catboost import CatBoostRegressor\nfrom sklearn.svm import LinearSVR\nfrom sklearn.dummy import DummyRegressor\n\nfrom sklearn.decomposition import PCA\nfrom sklearn import model_selection\nfrom sklearn import preprocessing\n\nfrom dataclasses import dataclass\nfrom typing import Optional, Dict\n\nimport matplotlib.pyplot as plt\n\ndef seeding(SEED):\n    np.random.seed(SEED)\n    random.seed(SEED)\n    os.environ['PYTHONHASHSEED'] = str(SEED)\n    torch.manual_seed(SEED)\n    # pl.seed_everything(SEED)\n    if torch.cuda.is_available(): \n        torch.cuda.manual_seed(SEED)\n        torch.cuda.manual_seed_all(SEED)\n        torch.backends.cudnn.deterministic = True\n        torch.backends.cudnn.benchmark = False\n    print('seeding done!!!')\n\ndef flush():\n    import gc\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n        torch.cuda.reset_peak_memory_stats()\n\nseeding(43)\n\n@dataclass\nclass Config:\n    # Data paths\n    DATA_PATH: Path = Path(\"/kaggle/input/csiro-biomass/\")\n    TRAIN_DATA_PATH: Path = DATA_PATH/'train'\n    TEST_DATA_PATH: Path = DATA_PATH/'test'\n\n    device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    seed = 42\n\ncfg = Config()\nseeding(cfg.seed)\n\ndef pivot_table(df: pd.DataFrame)->pd.DataFrame:\n\n    if 'target' in df.columns.tolist():\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index=['image_path', 'Sampling_Date', 'State', 'Species', 'Pre_GSHH_NDVI', 'Height_Ave_cm'], \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    else:\n        df['target'] = 0\n        df_pt = pd.pivot_table(\n            df, \n            values='target', \n            index='image_path', \n            columns='target_name', \n            aggfunc='mean'\n        ).reset_index()\n    return df_pt\n\n# train_df = pd.read_csv(cfg.DATA_PATH/'train.csv')\ntest_df = pd.read_csv(cfg.DATA_PATH/'test.csv')\n# train_df = pivot_table(df=train_df)\ntest_df = pivot_table(df=test_df)\n\n# train_df['image_path'] = train_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / p))\ntest_df['image_path'] = test_df['image_path'].apply(lambda p: str(cfg.DATA_PATH / p))\n# test_df.head()\n\ntrain_df = pd.read_csv(\"/kaggle/input/k/samu2505/csiro-datasplit/csiro_data_split.csv\")\n\n# 1. 定义要排除的 ID (转换为字符串以确保匹配)\nexclude_ids = [\n    '230058600', '1403107574', \n    'ID230058600', 'ID1403107574'\n]\n\n# 2. 确保从 image_path 中提取出的 ID 与排除列表格式一致\n# 截图中的路径是：/kaggle/input/csiro-biomass/train/ID1011485656.jpg\ndef extract_id(path):\n    # os.path.basename 得到 \"ID1011485656.jpg\"\n    # split('.')[0] 得到 \"ID1011485656\"\n    return os.path.basename(str(path)).split('.')[0]\n\ntrain_df['temp_check_id'] = train_df['image_path'].apply(extract_id)\n\n# 3. 执行过滤\ninitial_count = len(train_df)\ntrain_df = train_df[~train_df['temp_check_id'].isin(exclude_ids)].reset_index(drop=True)\n\n# 4. 删除临时列\ntrain_df = train_df.drop(columns=['temp_check_id'])\n\nprint(f\"⚠️ 数据清洗结果：\")\nprint(f\"   原始行数: {initial_count}\")\nprint(f\"   过滤后行数: {len(train_df)}\")\n\n\ndef melt_table(df: pd.DataFrame) -> pd.DataFrame:\n    TARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    melted = df.melt(\n        id_vars='image_path',\n        value_vars=TARGET_NAMES,\n        var_name='target_name',\n        value_name='target'\n    )\n    melted['sample_id'] = (\n        melted['image_path']\n        .str.replace(r'^.*/', '', regex=True)  # remove folder path, keep filename\n        .str.replace('.jpg', '', regex=False)  # remove extension\n        + '__' + melted['target_name']\n    )\n    \n    return melted[['sample_id', 'image_path', 'target_name', 'target']]\n\n\ndef split_image(image, patch_size=520, overlap=16):\n    h, w, c = image.shape\n    stride = patch_size - overlap\n    \n    patches = []\n    coords  = []   # (y1, x1, y2, x2)\n    \n    for y in range(0, h, stride):\n        for x in range(0, w, stride):\n            y1 = y\n            x1 = x\n            y2 = y + patch_size\n            x2 = x + patch_size\n            \n            # Pad last patch if needed (very rare with your fixed 1000×2000)\n            patch = image[y1:y2, x1:x2, :]\n            if patch.shape[0] < patch_size or patch.shape[1] < patch_size:\n                pad_h = patch_size - patch.shape[0]\n                pad_w = patch_size - patch.shape[1]\n                patch = np.pad(patch, ((0,pad_h), (0,pad_w), (0,0)), mode='reflect')\n            \n            patches.append(patch)\n            coords.append((y1, x1, y2, x2))\n    \n    return patches, coords\n\ndef get_model(model_path: str, device: str = 'cpu'):\n    model = AutoModel.from_pretrained(\n        model_path,\n        local_files_only=True\n    )\n    processor = AutoImageProcessor.from_pretrained(model_path)\n    return model.eval().to(device), processor\n\ndino_path = \"/kaggle/input/dinov2/pytorch/giant/1\"\nsiglip_path = \"/kaggle/input/google-siglip-so400m-patch14-384/transformers/default/1\"\n\ndef compute_embeddings(model_path, df, patch_size=520):\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    print(device)\n\n    model, processor = get_model(\n        model_path=model_path, device=device\n    )\n\n    IMAGE_PATHS = []\n    EMBEDDINGS = []\n\n    for i, row in tqdm(df.iterrows(), total=len(df)):\n        img_path = row['image_path']\n        img = cv2.imread(img_path, cv2.IMREAD_COLOR)\n        patches, coords = split_image(img, patch_size=patch_size)\n        images = [Image.fromarray(p).convert(\"RGB\") for p in patches]\n\n        inputs = processor(images=images, return_tensors=\"pt\").to(model.device)\n        with torch.no_grad():\n            if 'siglip' in model_path:\n                features = model.get_image_features(**inputs)\n            elif 'dino' in model_path:\n                features = model(**inputs).pooler_output\n                # patches = model(**inputs).last_hidden_state\n                # features = patches[:, 0, :]\n            else:\n                raise Exception(\"Model should be dino or siglip\")\n        embeds = features.mean(dim=0).detach().cpu().numpy()\n        EMBEDDINGS.append(embeds)\n        IMAGE_PATHS.append(img_path)\n\n    embeddings = np.stack(EMBEDDINGS, axis=0)\n    n_features = embeddings.shape[1]\n    emb_columns = [f\"emb{i+1}\" for i in range(n_features)]\n    emb_df = pd.DataFrame(embeddings, columns=emb_columns)\n    emb_df['image_path'] = IMAGE_PATHS\n    df_final = df.merge(emb_df, on='image_path', how='left')\n    flush()\n    return df_final \n\n# train_siglip_df = compute_embeddings(model_path=siglip_path, df=train_df, patch_size=520)\ntrain_siglip_df = train_df.copy()\ntest_siglip_df = compute_embeddings(model_path=siglip_path, df=test_df, patch_size=520)\n\nflush()\n\ndef generate_semantic_features(image_embeddings, model_path=siglip_path):\n    \"\"\"\n    Generates 'Concept Scores' by averaging synonyms and calculating biological ratios.\n    \"\"\"\n    print(f\"Loading SigLIP Text Encoder from {model_path}...\")\n    device = 'cuda' if torch.cuda.is_available() else 'cpu'\n    \n    try:\n        model = AutoModel.from_pretrained(model_path).to(device)\n        tokenizer = AutoTokenizer.from_pretrained(model_path)\n    except Exception as e:\n        print(f\"Error loading model: {e}\")\n        return None\n\n    # 1. Define Concept Ensembles (Grouping synonyms reduces noise)\n    concept_groups = {\n        # Quantity Anchors\n        \"bare\": [\"bare soil\", \"dirt ground\", \"sparse vegetation\", \"exposed earth\"],\n        \"sparse\": [\"low density pasture\", \"thin grass\", \"short clipped grass\"],\n        \"medium\": [\"average pasture cover\", \"medium height grass\", \"grazed pasture\"],\n        \"dense\": [\"dense tall pasture\", \"thick grassy volume\", \"high biomass\", \"overgrown vegetation\"],\n        \n        # State Anchors\n        \"green\": [\"lush green vibrant pasture\", \"photosynthesizing leaves\", \"fresh growth\"],\n        \"dead\": [\"dry brown dead grass\", \"yellow straw\", \"senesced material\", \"standing hay\"],\n        \n        # Species Anchors\n        \"clover\": [\"white clover\", \"trifolium repens\", \"broadleaf legume\", \"clover flowers\"],\n        \"grass\": [\"ryegrass\", \"blade-like leaves\", \"fescue\", \"grassy sward\"],\n        \"weeds\": [\"broadleaf weeds\", \"thistles\", \"non-pasture vegetation\"]\n    }\n    \n    # 2. Encode and Average Prompts for each Concept\n    concept_vectors = {}\n    with torch.no_grad():\n        for name, prompts in concept_groups.items():\n            inputs = tokenizer(prompts, padding=\"max_length\", return_tensors=\"pt\").to(device)\n            emb = model.get_text_features(**inputs)\n            emb = emb / emb.norm(p=2, dim=-1, keepdim=True)\n            # Average the embeddings of synonyms to get a stable \"Concept Vector\"\n            concept_vectors[name] = emb.mean(dim=0, keepdim=True)\n\n    # 3. Compute Concept Scores\n    if isinstance(image_embeddings, np.ndarray):\n        img_tensor = torch.tensor(image_embeddings, dtype=torch.float32).to(device)\n    else:\n        img_tensor = image_embeddings.to(device)\n    img_tensor = img_tensor / img_tensor.norm(p=2, dim=-1, keepdim=True)\n\n    scores = {}\n    for name, vec in concept_vectors.items():\n        # Dot product\n        scores[name] = torch.matmul(img_tensor, vec.T).cpu().numpy().flatten()\n    \n    # 4. Feature Engineering: Explicit Ratios\n    # These help models distinguish between \"High Biomass Dead\" vs \"High Biomass Green\"\n    \n    # Convert dict to DataFrame for easy math\n    df_scores = pd.DataFrame(scores)\n    \n    # A. Greenness Ratio: Green / (Green + Dead)\n    df_scores['ratio_greenness'] = df_scores['green'] / (df_scores['green'] + df_scores['dead'] + 1e-6)\n    \n    # B. Legume Fraction: Clover / (Clover + Grass)\n    df_scores['ratio_clover'] = df_scores['clover'] / (df_scores['clover'] + df_scores['grass'] + 1e-6)\n    \n    # C. Vegetation Cover: (Dense + Medium) / (Bare + Sparse)\n    df_scores['ratio_cover'] = (df_scores['dense'] + df_scores['medium']) / (df_scores['bare'] + df_scores['sparse'] + 1e-6)\n    \n    # D. \"Volume\": Max of density anchors\n    df_scores['max_density'] = df_scores[['bare', 'sparse', 'medium', 'dense']].max(axis=1)\n\n    print(f\"Generated {df_scores.shape[1]} semantic features (Ensembles + Ratios).\")\n    return df_scores.values\n\n\nimport numpy as np\nfrom sklearn.base import BaseEstimator, TransformerMixin\nfrom sklearn.preprocessing import StandardScaler\nfrom sklearn.decomposition import PCA\nfrom sklearn.cross_decomposition import PLSRegression\nfrom sklearn.mixture import GaussianMixture\nfrom sklearn.linear_model import BayesianRidge\nfrom sklearn.metrics.pairwise import cosine_similarity\n\n\nclass SupervisedEmbeddingEngine(BaseEstimator, TransformerMixin):\n    def __init__(self, \n                 n_pca=0.98,  # Slightly higher to keep texture details\n                 n_pls=8,     # Keep 8, it worked well\n                 n_gmm=5,     \n                 random_state=42):\n        \n        self.n_pca = n_pca\n        self.n_pls = n_pls\n        self.n_gmm = n_gmm\n        self.random_state = random_state\n        \n        self.scaler = StandardScaler()\n        self.pca = PCA(n_components=n_pca, random_state=random_state)\n        self.pls = PLSRegression(n_components=n_pls, scale=False)\n        self.gmm = GaussianMixture(n_components=n_gmm, covariance_type='diag', random_state=random_state)\n\n        self.pls_fitted_ = False\n\n    def fit(self, X, y=None, X_semantic=None):\n        # 1. Standard Scaling on IMAGE embeddings only\n        X_scaled = self.scaler.fit_transform(X)\n        \n        # 2. Fit Unsupervised on IMAGES\n        self.pca.fit(X_scaled)\n        self.gmm.fit(X_scaled)\n        \n        # 3. Fit PLS on IMAGES (Supervised)\n        if y is not None:\n            y_clean = y.values if hasattr(y, 'values') else y\n            self.pls.fit(X_scaled, y_clean)\n            self.pls_fitted_ = True\n        \n        return self\n\n    def transform(self, X, X_semantic=None):\n        X_scaled = self.scaler.transform(X)\n        return self._generate_features(X_scaled, X_semantic)\n\n    def _generate_features(self, X_scaled, X_semantic=None):\n        features = []\n        \n        # A. PCA (Texture/Structure from Images)\n        f_pca = self.pca.transform(X_scaled)\n        features.append(f_pca)\n        \n        # B. PLS (Biomass-correlated signals from Images)\n        if self.pls_fitted_:\n            f_pls = self.pls.transform(X_scaled)\n            features.append(f_pls)\n        \n        # C. GMM (Cluster probs)\n        f_gmm = self.gmm.predict_proba(X_scaled)\n        features.append(f_gmm)\n        \n        # D. Semantic Features (LATE FUSION)\n        # We append them raw. They are already high-level signals.\n        if X_semantic is not None:\n            # Normalize semantic scores relative to themselves to match scale of PCA/PLS\n            sem_norm = (X_semantic - np.mean(X_semantic, axis=0)) / (np.std(X_semantic, axis=0) + 1e-6)\n            features.append(sem_norm)\n\n        return np.hstack(features)\n\nCOLUMNS = train_df.filter(like=\"emb\").columns\nTARGET_NAMES = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nweights = {\n    'Dry_Green_g': 0.1,\n    'Dry_Dead_g': 0.1,\n    'Dry_Clover_g': 0.1,\n    'GDM_g': 0.2,\n    'Dry_Total_g': 0.5,\n}\n\nTARGET_MAX = {\n    \"Dry_Clover_g\": 71.7865,\n    \"Dry_Dead_g\": 83.8407,\n    \"Dry_Green_g\": 157.9836,\n    \"Dry_Total_g\": 185.70,\n    \"GDM_g\": 157.9836,\n}\n\ndef competition_metric(y_true, y_pred) -> float:\n    y_weighted = 0\n    for l, label in enumerate(TARGET_NAMES):\n        y_weighted = y_weighted + y_true[:, l].mean() * weights[label]\n\n    ss_res = 0\n    ss_tot = 0\n    for l, label in enumerate(TARGET_NAMES):\n        ss_res = ss_res + ((y_true[:, l] - y_pred[:, l])**2).mean() * weights[label]\n        ss_tot = ss_tot + ((y_true[:, l] - y_weighted)**2).mean() * weights[label]\n\n    return 1 - ss_res / ss_tot\n\n\ndef post_process_biomass(df_preds):\n    \"\"\"\n    Enforces physical mass balance constraints on biomass predictions.\n    \n    Constraints enforced:\n    1. Dry_Green_g + Dry_Clover_g = GDM_g\n    2. GDM_g + Dry_Dead_g = Dry_Total_g\n    \n    Method:\n    Uses Orthogonal Projection. It finds the set of values that satisfy\n    the constraints while minimizing the Euclidean distance to the \n    original model predictions.\n    \n    Args:\n        df_preds (pd.DataFrame): DataFrame containing the 5 prediction columns.\n        \n    Returns:\n        pd.DataFrame: A new DataFrame with consistent, non-negative values.\n    \"\"\"\n    # 1. Define the specific order required for the math\n    # We treat the vector x as: [Green, Clover, Dead, GDM, Total]\n    ordered_cols = [\n        \"Dry_Green_g\", \n        \"Dry_Clover_g\", \n        \"Dry_Dead_g\", \n        \"GDM_g\", \n        \"Dry_Total_g\"\n    ]\n    \n    # Check if columns exist\n    if not all(col in df_preds.columns for col in ordered_cols):\n        missing = [c for c in ordered_cols if c not in df_preds.columns]\n        raise ValueError(f\"Input DataFrame is missing columns: {missing}\")\n\n    # 2. Extract values in the specific order -> Shape (N_samples, 5)\n    Y = df_preds[ordered_cols].values.T  # Transpose to (5, N) for matrix math\n\n    # 3. Define the Constraint Matrix C\n    # We want Cx = 0\n    # Eq 1: 1*Green + 1*Clover + 0*Dead - 1*GDM + 0*Total = 0\n    # Eq 2: 0*Green + 0*Clover + 1*Dead + 1*GDM - 1*Total = 0\n    C = np.array([\n        [1, 1, 0, -1,  0],\n        [0, 0, 1,  1, -1]\n    ])\n\n    # 4. Calculate Projection Matrix P\n    # P = I - C^T * (C * C^T)^-1 * C\n    # This projects any vector onto the null space of C (the valid subspace)\n    C_T = C.T\n    inv_CCt = np.linalg.inv(C @ C_T)\n    P = np.eye(5) - C_T @ inv_CCt @ C\n\n    # 5. Apply Projection\n    # Y_new = P * Y\n    Y_reconciled = P @ Y\n\n    # 6. Transpose back to (N, 5)\n    Y_reconciled = Y_reconciled.T\n\n    # 7. Post-correction for negatives\n    # Projection can mathematically create negative values (e.g. if Total was predicted 0)\n    # We clip to 0. Note: This might slightly break the sum equality again, \n    # but exact equality with negatives is physically impossible anyway.\n    Y_reconciled = Y_reconciled.clip(min=0)\n\n    # 8. Create Output DataFrame\n    df_out = df_preds.copy()\n    df_out[ordered_cols] = Y_reconciled\n\n    return df_out\n\ndef compare_results(oof, train_data):\n    y_oof_df = pd.DataFrame(oof, columns=TARGET_NAMES) # ensure columns match\n    # 2. Check Score BEFORE Processing\n    raw_score = competition_metric(train_data[TARGET_NAMES].values, y_oof_df.values)\n    print(f\"Raw CV Score: {raw_score:.6f}\")\n    \n    # 3. Apply Post-Processing\n    y_oof_proc = post_process_biomass(y_oof_df)\n    \n    # 4. Check Score AFTER Processing\n    proc_score = competition_metric(train_data[TARGET_NAMES].values, y_oof_proc.values)\n    print(f\"Processed CV Score: {proc_score:.6f}\")\n    \n    print(f\"Improvement: {raw_score - proc_score:.6f}\")\n\n\n# Concatenate embeddings\nX_all_emb = np.vstack([\n    train_siglip_df[COLUMNS].values, \n    test_siglip_df[COLUMNS].values\n])\n\n# Generate Semantic Probes (Using the function defined in Part 1)\n# Make sure SIGLIP_PATH is correct for your environment\nprint(\"Generating Semantic Features via SigLIP Text Encoder...\")\ntry:\n    all_semantic_scores = generate_semantic_features(X_all_emb, model_path=siglip_path)\n    \n    # Split back into Train and Test\n    n_train = len(train_siglip_df)\n    sem_train_full = all_semantic_scores[:n_train]\n    sem_test_full = all_semantic_scores[n_train:]\n    print(f\"Semantic Features Generated. Train: {sem_train_full.shape}, Test: {sem_test_full.shape}\")\n    \nexcept Exception as e:\n    print(f\"Skipping Semantic Features due to error: {e}\")\n    # Fallback to None if model path is wrong or memory fails\n    sem_train_full = None\n    sem_test_full = None\n\n\n# --- STEP 2: Updated Cross-Validation Function ---\ndef cross_validate(model, train_data, test_data, feature_engine, \n                   semantic_train=None, semantic_test=None, # <--- NEW ARGS\n                   target_transform='max', seed=42):\n\n    n_splits = train_data['fold'].nunique()\n    # Setup Targets\n    target_max_arr = np.array([TARGET_MAX[t] for t in TARGET_NAMES], dtype=float)\n    y_true = train_data[TARGET_NAMES]\n    \n    # Setup Storage\n    y_pred = pd.DataFrame(0.0, index=train_data.index, columns=TARGET_NAMES)\n    y_pred_test = np.zeros([len(test_data), len(TARGET_NAMES)], dtype=float)\n\n    for fold in range(n_splits):\n        seeding(seed*(seed//2 + fold))\n        # Create masks\n        train_mask = train_data['fold'] != fold\n        valid_mask = train_data['fold'] == fold\n        val_idx = train_data[valid_mask].index\n\n        # Raw Inputs (Embeddings)\n        X_train_raw = train_data[train_mask][COLUMNS].values\n        X_valid_raw = train_data[valid_mask][COLUMNS].values\n        X_test_raw = test_data[COLUMNS].values\n        \n        # Semantic Inputs (Slicing)\n        # We handle the case where semantic features might be None\n        sem_train_fold = semantic_train[train_mask] if semantic_train is not None else None\n        sem_valid_fold = semantic_train[valid_mask] if semantic_train is not None else None\n        \n        # Raw Targets\n        y_train = train_data[train_mask][TARGET_NAMES].values\n        y_valid = train_data[valid_mask][TARGET_NAMES].values\n\n        # ===========================\n        # 1) TRANSFORM TARGETS\n        # ===========================\n        if target_transform == 'log':\n            y_train_proc = np.log1p(y_train)\n        elif target_transform == 'max':\n            y_train_proc = y_train / target_max_arr\n        else:\n            y_train_proc = y_train\n\n        # ==========================================\n        # 2) FEATURE ENGINEERING\n        # ==========================================\n        engine = deepcopy(feature_engine)\n        \n        # FIT: Now passes y (for PLS/RFE) and Semantic Features\n        engine.fit(X_train_raw, y=y_train_proc, X_semantic=sem_train_fold)\n        \n        # TRANSFORM: Pass Semantic Features\n        x_train_eng = engine.transform(X_train_raw, X_semantic=sem_train_fold)\n        x_valid_eng = engine.transform(X_valid_raw, X_semantic=sem_valid_fold)\n        # For test, we use the full test semantic set\n        x_test_eng = engine.transform(X_test_raw, X_semantic=semantic_test)\n        \n        # ==========================================\n        # 3) TRAIN & PREDICT\n        # ==========================================\n        fold_valid_pred = np.zeros_like(y_valid)\n        fold_test_pred = np.zeros([len(test_data), len(TARGET_NAMES)])\n\n        for k in range(len(TARGET_NAMES)):\n            regr = deepcopy(model)\n            \n            # Fit model\n            regr.fit(x_train_eng, y_train_proc[:, k])\n            \n            # Predict\n            pred_valid_raw = regr.predict(x_valid_eng)\n            pred_test_raw = regr.predict(x_test_eng)\n            \n            # ===========================\n            # 4) INVERSE TRANSFORM\n            # ===========================\n            if target_transform == 'log':\n                pred_valid_inv = np.expm1(pred_valid_raw)\n                pred_test_inv = np.expm1(pred_test_raw)\n            elif target_transform == 'max':\n                pred_valid_inv = (pred_valid_raw * target_max_arr[k])\n                pred_test_inv = (pred_test_raw * target_max_arr[k])\n            else:\n                pred_valid_inv = pred_valid_raw\n                pred_test_inv = pred_test_raw\n\n            fold_valid_pred[:, k] = pred_valid_inv\n            fold_test_pred[:, k] = pred_test_inv\n\n        # Store results\n        y_pred.loc[val_idx] = fold_valid_pred\n        y_pred_test += fold_test_pred / n_splits\n        \n        if fold == 0:\n            print(f\"  [Fold 0 Info] Target: {target_transform}, Feats: {x_train_eng.shape}\")\n\n    full_cv = competition_metric(y_true.values, y_pred.values)\n    print(f\"Full CV Score: {full_cv:.6f}\")\n    \n    return y_pred.values, y_pred_test\n\n# --- STEP 3: Run Models ---\n\n# Initialize the NEW Supervised Engine\nfeat_engine = SupervisedEmbeddingEngine(\n    n_pca=0.80,\n    n_pls=8,             # Supervised signals\n    n_gmm=6,             # Soft clusters\n)\n\nprint(\"\\n###### GradientBoosting Regressor #######\")\noof_gb, pred_test_gb = cross_validate(\n    GradientBoostingRegressor(), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_gb, train_siglip_df)\n\nprint(\"\\n###### Hist Gradient Boosting Regressor ######\")\noof_hb, pred_test_hb = cross_validate(\n    HistGradientBoostingRegressor(), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_hb, train_siglip_df)\n\nprint(\"\\n##### CAT Regressor ######\")\noof_cat, pred_test_cat = cross_validate(\n    CatBoostRegressor(verbose=0), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine,\n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full\n)\ncompare_results(oof_cat, train_siglip_df)\n\nprint(\"\\n######## LGBM #######\")\noof_lgbm, pred_test_lgbm = cross_validate(\n    LGBMRegressor(verbose=-1), \n    train_siglip_df, test_siglip_df, \n    feature_engine=feat_engine, \n    semantic_train=sem_train_full,\n    semantic_test=sem_test_full,\n    target_transform='max')\ncompare_results(oof_lgbm, train_siglip_df)\n\npred_test = (\n    pred_test_hb\n    #+ pred_test_gb\n    + pred_test_cat\n    + pred_test_lgbm\n) / 3\n\n# 2. 写入数据框并进行后处理（物理约束）\ntest_df[TARGET_NAMES] = pred_test\ntest_df = post_process_biomass(test_df)\n\n# 3. 【关键】提取 base_id（横版对齐的基准）\n# 确保 ID 格式为 \"1403107574\" 这种不带后缀的形式\ntest_df['base_id'] = test_df['image_path'].apply(lambda x: os.path.splitext(os.path.basename(x))[0])\n\n# 4. 定义集成脚本需要的列顺序 (宽表格式)\noutput_cols = ['base_id', 'Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n\n# 5. 保存为横版 CSV\ntest_df[output_cols].to_csv(\"preds_siglip_engine_wide.csv\", index=False)\n\nprint(\"✅ SigEngine 已保存为横版格式，准备进入 Part 5 集成。\")\nprint(test_df[output_cols].head())\n","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:18:02.639296Z","iopub.status.busy":"2026-01-11T03:18:02.639086Z","iopub.status.idle":"2026-01-11T03:20:58.32073Z","shell.execute_reply":"2026-01-11T03:20:58.319569Z"},"papermill":{"duration":175.690809,"end_time":"2026-01-11T03:20:58.322157","exception":false,"start_time":"2026-01-11T03:18:02.631348","status":"completed"},"tags":[]},"outputs":[],"execution_count":null},{"id":"2be087e4","cell_type":"markdown","source":"## Weighted Average Ensemble","metadata":{"papermill":{"duration":0.008196,"end_time":"2026-01-11T03:20:58.338694","exception":false,"start_time":"2026-01-11T03:20:58.330498","status":"completed"},"tags":[]}},{"id":"98687be4","cell_type":"code","source":"# ====================================================================================\n# [Part 5 Final] Robust Arithmetic Ensemble & Orthogonal Projection\n# ====================================================================================\nimport pandas as pd\nimport numpy as np\nimport os\nimport glob\nimport warnings\n\n# 忽略 pandas 的 FutureWarning\nwarnings.filterwarnings('ignore')\n\ndef robust_arithmetic_ensemble_inference():\n    print(\"🏁 Starting Robust Arithmetic Ensemble (No Scaling, With Projection)...\")\n    \n    targets = ['Dry_Total_g', 'GDM_g', 'Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g']\n    \n    # 1. 定义文件和权重\n    model_configs = {\n        'DINOMamba': {'file': 'preds_dino_wide.csv',          'weight': 0.35}, \n        'D3':        {'file': 'preds_dino3_wide.csv',         'weight': 0.20}, \n        'SigEngine': {'file': 'preds_siglip_engine_wide.csv', 'weight': 0.30}, \n        'CONV':       {'file': 'preds_convnext_wide.csv',     'weight': 0.15}  \n    }\n    \n    # ❌ 移除全局缩放因子\n    # SCALE_FACTOR = 1.02 \n\n    loaded_dfs = {}\n    valid_weights = {}\n    \n    print(\"  Loading and Expanding predictions...\")\n    for name, cfg in model_configs.items():\n        fpath = cfg['file']\n        if not os.path.exists(fpath):\n            alt = os.path.join(\"/kaggle/working\", fpath)\n            if os.path.exists(alt): fpath = alt\n\n        if os.path.exists(fpath):\n            df = pd.read_csv(fpath)\n            df['base_id'] = df['base_id'].astype(str)\n            df = df.drop_duplicates(subset=['base_id']).set_index('base_id')\n            \n            # 基础清洗\n            for c in ['Dry_Total_g', 'GDM_g', 'Dry_Green_g']:\n                if c in df.columns: df[c] = df[c].clip(lower=0)\n\n            # 补全逻辑\n            if 'Dry_Dead_g' not in df.columns:\n                safe_gdm = np.minimum(df['GDM_g'], df['Dry_Total_g'])\n                df['Dry_Dead_g'] = df['Dry_Total_g'] - safe_gdm\n            \n            if 'Dry_Clover_g' not in df.columns:\n                safe_green = np.minimum(df['Dry_Green_g'], df['GDM_g'])\n                df['Dry_Clover_g'] = df['GDM_g'] - safe_green\n\n            # 对齐列\n            for t in targets:\n                if t not in df.columns: df[t] = 0.0\n            df = df[targets]\n            \n            loaded_dfs[name] = df\n            valid_weights[name] = cfg['weight']\n            print(f\"    ✅ Loaded {name:<10} | Shape: {df.shape}\")\n        else:\n            print(f\"    ⚠️ File not found: {fpath}\")\n\n    # 权重归一化\n    total_w = sum(valid_weights.values())\n    if total_w == 0: return\n    final_weights = {k: v / total_w for k, v in valid_weights.items()}\n    print(f\"\\n  Normalized Weights: {final_weights}\")\n    \n    # 对齐索引\n    base_model = max(loaded_dfs, key=lambda k: len(loaded_dfs[k]))\n    all_ids = loaded_dfs[base_model].index\n    \n    # ====================================================\n    # 5. 回归到算术平均 (Arithmetic Mean)\n    # ====================================================\n    print(\"  ⚖️ Weighted Arithmetic Averaging (Standard)...\")\n    ensemble_df = pd.DataFrame(index=all_ids, columns=targets)\n    ensemble_df[:] = 0\n    \n    for col in targets:\n        weighted_sum = 0\n        for name, weight in final_weights.items():\n            # 获取数据，填充0，非负截断\n            series = loaded_dfs[name][col].reindex(all_ids).fillna(0).clip(lower=0)\n            # 普通加权\n            weighted_sum += series * weight\n        \n        ensemble_df[col] = weighted_sum\n\n    # ❌ 移除缩放\n    # ensemble_df[targets] = ensemble_df[targets] * SCALE_FACTOR\n\n    # ====================================================\n    # 6. 正交投影修正 (Orthogonal Projection) - 这个保留！\n    # ====================================================\n    print(\"  🔧 Applying Orthogonal Projection (Physical Constraints)...\")\n    \n    def apply_projection(df):\n        ordered_cols = [\"Dry_Green_g\", \"Dry_Clover_g\", \"Dry_Dead_g\", \"GDM_g\", \"Dry_Total_g\"]\n        Y = df[ordered_cols].values.T\n        \n        # 物理约束矩阵 C (Green+Clover=GDM, Dead+GDM=Total)\n        C = np.array([\n            [1, 1, 0, -1,  0],  \n            [0, 0, 1,  1, -1]   \n        ])\n        \n        # 投影矩阵\n        P = np.eye(5) - C.T @ np.linalg.inv(C @ C.T) @ C\n        Y_new = P @ Y\n        \n        return pd.DataFrame(Y_new.T, columns=ordered_cols, index=df.index)\n\n    # 执行投影\n    corrected_df = apply_projection(ensemble_df)\n    corrected_df[targets] = corrected_df[targets].clip(lower=0)\n    ensemble_df = corrected_df[targets]\n\n    # 生成提交\n    print(\"  📝 Generating submission file...\")\n    submission_rows = []\n    targets_order = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    \n    for base_id, row in ensemble_df.iterrows():\n        for t in targets_order:\n            submission_rows.append({\n                'sample_id': f\"{base_id}__{t}\",\n                'target': float(row[t])\n            })\n            \n    sub_df = pd.DataFrame(submission_rows)\n    sub_df.to_csv(\"submission.csv\", index=False)\n    print(f\"🎉 FINAL SUBMISSION GENERATED: submission.csv\")\n    print(f\"   Shape: {sub_df.shape}\")\n    print(sub_df.head(10))\n\nif __name__ == \"__main__\":\n    robust_arithmetic_ensemble_inference()","metadata":{"execution":{"iopub.execute_input":"2026-01-11T03:20:58.356394Z","iopub.status.busy":"2026-01-11T03:20:58.35614Z","iopub.status.idle":"2026-01-11T03:20:58.411519Z","shell.execute_reply":"2026-01-11T03:20:58.41034Z"},"papermill":{"duration":0.06647,"end_time":"2026-01-11T03:20:58.413105","exception":false,"start_time":"2026-01-11T03:20:58.346635","status":"completed"},"tags":[]},"outputs":[],"execution_count":null}]}