{"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.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":59093,"databundleVersionId":7469972,"isSourceIdPinned":false}],"dockerImageVersionId":31329,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true},"papermill":{"default_parameters":{},"duration":2665.270068,"end_time":"2026-04-30T22:19:17.277751+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-04-30T21:34:52.007683+00:00","version":"2.7.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"012f3b404690405a90d730048a84980d":{"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}},"054684e6db6a474999f5dfd97504737f":{"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}},"0ba33183ea16451f8a0192b7e49f09fe":{"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}},"0e0b0289ed604738ad1b35abba5ff11e":{"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_fc8f4dae4f7f4b9c81ab7c1f28bfc8ad","placeholder":"​","style":"IPY_MODEL_aaac08c664984557884bd886a9a6f5e0","tabbable":null,"tooltip":null,"value":"fold0 ep2/3: 100%"}},"11e8bb548c444e52af25d25e6df05000":{"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_3b9e708dc13b4dc89742f4a4afd46b94","IPY_MODEL_bbb52bc64a834b4e9122b19ac65a4415","IPY_MODEL_6c2e593503a84e3abca9fb89f52b0008"],"layout":"IPY_MODEL_2d76f0b579b9439b9df890fb70121932","tabbable":null,"tooltip":null}},"1228ec16361d45e7b4e9fc26794362bc":{"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_9ab96d075a1245be96aa3bab3ddd6260","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_a79e703950df456cae0197a35abcaf7d","tabbable":null,"tooltip":null,"value":1}},"1245ced8ea884fa499b933dd000e7205":{"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}},"14c351d309a840eb8c37c2d379cf4b4e":{"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}},"22283ff06a4944ccbfe391f4d766b71c":{"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}},"2d76f0b579b9439b9df890fb70121932":{"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}},"3b9e708dc13b4dc89742f4a4afd46b94":{"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_054684e6db6a474999f5dfd97504737f","placeholder":"​","style":"IPY_MODEL_22283ff06a4944ccbfe391f4d766b71c","tabbable":null,"tooltip":null,"value":"fold0 ep3/3: 100%"}},"42c2ea78ae4046adadf4c7ef7c4d3999":{"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}},"46824cbe3db34d37b48e83a2632ba248":{"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}},"47ca274254cf4bbeacae87a2bbe82970":{"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_9b7f64ead4b44286b714b35536314c40","max":499,"min":0,"orientation":"horizontal","style":"IPY_MODEL_a8a7a92e684e4d2f9a57a05a7aedc2fe","tabbable":null,"tooltip":null,"value":499}},"498d222bd2a94e2da828e1d425d23071":{"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}},"4b009cb0ac6b4ff58ede39241779d880":{"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_46824cbe3db34d37b48e83a2632ba248","placeholder":"​","style":"IPY_MODEL_82feb96ebf414aebb0934aab27323ebb","tabbable":null,"tooltip":null,"value":" 499/499 [11:17&lt;00:00,  1.36s/it, loss=0.4494, lr=7.00e-05]"}},"4b0c117b8a824dbbb24e2f9aeff66b7a":{"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}},"4bcd1aad294f420aa25090bd421f0128":{"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_f0e7351d42884e03909515ea5349ac4d","IPY_MODEL_1228ec16361d45e7b4e9fc26794362bc","IPY_MODEL_604614ac7e1e46429c907daf100cc86b"],"layout":"IPY_MODEL_75a46620d7ab4de4aecf1bdc4ac1f61d","tabbable":null,"tooltip":null}},"538fc86d79cb47dfafb01a68b763f590":{"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":""}},"543bdcf37fdb444eb13070f1880b55ed":{"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}},"5ef4e477826e4aeb97bd9a730b00362c":{"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}},"604614ac7e1e46429c907daf100cc86b":{"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_543bdcf37fdb444eb13070f1880b55ed","placeholder":"​","style":"IPY_MODEL_1245ced8ea884fa499b933dd000e7205","tabbable":null,"tooltip":null,"value":" 1/1 [00:00&lt;00:00,  3.82it/s]"}},"644ac1847d3f48dea4b07e84890c4a89":{"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_0e0b0289ed604738ad1b35abba5ff11e","IPY_MODEL_bee6196d636c4405a6434007fcd02cf2","IPY_MODEL_4b009cb0ac6b4ff58ede39241779d880"],"layout":"IPY_MODEL_ca24c663e75f4501b2cde34c46dc89ba","tabbable":null,"tooltip":null}},"6c2e593503a84e3abca9fb89f52b0008":{"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_8f19a2c1eae048d49583b61fb0eadef3","placeholder":"​","style":"IPY_MODEL_0ba33183ea16451f8a0192b7e49f09fe","tabbable":null,"tooltip":null,"value":" 499/499 [11:02&lt;00:00,  1.31s/it, loss=0.3338, lr=5.57e-06]"}},"6c53972f9fdf4366813bf5cf4477e9a2":{"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_14c351d309a840eb8c37c2d379cf4b4e","placeholder":"​","style":"IPY_MODEL_012f3b404690405a90d730048a84980d","tabbable":null,"tooltip":null,"value":"fold0 ep1/3: 100%"}},"75a46620d7ab4de4aecf1bdc4ac1f61d":{"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}},"7a3b9596cc104603af821364f38d790b":{"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}},"7c132a34abd64aebb16194e9c2f5ee0b":{"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":""}},"7c3740580de241559e70d890fe76a40f":{"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}},"82feb96ebf414aebb0934aab27323ebb":{"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}},"8cf31c8bf6554010ada29258997d7e8a":{"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}},"8f19a2c1eae048d49583b61fb0eadef3":{"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}},"9ab96d075a1245be96aa3bab3ddd6260":{"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}},"9b7f64ead4b44286b714b35536314c40":{"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}},"a79e703950df456cae0197a35abcaf7d":{"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":""}},"a8a7a92e684e4d2f9a57a05a7aedc2fe":{"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":""}},"aaac08c664984557884bd886a9a6f5e0":{"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}},"ae38e5440ba0409fa194f5191e8fbb1f":{"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_6c53972f9fdf4366813bf5cf4477e9a2","IPY_MODEL_47ca274254cf4bbeacae87a2bbe82970","IPY_MODEL_df5a75c3e20b49199a06630f8ac93ccc"],"layout":"IPY_MODEL_42c2ea78ae4046adadf4c7ef7c4d3999","tabbable":null,"tooltip":null}},"bbb52bc64a834b4e9122b19ac65a4415":{"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_8cf31c8bf6554010ada29258997d7e8a","max":499,"min":0,"orientation":"horizontal","style":"IPY_MODEL_7c132a34abd64aebb16194e9c2f5ee0b","tabbable":null,"tooltip":null,"value":499}},"bee6196d636c4405a6434007fcd02cf2":{"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_7a3b9596cc104603af821364f38d790b","max":499,"min":0,"orientation":"horizontal","style":"IPY_MODEL_538fc86d79cb47dfafb01a68b763f590","tabbable":null,"tooltip":null,"value":499}},"ca24c663e75f4501b2cde34c46dc89ba":{"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}},"df5a75c3e20b49199a06630f8ac93ccc":{"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_7c3740580de241559e70d890fe76a40f","placeholder":"​","style":"IPY_MODEL_498d222bd2a94e2da828e1d425d23071","tabbable":null,"tooltip":null,"value":" 499/499 [15:47&lt;00:00,  1.41s/it, loss=0.4164, lr=1.71e-04]"}},"f0e7351d42884e03909515ea5349ac4d":{"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_4b0c117b8a824dbbb24e2f9aeff66b7a","placeholder":"​","style":"IPY_MODEL_5ef4e477826e4aeb97bd9a730b00362c","tabbable":null,"tooltip":null,"value":"inference: 100%"}},"fc8f4dae4f7f4b9c81ab7c1f28bfc8ad":{"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}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"a492c68d-2ddd-4701-9917-56a072dbfb07","cell_type":"markdown","source":"# HMS - self-train (no external models)\n\n**Improvements:**\n- Bipolar EEG channels (18 pairs, differential montage)\n- Butterworth bandpass 0.5-20 Hz on EEG\n- Mu-law encoding of EEG signal\n- StratifiedGroupKFold (better class balance across folds)\n- Two-stage training: Stage1 (all data) -> Stage2 (high-vote finetune)\n- 3 folds x 6+3 epochs instead of 1 fold x 3 epochs\n- LayerNorm + Dropout in classification head\n- Freq-flip TTA + more time-shift steps\n- GBDT enabled (50k rows, 400 trees)\n","metadata":{}},{"id":"09260cbf-7814-4a7d-a43d-80d746b23711","cell_type":"code","source":"import os\nimport random\nimport gc\nimport time\nimport math\nfrom dataclasses import dataclass\nfrom collections import OrderedDict\n\nos.environ.setdefault(\"HF_HUB_OFFLINE\", \"1\")\n\nimport numpy as np\nimport pandas as pd\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\n\nimport torchvision\n\ntry:\n    import timm\nexcept Exception:\n    timm = None\n\nfrom scipy.signal import butter, lfilter\nfrom sklearn.model_selection import StratifiedGroupKFold\n\nSEED = 42\nMAIN_PID = os.getpid()\n\ndef seed_everything(seed=SEED):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available() and os.getpid() == MAIN_PID:\n        torch.cuda.manual_seed_all(seed)\n\nseed_everything(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\nTARGETS = [\n    \"seizure_vote\", \"lpd_vote\", \"gpd_vote\",\n    \"lrda_vote\",    \"grda_vote\", \"other_vote\",\n]\n\n# EEG electrode layout\nEEG_COLS = [\n    \"Fp1\",\"F3\",\"C3\",\"P3\",\"F7\",\"T3\",\"T5\",\"O1\",\n    \"Fz\",\"Cz\",\"Pz\",\n    \"Fp2\",\"F4\",\"C4\",\"P4\",\"F8\",\"T4\",\"T6\",\"O2\",\"EKG\",\n]\n\n# 18 bipolar (differential) montage pairs - standard clinical practice\nBIPOLAR_PAIRS = [\n    (\"Fp1\",\"F7\"),(\"F7\",\"T3\"),(\"T3\",\"T5\"),(\"T5\",\"O1\"),\n    (\"Fp1\",\"F3\"),(\"F3\",\"C3\"),(\"C3\",\"P3\"),(\"P3\",\"O1\"),\n    (\"Fp2\",\"F8\"),(\"F8\",\"T4\"),(\"T4\",\"T6\"),(\"T6\",\"O2\"),\n    (\"Fp2\",\"F4\"),(\"F4\",\"C4\"),(\"C4\",\"P4\"),(\"P4\",\"O2\"),\n    (\"Fz\",\"Cz\"), (\"Cz\",\"Pz\"),\n]\nN_BIPOLAR = len(BIPOLAR_PAIRS)  # 18\n\n@dataclass\nclass CFG:\n    data_dir: str = os.environ.get(\n        \"HMS_DATA_DIR\",\n        \"/kaggle/input/hms-harmful-brain-activity-classification\",\n    )\n    n_folds: int = 5\n    folds_to_train: tuple = (0, 1, 2)   # 3 folds -> ensemble\n    epochs: int = 6                       # Stage 1 epochs\n    batch_size: int = 48\n    lr: float = 5e-4\n    weight_decay: float = 1e-3\n    img_h: int = 256\n    img_w: int = 256\n    window_cols: int = 300\n    backbone: str = \"tf_efficientnet_b2\"\n    backbone_dropout: float = 0.1\n    pretrained_backbone: bool = False     # NO external weights\n    warmup_epochs: float = 0.5\n    min_lr: float = 1e-6\n    # EEG\n    use_eeg: bool = True\n    eeg_target_len: int = 2000\n    eeg_cache_nfiles: int = 40\n    eeg_bandpass_low: float = 0.5\n    eeg_bandpass_high: float = 20.0\n    eeg_bandpass_order: int = 2\n    eeg_mu: float = 256.0\n    # soft labels\n    min_total_votes_s1: int = 1\n    min_total_votes_s2: int = 10\n    soft_label_temp: float = 1.25\n    label_smoothing: float = 0.02\n    # mixup\n    mixup_alpha: float = 0.5\n    mixup_p: float = 0.60\n    num_workers: int = 0\n    amp: bool = True\n    # spec augment\n    spec_aug_p: float = 0.80\n    spec_time_shift: int = 16\n    spec_freq_masks: int = 2\n    spec_time_masks: int = 2\n    spec_freq_mask_param: int = 24\n    spec_time_mask_param: int = 48\n    spec_gauss_std: float = 0.015\n    spec_gauss_p: float = 0.30\n    spec_cutmix_p: float = 0.30\n    spec_cutmix_frac: float = 0.35\n    grad_clip: float = 1.0\n    early_stop_patience: int = 4\n    early_stop_min_delta: float = 0.0\n    nn_gbdt_blend: float = 0.75\n    training_seed: int = 42\n    # TTA\n    tta: bool = True\n    tta_time_shifts: tuple = (-16, -8, 0, 8, 16)\n    tta_noise_std: float = 0.005\n    # time limits\n    max_batches_per_epoch: int = 1200\n    max_train_minutes_s1: int = 270      # ~4.5h total stage1\n    max_train_minutes_s2: int = 90       # ~1.5h total stage2\n    # Stage 2\n    epochs_s2: int = 3\n    lr_s2: float = 5e-5\n    mixup_p_s2: float = 0.30\n\ncfg = CFG()\nseed_everything(int(cfg.training_seed))\nprint(\"device:\", DEVICE)\nprint(\"N_BIPOLAR EEG channels:\", N_BIPOLAR)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"ec92319d-90c9-43ac-b292-85bd5ae0ea76","cell_type":"code","source":"def resolve_data_dir(preferred):\n    def is_hms_root(path):\n        return (\n            os.path.exists(os.path.join(path, \"train.csv\"))\n            and os.path.exists(os.path.join(path, \"test.csv\"))\n            and os.path.exists(os.path.join(path, \"sample_submission.csv\"))\n            and os.path.isdir(os.path.join(path, \"train_spectrograms\"))\n            and os.path.isdir(os.path.join(path, \"test_spectrograms\"))\n        )\n    if is_hms_root(preferred):\n        return preferred\n    kaggle_input = \"/kaggle/input\"\n    if not os.path.isdir(kaggle_input):\n        return preferred\n    candidates = []\n    for name in sorted(os.listdir(kaggle_input)):\n        top = os.path.join(kaggle_input, name)\n        if not os.path.isdir(top): continue\n        for dirpath, dirnames, _files in os.walk(top):\n            depth = dirpath[len(top):].count(os.sep)\n            if depth > 4:\n                dirnames[:] = []; continue\n            if is_hms_root(dirpath):\n                candidates.append(dirpath)\n    candidates = sorted(set(candidates), key=len)\n    if not candidates:\n        raise FileNotFoundError(\"Could not locate HMS dataset root.\")\n    for c in candidates:\n        bn = os.path.basename(c).lower()\n        if \"hms\" in bn and \"harmful\" in bn:\n            return c\n    return candidates[0]\n\n\ncfg.data_dir = resolve_data_dir(cfg.data_dir)\nprint(\"Using cfg.data_dir =\", cfg.data_dir)\n\ntrain_raw = pd.read_csv(os.path.join(cfg.data_dir, \"train.csv\"))\ntest      = pd.read_csv(os.path.join(cfg.data_dir, \"test.csv\"))\nsub       = pd.read_csv(os.path.join(cfg.data_dir, \"sample_submission.csv\"))\n\ntrain_raw[\"total_votes\"] = train_raw[TARGETS].sum(axis=1)\n\n# Stage 1: all samples >= 1 vote\ntrain_all = train_raw[train_raw[\"total_votes\"] >= 1].reset_index(drop=True).copy()\ntrain_all[TARGETS] = train_all[TARGETS].div(train_all[\"total_votes\"], axis=0)\n\n# Stage 2: high-quality samples\ntrain_hq = train_raw[train_raw[\"total_votes\"] >= cfg.min_total_votes_s2].reset_index(drop=True).copy()\ntrain_hq[TARGETS] = train_hq[TARGETS].div(train_hq[\"total_votes\"], axis=0)\n\nprint(f\"train_all: {train_all.shape} | train_hq: {train_hq.shape} | test: {test.shape}\")\n\nHAS_EEG = (\n    os.path.isdir(os.path.join(cfg.data_dir, \"train_eegs\"))\n    and os.path.isdir(os.path.join(cfg.data_dir, \"test_eegs\"))\n)\nif not HAS_EEG:\n    cfg.use_eeg = False\nprint(\"HAS_EEG:\", HAS_EEG, \"| cfg.use_eeg:\", cfg.use_eeg)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"5063c370-84a4-4265-8218-7f31226bfeb5","cell_type":"code","source":"# ============================================================\n# SPECTROGRAM\n# ============================================================\nSPECTRO_REGIONS = [\"LL\", \"LP\", \"RL\", \"RP\"]\n_spectro_cache = {}\n\n\ndef _select_region_cols(cols, region):\n    out = [c for c in cols if str(c).endswith(f\"_{region}\")]\n    if out: return out\n    out = [c for c in cols if str(c).startswith(f\"{region}_\")]\n    if out: return out\n    return [c for c in cols if region in str(c)]\n\n\ndef load_spectrogram_image(spectrogram_id, offset_seconds, *, is_test):\n    key = (int(spectrogram_id), bool(is_test))\n    if key in _spectro_cache:\n        df = _spectro_cache[key]\n    else:\n        folder = \"test_spectrograms\" if is_test else \"train_spectrograms\"\n        path = os.path.join(cfg.data_dir, folder, f\"{int(spectrogram_id)}.parquet\")\n        df = pd.read_parquet(path)\n        _spectro_cache[key] = df\n\n    cols = list(df.columns)\n    df0  = df[[c for c in cols if c != \"time\"]] if \"time\" in cols else df\n\n    panels = []\n    for region in SPECTRO_REGIONS:\n        rcols = _select_region_cols(df0.columns, region)\n        arr   = df0[rcols].to_numpy(dtype=np.float32)\n        arr   = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)\n        panels.append(arr.T)\n\n    min_f  = min(p.shape[0] for p in panels)\n    panels = [p[:min_f] for p in panels]\n    img    = np.vstack(panels)\n\n    T   = img.shape[1]\n    win = int(cfg.window_cols)\n    center0 = int(round(float(offset_seconds) / 2.0))\n    center  = int(np.clip(center0, 0, max(T - 1, 0)))\n    start   = max(0, min(T - win, center - win // 2)) if T > 0 else 0\n    end     = start + win\n\n    if T >= win:\n        img = img[:, start:end]\n    else:\n        img = np.pad(img, ((0,0),(0, win-T)), mode=\"constant\", constant_values=0.0)\n\n    img = np.log1p(img)\n    m, s = float(img.mean()), float(img.std())\n    img  = (img - m) / (s + 1e-6)\n    return img.astype(np.float32)\n\n\n# ============================================================\n# EEG  (bipolar montage + bandpass + mu-law)\n# ============================================================\n\ndef _butter_bandpass_filter(x, low, high, fs, order):\n    # x: (T, C)\n    nyq = 0.5 * fs\n    lo  = max(low  / nyq, 1e-4)\n    hi  = min(high / nyq, 1.0 - 1e-4)\n    if lo >= hi:\n        return x\n    b, a = butter(order, [lo, hi], btype=\"band\")\n    try:\n        return lfilter(b, a, x, axis=0).astype(np.float32)\n    except Exception:\n        return x\n\n\ndef _mu_law_encode(x, mu=256.0):\n    return (np.sign(x) * np.log1p(mu * np.abs(x)) / np.log1p(mu)).astype(np.float32)\n\n\ndef _resample_c_t(x, new_t):\n    # (C, T) -> (C, new_t)\n    c, t = x.shape\n    if t == new_t:\n        return x.astype(np.float32, copy=False)\n    xi   = np.linspace(0, t - 1, new_t)\n    base = np.arange(t, dtype=np.float64)\n    out  = np.empty((c, new_t), dtype=np.float32)\n    for i in range(c):\n        out[i] = np.interp(xi, base, x[i].astype(np.float64))\n    return out\n\n\n_eeg_lru = OrderedDict()\n_EEG_LRU_MAX = int(getattr(cfg, \"eeg_cache_nfiles\", 40))\n\n\ndef _eeg_lru_get(key, factory):\n    if key in _eeg_lru:\n        _eeg_lru.move_to_end(key)\n        return _eeg_lru[key]\n    v = factory()\n    _eeg_lru[key] = v\n    while len(_eeg_lru) > _EEG_LRU_MAX:\n        _eeg_lru.popitem(last=False)\n    return v\n\n\ndef load_eeg_window(eeg_id, offset_seconds, *, is_test):\n    fallback = np.zeros((N_BIPOLAR, int(cfg.eeg_target_len)), dtype=np.float32)\n    if not bool(getattr(cfg, \"use_eeg\", False)):\n        return fallback\n\n    root = \"test_eegs\" if is_test else \"train_eegs\"\n    path = os.path.join(cfg.data_dir, root, f\"{int(eeg_id)}.parquet\")\n    if not os.path.isfile(path):\n        return fallback\n\n    try:\n        df = _eeg_lru_get((int(eeg_id), bool(is_test)), lambda: pd.read_parquet(path))\n    except Exception:\n        return fallback\n\n    n = len(df)\n    if n < 100:\n        return fallback\n\n    fs = float(n) / (50.0 * 60.0)\n    if not (35.0 <= fs <= 260.0):\n        fs = 200.0\n\n    off    = float(offset_seconds or 0.0)\n    center = int(round(off * fs))\n    half   = int(round(5.0 * fs))\n    st = int(np.clip(center - half, 0, max(0, n - 1)))\n    en = int(np.clip(center + half, st + 1, n))\n\n    try:\n        avail   = [c for c in EEG_COLS if c in df.columns]\n        col_idx = {c: i for i, c in enumerate(avail)}\n        if len(avail) < 4:\n            num_cols = [c for c in df.columns\n                        if c.lower() != \"time\" and np.issubdtype(df[c].dtype, np.number)]\n            raw = df.iloc[st:en][num_cols].to_numpy(dtype=np.float32)\n            col_idx = {c: i for i, c in enumerate(num_cols)}\n        else:\n            raw = df.iloc[st:en][avail].to_numpy(dtype=np.float32)  # (T, C)\n    except Exception:\n        return fallback\n\n    if raw.size == 0:\n        return fallback\n\n    # 1. Bandpass filter\n    raw = _butter_bandpass_filter(\n        raw,\n        low=float(cfg.eeg_bandpass_low),\n        high=float(cfg.eeg_bandpass_high),\n        fs=fs,\n        order=int(cfg.eeg_bandpass_order),\n    )\n\n    # 2. Bipolar montage -> (18, T)\n    bipolar = []\n    for a, b in BIPOLAR_PAIRS:\n        ia, ib = col_idx.get(a), col_idx.get(b)\n        if ia is not None and ib is not None:\n            bipolar.append(raw[:, ia] - raw[:, ib])\n        else:\n            bipolar.append(np.zeros(raw.shape[0], dtype=np.float32))\n    x = np.stack(bipolar, axis=0)   # (18, T)\n\n    # 3. Mu-law encoding\n    x = _mu_law_encode(x, mu=float(cfg.eeg_mu))\n\n    # 4. Clip\n    x = np.clip(x, -6.0, 6.0)\n\n    # 5. Resample to fixed length\n    x = _resample_c_t(x, int(cfg.eeg_target_len))\n\n    # 6. Per-channel z-score\n    m = x.mean(axis=1, keepdims=True)\n    s = x.std(axis=1,  keepdims=True)\n    x = (x - m) / (s + 1e-6)\n    return x.astype(np.float32)\n\n\n# ============================================================\n# SpecAugment\n# ============================================================\n\ndef _apply_spec_aug(x):\n    p = float(getattr(cfg, \"spec_aug_p\", 0.0) or 0.0)\n    if p <= 0.0 or random.random() > p:\n        return x\n    max_shift = int(getattr(cfg, \"spec_time_shift\", 0) or 0)\n    if max_shift > 0:\n        sh = random.randint(-max_shift, max_shift)\n        if sh != 0:\n            x = torch.roll(x, shifts=sh, dims=-1)\n    c, h, w = x.size(0), x.size(1), x.size(2)\n    if c == 0 or h <= 1 or w <= 1:\n        return x\n    n_fm = int(getattr(cfg, \"spec_freq_masks\", 0) or 0)\n    fm   = int(getattr(cfg, \"spec_freq_mask_param\", 0) or 0)\n    for _ in range(max(0, n_fm)):\n        if fm <= 0: break\n        width = random.randint(0, min(fm, h - 1))\n        if width <= 0: continue\n        f0 = random.randint(0, h - width)\n        x[:, f0:f0 + width, :] = 0.0\n    n_tm = int(getattr(cfg, \"spec_time_masks\", 0) or 0)\n    tm   = int(getattr(cfg, \"spec_time_mask_param\", 0) or 0)\n    for _ in range(max(0, n_tm)):\n        if tm <= 0: break\n        width = random.randint(0, min(tm, w - 1))\n        if width <= 0: continue\n        t0 = random.randint(0, w - width)\n        x[:, :, t0:t0 + width] = 0.0\n    gn_p   = float(getattr(cfg, \"spec_gauss_p\",   0.0) or 0.0)\n    gn_std = float(getattr(cfg, \"spec_gauss_std\", 0.0) or 0.0)\n    if gn_p > 0 and gn_std > 0 and random.random() < gn_p:\n        x = torch.clamp(x + torch.randn_like(x) * gn_std, -6.0, 6.0)\n    return x\n\n\n# ============================================================\n# Dataset + DataLoaders\n# ============================================================\n\nclass HMSDataset(Dataset):\n    def __init__(self, df, *, is_test):\n        self.df      = df.reset_index(drop=True)\n        self.is_test = bool(is_test)\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row    = self.df.iloc[idx]\n        sid    = int(row[\"spectrogram_id\"])\n        offset = float(row.get(\n            \"spectrogram_label_offset_seconds\",\n            row.get(\"spectogram_label_offset_seconds\", 0.0),\n        ))\n        img = load_spectrogram_image(sid, offset, is_test=self.is_test)\n        x   = torch.from_numpy(img).unsqueeze(0).unsqueeze(0)\n        x   = F.interpolate(\n            x, size=(int(cfg.img_h), int(cfg.img_w)),\n            mode=\"bilinear\", align_corners=False,\n        ).squeeze(0)\n        if not self.is_test:\n            x = _apply_spec_aug(x)\n        eeg_off = float(row.get(\"eeg_label_offset_seconds\", 0.0) or 0.0)\n        eeg = torch.from_numpy(\n            load_eeg_window(int(row[\"eeg_id\"]), eeg_off, is_test=self.is_test)\n        )\n        if self.is_test:\n            return {\"x\": x, \"eeg\": eeg, \"eeg_id\": int(row[\"eeg_id\"])}\n        y = torch.tensor(row[TARGETS].to_numpy(dtype=np.float32))\n        return {\"x\": x, \"eeg\": eeg, \"y\": y}\n\n\ndef make_loaders(tr_df, va_df):\n    extra = {}\n    if int(cfg.num_workers) > 0:\n        extra[\"persistent_workers\"] = True\n        extra[\"prefetch_factor\"]    = 2\n    dl_tr = DataLoader(\n        HMSDataset(tr_df, is_test=False),\n        batch_size=cfg.batch_size, shuffle=True,\n        num_workers=cfg.num_workers, pin_memory=True, drop_last=True, **extra,\n    )\n    dl_va = DataLoader(\n        HMSDataset(va_df, is_test=False),\n        batch_size=cfg.batch_size, shuffle=False,\n        num_workers=cfg.num_workers, pin_memory=True, drop_last=False, **extra,\n    )\n    return dl_tr, dl_va\n","metadata":{},"outputs":[],"execution_count":null},{"id":"22db38f0-6f5a-4792-b6aa-8576f2b65e2d","cell_type":"code","source":"class SpecEncoder(nn.Module):\n    def __init__(self, out_dim=128):\n        super().__init__()\n        name = str(getattr(cfg, \"backbone\", \"tf_efficientnet_b2\") or \"tf_efficientnet_b2\")\n        drop = float(getattr(cfg, \"backbone_dropout\", 0.0) or 0.0)\n        if timm is not None:\n            try:\n                m = timm.create_model(\n                    name, pretrained=False, in_chans=1,\n                    num_classes=out_dim, drop_rate=drop,\n                )\n            except Exception as e:\n                print(f\"[SpecEncoder] timm fallback ({e}) -> resnet18\")\n                m = timm.create_model(\n                    \"resnet18\", pretrained=False, in_chans=1,\n                    num_classes=out_dim, drop_rate=drop,\n                )\n            self.backbone = m\n            return\n        # torchvision fallback\n        m = torchvision.models.efficientnet_b0(weights=None)\n        first = m.features[0][0]\n        if isinstance(first, nn.Conv2d) and first.in_channels != 1:\n            m.features[0][0] = nn.Conv2d(\n                1, first.out_channels,\n                kernel_size=first.kernel_size, stride=first.stride,\n                padding=first.padding, bias=False,\n            )\n        m.classifier[-1] = nn.Linear(m.classifier[-1].in_features, out_dim)\n        self.backbone = m\n\n    def forward(self, x):\n        return self.backbone(x)\n\n\nclass Residual1DBlock(nn.Module):\n    def __init__(self, in_ch, out_ch, *, k=7, stride=1, drop=0.0):\n        super().__init__()\n        pad        = k // 2\n        self.conv1 = nn.Conv1d(in_ch,  out_ch, k, stride=stride, padding=pad, bias=False)\n        self.bn1   = nn.BatchNorm1d(out_ch)\n        self.act   = nn.GELU()\n        self.conv2 = nn.Conv1d(out_ch, out_ch, k, stride=1,      padding=pad, bias=False)\n        self.bn2   = nn.BatchNorm1d(out_ch)\n        self.drop  = nn.Dropout(float(drop)) if float(drop) > 0 else nn.Identity()\n        self.skip  = (\n            nn.Sequential(\n                nn.Conv1d(in_ch, out_ch, 1, stride=stride, bias=False),\n                nn.BatchNorm1d(out_ch),\n            )\n            if stride != 1 or in_ch != out_ch else nn.Identity()\n        )\n\n    def forward(self, x):\n        h = self.act(self.bn1(self.conv1(x)))\n        h = self.drop(h)\n        h = self.bn2(self.conv2(h))\n        return self.act(h + self.skip(x))\n\n\nclass EegEncoder(nn.Module):\n    def __init__(self, in_ch, out_dim=128):\n        super().__init__()\n        base        = 64\n        self.stem   = nn.Sequential(\n            nn.Conv1d(in_ch, base, 9, stride=2, padding=4, bias=False),\n            nn.BatchNorm1d(base), nn.GELU(),\n        )\n        self.stage1 = nn.Sequential(\n            Residual1DBlock(base,     base,     k=7, stride=1),\n            Residual1DBlock(base,     base,     k=7, stride=1),\n        )\n        self.stage2 = nn.Sequential(\n            Residual1DBlock(base,     base * 2, k=7, stride=2),\n            Residual1DBlock(base * 2, base * 2, k=7, stride=1),\n        )\n        self.stage3 = nn.Sequential(\n            Residual1DBlock(base * 2, base * 4, k=5, stride=2),\n            Residual1DBlock(base * 4, base * 4, k=5, stride=1),\n        )\n        self.pool = nn.AdaptiveAvgPool1d(1)\n        self.fc   = nn.Linear(base * 4, out_dim)\n\n    def forward(self, x):\n        if x.dtype != torch.float32:\n            x = x.float()\n        h = self.stem(x)\n        h = self.stage1(h)\n        h = self.stage2(h)\n        h = self.stage3(h)\n        return self.fc(self.pool(h).squeeze(-1))\n\n\nclass SpecEegNet(nn.Module):\n    def __init__(self, eeg_in_ch=N_BIPOLAR):\n        super().__init__()\n        self.spec = SpecEncoder(128)\n        self.eeg  = EegEncoder(in_ch=eeg_in_ch, out_dim=128)\n        self.head = nn.Sequential(\n            nn.LayerNorm(256),\n            nn.GELU(),\n            nn.Dropout(0.10),\n            nn.Linear(256, len(TARGETS)),\n        )\n\n    def forward(self, x, eeg):\n        fs = self.spec(x)\n        with torch.amp.autocast(\"cuda\", enabled=False):\n            fe = self.eeg(eeg.float())\n        return self.head(torch.cat([fs, fe], dim=1))\n\n\nclass SpecOnlyNet(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.enc  = SpecEncoder(128)\n        self.head = nn.Sequential(\n            nn.LayerNorm(128), nn.GELU(), nn.Dropout(0.10),\n            nn.Linear(128, len(TARGETS)),\n        )\n    def forward(self, x):\n        return self.head(self.enc(x))\n\n\ndef build_model():\n    if bool(getattr(cfg, \"use_eeg\", False)):\n        return SpecEegNet(eeg_in_ch=N_BIPOLAR)\n    return SpecOnlyNet()\n\n\ndef forward_logits(model, x, eeg):\n    if bool(getattr(cfg, \"use_eeg\", False)):\n        if eeg is None:\n            eeg = torch.zeros(x.size(0), N_BIPOLAR, int(cfg.eeg_target_len),\n                              device=x.device, dtype=torch.float32)\n        return model(x, eeg)\n    return model(x)\n\n\ndef kl_loss_from_logits(logits, targets):\n    return F.kl_div(F.log_softmax(logits, dim=1), targets, reduction=\"batchmean\")\n\n\ndef eeg_in_ch_from_ckpt(ckpt):\n    w = ckpt.get(\"model\", {}).get(\"eeg.stem.0.weight\")\n    if w is not None and hasattr(w, \"dim\") and w.dim() >= 2:\n        return int(w.shape[1])\n    return N_BIPOLAR\n","metadata":{},"outputs":[],"execution_count":null},{"id":"90f92c0a-7f66-4c6e-b4ca-502c12899f39","cell_type":"code","source":"from tqdm.auto import tqdm\n\n\ndef spec_cutmix_batch(x, y):\n    cm_p    = float(getattr(cfg, \"spec_cutmix_p\",   0.0) or 0.0)\n    cm_frac = float(getattr(cfg, \"spec_cutmix_frac\", 0.35) or 0.35)\n    if cm_p <= 0 or y is None or x.dim() != 4 or random.random() > cm_p:\n        return x, y\n    b, c, h, w = x.shape\n    if b < 2 or h <= 2 or w <= 2:\n        return x, y\n    idx   = torch.randperm(b, device=x.device)\n    frac  = random.uniform(cm_frac * 0.5, cm_frac)\n    cut_h = int(max(2, frac * h))\n    cut_w = int(max(2, frac * w))\n    fy    = random.randint(0, max(0, h - cut_h))\n    fx    = random.randint(0, max(0, w - cut_w))\n    lam   = 1.0 - cut_h * cut_w / max(h * w, 1)\n    x2    = x.clone()\n    x2[:, :, fy:fy + cut_h, fx:fx + cut_w] = x[idx, :, fy:fy + cut_h, fx:fx + cut_w]\n    return x2, lam * y + (1.0 - lam) * y[idx]\n\n\n@torch.no_grad()\ndef run_valid_epoch(model, loader):\n    model.eval()\n    total, n, bad = 0.0, 0, 0\n    for batch in loader:\n        x   = torch.nan_to_num(batch[\"x\"], nan=0.0).to(DEVICE, non_blocking=True)\n        eeg = batch.get(\"eeg\")\n        if eeg is not None:\n            eeg = torch.nan_to_num(eeg, nan=0.0).to(DEVICE, non_blocking=True)\n        y   = torch.nan_to_num(batch[\"y\"], nan=0.0).to(DEVICE, non_blocking=True)\n        logits = forward_logits(model, x, eeg)\n        loss   = kl_loss_from_logits(logits, y)\n        if not torch.isfinite(loss):\n            bad += x.size(0); continue\n        total += float(loss.cpu()) * x.size(0)\n        n     += x.size(0)\n    if bad: print(f\"[val] skipped {bad} bad rows\")\n    return total / max(n, 1)\n\n\ndef train_one_fold(\n    fold, train_df, *,\n    max_minutes, epochs, lr,\n    mixup_p_override=None,\n    init_path=None,\n    stage_name=\"s1\",\n):\n    tr_df = train_df[train_df[\"fold\"] != fold].reset_index(drop=True)\n    va_df = train_df[train_df[\"fold\"] == fold].reset_index(drop=True)\n    print(f\"\\n=== FOLD {fold} [{stage_name}] | tr={len(tr_df)} va={len(va_df)} ===\")\n\n    dl_tr, dl_va = make_loaders(tr_df, va_df)\n    model = build_model().to(DEVICE)\n\n    if init_path and os.path.isfile(init_path):\n        ckpt = torch.load(init_path, map_location=\"cpu\")\n        model.load_state_dict(ckpt[\"model\"], strict=True)\n        print(f\"  Loaded init weights from {init_path}\")\n\n    opt     = torch.optim.AdamW(model.parameters(), lr=lr, weight_decay=cfg.weight_decay)\n    use_amp = bool(cfg.amp) and DEVICE.type == \"cuda\"\n    scaler  = torch.amp.GradScaler(\"cuda\", enabled=use_amp)\n\n    max_batches     = int(getattr(cfg, \"max_batches_per_epoch\", 0) or 0)\n    steps_per_epoch = max(1, min(len(dl_tr), max_batches) if max_batches > 0 else len(dl_tr))\n    total_steps     = epochs * steps_per_epoch\n    warmup_steps    = int(max(0, round(float(cfg.warmup_epochs) * steps_per_epoch)))\n    min_lr_val      = float(cfg.min_lr)\n\n    def _lr_lambda(step):\n        if total_steps <= 1: return 1.0\n        if warmup_steps > 0 and step < warmup_steps:\n            return (step + 1) / max(1, warmup_steps)\n        t   = (step - warmup_steps) / max(1, total_steps - warmup_steps - 1)\n        cos = 0.5 * (1.0 + math.cos(math.pi * t))\n        floor = (min_lr_val / lr) if lr > 0 else 0.0\n        return floor + (1.0 - floor) * cos\n\n    sched = torch.optim.lr_scheduler.LambdaLR(opt, lr_lambda=_lr_lambda)\n\n    out_dir   = \"/kaggle/working\" if os.path.exists(\"/kaggle/working\") else \".\"\n    os.makedirs(out_dir, exist_ok=True)\n    best_path = os.path.join(out_dir, f\"model_fold{fold}_{stage_name}.pt\")\n\n    best, epochs_no_imp = 1e9, 0\n    hist = {\"epoch\": [], \"train_kl\": [], \"val_kl\": [], \"lr\": []}\n    t0, deadline = time.time(), max_minutes * 60.0\n    mix_p = float(mixup_p_override if mixup_p_override is not None else cfg.mixup_p)\n\n    for ep in range(epochs):\n        model.train()\n        total, n = 0.0, 0\n        pbar = tqdm(dl_tr, total=steps_per_epoch,\n                    desc=f\"fold{fold} {stage_name} ep{ep+1}/{epochs}\")\n\n        for bi, batch in enumerate(pbar):\n            if max_batches > 0 and bi >= max_batches: break\n            if (time.time() - t0) >= deadline:\n                print(f\"[train] deadline {max_minutes:.0f} min reached\")\n                torch.save({\"model\": model.state_dict(), \"hist\": hist}, best_path)\n                return best_path, hist\n\n            x   = torch.nan_to_num(batch[\"x\"], nan=0.0).to(DEVICE, non_blocking=True)\n            eeg = batch.get(\"eeg\")\n            if eeg is not None:\n                eeg = torch.nan_to_num(eeg, nan=0.0).to(DEVICE, non_blocking=True)\n            y   = torch.nan_to_num(batch[\"y\"], nan=0.0).to(DEVICE, non_blocking=True)\n\n            # Soft-label temperature\n            temp = float(getattr(cfg, \"soft_label_temp\", 1.0) or 1.0)\n            if temp != 1.0:\n                y = torch.clamp(y, min=1e-6)\n                y = torch.pow(y, 1.0 / temp)\n                y = y / y.sum(dim=1, keepdim=True)\n\n            # Label smoothing\n            ls = float(getattr(cfg, \"label_smoothing\", 0.0) or 0.0)\n            if ls > 0:\n                y = y * (1.0 - ls) + (ls / y.size(1))\n\n            # Mixup\n            alpha = float(getattr(cfg, \"mixup_alpha\", 0.0) or 0.0)\n            if alpha > 0 and mix_p > 0 and random.random() < mix_p:\n                lam  = float(np.random.beta(alpha, alpha))\n                perm = torch.randperm(x.size(0), device=x.device)\n                x    = lam * x   + (1.0 - lam) * x[perm]\n                if eeg is not None:\n                    eeg = lam * eeg + (1.0 - lam) * eeg[perm]\n                y    = lam * y   + (1.0 - lam) * y[perm]\n\n            x, y = spec_cutmix_batch(x, y)\n            opt.zero_grad(set_to_none=True)\n            gc_val = float(getattr(cfg, \"grad_clip\", 0.0) or 0.0)\n\n            if use_amp:\n                with torch.amp.autocast(\"cuda\", enabled=True):\n                    logits = forward_logits(model, x, eeg)\n                    loss   = kl_loss_from_logits(logits, y)\n                if not torch.isfinite(loss): continue\n                scaler.scale(loss).backward()\n                scaler.unscale_(opt)\n                if gc_val > 0:\n                    torch.nn.utils.clip_grad_norm_(model.parameters(), gc_val)\n                prev = float(scaler.get_scale())\n                scaler.step(opt); scaler.update()\n                if float(scaler.get_scale()) >= prev: sched.step()\n            else:\n                logits = forward_logits(model, x, eeg)\n                loss   = kl_loss_from_logits(logits, y)\n                if not torch.isfinite(loss): continue\n                loss.backward()\n                if gc_val > 0:\n                    torch.nn.utils.clip_grad_norm_(model.parameters(), gc_val)\n                opt.step(); sched.step()\n\n            total += float(loss.detach().cpu()) * x.size(0)\n            n     += x.size(0)\n            if bi % 50 == 0:\n                pbar.set_postfix({\"loss\": f\"{float(loss.detach().cpu()):.4f}\",\n                                  \"lr\":   f\"{opt.param_groups[0]['lr']:.2e}\"})\n\n        tr_loss = total / max(n, 1)\n        va_loss = run_valid_epoch(model, dl_va)\n        lr0     = float(opt.param_groups[0][\"lr\"])\n        hist[\"epoch\"].append(ep + 1)\n        hist[\"train_kl\"].append(tr_loss)\n        hist[\"val_kl\"].append(va_loss)\n        hist[\"lr\"].append(lr0)\n        print(f\"fold {fold} [{stage_name}] ep {ep+1}/{epochs} | \"\n              f\"train={tr_loss:.5f} val={va_loss:.5f} lr={lr0:.2e}\")\n\n        eps = float(getattr(cfg, \"early_stop_min_delta\", 0.0) or 0.0)\n        if va_loss + eps < best:\n            best, epochs_no_imp = va_loss, 0\n            torch.save({\"model\": model.state_dict(), \"hist\": hist}, best_path)\n            print(\"  saved best ->\", best_path)\n        else:\n            epochs_no_imp += 1\n\n        pat = int(getattr(cfg, \"early_stop_patience\", 0) or 0)\n        if pat > 0 and epochs_no_imp >= pat:\n            print(f\"  [early stop] {pat} epochs without improvement\"); break\n\n    return best_path, hist\n\n\n# ============================================================\n# StratifiedGroupKFold splits\n# ============================================================\n\ndef add_folds(df, n_folds, seed=SEED):\n    df = df.copy()\n    df[\"dominant\"] = df[TARGETS].values.argmax(axis=1)\n    sgkf = StratifiedGroupKFold(n_splits=n_folds, shuffle=True, random_state=seed)\n    df[\"fold\"] = -1\n    for f, (_, va_idx) in enumerate(\n        sgkf.split(df, y=df[\"dominant\"], groups=df[\"patient_id\"])\n    ):\n        df.loc[va_idx, \"fold\"] = f\n    # Leakage check\n    for f in range(n_folds):\n        p_f = set(df.loc[df[\"fold\"] == f, \"patient_id\"])\n        for g in range(n_folds):\n            if g != f:\n                assert not (p_f & set(df.loc[df[\"fold\"] == g, \"patient_id\"])),                     f\"Patient leakage folds {f} & {g}\"\n    print(f\"[CV] StratifiedGroupKFold OK | folds={n_folds} rows={len(df)}\")\n    return df\n\n\ntrain_s1 = add_folds(train_all, cfg.n_folds, seed=SEED)\ntrain_s2 = add_folds(train_hq,  cfg.n_folds, seed=SEED)\n\n# ============================================================\n# STAGE 1 - train on ALL data\n# ============================================================\nprint(\"\\n\" + \"=\" * 60)\nprint(\"STAGE 1 - full dataset\")\nprint(\"=\" * 60)\ns1_paths, s1_hists = [], []\nbudget_s1 = cfg.max_train_minutes_s1 / max(1, len(cfg.folds_to_train))\n\nfor fold in cfg.folds_to_train:\n    p, h = train_one_fold(\n        int(fold), train_s1,\n        max_minutes=budget_s1,\n        epochs=cfg.epochs,\n        lr=cfg.lr,\n        stage_name=\"s1\",\n    )\n    s1_paths.append(p); s1_hists.append(h)\n    gc.collect()\n    if DEVICE.type == \"cuda\": torch.cuda.empty_cache()\n\nprint(\"\\nStage 1 checkpoints:\")\nfor p in s1_paths: print(\" -\", p)\n\n# ============================================================\n# STAGE 2 - finetune on high-quality data\n# ============================================================\nprint(\"\\n\" + \"=\" * 60)\nprint(f\"STAGE 2 - high-quality finetune (total_votes >= {cfg.min_total_votes_s2})\")\nprint(\"=\" * 60)\nfold_paths, fold_hists = [], []\nbudget_s2 = cfg.max_train_minutes_s2 / max(1, len(cfg.folds_to_train))\n\nfor i, fold in enumerate(cfg.folds_to_train):\n    p, h = train_one_fold(\n        int(fold), train_s2,\n        max_minutes=budget_s2,\n        epochs=cfg.epochs_s2,\n        lr=cfg.lr_s2,\n        mixup_p_override=cfg.mixup_p_s2,\n        init_path=s1_paths[i],\n        stage_name=\"s2\",\n    )\n    fold_paths.append(p); fold_hists.append(h)\n    gc.collect()\n    if DEVICE.type == \"cuda\": torch.cuda.empty_cache()\n\nprint(\"\\nFinal checkpoints:\")\nfor p in fold_paths: print(\" -\", p)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"6e705073-c9a8-443f-8d92-8316e33c8c71","cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef _plot_hists(hists, prefix=\"\"):\n    if not hists: return\n    all_ep = [e for h in hists for e in h[\"epoch\"]]\n    xlim   = (min(all_ep) - 0.4, max(all_ep) + 0.4) if min(all_ep) == max(all_ep) else None\n    fig, axes = plt.subplots(1, 2, figsize=(14, 4))\n    for i, h in enumerate(hists):\n        axes[0].plot(h[\"epoch\"], h[\"train_kl\"], \"-o\", ms=6, label=f\"fold{i} train\")\n        axes[0].plot(h[\"epoch\"], h[\"val_kl\"],   \"-o\", ms=6, label=f\"fold{i} val\")\n        axes[1].plot(h[\"epoch\"], h[\"lr\"],        \"-o\", ms=6, label=f\"fold{i}\")\n    if xlim:\n        axes[0].set_xlim(xlim); axes[1].set_xlim(xlim)\n    axes[0].set(title=f\"{prefix} KL divergence\", xlabel=\"Epoch\", ylabel=\"KL\")\n    axes[0].legend(); axes[0].grid(alpha=0.3)\n    axes[1].set(title=\"LR\", xlabel=\"Epoch\"); axes[1].legend(); axes[1].grid(alpha=0.3)\n    plt.tight_layout(); plt.show()\n\nprint(\"Stage 1:\"); _plot_hists(s1_hists, \"Stage 1\")\nprint(\"Stage 2:\"); _plot_hists(fold_hists, \"Stage 2\")\n","metadata":{},"outputs":[],"execution_count":null},{"id":"f2960dec-b50b-4a00-9a4e-b631616059c6","cell_type":"code","source":"import joblib\nfrom sklearn.multioutput import MultiOutputRegressor\nfrom sklearn.ensemble import HistGradientBoostingRegressor\n\nUSE_GBDT     = True\nGBDT_MAX_ROWS = 50000\nGBDT_OUT_DIR  = \"/kaggle/working\" if os.path.exists(\"/kaggle/working\") else \".\"\nos.makedirs(GBDT_OUT_DIR, exist_ok=True)\n\n\ndef _safe_stats(a):\n    a = np.nan_to_num(np.asarray(a, np.float32), nan=0.0, posinf=0.0, neginf=0.0)\n    return [float(a.mean()), float(a.std()),\n            float(np.percentile(a, 10)), float(np.percentile(a, 50)), float(np.percentile(a, 90))]\n\n\ndef _bandpower_rough(x, fs=200.0):\n    x = np.nan_to_num(np.asarray(x, np.float32), nan=0.0) - x.mean()\n    n = x.size\n    if n < 16: return [0.0, 0.0, 0.0, 0.0]\n    p = np.abs(np.fft.rfft(x)) ** 2\n    freqs = np.fft.rfftfreq(n, 1.0 / fs)\n    def bp(lo, hi):\n        m = (freqs >= lo) & (freqs < hi)\n        return float(p[m].mean()) if m.any() else 0.0\n    return [bp(0.5, 4), bp(4, 8), bp(8, 13), bp(13, 30)]\n\n\ndef extract_features_row(row, *, is_test):\n    sid   = int(row[\"spectrogram_id\"])\n    off   = float(row.get(\"spectrogram_label_offset_seconds\", 0.0) or 0.0)\n    img   = load_spectrogram_image(sid, off, is_test=is_test)\n    feats = []\n    # Always include total_votes features (0 for test) so feature count stays constant\n    tv = float(row.get(\"total_votes\", 0.0) or 0.0) if not is_test else 0.0\n    feats += [tv, math.log1p(max(tv, 0.0))]\n    feats += _safe_stats(img) + _safe_stats(img.mean(axis=0)) + _safe_stats(img.mean(axis=1))\n    feats += [float(np.mean(np.square(img))), float(np.mean(np.abs(img)))]\n    try:\n        f = img.shape[0]; q = max(1, f // 4)\n        for i in range(4): feats += _safe_stats(img[i * q:(i + 1) * q])\n    except Exception:\n        feats += [0.0] * 20\n    if bool(getattr(cfg, \"use_eeg\", False)):\n        eid  = int(row[\"eeg_id\"])\n        eoff = float(row.get(\"eeg_label_offset_seconds\", 0.0) or 0.0)\n        eeg  = np.nan_to_num(load_eeg_window(eid, eoff, is_test=is_test), nan=0.0)\n        feats += _safe_stats(eeg)\n        feats += _safe_stats(eeg.mean(axis=1)) + _safe_stats(eeg.std(axis=1))\n        x1    = eeg.mean(axis=0)\n        feats += _safe_stats(x1) + _bandpower_rough(x1)\n        feats += [float(np.sqrt(np.mean(np.square(x1)))),\n                  float(np.mean(np.abs(np.diff(x1)))),\n                  float(np.mean(x1 > 0.0))]\n        for ci in range(min(3, eeg.shape[0])): feats += _bandpower_rough(eeg[ci])\n    else:\n        feats += [0.0] * (5 + 5 + 5 + 4 + 3 + 12)\n    return np.asarray(feats, dtype=np.float32)\n\n\ndef build_features(df, *, is_test):\n    out = []\n    for i in tqdm(range(len(df)), desc=\"GBDT features\"):\n        out.append(extract_features_row(df.iloc[i], is_test=is_test))\n    return np.stack(out)\n\n\ndef train_gbdt_folds(train_df):\n    if not USE_GBDT: return []\n    df = train_df.copy().reset_index(drop=True)\n    if GBDT_MAX_ROWS and len(df) > GBDT_MAX_ROWS:\n        df = df.sample(GBDT_MAX_ROWS, random_state=SEED).reset_index(drop=True)\n    if \"fold\" not in df.columns:\n        df = add_folds(df, cfg.n_folds)\n    paths = []\n    for fold in cfg.folds_to_train:\n        tr = df[df[\"fold\"] != fold].reset_index(drop=True)\n        va = df[df[\"fold\"] == fold].reset_index(drop=True)\n        print(f\"[GBDT] fold {fold} | tr={len(tr)} va={len(va)}\")\n        Xtr, ytr = build_features(tr, is_test=False), tr[TARGETS].to_numpy(np.float32)\n        Xva, yva = build_features(va, is_test=False), va[TARGETS].to_numpy(np.float32)\n        base  = HistGradientBoostingRegressor(\n            learning_rate=0.05, max_depth=6, max_iter=400,\n            l2_regularization=1e-3, random_state=SEED,\n        )\n        model = MultiOutputRegressor(base)\n        model.fit(Xtr, ytr)\n        p  = np.clip(model.predict(Xva), 1e-8, 1.0)\n        p /= p.sum(axis=1, keepdims=True)\n        kl = float(np.mean(np.sum(yva * (np.log(yva + 1e-12) - np.log(p + 1e-12)), axis=1)))\n        print(f\"[GBDT] val_KL={kl:.5f}\")\n        out_p = os.path.join(GBDT_OUT_DIR, f\"gbdt_fold{fold}.pkl\")\n        joblib.dump({\"model\": model, \"n_features\": int(Xtr.shape[1])}, out_p)\n        paths.append(out_p)\n    return paths\n\n\ngbdt_paths = train_gbdt_folds(train_s1)\nprint(\"GBDT paths:\", gbdt_paths)\n","metadata":{},"outputs":[],"execution_count":null},{"id":"60d9be92-ec2b-4692-ac21-5f880a2d1c5e","cell_type":"code","source":"out_dir = \"/kaggle/working\" if os.path.exists(\"/kaggle/working\") else \".\"\n\ndef load_model(path):\n    ckpt = torch.load(path, map_location=\"cpu\")\n    n_ch = eeg_in_ch_from_ckpt(ckpt)\n    m    = (SpecEegNet(eeg_in_ch=n_ch) if bool(cfg.use_eeg) else SpecOnlyNet()).to(DEVICE)\n    m.load_state_dict(ckpt[\"model\"], strict=True)\n    m.eval()\n    return m\n\n\nexisting = [p for p in fold_paths if os.path.isfile(p)]\nif not existing:\n    raise FileNotFoundError(f\"No checkpoints. fold_paths={fold_paths}\")\nmodels = [load_model(p) for p in existing]\nprint(f\"Loaded {len(models)} NN model(s)\")\n\n\ndef _load_gbdt():\n    out = []\n    for f in cfg.folds_to_train:\n        p = os.path.join(out_dir, f\"gbdt_fold{int(f)}.pkl\")\n        if os.path.isfile(p):\n            obj = joblib.load(p)\n            out.append(obj[\"model\"] if isinstance(obj, dict) else obj)\n    return out\n\ngbdt_models = _load_gbdt()\nprint(f\"Loaded {len(gbdt_models)} GBDT model(s)\")\n\nextra = {}\nif int(cfg.num_workers) > 0:\n    extra[\"persistent_workers\"] = True\n    extra[\"prefetch_factor\"]    = 2\n\ntest_loader = DataLoader(\n    HMSDataset(test, is_test=True),\n    batch_size=cfg.batch_size, shuffle=False,\n    num_workers=cfg.num_workers, pin_memory=True, **extra,\n)\n\n_nn_gbdt_blend = float(cfg.nn_gbdt_blend)\nall_eeg, all_p = [], []\n\nwith torch.no_grad():\n    for batch in tqdm(test_loader, desc=\"inference\"):\n        x      = batch[\"x\"].to(DEVICE, non_blocking=True)\n        eeg    = batch.get(\"eeg\")\n        if eeg is not None:\n            eeg = eeg.to(DEVICE, non_blocking=True)\n        eeg_id = batch[\"eeg_id\"].cpu().numpy()\n\n        ps = []\n        for m in models:\n            logits_accum = []\n            def _fwd(xx, ee=eeg): return forward_logits(m, xx, ee)\n            logits_accum.append(_fwd(x))\n            if bool(cfg.tta):\n                logits_accum.append(_fwd(torch.flip(x, dims=[-1])))    # time flip\n                logits_accum.append(_fwd(torch.flip(x, dims=[-2])))    # freq flip\n                for sh in cfg.tta_time_shifts:\n                    sh = int(sh)\n                    if sh == 0: continue\n                    logits_accum.append(_fwd(torch.roll(x, shifts=sh, dims=-1)))\n            ns = float(cfg.tta_noise_std or 0.0)\n            if ns > 0:\n                logits_accum.append(_fwd(x + torch.randn_like(x) * ns))\n            logp = torch.stack([F.log_softmax(lg, dim=1) for lg in logits_accum], dim=0)\n            ps.append(torch.exp(logp.mean(dim=0)))\n\n        p_cnn = torch.stack(ps, dim=0).mean(dim=0).cpu().numpy()\n\n        if gbdt_models:\n            feats = []\n            for _eid in eeg_id:\n                r = test.loc[test[\"eeg_id\"] == int(_eid)].iloc[0]\n                feats.append(extract_features_row(r, is_test=True))\n            X  = np.stack(feats)\n            p2 = []\n            for gm in gbdt_models:\n                # Guard against feature count mismatch\n                n_expected = getattr(gm.estimators_[0], 'n_features_in_', X.shape[1])\n                if X.shape[1] != n_expected:\n                    if X.shape[1] < n_expected:\n                        X = np.pad(X, ((0,0),(0, n_expected - X.shape[1])), mode='constant')\n                    else:\n                        X = X[:, :n_expected]\n                pg  = np.clip(gm.predict(X).astype(np.float64), 1e-8, 1.0)\n                pg /= pg.sum(axis=1, keepdims=True)\n                p2.append(pg)\n            p_gbdt = np.mean(p2, axis=0)\n            p      = _nn_gbdt_blend * p_cnn + (1.0 - _nn_gbdt_blend) * p_gbdt\n            p      = np.clip(p, 1e-8, 1.0)\n            p     /= p.sum(axis=1, keepdims=True)\n        else:\n            p = p_cnn\n\n        all_eeg.append(eeg_id)\n        all_p.append(p)\n\nall_eeg = np.concatenate(all_eeg)\nall_p   = np.concatenate(all_p)\n\npred_df = pd.DataFrame(all_p, columns=TARGETS)\npred_df[\"eeg_id\"] = all_eeg\npred_df = pred_df.groupby(\"eeg_id\", as_index=False)[TARGETS].mean()\n\nsubmission = sub[[\"eeg_id\"]].merge(pred_df, on=\"eeg_id\", how=\"left\")\nmiss = submission[TARGETS].isna().any(axis=1)\nif miss.any():\n    submission.loc[miss, TARGETS] = 1.0 / len(TARGETS)\n\nprobs = submission[TARGETS].to_numpy(np.float64)\nprobs = np.clip(probs, 1e-8, 1.0)\nprobs /= probs.sum(axis=1, keepdims=True)\nsubmission[TARGETS] = probs\n\nout_path = os.path.join(out_dir, \"submission.csv\")\nsubmission.to_csv(out_path, index=False)\nprint(\"saved:\", out_path)\nif (submission[TARGETS].sum(axis=1) - 1.0).abs().max() > 1e-3:\n    raise ValueError(\"Rows must sum to 1\")\nprint(\"SUBMISSION OK\")\nsubmission.head()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"4769b63d-9b57-4bce-a28c-8e513b8a42e6","cell_type":"markdown","source":"## Analytics\n\nRun after inference.","metadata":{}},{"id":"d815fc49-8d2e-49c5-8f12-ab5eee176b21","cell_type":"code","source":"import matplotlib.pyplot as plt\n\ndef _row_entropy(p, eps=1e-12):\n    p = np.clip(np.asarray(p, np.float64), eps, 1.0)\n    p = p / p.sum(axis=1, keepdims=True)\n    return (-p * np.log(p)).sum(axis=1)\n\nprint(\"rows:\", len(train_all), \"| unique patients:\", train_all[\"patient_id\"].nunique())\nprior = train_all[TARGETS].mean().sort_values(ascending=False)\nprint(\"train prior:\"); print(prior)\nent = _row_entropy(train_all[TARGETS].to_numpy())\nprint(f\"label entropy: min={ent.min():.3f} mean={ent.mean():.3f} max={ent.max():.3f}\")\n\nplt.figure(figsize=(12, 4))\nplt.subplot(1, 2, 1); plt.hist(ent, bins=60); plt.title(\"Train label entropy\")\nplt.subplot(1, 2, 2); prior.plot(kind=\"bar\"); plt.title(\"Train prior\")\nplt.tight_layout(); plt.show()\n","metadata":{},"outputs":[],"execution_count":null},{"id":"ade7a6c7-9da6-4da1-8e53-fe2d1c753d5f","cell_type":"code","source":"try:   _sub_df = submission.copy()\nexcept NameError: _sub_df = None\nif _sub_df is None:\n    print(\"Run inference cell first.\")\nelse:\n    p   = _sub_df[TARGETS].to_numpy(np.float64)\n    ent = _row_entropy(p)\n    mx  = p.max(axis=1)\n    plt.figure(figsize=(12, 4))\n    plt.subplot(1, 2, 1); plt.hist(ent, bins=60); plt.title(\"Test prediction entropy\")\n    plt.subplot(1, 2, 2); plt.hist(mx,  bins=60); plt.title(\"Test max prob\")\n    plt.tight_layout(); plt.show()\n    print(\"entropy mean:\", float(ent.mean()), \"| maxprob mean:\", float(mx.mean()))\n","metadata":{},"outputs":[],"execution_count":null},{"id":"46e12d96-f7bb-4438-ba1e-6ecb891c2ade","cell_type":"code","source":"out_path = \"/kaggle/working/submission.csv\" if os.path.exists(\"/kaggle/working\") else \"submission.csv\"\nchk = pd.read_csv(out_path)\nassert list(chk.columns) == [\"eeg_id\"] + TARGETS, f\"Bad columns: {list(chk.columns)}\"\nassert not chk[\"eeg_id\"].duplicated().any(), \"Duplicate eeg_id\"\nassert not chk[TARGETS].isna().any().any(), \"NaNs in probs\"\nsums = chk[TARGETS].sum(axis=1)\nprint(\"row sum min/max:\", float(sums.min()), float(sums.max()))\nassert (sums - 1.0).abs().max() < 1e-3, \"Rows must sum to 1\"\nprint(\"SUBMISSION FILE OK:\", out_path, \"rows:\", len(chk))\n","metadata":{},"outputs":[],"execution_count":null}]}