{"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.13"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":106809},{"sourceType":"datasetVersion","sourceId":14326483},{"sourceType":"datasetVersion","sourceId":14338607},{"sourceType":"datasetVersion","sourceId":19238725}],"dockerImageVersionId":28755,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false},"papermill":{"default_parameters":{},"duration":33989.930559,"end_time":"2026-08-29T13:38:03.592407+00:00","environment_variables":{},"exception":null,"input_path":"__notebook__.ipynb","output_path":"__notebook__.ipynb","parameters":{},"start_time":"2026-08-29T04:11:33.661848+00:00","version":"2.7.0"},"widgets":{"application/vnd.jupyter.widget-state+json":{"state":{"0041b49d97ba42359a492685141368e6":{"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}},"093711247cd04cb9a3ff3de554550bcd":{"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_a0ab4abdda9d4ae283e954f581df88c3","max":339,"min":0,"orientation":"horizontal","style":"IPY_MODEL_61a6ab3297a04758aab524476a6ddef1","tabbable":null,"tooltip":null,"value":339}},"0c8abe1c41c0447d8943fdc9e993c2a2":{"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":"20px"}},"0cb1cc2598da446c9a57d16c8b4c95ba":{"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}},"110c7960882f48759009ac95ae763203":{"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":""}},"11734a27239542db8a2f55b0547822d2":{"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":"20px"}},"128960027c354adc90f3ca4d253e2bc0":{"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}},"129b28e840ce47fcba6642fdfa529837":{"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_bbe23401d71f4a08b1a16015451ba501","placeholder":"​","style":"IPY_MODEL_14ac3f4e3101407a98023626b3fcfb13","tabbable":null,"tooltip":null,"value":"Loading weights: 100%"}},"14ac3f4e3101407a98023626b3fcfb13":{"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}},"14f5c306368b4bf4bd2fe402f106eb3d":{"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}},"1624b2a19fad4ca4a29b9dd6b0b2e600":{"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_0c8abe1c41c0447d8943fdc9e993c2a2","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_110c7960882f48759009ac95ae763203","tabbable":null,"tooltip":null,"value":1}},"169acfdae1c0435a9807b6a3f62594e3":{"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":""}},"18668b117cb542eda6061fcb3710c9bd":{"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_bbcb539b8bb04049ad94d77214c372fc","placeholder":"​","style":"IPY_MODEL_544cc5daca7641ea9ef63185fb191146","tabbable":null,"tooltip":null,"value":" 339/339 [01:12&lt;00:00, 10.24it/s, Materializing param=model.norm.weight]"}},"1947f9eb9b184ddf94c3027355197ee9":{"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_ea048519e75d4cc9aec926ae96eb546f","placeholder":"​","style":"IPY_MODEL_cb552d053b584619955ef394692f5f52","tabbable":null,"tooltip":null,"value":" 1.67M/? [00:00&lt;00:00, 75.4MB/s]"}},"1b55b3109d684ed7a5fb05e8aa7b3916":{"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_b7893fddfa22439da71800ff2bf4fb32","placeholder":"​","style":"IPY_MODEL_c3d657c5f20c4162a7323f44d900307b","tabbable":null,"tooltip":null,"value":"tokenizer.json: "}},"1e71c778780940d7a619ce8ae9a59724":{"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":"20px"}},"1fc082e3e976431d81e2bb8f6efda615":{"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}},"22adb091f75c45a0994acf681c3eec1f":{"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}},"26eb6360f635450f8c88d4f7d5e04af0":{"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}},"2ae1343da7834b7fa9841c66a4b252e5":{"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}},"2b39e5fc48ec45a09f6a505fe5face80":{"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}},"2f098dccf7ac4ea887644ef04f7085ec":{"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_3d2b5cb1f5694efea3cbe98345664de2","IPY_MODEL_ff24c0337a27475c92822748a13cc2e2","IPY_MODEL_4c8deab4ba9a4aed943b36e982cda9b2"],"layout":"IPY_MODEL_ddc11d71c4754b56bdbc0f3cfd97a5d8","tabbable":null,"tooltip":null}},"307ec037658745368002aabf39871500":{"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}},"31ecef5424d5499f993a6417073b6716":{"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_ab0175312cf0413c8880c36d58f14ace","IPY_MODEL_81d9ff147ab74fb4bf0fe0e4fab67600","IPY_MODEL_1947f9eb9b184ddf94c3027355197ee9"],"layout":"IPY_MODEL_d68a6b851e28460a9d46f1812f7904fc","tabbable":null,"tooltip":null}},"38e2cbaf75a8429e9bf3fcae39b23f29":{"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}},"3c63c34f011a473ab6ef86669240364e":{"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}},"3c903873b76243d1b8b5e23e1dde2207":{"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}},"3d2b5cb1f5694efea3cbe98345664de2":{"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_96842498e4764658980396055b491a21","placeholder":"​","style":"IPY_MODEL_690f2aac089d4ef18b0abcfa0d826acb","tabbable":null,"tooltip":null,"value":"vocab.json: "}},"3d834712c27c45949f9c405988b0c668":{"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}},"3e4785c4e1dc44038f9f72818346b878":{"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}},"3f0dc247660c412b8b119580009ae774":{"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}},"417277f0b33b449e8207aedb90710f49":{"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}},"459fa23e000145f493e25e8f42886b3c":{"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_1b55b3109d684ed7a5fb05e8aa7b3916","IPY_MODEL_f982737c67354ebfbc1295ab9f3ac9a0","IPY_MODEL_e1b31850207d4341afde3678431db3a0"],"layout":"IPY_MODEL_26eb6360f635450f8c88d4f7d5e04af0","tabbable":null,"tooltip":null}},"4acaf577b7c84a48b79087525fe3be71":{"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}},"4c8deab4ba9a4aed943b36e982cda9b2":{"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_3d834712c27c45949f9c405988b0c668","placeholder":"​","style":"IPY_MODEL_0cb1cc2598da446c9a57d16c8b4c95ba","tabbable":null,"tooltip":null,"value":" 2.78M/? [00:00&lt;00:00, 82.7MB/s]"}},"4c9302592e1642e39feb382035a04345":{"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}},"4eb133241ae943a0aa885669fcf0c55d":{"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_e062357ba21547c7bc870c80272cbeca","max":138,"min":0,"orientation":"horizontal","style":"IPY_MODEL_6fad18e84b8b454095f1afe4b40fd6f5","tabbable":null,"tooltip":null,"value":138}},"4f1dd5a44b8d4149ac91378c0c1bf968":{"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":""}},"544cc5daca7641ea9ef63185fb191146":{"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}},"56c63753fe1d4a83b988cc6d4fd55c4b":{"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}},"5887eace446a4b69af7874119862b67e":{"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}},"58c1662ae960470faf59648ad4d206f3":{"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_0041b49d97ba42359a492685141368e6","placeholder":"​","style":"IPY_MODEL_6745233fff944fa3ba645ee0ff2293f3","tabbable":null,"tooltip":null,"value":" 339/339 [01:27&lt;00:00, 24.95it/s, Materializing param=model.norm.weight]"}},"5a808e62acfe418a9298b87ae5b35809":{"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_8b82b037ae5c4be0bb52e4633cfb4e6f","max":339,"min":0,"orientation":"horizontal","style":"IPY_MODEL_7b1ec6850e9a4bf19e1c6a012e7fa0d1","tabbable":null,"tooltip":null,"value":339}},"5b6c620a8c384072aa3193047b5096b3":{"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}},"5c74ccf376264c0e9aab808c7c5ea0d2":{"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}},"5f141bb77e664fd7990ee3380e899193":{"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":"20px"}},"60e10ee5ae2b4367b77ac7ca499c2986":{"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}},"61a6ab3297a04758aab524476a6ddef1":{"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":""}},"620db643adff4ec09886e940a973320d":{"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":""}},"638c0ccfdee6491281a08a18a77cf7ae":{"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_db4b5462da33476b888524d88d2359e0","max":4,"min":0,"orientation":"horizontal","style":"IPY_MODEL_4f1dd5a44b8d4149ac91378c0c1bf968","tabbable":null,"tooltip":null,"value":4}},"65e137beb72f4b1891bc68ce9bc98cdb":{"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}},"6745233fff944fa3ba645ee0ff2293f3":{"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}},"6769555b75a7480ea2cf4f36e43e2cbe":{"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_5b6c620a8c384072aa3193047b5096b3","placeholder":"​","style":"IPY_MODEL_a0e4499d9fc94a98b230f8202435c627","tabbable":null,"tooltip":null,"value":" 686/686 [00:00&lt;00:00, 63.0kB/s]"}},"681e9d9657ac47709706e8c45ec084e6":{"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_ef20d0181116414e87175d5299588538","placeholder":"​","style":"IPY_MODEL_56c63753fe1d4a83b988cc6d4fd55c4b","tabbable":null,"tooltip":null,"value":"Download complete: 100%"}},"690f2aac089d4ef18b0abcfa0d826acb":{"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}},"6aee751ef51647ab9386cc4b6fa4160a":{"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_129b28e840ce47fcba6642fdfa529837","IPY_MODEL_093711247cd04cb9a3ff3de554550bcd","IPY_MODEL_9da8951faf32422686e82590390cab35"],"layout":"IPY_MODEL_22adb091f75c45a0994acf681c3eec1f","tabbable":null,"tooltip":null}},"6cf573d9980c4fc0ab697b99a493f5df":{"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}},"6e7069d00f7a4aaa9870750e0722686a":{"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":""}},"6f1985e42dbc4f208e10f088397ade7b":{"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_d29c3fb2201c48358929d970d89b47d6","placeholder":"​","style":"IPY_MODEL_e9773bb6fb2a4c38a13e4d567b5ca65b","tabbable":null,"tooltip":null,"value":"tokenizer_config.json: "}},"6fad18e84b8b454095f1afe4b40fd6f5":{"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":""}},"700d7a11ce8e4ddeb0ae7f36c4218487":{"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_89da328306744ecdb5908ec04602005a","placeholder":"​","style":"IPY_MODEL_d14dbed61feb449aa749f2f3d7739378","tabbable":null,"tooltip":null,"value":"Loading weights: 100%"}},"712d13af01704694a90d79e07de8ae87":{"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":""}},"71c3b278154043cc83a20215d157bc9b":{"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_11734a27239542db8a2f55b0547822d2","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_c8e92d16edf945d891b0b6cd53bf75af","tabbable":null,"tooltip":null,"value":1}},"73d62ac8e265419f8481c4580eb13e5c":{"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}},"7aa6b9e780c2406ca5119fdc8a7baf3b":{"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}},"7b1ec6850e9a4bf19e1c6a012e7fa0d1":{"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":""}},"81064334b7fc4de6ac24abe7a4aa1b00":{"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}},"81d9ff147ab74fb4bf0fe0e4fab67600":{"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_1e71c778780940d7a619ce8ae9a59724","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_99b4c4596c92417a8bfe06bf73934828","tabbable":null,"tooltip":null,"value":1}},"89da328306744ecdb5908ec04602005a":{"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}},"8b82b037ae5c4be0bb52e4633cfb4e6f":{"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}},"90d34e478b964cf7bf115179f1848a60":{"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_681e9d9657ac47709706e8c45ec084e6","IPY_MODEL_dc9be8126b6c41eda1a6bbe238363c4a","IPY_MODEL_d4510c8e53c8488e9a99135cf11f8fce"],"layout":"IPY_MODEL_5887eace446a4b69af7874119862b67e","tabbable":null,"tooltip":null}},"938cca9eff734e81ab119d0c63ad2874":{"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}},"947cc8c35ee64b92a5665869138182c2":{"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_2b39e5fc48ec45a09f6a505fe5face80","placeholder":"​","style":"IPY_MODEL_128960027c354adc90f3ca4d253e2bc0","tabbable":null,"tooltip":null,"value":" 4/4 [01:00&lt;00:00, 60.06s/it]"}},"95ad00690b324bb0a8dc793c91256158":{"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}},"96842498e4764658980396055b491a21":{"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}},"9865b08e3d4645eeaf945f2ee4daf5ed":{"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_e21a21afc7294e678a6ba5c5556ea35d","IPY_MODEL_5a808e62acfe418a9298b87ae5b35809","IPY_MODEL_18668b117cb542eda6061fcb3710c9bd"],"layout":"IPY_MODEL_14f5c306368b4bf4bd2fe402f106eb3d","tabbable":null,"tooltip":null}},"99b4c4596c92417a8bfe06bf73934828":{"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":""}},"9da8951faf32422686e82590390cab35":{"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_addc1974b3ce4ff8bc3c6909f83c1268","placeholder":"​","style":"IPY_MODEL_3f0dc247660c412b8b119580009ae774","tabbable":null,"tooltip":null,"value":" 339/339 [01:50&lt;00:00,  8.61it/s, Materializing param=model.norm.weight]"}},"a0ab4abdda9d4ae283e954f581df88c3":{"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}},"a0c94bea97364b8bad65216438df44e7":{"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_f939e0fb59524a048f3997006bec7ada","placeholder":"​","style":"IPY_MODEL_c68502248e864e3b82cb5886fe72aaee","tabbable":null,"tooltip":null,"value":" 7.23k/? [00:00&lt;00:00, 844kB/s]"}},"a0e4499d9fc94a98b230f8202435c627":{"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}},"a9f9d2780a7d49858b49552411117922":{"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}},"ab0175312cf0413c8880c36d58f14ace":{"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_2ae1343da7834b7fa9841c66a4b252e5","placeholder":"​","style":"IPY_MODEL_38e2cbaf75a8429e9bf3fcae39b23f29","tabbable":null,"tooltip":null,"value":"merges.txt: "}},"addc1974b3ce4ff8bc3c6909f83c1268":{"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}},"afd1ce70530148f483854664b494274a":{"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}},"b14982b4185e44f3a467857f11f69ca9":{"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_6f1985e42dbc4f208e10f088397ade7b","IPY_MODEL_71c3b278154043cc83a20215d157bc9b","IPY_MODEL_a0c94bea97364b8bad65216438df44e7"],"layout":"IPY_MODEL_65e137beb72f4b1891bc68ce9bc98cdb","tabbable":null,"tooltip":null}},"b7893fddfa22439da71800ff2bf4fb32":{"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}},"b87b8da42745444ea0cec866a7b10fc1":{"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_4c9302592e1642e39feb382035a04345","max":339,"min":0,"orientation":"horizontal","style":"IPY_MODEL_6e7069d00f7a4aaa9870750e0722686a","tabbable":null,"tooltip":null,"value":339}},"b8afd0a66cf7427ea78a14a24a57ff98":{"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}},"bbcb539b8bb04049ad94d77214c372fc":{"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}},"bbe23401d71f4a08b1a16015451ba501":{"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}},"bca8dab2f567442da4860b83fb56715b":{"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_c35e46c060fc44eba9a1d811e93d671a","IPY_MODEL_4eb133241ae943a0aa885669fcf0c55d","IPY_MODEL_fe3b7a966ecc4c8e8f40d1b3b8c69e56"],"layout":"IPY_MODEL_eb106066cf7e4ee8b44bd5b47cd27ebe","tabbable":null,"tooltip":null}},"be9ef903a8494137a0f3e64ae30dcb3f":{"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_e73bcb6054b44478bcf79deb825a00dc","IPY_MODEL_1624b2a19fad4ca4a29b9dd6b0b2e600","IPY_MODEL_edbd78248f2e4f6f9346f04a1244b882"],"layout":"IPY_MODEL_417277f0b33b449e8207aedb90710f49","tabbable":null,"tooltip":null}},"c35e46c060fc44eba9a1d811e93d671a":{"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_fd65fa5074604233a7ddb50f39a8c501","placeholder":"​","style":"IPY_MODEL_3c63c34f011a473ab6ef86669240364e","tabbable":null,"tooltip":null,"value":"generation_config.json: 100%"}},"c3d657c5f20c4162a7323f44d900307b":{"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}},"c54e354210dd48b894b88264063f1442":{"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":"20px"}},"c68502248e864e3b82cb5886fe72aaee":{"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}},"c7ca7f971d704c86935edf4e8f4e7161":{"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_ed2288c2de6b4a72b875903af5138233","IPY_MODEL_638c0ccfdee6491281a08a18a77cf7ae","IPY_MODEL_947cc8c35ee64b92a5665869138182c2"],"layout":"IPY_MODEL_db07b9bbf79d4d76a46ecb8509e71787","tabbable":null,"tooltip":null}},"c8e92d16edf945d891b0b6cd53bf75af":{"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":""}},"cb552d053b584619955ef394692f5f52":{"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}},"d0feed9234b94e2999c46346cfa9d5cd":{"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_f3e887b43c3b4b9bb3a381e309eeb873","IPY_MODEL_f778ebc911304ff5b5be79374f5893e3","IPY_MODEL_6769555b75a7480ea2cf4f36e43e2cbe"],"layout":"IPY_MODEL_81064334b7fc4de6ac24abe7a4aa1b00","tabbable":null,"tooltip":null}},"d14dbed61feb449aa749f2f3d7739378":{"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}},"d21727a4963843e6b5eb969aab30f846":{"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}},"d29c3fb2201c48358929d970d89b47d6":{"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}},"d4510c8e53c8488e9a99135cf11f8fce":{"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_60e10ee5ae2b4367b77ac7ca499c2986","placeholder":"​","style":"IPY_MODEL_b8afd0a66cf7427ea78a14a24a57ff98","tabbable":null,"tooltip":null,"value":" 15.2G/15.2G [01:00&lt;00:00, 328MB/s]"}},"d68a6b851e28460a9d46f1812f7904fc":{"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}},"db07b9bbf79d4d76a46ecb8509e71787":{"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}},"db4b5462da33476b888524d88d2359e0":{"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}},"dc9be8126b6c41eda1a6bbe238363c4a":{"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_e309986bcb7d44f0be30baa82a54e8cd","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_712d13af01704694a90d79e07de8ae87","tabbable":null,"tooltip":null,"value":1}},"ddc11d71c4754b56bdbc0f3cfd97a5d8":{"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}},"e062357ba21547c7bc870c80272cbeca":{"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}},"e07d9048f88f4591bbccdc794ee1ee00":{"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}},"e1b31850207d4341afde3678431db3a0":{"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_3e4785c4e1dc44038f9f72818346b878","placeholder":"​","style":"IPY_MODEL_afd1ce70530148f483854664b494274a","tabbable":null,"tooltip":null,"value":" 7.03M/? [00:00&lt;00:00, 115MB/s]"}},"e21a21afc7294e678a6ba5c5556ea35d":{"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_95ad00690b324bb0a8dc793c91256158","placeholder":"​","style":"IPY_MODEL_938cca9eff734e81ab119d0c63ad2874","tabbable":null,"tooltip":null,"value":"Loading weights: 100%"}},"e309986bcb7d44f0be30baa82a54e8cd":{"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":"20px"}},"e73bcb6054b44478bcf79deb825a00dc":{"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_6cf573d9980c4fc0ab697b99a493f5df","placeholder":"​","style":"IPY_MODEL_7aa6b9e780c2406ca5119fdc8a7baf3b","tabbable":null,"tooltip":null,"value":"model.safetensors.index.json: "}},"e9773bb6fb2a4c38a13e4d567b5ca65b":{"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}},"ea048519e75d4cc9aec926ae96eb546f":{"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}},"eb106066cf7e4ee8b44bd5b47cd27ebe":{"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}},"ed2288c2de6b4a72b875903af5138233":{"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_307ec037658745368002aabf39871500","placeholder":"​","style":"IPY_MODEL_a9f9d2780a7d49858b49552411117922","tabbable":null,"tooltip":null,"value":"Fetching 4 files: 100%"}},"edbd78248f2e4f6f9346f04a1244b882":{"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_5c74ccf376264c0e9aab808c7c5ea0d2","placeholder":"​","style":"IPY_MODEL_e07d9048f88f4591bbccdc794ee1ee00","tabbable":null,"tooltip":null,"value":" 27.8k/? [00:00&lt;00:00, 2.54MB/s]"}},"ef20d0181116414e87175d5299588538":{"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}},"f155335a994348f2bbcb8881e4fe6f8c":{"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":""}},"f3e887b43c3b4b9bb3a381e309eeb873":{"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_f5a556a732f648ebbc8a5269e010cb36","placeholder":"​","style":"IPY_MODEL_3c903873b76243d1b8b5e23e1dde2207","tabbable":null,"tooltip":null,"value":"config.json: 100%"}},"f41ab905bf1a463abaa200d920db3d23":{"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_700d7a11ce8e4ddeb0ae7f36c4218487","IPY_MODEL_b87b8da42745444ea0cec866a7b10fc1","IPY_MODEL_58c1662ae960470faf59648ad4d206f3"],"layout":"IPY_MODEL_4acaf577b7c84a48b79087525fe3be71","tabbable":null,"tooltip":null}},"f5a556a732f648ebbc8a5269e010cb36":{"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}},"f778ebc911304ff5b5be79374f5893e3":{"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_73d62ac8e265419f8481c4580eb13e5c","max":686,"min":0,"orientation":"horizontal","style":"IPY_MODEL_169acfdae1c0435a9807b6a3f62594e3","tabbable":null,"tooltip":null,"value":686}},"f939e0fb59524a048f3997006bec7ada":{"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}},"f982737c67354ebfbc1295ab9f3ac9a0":{"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_c54e354210dd48b894b88264063f1442","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_f155335a994348f2bbcb8881e4fe6f8c","tabbable":null,"tooltip":null,"value":1}},"fd65fa5074604233a7ddb50f39a8c501":{"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}},"fe3b7a966ecc4c8e8f40d1b3b8c69e56":{"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_d21727a4963843e6b5eb969aab30f846","placeholder":"​","style":"IPY_MODEL_1fc082e3e976431d81e2bb8f6efda615","tabbable":null,"tooltip":null,"value":" 138/138 [00:00&lt;00:00, 16.0kB/s]"}},"ff24c0337a27475c92822748a13cc2e2":{"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_5f141bb77e664fd7990ee3380e899193","max":1,"min":0,"orientation":"horizontal","style":"IPY_MODEL_620db643adff4ec09886e940a973320d","tabbable":null,"tooltip":null,"value":1}}},"version_major":2,"version_minor":0}}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"a9c6108f","cell_type":"markdown","source":"## Block 1 · Setup, configuration & utilities\n","metadata":{"papermill":{"duration":0.023546,"end_time":"2026-08-29T04:11:36.275273+00:00","exception":false,"start_time":"2026-08-29T04:11:36.251727+00:00","status":"completed"},"tags":[]}},{"id":"7263586a","cell_type":"code","source":"!pip install transformers accelerate bitsandbytes scipy jiwer -q\n","metadata":{"papermill":{"duration":7.644853,"end_time":"2026-08-29T04:11:43.942008+00:00","exception":false,"start_time":"2026-08-29T04:11:36.297155+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8cbf259f","cell_type":"code","source":"# ============================================================================\n# SETUP, IMPORTS & UTILS\n# ============================================================================\n\nimport os\nimport h5py\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch.optim as optim\nfrom torch.cuda.amp import autocast, GradScaler\nfrom torch.utils.data import Dataset, DataLoader\nfrom torch.nn.utils.rnn import pad_sequence, pack_padded_sequence, pad_packed_sequence\nimport numpy as np\nimport pandas as pd\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport random\nimport editdistance\nimport jiwer\nfrom scipy.ndimage import gaussian_filter1d\n\ntry:\n    import kagglehub\nexcept ImportError:\n    kagglehub = None\n\n\ndef resolve_competition_path(local_path, competition_slug):\n    \"\"\"Use the local Kaggle mount if present, otherwise fetch via kagglehub.\n    Removes the hard dependency on one specific mount path/environment.\"\"\"\n    if os.path.exists(local_path):\n        return local_path\n    if kagglehub is not None:\n        return os.path.join(kagglehub.competition_download(competition_slug),\n                             \"t15_copyTask_neuralData/hdf5_data_final\")\n    raise FileNotFoundError(f\"{local_path} not found and kagglehub unavailable.\")\n\n\n# 40-way phoneme inventory used by the Brain-to-Text baseline (index 0 = CTC blank,\n# last entry = word-boundary/silence). These IDs are exactly what's already stored\n# in each trial's `seq_class_ids` inside the hdf5 files, so we decode phonemes\n# directly instead of building a character vocabulary from the raw sentence text.\n# This is the single biggest accuracy lever available here: neural activity in this\n# dataset aligns to articulatory/phoneme structure far more cleanly than to letters,\n# and phoneme CTC + lexicon-constrained decoding is what the strongest solutions on\n# this benchmark use.\nPHONEME_VOCAB = [\n    'BLANK', 'AA', 'AE', 'AH', 'AO', 'AW', 'AY', 'B', 'CH', 'D', 'DH',\n    'EH', 'ER', 'EY', 'F', 'G', 'HH', 'IH', 'IY', 'JH', 'K', 'L', 'M', 'N',\n    'NG', 'OW', 'OY', 'P', 'R', 'S', 'SH', 'T', 'TH', 'UH', 'UW', 'V', 'W',\n    'Y', 'Z', 'ZH', ' | ',\n]\n\nCONFIG = {\n    'data_dir': resolve_competition_path(\n        '/kaggle/input/competitions/brain-to-text-25/t15_copyTask_neuralData/hdf5_data_final',\n        'brain-to-text-25',\n    ),\n    'device': 'cuda' if torch.cuda.is_available() else 'cpu',\n    'batch_size': 16,\n    'num_epochs': 82,\n\n    # Model architecture\n    'd_model': 384,\n    'n_heads': 6,\n    'n_layers': 4,\n    'd_ff': 1536,\n    'patch_size': 3,\n    'lstm_hidden': 256,\n    'lstm_layers': 2,\n    'n_classes': len(PHONEME_VOCAB),   # was a small character vocab built from raw text\n\n    # Regularization & Adaptation\n    'dropout': 0.4,\n    'head_dim': 256,\n    'attn_dropout': 0.5,\n    'drop_path_rate': 0.2,\n    'smooth_kernel_std': 2.0,\n    'smooth_kernel_size': 100,\n    'drift_lambda': 0.01,\n\n    # Optimizer\n    'learning_rate': 5e-4,\n    'weight_decay': 1e-4,\n    'use_augmentation': True,\n\n    # Memory / stability\n    'use_amp': True,          # NEW: mixed precision -> lower memory, room for bigger batches\n    'grad_clip': 5.0,\n\n    # Checkpointing - no hardcoded personal Kaggle model path. Leave None to train\n    # from scratch, or point it at your own checkpoint to resume.\n    'resume_from_checkpoint': None,\n    'checkpoint_dir': '/kaggle/working',\n}\n\n# ============================================================================\n# SKIP_TRAINING: reuse an already-trained checkpoint instead of training again\n# ============================================================================\n# Set SKIP_TRAINING = True once you have best_model.pt (and ideally\n# swa_model.pt + norm_stats.pt) saved permanently as a Kaggle Dataset - e.g.\n# from a previous Save Version run's Output (\"+ New Dataset\" on that Output\n# tab). Attach that dataset to this notebook via '+ Add Input', then put its\n# slug below. With this on, the training-driver cell and the early\n# (pre-checkpoint-reload) eval plots skip entirely instead of reloading the\n# full 45-session train split just to throw the result away - that reload\n# alone is what was taking several minutes to hours for nothing.\nSKIP_TRAINING = True\nPRETRAINED_CKPT_DATASET = 'shohan3125/brain-to-text-checkpoint'\n\nos.makedirs(CONFIG['checkpoint_dir'], exist_ok=True)\n\n# Defined here (unconditionally, at the very top) instead of only inside the\n# Block 4b eval-plot cell - those are SKIPPED entirely when SKIP_TRAINING=True,\n# which used to leave `figures_dir` completely undefined and crash every plot\n# cell in the Block 5 validation pipeline with a NameError, even though nothing\n# about that crash had anything to do with plotting logic itself.\nfigures_dir = os.path.join(CONFIG['checkpoint_dir'], 'figures')\nos.makedirs(figures_dir, exist_ok=True)\n\nprint(f\"Device: {CONFIG['device']}\")\nprint(f\"PyTorch version: {torch.__version__}\")\nprint(f\"SKIP_TRAINING: {SKIP_TRAINING}\")\nprint(f\"Phoneme classes (incl. blank): {CONFIG['n_classes']}\")\n\n# --- Helper: Stochastic Depth ---\ndef drop_path(x, drop_prob: float = 0., training: bool = False, scale_by_keep: bool = True):\n    if drop_prob == 0. or not training:\n        return x\n    keep_prob = 1 - drop_prob\n    shape = (x.shape[0],) + (1,) * (x.ndim - 1)\n    random_tensor = x.new_empty(shape).bernoulli_(keep_prob)\n    if keep_prob > 0.0 and scale_by_keep:\n        random_tensor.div_(keep_prob)\n    return x * random_tensor\n\n# --- Helper: Global Session Mapper ---\ndef get_session2idx(data_dir):\n    \"\"\"Scans data directory to find all unique sessions chronologically.\"\"\"\n    from glob import glob\n    paths = glob(f'{data_dir}/**/data_*.hdf5', recursive=True)\n    sessions = sorted(list(set([Path(p).parent.name for p in paths])))\n    return {s: i for i, s in enumerate(sessions)}\n\n\n# --- Helper: Kaggle dataset resolver (avoids the kagglehub non-interactive error) ---\ndef resolve_kaggle_dataset(slug, local_dirname=None):\n    \"\"\"Prefer an already-attached Kaggle dataset under /kaggle/input (works in\n    BOTH interactive editing AND non-interactive 'Save & Run All' / submission\n    runs), and only fall back to kagglehub.dataset_download() - which only\n    works interactively - when nothing is found locally.\n\n    kagglehub.dataset_download() tries to attach the dataset to the current\n    session on first use. That works while you're clicking through cells\n    yourself, but Kaggle refuses to attach a *new* dataset during a\n    non-interactive commit/submission run ('New Datasets cannot be attached\n    in non-interactive sessions'), which is exactly the run that matters for\n    scoring. Fix: attach the dataset once via '+ Add Input' in the notebook\n    editor.\n\n    Manually-attached datasets don't always land at the \"clean\"\n    /kaggle/input/<name> path - depending on how they were added they can be\n    nested under /kaggle/input/datasets/<owner>/<name>/... instead - so this\n    checks both layouts, then falls back to a recursive search by folder name\n    (handles version-suffix mismatches like \".../foo\" vs \".../foo-updated-v3\"\n    too), before finally trying kagglehub as a last resort.\n    \"\"\"\n    if local_dirname is None:\n        local_dirname = slug.split('/')[-1]\n    owner = slug.split('/')[0] if '/' in slug else None\n\n    candidates = [f'/kaggle/input/{local_dirname}']\n    if owner:\n        candidates.append(f'/kaggle/input/datasets/{owner}/{local_dirname}')\n\n    if os.path.isdir('/kaggle/input'):\n        for root, dirs, _ in os.walk('/kaggle/input'):\n            for d in dirs:\n                if local_dirname.lower() in d.lower() or d.lower() in local_dirname.lower():\n                    candidates.append(os.path.join(root, d))\n\n    for path in candidates:\n        if os.path.exists(path):\n            return path\n    try:\n        import kagglehub\n        return kagglehub.dataset_download(slug)\n    except Exception as e:\n        raise FileNotFoundError(\n            f\"Could not find dataset '{slug}' under /kaggle/input (tried \"\n            f\"{candidates}) and kagglehub.dataset_download() also failed: {e}\\n\"\n            f\"Fix: open this notebook in the editor, click '+ Add Input' \"\n            f\"(top-right), search for '{slug.split('/')[-1]}', attach it, \"\n            f\"then re-run / Save & Run All.\"\n        ) from e\n\n\ndef find_file(root_dir, filename_pattern):\n    \"\"\"Recursively find a file under root_dir matching filename_pattern\n    (e.g. '*.bin' or 'lexicon.txt'). Used because attached-dataset internal\n    folder/file layout can vary slightly between dataset versions - safer\n    than hardcoding one exact relative path.\"\"\"\n    from glob import glob\n    matches = sorted(glob(os.path.join(root_dir, '**', filename_pattern), recursive=True))\n    if not matches:\n        raise FileNotFoundError(f\"No file matching '{filename_pattern}' found under {root_dir}\")\n    return matches[0]\n","metadata":{"papermill":{"duration":7.384973,"end_time":"2026-08-29T04:11:51.349127+00:00","exception":false,"start_time":"2026-08-29T04:11:43.964154+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d8454883","cell_type":"markdown","source":"## Block 2 · Data loading, augmentation & dataset\n","metadata":{"papermill":{"duration":0.021738,"end_time":"2026-08-29T04:11:51.392868+00:00","exception":false,"start_time":"2026-08-29T04:11:51.37113+00:00","status":"completed"},"tags":[]}},{"id":"6403739c","cell_type":"code","source":"# ============================================================================\n# DATA AUGMENTATION, LOADING & DATASET  (phoneme targets + global normalization)\n# ============================================================================\n\nclass NeuralAugmentation:\n    def __init__(self, p=0.5):\n        self.p = p\n    def __call__(self, neural):\n        if random.random() > self.p:\n            return neural\n        if random.random() < 0.3:\n            neural = self.time_warp(neural)\n        if random.random() < 0.3:\n            noise_level = random.uniform(0.01, 0.05)\n            neural = neural + torch.randn_like(neural) * noise_level\n        if random.random() < 0.2:\n            n_channels = neural.shape[1]\n            n_drop = int(n_channels * 0.1)\n            drop_indices = random.sample(range(n_channels), n_drop)\n            neural[:, drop_indices] = 0\n        return neural\n    def time_warp(self, neural):\n        seq_len = len(neural)\n        warp_factor = random.uniform(0.9, 1.1)\n        new_len = int(seq_len * warp_factor)\n        if new_len < 10: return neural\n        indices = torch.linspace(0, seq_len - 1, new_len)\n        indices_floor = indices.long()\n        indices_ceil = torch.clamp(indices_floor + 1, max=seq_len - 1)\n        alpha = (indices - indices_floor.float()).unsqueeze(1)\n        warped = (1 - alpha) * neural[indices_floor] + alpha * neural[indices_ceil]\n        final_indices = torch.linspace(0, new_len - 1, seq_len).long()\n        return warped[final_indices]\n\n\ndef load_split(data_dir, split='train'):\n    from glob import glob\n    pattern = f'{data_dir}/**/data_{split}.hdf5'\n    files = sorted(glob(pattern, recursive=True))\n\n    print(f\"\\nLoading {split} split...\")\n    all_data = {k: [] for k in ['neural', 'n_steps', 'sentence', 'phonemes',\n                                 'phoneme_len', 'session', 'block', 'trial']}\n    for filepath in tqdm(files):\n        session_name = Path(filepath).parent.name\n        with h5py.File(filepath, 'r') as f:\n            for trial_key in f.keys():\n                trial = f[trial_key]\n                all_data['neural'].append(trial['input_features'][:])\n                all_data['n_steps'].append(trial.attrs['n_time_steps'])\n                all_data['session'].append(session_name)\n                all_data['block'].append(trial.attrs['block_num'])\n                all_data['trial'].append(trial.attrs['trial_num'])\n\n                sentence = trial.attrs.get('sentence_label')\n                all_data['sentence'].append(sentence.decode('utf-8') if isinstance(sentence, bytes) else sentence)\n\n                # Phoneme targets already ship inside the hdf5 - use them directly\n                # instead of deriving a character vocabulary from the sentence text.\n                phon = trial['seq_class_ids'][:] if 'seq_class_ids' in trial else np.array([], dtype=np.int64)\n                phon_len = int(trial.attrs['seq_len']) if 'seq_len' in trial.attrs else len(phon)\n                all_data['phonemes'].append(phon)\n                all_data['phoneme_len'].append(phon_len)\n    print(f\"✓ Loaded {len(all_data['neural'])} samples\")\n    return all_data\n\n\ndef compute_channel_stats(data):\n    \"\"\"\n    Streaming (Welford) per-channel mean/std over every timestep of every TRAIN\n    trial, computed once and reused unchanged for val/test. Fixes the previous\n    setup, where every trial (train, val, and test alike) normalized itself\n    independently and test additionally clipped while train did not - a\n    train/test mismatch that especially hurts the day-adaptive layer, which\n    needs a stable input scale to learn a meaningful per-day transform.\n    \"\"\"\n    n_channels = data['neural'][0].shape[1]\n    count = 0\n    mean = np.zeros(n_channels, dtype=np.float64)\n    M2 = np.zeros(n_channels, dtype=np.float64)\n    for feat, n_steps in zip(data['neural'], data['n_steps']):\n        x = feat[:n_steps].astype(np.float64)\n        for row in x:\n            count += 1\n            delta = row - mean\n            mean += delta / count\n            M2 += delta * (row - mean)\n    std = np.sqrt(M2 / max(count - 1, 1))\n    std[std < 1e-6] = 1e-6\n    return torch.tensor(mean, dtype=torch.float32), torch.tensor(std, dtype=torch.float32)\n\n\nclass BrainToTextDataset(Dataset):\n    def __init__(self, data, session2idx, feat_mean, feat_std, augment=False, clip=5.0):\n        self.neural = data['neural']\n        self.n_steps = data['n_steps']\n        self.sentences = data['sentence']\n        self.sessions = data['session']\n        self.phonemes = data['phonemes']\n        self.phoneme_len = data['phoneme_len']\n        self.session2idx = session2idx\n        self.feat_mean = feat_mean\n        self.feat_std = feat_std\n        self.clip = clip\n        self.augment = augment\n        self.augmentation = NeuralAugmentation(p=0.5) if augment else None\n\n    def __len__(self): return len(self.neural)\n\n    def __getitem__(self, idx):\n        neural = self.neural[idx][:self.n_steps[idx]]\n        neural = torch.FloatTensor(neural)\n\n        # Global, split-independent normalization - same stats for train/val/test.\n        neural = (neural - self.feat_mean) / self.feat_std\n        neural = torch.clamp(neural, -self.clip, self.clip)\n\n        if self.augment and self.augmentation:\n            neural = self.augmentation(neural)\n\n        target = torch.LongTensor(self.phonemes[idx])\n        target_length = self.phoneme_len[idx]\n\n        return {\n            'neural': neural,\n            'target': target,\n            'length': len(neural),\n            'target_length': target_length,\n            'sentence': self.sentences[idx] if self.sentences[idx] else \"\",\n            'day_idx': self.session2idx[self.sessions[idx]]\n        }\n\n\ndef collate_fn(batch):\n    batch = sorted(batch, key=lambda x: x['length'], reverse=True)\n    neurals = pad_sequence([item['neural'] for item in batch], batch_first=True)\n    targets = pad_sequence([item['target'] for item in batch], batch_first=True)\n    return {\n        'neural': neurals,\n        'target': targets,\n        'lengths': torch.LongTensor([item['length'] for item in batch]),\n        'target_lengths': torch.LongTensor([item['target_length'] for item in batch]),\n        'sentences': [item['sentence'] for item in batch],\n        'day_idx': torch.LongTensor([item['day_idx'] for item in batch])\n    }\n","metadata":{"papermill":{"duration":0.044717,"end_time":"2026-08-29T04:11:51.458814+00:00","exception":false,"start_time":"2026-08-29T04:11:51.414097+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9d72366b","cell_type":"markdown","source":"## Block 3 · Hybrid acoustic model (CNN → BiLSTM → patch-Transformer, phoneme CTC)\n","metadata":{"papermill":{"duration":0.021285,"end_time":"2026-08-29T04:11:51.501541+00:00","exception":false,"start_time":"2026-08-29T04:11:51.480256+00:00","status":"completed"},"tags":[]}},{"id":"4311b70c","cell_type":"code","source":"# ============================================================================\n# HYBRID MODEL (CNN -> BiLSTM -> Patch -> Transformer, day-adaptive input,\n# Gaussian smoothing). Architecture unchanged - output head now predicts\n# phoneme classes (vocab_size = CONFIG['n_classes'] = 40) instead of\n# characters; that swap happens at construction time, not in this class.\n# ============================================================================\n\n\nclass RoPE(nn.Module):\n    def __init__(self, head_dim, max_seq_len=2048):\n        super().__init__()\n        half_dim = head_dim // 2\n        freq = 1.0 / (10000 ** (torch.arange(0, half_dim, 2).float() / half_dim))\n        t = torch.arange(max_seq_len).float().unsqueeze(1)\n        angles = t * freq.unsqueeze(0)\n        cos = torch.cos(angles).repeat_interleave(2, dim=1)\n        sin = torch.sin(angles).repeat_interleave(2, dim=1)\n        self.register_buffer(\"cos\", cos.unsqueeze(0))\n        self.register_buffer(\"sin\", sin.unsqueeze(0))\n\n    def forward(self, x, seq_len):\n        cos = self.cos[:, :seq_len, :].to(x.device)\n        sin = self.sin[:, :seq_len, :].to(x.device)\n        x1, x2 = x.chunk(2, -1)\n        return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], -1)\n\nclass WideAttention(nn.Module):\n    def __init__(self, d_model, n_heads, head_dim, dropout):\n        super().__init__()\n        self.n_heads = n_heads\n        self.head_dim = head_dim\n        self.inner_dim = n_heads * head_dim\n        self.scale = head_dim ** -0.5\n        \n        self.qkv = nn.Linear(d_model, self.inner_dim * 3, bias=False)\n        self.out = nn.Linear(self.inner_dim, d_model)\n        self.dropout = nn.Dropout(dropout)\n        self.rope = RoPE(head_dim)\n    \n    def forward(self, x, mask=None):\n        B, T, C = x.shape\n        qkv = self.qkv(x).reshape(B, T, 3, self.n_heads, self.head_dim).permute(2, 0, 3, 1, 4)\n        q, k, v = qkv[0], qkv[1], qkv[2]\n        \n        q = self.rope(q, T)\n        k = self.rope(k, T)\n        \n        attn = (q @ k.transpose(-2, -1)) * self.scale\n        if mask is not None:\n            attn = attn.masked_fill(mask == 0, float('-inf'))\n        \n        attn = self.dropout(F.softmax(attn, -1))\n        out = (attn @ v).transpose(1, 2).reshape(B, T, -1)\n        return self.out(out)\n\nclass TransformerBlock(nn.Module):\n    def __init__(self, d_model, n_heads, d_ff, dropout, head_dim, attn_dropout):\n        super().__init__()\n        self.norm1 = nn.LayerNorm(d_model)\n        self.attn = WideAttention(d_model, n_heads, head_dim, attn_dropout)\n        self.norm2 = nn.LayerNorm(d_model)\n        self.ffn = nn.Sequential(\n            nn.Linear(d_model, d_ff),\n            nn.GELU(),\n            nn.Dropout(dropout),\n            nn.Linear(d_ff, d_model),\n            nn.Dropout(dropout)\n        )\n    \n    def forward(self, x, mask=None, drop_path_rate=0.0):\n        # Apply stochastic depth (drop path) around the residual connections\n        x = x + drop_path(self.attn(self.norm1(x), mask), drop_path_rate, self.training)\n        x = x + drop_path(self.ffn(self.norm2(x)), drop_path_rate, self.training)\n        return x\n\nclass HybridLSTMTransformerCTC(nn.Module):\n    def __init__(self, n_days, input_size=512, d_model=384, n_heads=6, n_layers=4,\n                 d_ff=1536, patch_size=3, vocab_size=50, dropout=0.4, head_dim=256, \n                 attn_dropout=0.5, lstm_hidden=256, lstm_layers=2, smooth_std=2.0, \n                 smooth_size=100, drop_path_rate=0.2):\n        super().__init__()\n        self.patch_size = patch_size\n        self.n_days = n_days\n        \n        # --- NEW: Gaussian Smoothing Initialization ---\n        inp = np.zeros(smooth_size, dtype=np.float32)\n        inp[smooth_size // 2] = 1\n        gaussKernel = gaussian_filter1d(inp, smooth_std)\n        valid_idx = np.argwhere(gaussKernel > 0.01)\n        gaussKernel = gaussKernel[valid_idx]\n        gaussKernel = np.squeeze(gaussKernel / np.sum(gaussKernel))\n        self.register_buffer(\"gauss_kernel\", torch.tensor(gaussKernel, dtype=torch.float32).view(1, 1, -1))\n        \n        # --- NEW: Day-Specific Linear Transformation ---\n        self.day_weights = nn.ParameterList([nn.Parameter(torch.eye(input_size)) for _ in range(n_days)])\n        self.day_biases = nn.ParameterList([nn.Parameter(torch.zeros(1, input_size)) for _ in range(n_days)])\n        self.day_activation = nn.Softsign()\n\n        self.cnn = nn.Sequential(\n            nn.Conv1d(input_size, 256, kernel_size=3, padding=1),\n            nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(dropout * 0.5),\n            nn.Conv1d(256, 256, kernel_size=3, padding=1),\n            nn.BatchNorm1d(256), nn.ReLU(), nn.Dropout(dropout * 0.5)\n        )\n        \n        self.lstm = nn.LSTM(\n            256, lstm_hidden, lstm_layers, batch_first=True, bidirectional=True,\n            dropout=dropout if lstm_layers > 1 else 0\n        )\n        \n        lstm_output_dim = lstm_hidden * 2\n        self.patch_embed = nn.Sequential(\n            nn.LayerNorm(lstm_output_dim * patch_size),\n            nn.Linear(lstm_output_dim * patch_size, d_model),\n            nn.LayerNorm(d_model), nn.Dropout(dropout)\n        )\n        \n        self.blocks = nn.ModuleList([\n            TransformerBlock(d_model, n_heads, d_ff, dropout, head_dim, attn_dropout)\n            for _ in range(n_layers)\n        ])\n        \n        self.drop_path_rates = [x.item() for x in torch.linspace(0, drop_path_rate, n_layers)]\n        \n        self.norm = nn.LayerNorm(d_model)\n        self.head = nn.Linear(d_model, vocab_size)\n    \n    def forward(self, x, lengths, day_idx):\n        B, T, C = x.shape\n        \n        # 1. Day Specific Weighting\n        W = torch.stack([self.day_weights[i] for i in day_idx], dim=0)\n        b = torch.cat([self.day_biases[i] for i in day_idx], dim=0).unsqueeze(1)\n        x = torch.einsum(\"btd,bdk->btk\", x, W) + b\n        x = self.day_activation(x)\n\n        # 2. Gaussian Smoothing via Conv1D\n        x = x.permute(0, 2, 1) # [B, C, T]\n        kernel = self.gauss_kernel.repeat(C, 1, 1).to(x.device)\n        x = F.conv1d(x, kernel, padding='same', groups=C)\n        \n        # 3. CNN\n        x = self.cnn(x)\n        x = x.permute(0, 2, 1) # [B, T, C]\n        \n        # 4. LSTM\n        x_packed = pack_padded_sequence(x, lengths.cpu(), batch_first=True, enforce_sorted=True)\n        lstm_out, _ = self.lstm(x_packed)\n        lstm_out, _ = pad_packed_sequence(lstm_out, batch_first=True)\n        \n        # 5. Patching\n        T_lstm = lstm_out.shape[1]\n        n_patches = T_lstm // self.patch_size\n        \n        if n_patches == 0:\n            n_patches = 1\n            x = lstm_out.mean(dim=1, keepdim=True)\n            x = self.patch_embed(x.reshape(B, 1, -1))\n            patch_lens = torch.ones(B, dtype=torch.long, device=x.device)\n        else:\n            x = lstm_out[:, :n_patches * self.patch_size].reshape(B, n_patches, -1)\n            x = self.patch_embed(x)\n            patch_lens = torch.clamp((lengths // self.patch_size).to(x.device), min=1)\n        \n        # 6. Transformer\n        mask = (torch.arange(n_patches, device=x.device)[None, :] < patch_lens[:, None])\n        mask = mask[:, None, None, :]\n        \n        for i, block in enumerate(self.blocks):\n            x = block(x, mask, drop_path_rate=self.drop_path_rates[i])\n        \n        # 7. Output Projection\n        logits = self.head(self.norm(x))\n        log_probs = torch.log_softmax(logits, dim=-1)\n        \n        return log_probs.transpose(0, 1), patch_lens","metadata":{"papermill":{"duration":0.048598,"end_time":"2026-08-29T04:11:51.571193+00:00","exception":false,"start_time":"2026-08-29T04:11:51.522595+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a8a34a46","cell_type":"markdown","source":"## Block 4 · Training & validation loop (phoneme CTC; tracks WER + PER per epoch)\n","metadata":{"papermill":{"duration":0.021173,"end_time":"2026-08-29T04:11:51.614436+00:00","exception":false,"start_time":"2026-08-29T04:11:51.593263+00:00","status":"completed"},"tags":[]}},{"id":"21276765","cell_type":"code","source":"# ============================================================================\n# TRAINING FUNCTIONS  (mixed precision + phoneme CTC + drift loss + lightweight SWA)\n# ============================================================================\n# UPDATED: validation now tracks BOTH Word Error Rate (WER) and Phoneme Error\n# Rate (PER) every epoch. PER is essentially free to compute here (it just\n# compares the raw CTC-greedy phoneme ids to the ground-truth phoneme ids\n# already sitting in the batch - no lexicon lookup needed), so there is no\n# reason to leave it out anymore. WER still needs the lexicon to turn\n# phonemes into words. Character Error Rate (CER) is NOT tracked per-epoch\n# here on purpose - CER only makes sense on the final decoded *text*, and\n# during training that text still comes from the fast, LM-free lexicon\n# lookup (lots of '<unk>' placeholders), which would make an epoch-level CER\n# curve noisy and misleading. CER is computed properly later, in the\n# dedicated evaluation section, on the actual beam-search + n-gram (+ LLM)\n# output.\n\nimport os\nimport json\nimport matplotlib.pyplot as plt\n\n\ndef greedy_decode_phonemes(log_probs, output_lengths):\n    \"\"\"CTC greedy decode -> list of phoneme-id sequences (blank/duplicates collapsed).\"\"\"\n    _, max_indices = log_probs.max(dim=-1)  # [T, B]\n    decoded = []\n    for b in range(max_indices.size(1)):\n        seq = max_indices[:output_lengths[b], b].cpu().numpy()\n        out, prev = [], None\n        for token in seq:\n            if token != 0 and token != prev:\n                out.append(int(token))\n            prev = token\n        decoded.append(out)\n    return decoded\n\n\ndef phoneme_ids_to_str(phoneme_ids):\n    \"\"\"Phoneme ids -> a whitespace-joined string of phoneme symbols, e.g.\n    [7, 11, 32] -> 'B EH T'. This lets us reuse jiwer's word-level machinery\n    (built for space-separated tokens) to get PER, alignments, and a\n    substitution/deletion/insertion breakdown at the phoneme level, instead\n    of hand-rolling a second edit-distance implementation.\"\"\"\n    return ' '.join(PHONEME_VOCAB[p] for p in phoneme_ids)\n\n\ndef build_lexicon_reverse(lexicon_path):\n    \"\"\"Parses lexicon.txt (word <TAB/space> phoneme sequence) into a\n    phoneme-tuple -> word map, for fast exact-match lookup. First occurrence\n    wins on homophones/duplicate pronunciations.\"\"\"\n    reverse = {}\n    with open(lexicon_path, 'r') as f:\n        for line in f:\n            line = line.strip()\n            if not line:\n                continue\n            parts = line.split('\\t')\n            if len(parts) == 2:\n                word, phon_str = parts[0], parts[1]\n                phonemes = [p for p in phon_str.rstrip(' |').split(' ') if p]\n            else:\n                parts = line.split()\n                if len(parts) < 2:\n                    continue\n                word, phonemes = parts[0], parts[1:]\n            key = tuple(phonemes)\n            if key not in reverse:\n                reverse[key] = word\n    return reverse\n\n\ndef phoneme_ids_to_words(phoneme_ids, lexicon_reverse):\n    \"\"\"Fast, LM-free phoneme -> word decoding for use during training:\n    split the CTC-collapsed id sequence on the word-boundary token (' | ',\n    the last entry in PHONEME_VOCAB), then exact-match each chunk's phoneme\n    labels against the lexicon. Unmatched chunks fall back to '<unk>' -\n    still counts as one word for WER purposes.\"\"\"\n    sil_id = len(PHONEME_VOCAB) - 1\n    words, current = [], []\n    for pid in phoneme_ids:\n        if pid == sil_id:\n            if current:\n                key = tuple(PHONEME_VOCAB[p] for p in current)\n                words.append(lexicon_reverse.get(key, '<unk>'))\n                current = []\n        else:\n            current.append(pid)\n    if current:\n        key = tuple(PHONEME_VOCAB[p] for p in current)\n        words.append(lexicon_reverse.get(key, '<unk>'))\n    return ' '.join(words)\n\n\ndef validate_model(model, val_loader, device, lexicon_reverse):\n    \"\"\"\n    Every epoch, computes BOTH:\n      - WER: decode phonemes -> words (lexicon) -> compare to the\n        ground-truth sentence with word-level edit distance.\n      - PER: compare the raw CTC-greedy phoneme ids directly to the\n        ground-truth phoneme ids (batch['target'] / batch['target_lengths']),\n        with no lexicon involved at all.\n    Returns (wer_percent, per_percent).\n    \"\"\"\n    model.eval()\n    total_word_edits, total_words = 0, 0\n    total_phon_edits, total_phons = 0, 0\n    with torch.no_grad():\n        for batch in val_loader:\n            neural = batch['neural'].to(device)\n            lengths = batch['lengths']\n            day_idx = batch['day_idx'].to(device)\n            sentences = batch['sentences']\n            targets = batch['target']              # [B, T_max] padded phoneme ids\n            target_lengths = batch['target_lengths']\n\n            log_probs, output_lengths = model(neural, lengths, day_idx)\n            decoded_phonemes = greedy_decode_phonemes(log_probs, output_lengths)\n\n            for b, phon_ids in enumerate(decoded_phonemes):\n                # ---- PER ----\n                ref_phon_ids = targets[b, :target_lengths[b]].tolist()\n                if len(ref_phon_ids) > 0:\n                    total_phon_edits += editdistance.eval(phon_ids, ref_phon_ids)\n                    total_phons += len(ref_phon_ids)\n\n                # ---- WER ----\n                ref = sentences[b]\n                if not ref or not ref.strip():\n                    continue\n                hyp = phoneme_ids_to_words(phon_ids, lexicon_reverse)\n                ref_words, hyp_words = ref.split(), hyp.split()\n                total_word_edits += editdistance.eval(hyp_words, ref_words)\n                total_words += max(len(ref_words), 1)\n\n    wer = 100.0 * total_word_edits / max(total_words, 1)\n    per = 100.0 * total_phon_edits / max(total_phons, 1)\n    return wer, per\n\n\ndef average_state_dicts(state_dicts):\n    \"\"\"Simple SWA-style weight averaging across a handful of checkpoints - a\n    memory-free way to get an ensembling boost without loading multiple full\n    models at inference time (which is what was causing the OOMs before).\"\"\"\n    avg = {k: torch.zeros_like(v, dtype=torch.float32) for k, v in state_dicts[0].items()}\n    for sd in state_dicts:\n        for k, v in sd.items():\n            avg[k] += v.float()\n    for k in avg:\n        avg[k] /= len(state_dicts)\n    return avg\n\n\ndef train_model(train_loader, val_loader, config, n_days, lexicon_reverse):\n    model = HybridLSTMTransformerCTC(\n        n_days=n_days,\n        input_size=512, d_model=config['d_model'], n_heads=config['n_heads'],\n        n_layers=config['n_layers'], d_ff=config['d_ff'], patch_size=config['patch_size'],\n        vocab_size=config['n_classes'], dropout=config['dropout'], head_dim=config['head_dim'],\n        attn_dropout=config['attn_dropout'], lstm_hidden=config['lstm_hidden'],\n        lstm_layers=config['lstm_layers'], smooth_std=config['smooth_kernel_std'],\n        smooth_size=config['smooth_kernel_size'], drop_path_rate=config['drop_path_rate']\n    ).to(config['device'])\n\n    criterion = nn.CTCLoss(blank=0, zero_infinity=True)\n    optimizer = optim.AdamW(model.parameters(), lr=config['learning_rate'], weight_decay=config['weight_decay'])\n    scaler = GradScaler(enabled=config['use_amp'])\n\n    start_epoch = 0\n    if config.get('resume_from_checkpoint'):\n        print(f\"Resuming from {config['resume_from_checkpoint']}...\")\n        checkpoint = torch.load(config['resume_from_checkpoint'], map_location=config['device'])\n        model.load_state_dict(checkpoint['model_state_dict'])\n        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n        start_epoch = checkpoint.get('epoch', -1) + 1\n        print(f\"Resumed at epoch {start_epoch}.\")\n\n    scheduler = optim.lr_scheduler.OneCycleLR(\n        optimizer, max_lr=config['learning_rate'], epochs=config['num_epochs'],\n        steps_per_epoch=len(train_loader), pct_start=0.1,\n        last_epoch=(start_epoch * len(train_loader) - 1) if start_epoch else -1\n    )\n\n    epoch_losses = []\n    epoch_wers = []\n    epoch_pers = []\n    best_wer = float('inf')\n    patience, no_improve = 10, 0\n    top_checkpoints = []  # (wer, state_dict) kept for end-of-training SWA averaging\n\n    for epoch in range(start_epoch, config['num_epochs']):\n        model.train()\n        train_loss = 0\n\n        for batch in tqdm(train_loader, desc=f'Epoch {epoch+1}/{config[\"num_epochs\"]}'):\n            neural = batch['neural'].to(config['device'])\n            target = batch['target'].to(config['device'])\n            lengths = batch['lengths']\n            target_lengths = batch['target_lengths']\n            day_idx = batch['day_idx'].to(config['device'])\n\n            optimizer.zero_grad()\n            with autocast(enabled=config['use_amp']):\n                log_probs, output_lengths = model(neural, lengths, day_idx)\n                ctc_loss = criterion(log_probs.float(), target, output_lengths, target_lengths)\n\n                drift_loss = 0.0\n                if config['drift_lambda'] > 0 and n_days > 1:\n                    for d in range(1, n_days):\n                        w_diff = model.day_weights[d] - model.day_weights[d - 1]\n                        b_diff = model.day_biases[d] - model.day_biases[d - 1]\n                        drift_loss += (torch.sum(w_diff ** 2) + torch.sum(b_diff ** 2))\n                    drift_loss = drift_loss / (n_days - 1)\n\n                loss = ctc_loss + (config['drift_lambda'] * drift_loss)\n\n            scaler.scale(loss).backward()\n            scaler.unscale_(optimizer)\n            torch.nn.utils.clip_grad_norm_(model.parameters(), config['grad_clip'])\n            scaler.step(optimizer)\n            scaler.update()\n            scheduler.step()\n            train_loss += loss.item()\n\n        avg_loss = train_loss / len(train_loader)\n        epoch_losses.append(avg_loss)\n        val_wer, val_per = validate_model(model, val_loader, config['device'], lexicon_reverse)\n        epoch_wers.append(val_wer)\n        epoch_pers.append(val_per)\n\n        print(f\"Epoch {epoch+1}: Loss={avg_loss:.4f}, WER={val_wer:.2f}%, PER={val_per:.2f}%\")\n\n        ckpt = {\n            'model_state_dict': model.state_dict(),\n            'optimizer_state_dict': optimizer.state_dict(),\n            'epoch': epoch,\n            'config': config,\n        }\n        torch.save(ckpt, os.path.join(config['checkpoint_dir'], 'latest_model.pt'))\n\n        if val_wer < best_wer:\n            best_wer = val_wer\n            no_improve = 0\n            torch.save({**ckpt, 'wer': best_wer, 'per': val_per}, os.path.join(config['checkpoint_dir'], 'best_model.pt'))\n        else:\n            no_improve += 1\n\n        # Keep the top-3 checkpoints (by WER) in CPU memory for SWA averaging -\n        # a single extra model's worth of RAM, no extra GPU memory.\n        top_checkpoints.append((val_wer, {k: v.cpu().clone() for k, v in model.state_dict().items()}))\n        top_checkpoints = sorted(top_checkpoints, key=lambda x: x[0])[:3]\n\n        if no_improve >= patience:\n            print(f\"\\nEarly stopping after {epoch+1} epochs\")\n            break\n\n    if len(top_checkpoints) > 1:\n        swa_state = average_state_dicts([sd for _, sd in top_checkpoints])\n        torch.save({'model_state_dict': swa_state, 'config': config},\n                    os.path.join(config['checkpoint_dir'], 'swa_model.pt'))\n        print(f\"Saved SWA average of top {len(top_checkpoints)} checkpoints -> swa_model.pt \"\n              f\"(try this at inference time - it usually beats the single best checkpoint).\")\n\n    # Save raw history so the curves below can be regenerated later (e.g. for\n    # the paper) without re-running training.\n    figures_dir = os.path.join(config['checkpoint_dir'], 'figures')\n    os.makedirs(figures_dir, exist_ok=True)\n    with open(os.path.join(config['checkpoint_dir'], 'training_history.json'), 'w') as f:\n        json.dump({'loss': epoch_losses, 'wer': epoch_wers, 'per': epoch_pers}, f, indent=2)\n\n    # ---- Three SEPARATE plots (loss / WER / PER), one figure each ----\n    epochs_range = range(1, len(epoch_losses) + 1)\n\n    fig1, ax = plt.subplots(figsize=(8, 5))\n    ax.plot(epochs_range, epoch_losses, color='tab:blue')\n    ax.set_xlabel('Epoch'); ax.set_ylabel('CTC Loss')\n    ax.set_title('Training Loss over Epochs')\n    ax.grid(True, alpha=0.3)\n    fig1.tight_layout()\n    fig1.savefig(os.path.join(figures_dir, 'training_loss.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    fig2, ax = plt.subplots(figsize=(8, 5))\n    ax.plot(epochs_range, epoch_wers, color='tab:red', marker='o', markersize=3)\n    ax.set_xlabel('Epoch'); ax.set_ylabel('Word Error Rate (%)')\n    ax.set_title('Validation WER over Epochs')\n    ax.axhline(best_wer, color='black', linestyle='--', linewidth=1, label=f'Best = {best_wer:.2f}%')\n    ax.legend()\n    ax.grid(True, alpha=0.3)\n    fig2.tight_layout()\n    fig2.savefig(os.path.join(figures_dir, 'training_wer.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    fig3, ax = plt.subplots(figsize=(8, 5))\n    ax.plot(epochs_range, epoch_pers, color='tab:green', marker='o', markersize=3)\n    ax.set_xlabel('Epoch'); ax.set_ylabel('Phoneme Error Rate (%)')\n    ax.set_title('Validation PER over Epochs')\n    ax.axhline(min(epoch_pers), color='black', linestyle='--', linewidth=1,\n               label=f'Best = {min(epoch_pers):.2f}%')\n    ax.legend()\n    ax.grid(True, alpha=0.3)\n    fig3.tight_layout()\n    fig3.savefig(os.path.join(figures_dir, 'training_per.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    return model, best_wer, epoch_losses, epoch_wers, epoch_pers\n","metadata":{"papermill":{"duration":0.055379,"end_time":"2026-08-29T04:11:51.691065+00:00","exception":false,"start_time":"2026-08-29T04:11:51.635686+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e1389ff7","cell_type":"code","source":"# ============================================================================\n# BUILD DATASETS & LAUNCH TRAINING\n# ============================================================================\n# NOTE: in the original notebook this driver cell was empty, and the actual\n# train_model()/validate_model() definitions below lived inside a *markdown*\n# cell - so none of that training code ever actually ran as code. Both are\n# fixed here: this cell wires everything up, and the next cell is a proper\n# code cell.\n\nif SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - not loading the train split or running any \"\n          \"epochs. The early eval plots right below this cell also need \"\n          \"`model`/`val_loader` from an actual training run, so they're \"\n          \"skipped too (guarded the same way) - go straight to the 'Force \"\n          \"Reload Model & Load Datasets' section (Block 5) below, which \"\n          \"reloads everything from PRETRAINED_CKPT_DATASET instead.\")\nelse:\n    print(\"Scanning sessions...\")\n    session2idx = get_session2idx(CONFIG['data_dir'])\n    n_days = len(session2idx)\n    print(f\"Found {n_days} recording sessions/days.\")\n\n    train_data = load_split(CONFIG['data_dir'], 'train')\n    val_data = load_split(CONFIG['data_dir'], 'val')\n\n    print(\"Computing global per-channel normalization stats from TRAIN only...\")\n    feat_mean, feat_std = compute_channel_stats(train_data)\n    torch.save({'mean': feat_mean, 'std': feat_std},\n               os.path.join(CONFIG['checkpoint_dir'], 'norm_stats.pt'))\n\n    train_dataset = BrainToTextDataset(train_data, session2idx, feat_mean, feat_std,\n                                        augment=CONFIG['use_augmentation'])\n    val_dataset = BrainToTextDataset(val_data, session2idx, feat_mean, feat_std, augment=False)\n\n    train_loader = DataLoader(train_dataset, batch_size=CONFIG['batch_size'], shuffle=True,\n                               collate_fn=collate_fn, num_workers=2, pin_memory=True)\n    val_loader = DataLoader(val_dataset, batch_size=CONFIG['batch_size'], shuffle=False,\n                             collate_fn=collate_fn, num_workers=2, pin_memory=True)\n\n    # Build the phoneme -> word lexicon reverse-map once, up front, so\n    # validate_model() can report end-to-end WORD error rate (and phoneme error\n    # rate) every epoch instead of just training loss. This is a plain dict\n    # lookup (no beam search / n-gram LM), so it adds negligible time per epoch.\n    print(\"Loading phoneme lexicon for end-to-end WER...\")\n    lexicon_ds = resolve_kaggle_dataset('heyyousum/quality-english-dataset-for-ngram-model-v2')\n    lexicon_path_train = os.path.join(lexicon_ds, 'lexicon.txt')\n    lexicon_reverse = build_lexicon_reverse(lexicon_path_train)\n    print(f\"Lexicon reverse-map built: {len(lexicon_reverse)} unique phoneme sequences.\")\n\n    model, best_wer, epoch_losses, epoch_wers, epoch_pers = train_model(\n        train_loader, val_loader, CONFIG, n_days, lexicon_reverse\n    )\n    print(f\"Training finished. Best validation WER: {best_wer:.2f}% \"\n          f\"(PER at that point: {epoch_pers[epoch_wers.index(best_wer)]:.2f}%)\")\n","metadata":{"papermill":{"duration":0.032194,"end_time":"2026-08-29T04:11:51.744588+00:00","exception":false,"start_time":"2026-08-29T04:11:51.712394+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"af3ad087","cell_type":"markdown","source":"### Block 4b: Evaluation Plots (for the paper)\n\nRuns entirely on the validation set (test labels aren't available for local\nscoring). All three error rates are now computed and plotted **separately**:\n- **WER** (word-level, via the lexicon)\n- **CER** (character-level, on the same decoded text)\n- **PER** (phoneme-level, comparing raw CTC-greedy output to the ground-truth\n  phoneme ids directly - no lexicon involved)\n\nEverything below uses the same fast, LM-free lexicon + greedy decode as\ntraining-time validation (not the full beam-search + n-gram + LLM pipeline) -\nthat more accurate, submission-matching set of numbers is computed later, in\nthe dedicated Evaluation section, once the real decoder is built.\n\nProduces, as 300dpi PNGs in `CONFIG['checkpoint_dir']/figures/`:\n- `training_loss.png`, `training_wer.png`, `training_per.png` — one metric\n  per plot, saved automatically at the end of training\n- `wer_breakdown.png` / `cer_breakdown.png` / `per_breakdown.png` — each\n  error split into substitution/deletion/insertion %, plus the most frequent\n  substitutions at that granularity (word / character / phoneme)\n- `wer_by_day_greedy.png` / `cer_by_day_greedy.png` / `per_by_day_greedy.png`\n  — validation error broken down by recording day/session, one chart per metric\n- `day_adaptive_drift.png` — magnitude of the learned day-specific transform vs. day index\n- `sequence_length_hist.png` — neural trial length and phoneme sequence length distributions\n","metadata":{"papermill":{"duration":0.022274,"end_time":"2026-08-29T04:11:51.788986+00:00","exception":false,"start_time":"2026-08-29T04:11:51.766712+00:00","status":"completed"},"tags":[]}},{"id":"0b8f1668","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 1a (Word WER breakdown)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 1a: Word-Level WER Error Breakdown & Most-Confused Words (val set)\n    # ============================================================================\n    # Every count here is at the WORD level - substitutions, deletions and\n    # insertions are exactly the three components that sum to WER, and the\n    # \"most frequent substitutions\" list is the word-level analogue of a\n    # confusion matrix (a full word x word confusion matrix isn't meaningful\n    # here - the vocabulary is open-ended and it would be enormous and sparse).\n    # Uses the same fast, LM-free lexicon + greedy decode as training-time\n    # validation - the accurate beam-search + n-gram + LLM version of this same\n    # breakdown can be computed later the same way, once that decoder is built.\n    #\n    # This loop ALSO collects the raw phoneme strings (ref + greedy hyp) for\n    # every sample, at zero extra forward-pass cost, so the PER breakdown in the\n    # next cell and the CER breakdown two cells down can reuse this same pass\n    # instead of re-running the model.\n    #\n    # Alignment + error accounting is done with jiwer (jiwer.process_words)\n    # instead of a hand-rolled DP edit-distance/backtrace - jiwer already\n    # returns the per-token alignment chunks (substitution/deletion/insertion)\n    # we need, and the exact same call works for phonemes if we feed it\n    # space-separated phoneme-symbol strings instead of words.\n    import numpy as np\n    import matplotlib.pyplot as plt\n    from collections import Counter\n\n    figures_dir = os.path.join(CONFIG['checkpoint_dir'], 'figures')\n    os.makedirs(figures_dir, exist_ok=True)\n\n    refs_all, hyps_all = [], []                 # word-level (WER, and also used for CER)\n    ref_phon_strs_all, hyp_phon_strs_all = [], []  # phoneme-level (PER)\n\n    model.eval()\n    with torch.no_grad():\n        for batch in tqdm(val_loader, desc=\"Word/Phoneme-level breakdown (greedy)\"):\n            neural = batch['neural'].to(CONFIG['device'])\n            lengths = batch['lengths']\n            day_idx = batch['day_idx'].to(CONFIG['device'])\n            sentences = batch['sentences']\n            targets = batch['target']\n            target_lengths = batch['target_lengths']\n\n            log_probs, output_lengths = model(neural, lengths, day_idx)\n            decoded_phonemes = greedy_decode_phonemes(log_probs, output_lengths)\n\n            for b, phon_ids in enumerate(decoded_phonemes):\n                ref_phon_ids = targets[b, :target_lengths[b]].tolist()\n                if len(ref_phon_ids) > 0:\n                    ref_phon_strs_all.append(phoneme_ids_to_str(ref_phon_ids))\n                    hyp_phon_strs_all.append(phoneme_ids_to_str(phon_ids) if phon_ids else \"BLANK\")\n\n                ref = sentences[b]\n                if not ref or not ref.strip():\n                    continue\n                hyp = phoneme_ids_to_words(phon_ids, lexicon_reverse)\n                refs_all.append(ref)\n                hyps_all.append(hyp if hyp.strip() else \"<empty>\")\n\n    # Single corpus-level jiwer call: gives overall WER plus, via `.alignments`,\n    # the exact substitution/deletion/insertion word pairs per sentence.\n    word_output = jiwer.process_words(refs_all, hyps_all)\n\n    total_ref_words = sum(len(r) for r in word_output.references)\n    total_sub = word_output.substitutions\n    total_del = word_output.deletions\n    total_ins = word_output.insertions\n\n    substitution_counts = Counter()   # (ref_word, hyp_word) -> count\n    deletion_counts = Counter()       # ref_word -> count (missed entirely)\n    insertion_counts = Counter()      # hyp_word -> count (hallucinated extra)\n\n    for ref_words, hyp_words, chunks in zip(word_output.references, word_output.hypotheses,\n                                             word_output.alignments):\n        for chunk in chunks:\n            if chunk.type == \"substitute\":\n                for r, h in zip(ref_words[chunk.ref_start_idx:chunk.ref_end_idx],\n                                 hyp_words[chunk.hyp_start_idx:chunk.hyp_end_idx]):\n                    substitution_counts[(r, h)] += 1\n            elif chunk.type == \"delete\":\n                for r in ref_words[chunk.ref_start_idx:chunk.ref_end_idx]:\n                    deletion_counts[r] += 1\n            elif chunk.type == \"insert\":\n                for h in hyp_words[chunk.hyp_start_idx:chunk.hyp_end_idx]:\n                    insertion_counts[h] += 1\n\n    wer_pct = 100.0 * word_output.wer\n    sub_pct = 100.0 * total_sub / max(total_ref_words, 1)\n    del_pct = 100.0 * total_del / max(total_ref_words, 1)\n    ins_pct = 100.0 * total_ins / max(total_ref_words, 1)\n\n    fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n\n    # Left: WER split into its three components (these sum to total WER).\n    bars = axes[0].bar(['Substitutions', 'Deletions', 'Insertions'],\n                        [sub_pct, del_pct, ins_pct],\n                        color=['tab:red', 'tab:orange', 'tab:blue'])\n    axes[0].set_ylabel('% of reference words')\n    axes[0].set_title(f'Word-Level WER Breakdown (Total WER = {wer_pct:.1f}%)')\n    for bar, val in zip(bars, [sub_pct, del_pct, ins_pct]):\n        axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),\n                     f'{val:.1f}%', ha='center', va='bottom')\n\n    # Right: most frequent word-for-word substitutions.\n    top_subs = substitution_counts.most_common(15)\n    labels = [f'{r} → {h}' for (r, h), _ in top_subs]\n    counts = [c for _, c in top_subs]\n    axes[1].barh(range(len(labels)), counts, color='tab:purple')\n    axes[1].set_yticks(range(len(labels)))\n    axes[1].set_yticklabels(labels, fontsize=8)\n    axes[1].invert_yaxis()\n    axes[1].set_xlabel('Count')\n    axes[1].set_title('Most Frequent Word Substitutions')\n\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'wer_breakdown.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    print(f\"WER = {wer_pct:.2f}%  (Sub={sub_pct:.2f}%, Del={del_pct:.2f}%, Ins={ins_pct:.2f}%)\")\n","metadata":{"papermill":{"duration":0.039191,"end_time":"2026-08-29T04:11:51.849559+00:00","exception":false,"start_time":"2026-08-29T04:11:51.810368+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"abf7df71","cell_type":"markdown","source":"### Eval Plot 1b & 1c: CER and PER breakdowns (same pass as above)","metadata":{"papermill":{"duration":0.02117,"end_time":"2026-08-29T04:11:51.892167+00:00","exception":false,"start_time":"2026-08-29T04:11:51.870997+00:00","status":"completed"},"tags":[]}},{"id":"7d15ba9a","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 1b (CER breakdown)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 1b: Character-Level CER Breakdown & Most-Confused Characters\n    # ============================================================================\n    # Same idea as the WER breakdown above, but at the character level, on the\n    # exact same decoded sentences (refs_all / hyps_all from the previous cell -\n    # no extra forward pass). jiwer.process_characters is the character-level\n    # twin of jiwer.process_words: same alignment-chunk structure, just applied\n    # to individual characters instead of whitespace-separated words.\n    import numpy as np\n    import matplotlib.pyplot as plt\n    from collections import Counter\n\n    char_output = jiwer.process_characters(refs_all, hyps_all)\n\n    total_ref_chars = sum(len(r) for r in char_output.references)\n    total_sub_c = char_output.substitutions\n    total_del_c = char_output.deletions\n    total_ins_c = char_output.insertions\n\n    char_sub_counts = Counter()\n    char_del_counts = Counter()\n    char_ins_counts = Counter()\n\n    for ref_chars, hyp_chars, chunks in zip(char_output.references, char_output.hypotheses,\n                                             char_output.alignments):\n        for chunk in chunks:\n            if chunk.type == \"substitute\":\n                for r, h in zip(ref_chars[chunk.ref_start_idx:chunk.ref_end_idx],\n                                 hyp_chars[chunk.hyp_start_idx:chunk.hyp_end_idx]):\n                    char_sub_counts[(r, h)] += 1\n            elif chunk.type == \"delete\":\n                for r in ref_chars[chunk.ref_start_idx:chunk.ref_end_idx]:\n                    char_del_counts[r] += 1\n            elif chunk.type == \"insert\":\n                for h in hyp_chars[chunk.hyp_start_idx:chunk.hyp_end_idx]:\n                    char_ins_counts[h] += 1\n\n    cer_pct = 100.0 * char_output.cer\n    csub_pct = 100.0 * total_sub_c / max(total_ref_chars, 1)\n    cdel_pct = 100.0 * total_del_c / max(total_ref_chars, 1)\n    cins_pct = 100.0 * total_ins_c / max(total_ref_chars, 1)\n\n    fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n\n    bars = axes[0].bar(['Substitutions', 'Deletions', 'Insertions'],\n                        [csub_pct, cdel_pct, cins_pct],\n                        color=['tab:red', 'tab:orange', 'tab:blue'])\n    axes[0].set_ylabel('% of reference characters')\n    axes[0].set_title(f'Character-Level CER Breakdown (Total CER = {cer_pct:.1f}%)')\n    for bar, val in zip(bars, [csub_pct, cdel_pct, cins_pct]):\n        axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),\n                     f'{val:.1f}%', ha='center', va='bottom')\n\n    top_char_subs = char_sub_counts.most_common(15)\n    labels = [f'{repr(r)} → {repr(h)}' for (r, h), _ in top_char_subs]\n    counts = [c for _, c in top_char_subs]\n    axes[1].barh(range(len(labels)), counts, color='tab:brown')\n    axes[1].set_yticks(range(len(labels)))\n    axes[1].set_yticklabels(labels, fontsize=8)\n    axes[1].invert_yaxis()\n    axes[1].set_xlabel('Count')\n    axes[1].set_title('Most Frequent Character Substitutions')\n\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'cer_breakdown.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    print(f\"CER = {cer_pct:.2f}%  (Sub={csub_pct:.2f}%, Del={cdel_pct:.2f}%, Ins={cins_pct:.2f}%)\")\n","metadata":{"papermill":{"duration":0.036303,"end_time":"2026-08-29T04:11:51.949517+00:00","exception":false,"start_time":"2026-08-29T04:11:51.913214+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3b9496d1","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 1c (PER breakdown)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 1c: Phoneme-Level PER Breakdown & Most-Confused Phonemes\n    # ============================================================================\n    # Same jiwer.process_words machinery again, but this time fed space-joined\n    # PHONEME symbol strings (ref_phon_strs_all / hyp_phon_strs_all, collected in\n    # the first eval cell) instead of words - jiwer just sees whitespace-\n    # separated tokens, so this gives a proper phoneme-level alignment and\n    # substitution/deletion/insertion breakdown for free, no lexicon involved.\n    # This is the diagnostic that most directly reflects what CTC training\n    # actually optimizes.\n    import numpy as np\n    import matplotlib.pyplot as plt\n    from collections import Counter\n\n    phon_output = jiwer.process_words(ref_phon_strs_all, hyp_phon_strs_all)\n\n    total_ref_phons = sum(len(r) for r in phon_output.references)\n    total_sub_p = phon_output.substitutions\n    total_del_p = phon_output.deletions\n    total_ins_p = phon_output.insertions\n\n    phon_sub_counts = Counter()\n    phon_del_counts = Counter()\n    phon_ins_counts = Counter()\n\n    for ref_phons, hyp_phons, chunks in zip(phon_output.references, phon_output.hypotheses,\n                                             phon_output.alignments):\n        for chunk in chunks:\n            if chunk.type == \"substitute\":\n                for r, h in zip(ref_phons[chunk.ref_start_idx:chunk.ref_end_idx],\n                                 hyp_phons[chunk.hyp_start_idx:chunk.hyp_end_idx]):\n                    phon_sub_counts[(r, h)] += 1\n            elif chunk.type == \"delete\":\n                for r in ref_phons[chunk.ref_start_idx:chunk.ref_end_idx]:\n                    phon_del_counts[r] += 1\n            elif chunk.type == \"insert\":\n                for h in hyp_phons[chunk.hyp_start_idx:chunk.hyp_end_idx]:\n                    phon_ins_counts[h] += 1\n\n    per_pct = 100.0 * phon_output.wer   # jiwer still calls the field `.wer` internally - it's PER here\n    psub_pct = 100.0 * total_sub_p / max(total_ref_phons, 1)\n    pdel_pct = 100.0 * total_del_p / max(total_ref_phons, 1)\n    pins_pct = 100.0 * total_ins_p / max(total_ref_phons, 1)\n\n    fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n\n    bars = axes[0].bar(['Substitutions', 'Deletions', 'Insertions'],\n                        [psub_pct, pdel_pct, pins_pct],\n                        color=['tab:red', 'tab:orange', 'tab:blue'])\n    axes[0].set_ylabel('% of reference phonemes')\n    axes[0].set_title(f'Phoneme-Level PER Breakdown (Total PER = {per_pct:.1f}%)')\n    for bar, val in zip(bars, [psub_pct, pdel_pct, pins_pct]):\n        axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),\n                     f'{val:.1f}%', ha='center', va='bottom')\n\n    top_phon_subs = phon_sub_counts.most_common(15)\n    labels = [f'{r} → {h}' for (r, h), _ in top_phon_subs]\n    counts = [c for _, c in top_phon_subs]\n    axes[1].barh(range(len(labels)), counts, color='tab:cyan')\n    axes[1].set_yticks(range(len(labels)))\n    axes[1].set_yticklabels(labels, fontsize=8)\n    axes[1].invert_yaxis()\n    axes[1].set_xlabel('Count')\n    axes[1].set_title('Most Frequent Phoneme Substitutions')\n\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'per_breakdown.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n\n    print(f\"PER = {per_pct:.2f}%  (Sub={psub_pct:.2f}%, Del={pdel_pct:.2f}%, Ins={pins_pct:.2f}%)\")\n","metadata":{"papermill":{"duration":0.035358,"end_time":"2026-08-29T04:11:52.006325+00:00","exception":false,"start_time":"2026-08-29T04:11:51.970967+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1ecfd3a0","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 2 (WER by day)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 2: Validation WER / CER / PER by Recording Day (fast greedy decode)\n    # ============================================================================\n    # Shows whether the day-adaptive input layer is actually closing the gap\n    # across sessions, or whether some days are still much harder than others -\n    # now broken out into three SEPARATE charts, one per error metric, instead\n    # of just WER. Uses the same fast, LM-free greedy phoneme->word decode as\n    # training-time validation (not the full beam-search + n-gram + LLM\n    # pipeline) - that more accurate version is computed later, in the dedicated\n    # Evaluation section, once the real decoder is built.\n    idx2session = {v: k for k, v in session2idx.items()}\n    day_word_edits = {d: 0 for d in idx2session}\n    day_word_counts = {d: 0 for d in idx2session}\n    day_char_edits = {d: 0 for d in idx2session}\n    day_char_counts = {d: 0 for d in idx2session}\n    day_phon_edits = {d: 0 for d in idx2session}\n    day_phon_counts = {d: 0 for d in idx2session}\n\n    model.eval()\n    with torch.no_grad():\n        for batch in tqdm(val_loader, desc=\"Per-day WER/CER/PER (greedy)\"):\n            neural = batch['neural'].to(CONFIG['device'])\n            lengths = batch['lengths']\n            day_idx_batch = batch['day_idx']\n            sentences = batch['sentences']\n            targets = batch['target']\n            target_lengths = batch['target_lengths']\n\n            log_probs, output_lengths = model(neural, lengths, day_idx_batch.to(CONFIG['device']))\n            decoded_phonemes = greedy_decode_phonemes(log_probs, output_lengths)\n\n            for b, phon_ids in enumerate(decoded_phonemes):\n                d = int(day_idx_batch[b])\n\n                # ---- PER (per day) ----\n                ref_phon_ids = targets[b, :target_lengths[b]].tolist()\n                if len(ref_phon_ids) > 0:\n                    day_phon_edits[d] += editdistance.eval(phon_ids, ref_phon_ids)\n                    day_phon_counts[d] += len(ref_phon_ids)\n\n                ref = sentences[b]\n                if not ref or not ref.strip():\n                    continue\n                hyp = phoneme_ids_to_words(phon_ids, lexicon_reverse)\n\n                # ---- WER (per day) ----\n                ref_words, hyp_words = ref.split(), hyp.split()\n                day_word_edits[d] += editdistance.eval(hyp_words, ref_words)\n                day_word_counts[d] += max(len(ref_words), 1)\n\n                # ---- CER (per day) ----\n                day_char_edits[d] += editdistance.eval(list(hyp), list(ref))\n                day_char_counts[d] += max(len(ref), 1)\n\n    days_sorted = sorted(idx2session.keys())\n    session_names = [idx2session[d] for d in days_sorted]\n\n\n    def _plot_metric_by_day(edits, counts, ylabel, title, color, filename):\n        values = [100.0 * edits[d] / counts[d] if counts[d] > 0 else np.nan for d in days_sorted]\n        fig, ax = plt.subplots(figsize=(max(10, len(days_sorted) * 0.35), 5))\n        ax.bar(range(len(days_sorted)), values, color=color)\n        ax.set_xticks(range(len(days_sorted)))\n        ax.set_xticklabels(session_names, rotation=90, fontsize=7)\n        ax.set_ylabel(ylabel)\n        ax.set_title(title)\n        ax.axhline(np.nanmean(values), color='black', linestyle='--', linewidth=1,\n                   label=f'Mean = {np.nanmean(values):.1f}%')\n        ax.legend()\n        fig.tight_layout()\n        fig.savefig(os.path.join(figures_dir, filename), dpi=300, bbox_inches='tight')\n        plt.show()\n        return values\n\n\n    wer_by_day = _plot_metric_by_day(day_word_edits, day_word_counts, 'Word Error Rate (%)',\n                                      'Validation WER by Recording Session/Day (greedy decode)',\n                                      'tab:orange', 'wer_by_day_greedy.png')\n\n    cer_by_day = _plot_metric_by_day(day_char_edits, day_char_counts, 'Character Error Rate (%)',\n                                      'Validation CER by Recording Session/Day (greedy decode)',\n                                      'tab:blue', 'cer_by_day_greedy.png')\n\n    per_by_day = _plot_metric_by_day(day_phon_edits, day_phon_counts, 'Phoneme Error Rate (%)',\n                                      'Validation PER by Recording Session/Day (greedy decode)',\n                                      'tab:green', 'per_by_day_greedy.png')\n","metadata":{"papermill":{"duration":0.036552,"end_time":"2026-08-29T04:11:52.064204+00:00","exception":false,"start_time":"2026-08-29T04:11:52.027652+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c818820f","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 3 (day-adaptive drift)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 3: Day-Adaptive Layer Drift\n    # ============================================================================\n    # Visualizes how far each day's learned transform has moved from identity\n    # (the initialization) and from the previous day - directly illustrates what\n    # the drift regularization term is constraining during training.\n    with torch.no_grad():\n        identity = torch.eye(model.day_weights[0].shape[0], device=CONFIG['device'])\n        dist_from_identity = [\n            torch.norm(model.day_weights[d] - identity).item() for d in range(n_days)\n        ]\n        consecutive_drift = [0.0] + [\n            torch.norm(model.day_weights[d] - model.day_weights[d - 1]).item()\n            for d in range(1, n_days)\n        ]\n\n    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))\n    ax1.plot(range(n_days), dist_from_identity, marker='o', markersize=3)\n    ax1.set_xlabel('Day index (chronological)'); ax1.set_ylabel('‖W_day − I‖_F')\n    ax1.set_title('Day-Specific Transform: Distance from Identity')\n    ax1.grid(True, alpha=0.3)\n\n    ax2.plot(range(n_days), consecutive_drift, marker='o', markersize=3, color='tab:green')\n    ax2.set_xlabel('Day index (chronological)'); ax2.set_ylabel('‖W_day − W_(day-1)‖_F')\n    ax2.set_title('Day-Specific Transform: Consecutive-Day Drift')\n    ax2.grid(True, alpha=0.3)\n\n    fig.suptitle('Effect of Day-Specific Drift Regularization (λ = %.3f)' % CONFIG['drift_lambda'])\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'day_adaptive_drift.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n","metadata":{"papermill":{"duration":0.031348,"end_time":"2026-08-29T04:11:52.117379+00:00","exception":false,"start_time":"2026-08-29T04:11:52.086031+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"37f2840d","cell_type":"code","source":"if SKIP_TRAINING:\n    print(\"SKIP_TRAINING=True - skipping 'Eval Plot 4 (sequence length distributions)' (needs the model/data from an actual training run). Use the Block 5 validation pipeline further down instead, which reloads everything from PRETRAINED_CKPT_DATASET.\")\nelse:\n    # ============================================================================\n    # EVAL PLOT 4: Sequence Length Distributions (dataset stats for the paper)\n    # ============================================================================\n    train_neural_lens = train_data['n_steps']\n    train_phoneme_lens = train_data['phoneme_len']\n    val_neural_lens = val_data['n_steps']\n    val_phoneme_lens = val_data['phoneme_len']\n\n    fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n\n    axes[0].hist(train_neural_lens, bins=40, alpha=0.6, label='Train', density=True)\n    axes[0].hist(val_neural_lens, bins=40, alpha=0.6, label='Val', density=True)\n    axes[0].set_xlabel('Trial length (timesteps)'); axes[0].set_ylabel('Density')\n    axes[0].set_title('Neural Trial Length Distribution'); axes[0].legend()\n\n    axes[1].hist(train_phoneme_lens, bins=30, alpha=0.6, label='Train', density=True)\n    axes[1].hist(val_phoneme_lens, bins=30, alpha=0.6, label='Val', density=True)\n    axes[1].set_xlabel('Phoneme sequence length'); axes[1].set_ylabel('Density')\n    axes[1].set_title('Target Phoneme Length Distribution'); axes[1].legend()\n\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'sequence_length_hist.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n","metadata":{"papermill":{"duration":0.031123,"end_time":"2026-08-29T04:11:52.169962+00:00","exception":false,"start_time":"2026-08-29T04:11:52.138839+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2ab40261","cell_type":"markdown","source":"## Block 5 · Inference, decoding & evaluation\n\n### [Removed] Legacy `main()` / character-vocab submission block\n\nThis cell used to hold an old `main()` (with `char2idx`, `idx2char`, a\ncharacter-level greedy decoder, and its own copy of dataset/training calls)\nleft over from before the switch to phoneme-level CTC. It referenced\n`char2idx` and a `train_model` signature that no longer exist in this\nnotebook, so if it was ever run as code it would fail immediately with a\n`NameError`. That's a likely cause of your submission run not working if you\nwere executing this cell (or \"Run All\").\n\nIts job is already done, correctly, by the cells below:\n- Training: **Block 4** (dataset build + `train_model` call)\n- Test inference + decoding + submission file: **Force Reload Model & Load\n  Datasets** → **Attach Lexicon** → **Build Decoder** → **Extract Emissions,\n  Beam Search & LLM Rescoring** (writes `submission.csv`)\n\nNothing else to do here - just don't run this cell.\n","metadata":{"papermill":{"duration":0.021996,"end_time":"2026-08-29T04:11:52.213746+00:00","exception":false,"start_time":"2026-08-29T04:11:52.19175+00:00","status":"completed"},"tags":[]}},{"id":"2f322b23","cell_type":"markdown","source":"### Block 5.1 · Reload trained model & build validation set\n","metadata":{"papermill":{"duration":0.023236,"end_time":"2026-08-29T04:11:52.25874+00:00","exception":false,"start_time":"2026-08-29T04:11:52.235504+00:00","status":"completed"},"tags":[]}},{"id":"93477761","cell_type":"code","source":"# ============================================================================\n# 1. RELOAD BEST MODEL & BUILD VALIDATION SET\n# ============================================================================\n# Reuses the exact normalization stats saved during training - no more\n# per-trial re-normalization at test time, and no more train/test mismatch\n# from clipping only on one side.\n#\n# NOTE: this used to also reload the *full* train split here (\"needed for\n# the LM corpus later\") and the competition test set. Neither is actually\n# used by anything between here and the validation WER cells below - the\n# n-gram LM comes prebuilt from the attached KenLM dataset, not from\n# retokenizing the train split, so that reload was pure dead weight. It\n# re-reads all 45 sessions' worth of neural feature data end-to-end, which\n# is exactly what was silently stalling this cell for several minutes with\n# nothing to show for it. Both have been moved to the OPTIONAL\n# submission-generation cell at the very end of the notebook, which is the\n# only place that actually needs them - this cell now only loads what the\n# validation WER pipeline needs, so it should finish in seconds, not minutes.\nimport os\nimport torch\nfrom torch.utils.data import DataLoader\nimport pandas as pd\nimport numpy as np\nimport h5py\nfrom glob import glob\nfrom pathlib import Path\nfrom tqdm import tqdm\n\nprint('Rebuilding session2idx / validation dataset / model...')\nsession2idx = get_session2idx(CONFIG['data_dir'])\nn_days = len(session2idx)\n\n# If SKIP_TRAINING is on, checkpoint_dir ('/kaggle/working') is a *fresh*\n# non-interactive session and won't have norm_stats.pt / best_model.pt /\n# swa_model.pt in it - those only exist if this exact session trained the\n# model. Instead, look inside PRETRAINED_CKPT_DATASET (the Dataset you made\n# from a previous Save Version run's Output) first, and only fall back to\n# checkpoint_dir if that isn't set - so this cell works the same way whether\n# you just finished training in this session or are reusing an old checkpoint.\nckpt_search_dir = CONFIG['checkpoint_dir']\nif PRETRAINED_CKPT_DATASET:\n    ckpt_search_dir = resolve_kaggle_dataset(PRETRAINED_CKPT_DATASET)\n    print(f\"SKIP_TRAINING checkpoint source: {ckpt_search_dir}\")\n\nnorm_stats_path = find_file(ckpt_search_dir, 'norm_stats.pt')\nnorm_stats = torch.load(norm_stats_path)\nfeat_mean, feat_std = norm_stats['mean'], norm_stats['std']\n\nval_data = load_split(CONFIG['data_dir'], 'val')\nval_dataset = BrainToTextDataset(val_data, session2idx, feat_mean, feat_std, augment=False)\nval_loader = DataLoader(val_dataset, batch_size=CONFIG['batch_size'], shuffle=False,\n                         collate_fn=collate_fn, num_workers=2)\n\n# Prefer the SWA-averaged checkpoint if training produced one - same memory\n# footprint as a single model at inference, usually a bit better than best_model.pt.\n# Note: swa_model.pt has no 'optimizer_state_dict'/'epoch' keys (it's a pure\n# weight average, not a resumable training checkpoint) - fine here since we\n# only ever read model_state_dict from it.\nCKPT_CANDIDATES = [\n    os.path.join(ckpt_search_dir, 'swa_model.pt'),\n    os.path.join(ckpt_search_dir, 'best_model.pt'),\n]\nCKPT_PATH = next((p for p in CKPT_CANDIDATES if os.path.exists(p)), None)\nassert CKPT_PATH is not None, (\n    f\"No checkpoint found under {ckpt_search_dir} (tried {CKPT_CANDIDATES}). \"\n    f\"If reusing a saved checkpoint, set PRETRAINED_CKPT_DATASET in the CONFIG \"\n    f\"cell to its attached dataset slug, or point CKPT_PATH at one manually.\"\n)\nprint(f\"Loading checkpoint from: {CKPT_PATH}\")\ncheckpoint = torch.load(CKPT_PATH, map_location=CONFIG['device'])\n\nmodel = HybridLSTMTransformerCTC(\n    n_days=n_days, input_size=512, vocab_size=CONFIG['n_classes'],\n    d_model=CONFIG['d_model'], n_heads=CONFIG['n_heads'], n_layers=CONFIG['n_layers'],\n    d_ff=CONFIG['d_ff'], patch_size=CONFIG['patch_size'],\n    lstm_hidden=CONFIG['lstm_hidden'], lstm_layers=CONFIG['lstm_layers'],\n    dropout=CONFIG['dropout'], head_dim=CONFIG['head_dim'], attn_dropout=CONFIG['attn_dropout'],\n    smooth_std=CONFIG['smooth_kernel_std'], smooth_size=CONFIG['smooth_kernel_size'],\n    drop_path_rate=CONFIG['drop_path_rate'],\n).to(CONFIG['device'])\n\nmodel.load_state_dict(checkpoint['model_state_dict'])\nmodel.eval()\n\nprint(\"Setup complete (validation set only). Test-set loading / submission.csv \"\n      \"generation is now in the OPTIONAL cell at the very end of the notebook - \"\n      \"skip it entirely if you don't need submission.csv right now.\")\n","metadata":{"papermill":{"duration":35.002924,"end_time":"2026-08-29T04:12:27.283195+00:00","exception":false,"start_time":"2026-08-29T04:11:52.280271+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1e445b7e","cell_type":"code","source":"# ============================================================================\n# 2. ATTACH PHONEME LEXICON + N-GRAM LM\n# ============================================================================\n# Reuses the same lexicon / tokens / KenLM binary already validated in your\n# mamba+GRU pipeline instead of compiling KenLM from source and training a new\n# *character*-level n-gram model from scratch. A phoneme-level n-gram LM is a\n# better match for phoneme CTC output, and these assets ship prebuilt - no\n# apt-get/cmake build step needed here.\nimport os\n\nlexicon_ds = resolve_kaggle_dataset('heyyousum/quality-english-dataset-for-ngram-model-v2')\n# v3 of the KenLM dataset (attached manually) - update this slug again if you\n# attach a different/newer version later.\nkenlm_ds = resolve_kaggle_dataset('heyyousum/custom-4-gram-wiki-news-switchboard-updated-v3')\n\n# find_file() searches recursively instead of assuming an exact relative path,\n# since attached-dataset internal folder layout can shift between versions\n# (e.g. this v3 KenLM dataset's binary may not sit at the same top-level\n# path the original 'custom_4gram_full.bin' did).\nlexicon_path = find_file(lexicon_ds, 'lexicon.txt')\ntokens_path = find_file(lexicon_ds, 'tokens.txt')\nkenlm_binary_path = find_file(kenlm_ds, '*.bin')\n\nprint('Lexicon:', lexicon_path)\nprint('Tokens:', tokens_path)\nprint('KenLM binary:', kenlm_binary_path)\n# NOTE: torchaudio's ctc_decoder (next cell) needs blank_token / sil_token to\n# match the exact strings used in tokens.txt - double check those two values\n# against tokens.txt if the decoder complains about an unknown token.\n","metadata":{"papermill":{"duration":0.204375,"end_time":"2026-08-29T04:12:27.511432+00:00","exception":false,"start_time":"2026-08-29T04:12:27.307057+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c4abcb9f","cell_type":"code","source":"# ============================================================================\n# SAFETY CHECK: does tokens.txt (external lexicon/KenLM package) actually\n# match PHONEME_VOCAB (this model's own output order)?\n# ============================================================================\n# torchaudio's ctc_decoder maps column i of the model's log_probs to line i\n# of tokens.txt - blindly. If this external tokens.txt (built for a\n# different pipeline) doesn't list the same 40 phonemes in the same order as\n# PHONEME_VOCAB, decoding will NOT crash - it will just silently decode\n# every emission against the wrong symbol, and WER will come out badly wrong\n# (often 80-100%) even though the acoustic model itself is fine. This cell\n# only WARNS (it doesn't stop execution) - if you see MISMATCH lines below,\n# stop and fix tokens.txt / PHONEME_VOCAB alignment before trusting any WER\n# number that comes out of the decoder built next.\nwith open(tokens_path) as f:\n    _file_tokens = [line.rstrip('\\n') for line in f]\n\nprint(f\"tokens.txt has {len(_file_tokens)} entries, PHONEME_VOCAB has {len(PHONEME_VOCAB)}\")\n_mismatches = 0\nfor i in range(max(len(_file_tokens), len(PHONEME_VOCAB))):\n    a = _file_tokens[i] if i < len(_file_tokens) else '<missing>'\n    b = PHONEME_VOCAB[i] if i < len(PHONEME_VOCAB) else '<missing>'\n    if a.strip() != b.strip():\n        _mismatches += 1\n        print(f\"  [{i:2d}] tokens.txt={a!r:12s}  PHONEME_VOCAB={b!r:12s}  <-- MISMATCH\")\n\nif _mismatches == 0:\n    print(\"OK: tokens.txt order matches PHONEME_VOCAB exactly - safe to build the decoder.\")\nelse:\n    print(f\"\\n WARNING: {_mismatches} mismatched position(s) above. The beam-search decoder \"\n          f\"built in the next cell will very likely produce garbage WER/CER until this is \"\n          f\"fixed (either regenerate tokens.txt in PHONEME_VOCAB's order, or adjust \"\n          f\"PHONEME_VOCAB / the model's output order to match tokens.txt).\")\n","metadata":{"papermill":{"duration":0.035844,"end_time":"2026-08-29T04:12:27.571978+00:00","exception":false,"start_time":"2026-08-29T04:12:27.536134+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8525ee87","cell_type":"code","source":"# ============================================================================\n# 3. BUILD THE BEAM-SEARCH DECODER (phonemes -> lexicon-constrained words)\n# ============================================================================\n# torchaudio's ctc_decoder is a thin wrapper over flashlight-text: internally\n# it does `from flashlight.lib.text.decoder import ...`. If that package isn't\n# importable you get the confusing \"No module named 'flashlight'\" ->\n# \"CTC Decoder suit requires flashlight-text\" error, even though the real\n# cause is usually just that the pip install never succeeded (Internet off on\n# Kaggle, or the install cell wasn't run). This cell installs it itself and\n# VERIFIES the import before continuing, so the failure mode is explicit.\nimport sys, subprocess, importlib\n\n\ndef _ensure_flashlight():\n    # Already importable? (flashlight is a PEP-420 namespace package; import\n    # the actual submodule torchaudio needs, not just the bare top level.)\n    try:\n        import flashlight.lib.text.decoder  # noqa: F401\n        return True\n    except Exception:\n        pass\n    # A cp312 manylinux wheel for flashlight_text 0.0.7 exists on PyPI, so on\n    # Kaggle (Linux x86-64, Python 3.12) this installs from a wheel with no\n    # build step - as long as Internet is enabled.\n    print(\"flashlight not importable - installing flashlight-text ...\")\n    subprocess.run(\n        [sys.executable, \"-m\", \"pip\", \"install\", \"flashlight-text\"],\n        check=False,\n    )\n    importlib.invalidate_caches()\n    try:\n        import flashlight.lib.text.decoder  # noqa: F401\n        return True\n    except Exception:\n        return False\n\n\nif not _ensure_flashlight():\n    raise RuntimeError(\n        \"flashlight-text could not be imported after install.\\n\"\n        \"On Kaggle this almost always means one of:\\n\"\n        \"  1. INTERNET IS OFF for this notebook. Turn it on: right-hand panel \"\n        \"-> Settings/Notebook options -> Internet = On, then re-run. (With \"\n        \"Internet off, pip silently fails to download the wheel.)\\n\"\n        \"     If the competition forbids Internet at submission time, attach \"\n        \"flashlight_text-0.0.7-cp312-*-manylinux*_x86_64.whl as an input \"\n        \"dataset and `pip install --no-index --find-links <that_dir> \"\n        \"flashlight-text` instead.\\n\"\n        \"  2. The package installed but this kernel imported torchaudio's \"\n        \"decoder earlier and cached the failure - do Run -> Restart & Run All \"\n        \"once after the install succeeds.\\n\"\n        f\"(Python {sys.version.split()[0]}, executable {sys.executable})\"\n    )\n\nfrom torchaudio.models.decoder import ctc_decoder\n\nBEAM_WIDTH = 250      # was 150 -> 100: wider beam, more candidates for the LLM to choose between\nNBEST = 30            # was 20 -> 10: deeper n-best list gives the LLM more room to help\nLM_WEIGHT = 2.5       # FIXED. Confirmed via the Block 5.3d sweep: lm_weight=2.5 + \n                      # LLM_FUSION_WEIGHT=0.75 -> WER=9.25%/CER=6.28% on validation. The sweep\n                      # grid ([1.0,1.5,2.0,2.5]) was still improving at its own upper edge, so\n                      # the true optimum may sit above 2.5 - but per project decision this is\n                      # now pinned here and NOT re-swept on every run. (To explore further,\n                      # rerun Block 5.3d with LM_WEIGHT_GRID = [2.5, 3.0, 3.5] as a one-off.)\n\ndef build_decoder(lm_weight=LM_WEIGHT, beam_size=BEAM_WIDTH, nbest=NBEST):\n    \"\"\"Builds the CTC beam-search decoder. Kept as a function (not just a\n    one-off object) so the lm_weight sweep cell later on can rebuild it\n    cheaply for each candidate lm_weight without re-running this whole cell.\"\"\"\n    return ctc_decoder(\n        lexicon=lexicon_path,\n        tokens=tokens_path,\n        lm=kenlm_binary_path,\n        nbest=nbest,\n        beam_size=beam_size,\n        lm_weight=lm_weight,\n        word_score=0.0,\n        blank_token='BLANK',\n        sil_token='|',   # matches tokens.txt (was ' | ' with spaces before)\n    )\n\ndecoder = build_decoder()\nprint(f\"Decoder ready (beam_size={BEAM_WIDTH}, nbest={NBEST}, lm_weight={LM_WEIGHT}).\")","metadata":{"papermill":{"duration":203.081705,"end_time":"2026-08-29T04:15:50.67756+00:00","exception":false,"start_time":"2026-08-29T04:12:27.595855+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"bf27c9cf","cell_type":"markdown","source":"### Block 5.3 · LLM rescoring — score fusion + verbatim-aware conditioning\n\nAdding the LLM naïvely made WER *worse* (12.00% → 12.45%): the old rescorer picked the hypothesis with the highest LLM likelihood outright, letting a fluency prior **override** the acoustic + n-gram evidence. Because this dataset contains verbatim, sometimes-disfluent speech (and even non-semantic \"random word\" calibration trials), the correct transcript is frequently the *less fluent* one already sitting in the n-best list — so overriding on \"sounds unnatural\" grounds swaps a correct-but-noisy hypothesis for a fluent-but-wrong one.\n\nTwo changes address this directly:\n\n- **Score fusion, not override.** Each hypothesis is ranked by `standardize(decoder_score) + λ · standardize(llm_score)`. The LLM can only re-rank *within* the n-best; at `λ = 0` this is exactly the n-gram top-1, so it can never do worse than beam+n-gram on the tuning set. `λ` is tuned on validation (grid includes 0).\n- **Verbatim-aware conditioning.** The LLM likelihood is computed *conditioned* on a prefix stating the text is a raw, possibly-disfluent transcript, so disfluent-but-correct hypotheses are no longer penalized for reading as \"unnatural.\" Only the candidate's tokens are scored; the prefix is masked out.\n\nThe confidence gate is kept as a compute saver and a hard safety net for random/incoherent trials, but it is no longer what prevents the LLM from doing harm — the fusion weight is.\n","metadata":{"papermill":{"duration":0.023891,"end_time":"2026-08-29T04:15:50.725611+00:00","exception":false,"start_time":"2026-08-29T04:15:50.70172+00:00","status":"completed"},"tags":[]}},{"id":"fce8758c","cell_type":"code","source":"# ============================================================================\n# 4. LOAD 4-BIT QUANTIZED LLM  (fluency rescoring via SCORE FUSION, not override)\n# ============================================================================\n# Only the LLM + scoring/rescoring functions live here. Test-set emission\n# extraction, beam search, and submission.csv writing are in the OPTIONAL cell\n# at the very end, so this cell (and the validation pipeline below it) stays\n# independent of the competition test set.\n#\n# WHY THIS CELL CHANGED (the LLM used to make WER *worse*):\n#   Beam+n-gram alone   -> WER 12.00%\n#   Beam+n-gram + LLM   -> WER 12.45%  (regression)\n# The old rescorer picked the hypothesis with the single highest LLM\n# likelihood: `best_idx = argmax(llm_scores)`. That lets a pure fluency prior\n# *override* the acoustic + n-gram evidence completely. On this dataset the\n# correct transcript is often the verbatim, slightly disfluent one (spontaneous\n# speech, repetitions, unusual phrasing) - and the acoustic model already had\n# it in the n-best list. Letting the LLM overrule the acoustics on \"sounds\n# unnatural\" grounds is exactly how a correct-but-noisy hypothesis gets swapped\n# for a fluent-but-wrong one.\n#\n# TWO CHANGES fix this, both aimed at your observation that \"noisy words are\n# sometimes expected\":\n#   (A) SCORE FUSION instead of override. The LLM no longer chooses on its own.\n#       Each hypothesis is ranked by  standardize(decoder_score) +\n#       lambda * standardize(llm_score), where decoder_score is the acoustic +\n#       n-gram log-score the beam search already produced. At lambda = 0 this\n#       is identical to the n-gram top-1 (so it can never do worse than 12.00%\n#       on the tuning set); as lambda grows the LLM is allowed to break ties,\n#       but it can only re-rank *within* the n-best and can never clobber a\n#       decisive acoustic decision. lambda is tuned on validation below.\n#   (B) VERBATIM-AWARE CONDITIONING. The LLM likelihood is computed CONDITIONED\n#       on a short prefix stating the text is a raw, word-for-word transcript\n#       that may be disfluent. This shifts the model's prior away from clean,\n#       edited prose, so a genuinely disfluent-but-correct transcript is no\n#       longer penalised just for reading as \"unnatural\". Only the candidate's\n#       own tokens are scored; the prefix tokens are masked out.\nfrom transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig\nimport gc\n\n# Candidates to benchmark head-to-head as the rescoring LLM. Llama-3.1-8B is\n# gated on the HF Hub - accept its license on the model page and log in\n# (huggingface_hub.login() or an HF_TOKEN env var / Kaggle secret) before it\n# will load, or it will fail with a 401.\nLLM_CANDIDATES = {\n    \"qwen2.5-7b\": \"Qwen/Qwen2.5-7B\",   # ONLY candidate for now - restricted per request,\n                                        # so Block 5.3b's model-comparison loop no longer\n                                        # loads/runs llama3.1-8b, qwen3-4b, qwen3-14b, or\n                                        # mistral-7b-v0.3. Add entries back here to compare again.\n}\nLLM_KEY = \"qwen2.5-7b\"        # <-- only option now; kept for compatibility with load_llm() calls\nLLM_NAME = LLM_CANDIDATES[LLM_KEY]\n\n\ndef load_llm(model_name):\n    \"\"\"(Re)loads a 4-bit quantized causal LM for scoring. Frees whatever\n    model was loaded before so switching candidates (see the model-comparison\n    cell below) doesn't leak GPU memory across loads.\"\"\"\n    global llm_model, tokenizer\n    if \"llm_model\" in globals():\n        del llm_model\n        gc.collect()\n        torch.cuda.empty_cache()\n    print(f\"Loading rescoring LLM ({model_name}) in 4-bit...\")\n    bnb_config = BitsAndBytesConfig(\n        load_in_4bit=True,\n        bnb_4bit_compute_dtype=torch.float16,\n        bnb_4bit_quant_type=\"nf4\",\n        bnb_4bit_use_double_quant=True,\n    )\n    tok = AutoTokenizer.from_pretrained(model_name)\n    if tok.pad_token is None:\n        tok.pad_token = tok.eos_token\n    model = AutoModelForCausalLM.from_pretrained(\n        model_name, quantization_config=bnb_config, device_map=\"auto\"\n    )\n    model.eval()\n    tokenizer = tok\n    llm_model = model\n    return model, tok\n\n\nllm_model, tokenizer = load_llm(LLM_NAME)\n\n\n# --- (B) Verbatim-aware conditioning prefix -------------------------------\n# Framing the candidate as a raw transcript of spontaneous speech tells the\n# LLM to expect disfluency, so it stops treating \"unnatural\" phrasing as\n# implausible. This is the \"take expected noise into account\" change, applied\n# at the level of the likelihood the model actually reports.\nVERBATIM_PREFIX = (\n    \"The following is a raw, word-for-word transcript of someone speaking out \"\n    \"loud. It is unedited and may contain repetitions, false starts, filler \"\n    \"words, or unusual phrasing exactly as spoken:\\n\"\n)\n\n# Alternative framing to A/B against VERBATIM_PREFIX (see the A/B test cell\n# after the fusion-weight tuning below). Shorter and more directive (\"do not\n# correct\") instead of descriptive - worth checking whether that shifts the\n# LLM's likelihoods enough to change which hypothesis wins fusion.\nVERBATIM_PREFIX_ALT = (\n    \"The following is an exact, unedited transcript of spontaneous, \"\n    \"possibly ungrammatical speech. Do not correct or normalize it:\\n\"\n)\n\n\ndef compute_llm_scores(sentences, batch_size=16, prefix=VERBATIM_PREFIX):\n    \"\"\"Length-normalized conditional log-likelihood (higher = more plausible)\n    for a list of candidate strings.\n\n    When `prefix` is given, each candidate is scored as P(candidate | prefix):\n    the prefix is fed to the model but EXCLUDED from the score (its label\n    positions are masked), so only the candidate's own tokens contribute. Pass\n    prefix=None to recover the old unconditioned P(candidate) behaviour.\n\n    Ids are concatenated at the token-id level (prefix_ids + candidate_ids) so\n    the prefix/candidate boundary is exact and unaffected by BPE merges at the\n    join; sequences are right-padded and the padding is masked out too.\n    \"\"\"\n    prefix_ids = tokenizer(prefix, add_special_tokens=False)[\"input_ids\"] if prefix else []\n    n_prefix = len(prefix_ids)\n    pad_id = tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id\n\n    scores = []\n    for i in range(0, len(sentences), batch_size):\n        chunk = [s if s.strip() else \"<empty>\" for s in sentences[i:i + batch_size]]\n        seqs = []\n        for s in chunk:\n            cand_ids = tokenizer(s, add_special_tokens=False)[\"input_ids\"]\n            if len(cand_ids) == 0:\n                cand_ids = [tokenizer.eos_token_id]\n            seqs.append(prefix_ids + cand_ids)\n\n        maxlen = max(len(x) for x in seqs)\n        input_ids = torch.full((len(seqs), maxlen), pad_id, dtype=torch.long)\n        attn = torch.zeros((len(seqs), maxlen), dtype=torch.long)\n        score_mask = torch.zeros((len(seqs), maxlen), dtype=torch.float)  # 1 only on candidate tokens\n        for r, ids in enumerate(seqs):\n            L = len(ids)\n            input_ids[r, :L] = torch.tensor(ids, dtype=torch.long)\n            attn[r, :L] = 1\n            score_mask[r, n_prefix:L] = 1.0\n\n        input_ids = input_ids.to(llm_model.device)\n        attn = attn.to(llm_model.device)\n        score_mask = score_mask.to(llm_model.device)\n\n        with torch.no_grad():\n            logits = llm_model(input_ids=input_ids, attention_mask=attn).logits\n\n        shift_logits = logits[:, :-1, :]\n        shift_labels = input_ids[:, 1:]\n        shift_smask = score_mask[:, 1:]\n        nll = F.cross_entropy(\n            shift_logits.reshape(-1, shift_logits.size(-1)),\n            shift_labels.reshape(-1),\n            reduction=\"none\",\n        ).view(shift_labels.shape)\n        token_counts = shift_smask.sum(dim=1).clamp(min=1)\n        per_seq_nll = (nll * shift_smask).sum(dim=1) / token_counts\n        scores.extend((-per_seq_nll).tolist())  # negative NLL -> higher is better\n    return scores\n\n\nprint(\"LLM ready for rescoring (verbatim-conditioned scoring + score fusion).\")\n\n\n# ============================================================================\n# CONFIDENCE GATING  (skip the LLM where it can only hurt or can't help)\n# ============================================================================\n# Kept from before, but its role is now smaller: with score fusion (below) the\n# LLM can no longer override a confident acoustic decision anyway, so gating is\n# mainly a compute saver + a hard safety net for random/incoherent trials.\n#   1. LOW ABSOLUTE SCORE  -> utterance looks incoherent/random (this dataset\n#      includes non-semantic \"random word\" calibration trials); the decoder has\n#      no confident answer, so keep its top-1 and don't invite the LLM to\n#      \"clean it up\" into fluent-but-wrong text.\n#   2. LARGE TOP1-VS-RUNNERUP MARGIN -> the decoder is already sure; nothing\n#      genuinely ambiguous for the LLM to resolve, so skip the call.\n# Both thresholds are calibrated from this run's own validation score\n# distribution in the full-pipeline cell below.\nCONFIG['llm_gate_percentile'] = CONFIG.get('llm_gate_percentile', 1)          # was 3 - only skip the most clearly incoherent trials\nCONFIG['llm_gate_margin_percentile'] = CONFIG.get('llm_gate_margin_percentile', 75)  # was 60 - let the LLM rescore more \"confident\" cases too\nLLM_GATE_THRESHOLD = None            # set by calibrate_llm_gate_threshold()\nLLM_GATE_MARGIN_THRESHOLD = None\n\n# --- (A) Score-fusion weight (lambda) -------------------------------------\n# How much the LLM is allowed to influence the ranking. 0.0 == n-gram top-1\n# (LLM disabled); tune_and_evaluate() on the validation set sweeps LAMBDA_GRID\n# below and picks the best value (the grid always includes 0.0, so the tuned\n# pipeline can never score worse than beam+n-gram on the tuning set).\nLLM_FUSION_WEIGHT = CONFIG.get('llm_fusion_weight', 0.75)\n\n# Coarse grid for the fusion-weight sweep (Block 5.3b/c/d all call\n# tune_and_evaluate(), which sweeps this grid). Full range now swept end to\n# end: 0.0 (LLM fully disabled -> reproduces beam+n-gram top-1) up through\n# 2.5 (LLM score dominates the acoustic + n-gram score), so the tuner can\n# find the true optimum instead of assuming it sits in a narrower band.\nLAMBDA_GRID = [0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5]  # full [0, 2.5] sweep\n\n\ndef tune_and_evaluate(decoder_results, hyps_cache, llm_scores_cache, refs=None,\n                       lambda_grid=None, label=\"\"):\n    \"\"\"Sweep a flat (non-adaptive) fusion weight lambda over `lambda_grid`,\n    score each candidate's WER/CER on `refs` (defaults to the global\n    `val_refs`), and return the best (lambda, wer, cer, all_results).\n\n    0.0 is always included in the grid as a safety floor: at lambda=0,\n    fuse_pick() reproduces the decoder's own top-1 exactly (see fuse_pick\n    below), so the tuned result can never be worse than beam+n-gram alone.\n    Gated utterances (llm_scores_cache[i] is None) always keep the decoder's\n    top-1 regardless of lambda, since there's nothing for the LLM to rerank.\n    \"\"\"\n    refs = val_refs if refs is None else refs\n    grid = list(LAMBDA_GRID if lambda_grid is None else lambda_grid)\n    if 0.0 not in grid:\n        grid = [0.0] + grid\n\n    all_results = {}\n    best_lambda, best_wer, best_cer = 0.0, float('inf'), float('inf')\n    for lam in grid:\n        preds = []\n        for res, hyps, llm_s in zip(decoder_results, hyps_cache, llm_scores_cache):\n            if not res or not any(hyps):\n                preds.append(\"\")\n            elif llm_s is None or lam == 0.0:\n                preds.append(hyps[0])\n            else:\n                preds.append(hyps[fuse_pick(res, llm_s, lam)])\n\n        pairs = [(r, h) for r, h in zip(refs, preds) if r and r.strip()]\n        refs_clean_ = [r for r, _ in pairs]\n        hyps_clean_ = [h if h.strip() else \"<empty>\" for _, h in pairs]\n        wer = corpus_wer(refs_clean_, hyps_clean_)\n        cer = corpus_cer(refs_clean_, hyps_clean_)\n        all_results[lam] = {\"wer\": wer, \"cer\": cer}\n        print(f\"  [{label}] lambda={lam:.2f}  WER={wer*100:.2f}%  CER={cer*100:.2f}%\")\n\n        if wer < best_wer:\n            best_lambda, best_wer, best_cer = lam, wer, cer\n\n    print(f\"[{label}] best lambda={best_lambda:.2f} -> WER={best_wer*100:.2f}%  CER={best_cer*100:.2f}%\")\n    return best_lambda, best_wer, best_cer, all_results\n\n\n# Master switch for the brute-force sweep cells further down (Block 5.3b\n# model comparison, 5.3c prefix A/B, 5.3d lm_weight sweep, 5.3e apply-best).\n# Turned back ON: lm_weight is swept over LM_WEIGHT_GRID = [0.5 .. 6.5] and\n# lambda over LAMBDA_GRID = [0.0 .. 2.5] instead of using the old pinned\n# 2.5 / 0.75 point estimate. Flip back to False if you just want the fast\n# fixed-hyperparameter pipeline without re-running the sweeps.\nRUN_HYPERPARAM_SWEEPS = True\n\n\ndef normalized_ngram_score(hyp):\n    \"\"\"Decoder's combined acoustic + n-gram LM log-score for one hypothesis,\n    normalized by word count to remove the length bias (longer sentences\n    accumulate more negative log-prob).\"\"\"\n    return hyp.score / max(len(hyp.words), 1)\n\n\ndef top1_margin(nbest_result):\n    \"\"\"Gap between top-1 and runner-up normalized decoder scores. Large =\n    decoder confident; small = genuinely ambiguous. Single-hypothesis result\n    is treated as maximally confident (infinite margin).\"\"\"\n    if len(nbest_result) < 2:\n        return float('inf')\n    return normalized_ngram_score(nbest_result[0]) - normalized_ngram_score(nbest_result[1])\n\n\ndef _standardize(vals):\n    \"\"\"Zero-mean/unit-std within one utterance's n-best list, so the fusion\n    weight lambda is scale-free and comparable across utterances (decoder\n    log-scores and LLM log-likelihoods live on very different scales). A\n    near-constant list collapses to zeros (no contribution).\"\"\"\n    v = np.asarray(vals, dtype=np.float64)\n    sd = v.std()\n    if sd < 1e-8:\n        return np.zeros_like(v)\n    return (v - v.mean()) / sd\n\n\ndef fuse_pick(nbest_result, llm_scores, lam):\n    \"\"\"Index of the hypothesis maximizing\n        standardize(decoder_score) + lam * standardize(llm_score).\n    lam == 0 (or missing/mismatched llm_scores) reproduces the decoder's own\n    top-1 exactly, so the LLM can only re-rank within the n-best and never\n    overrule a decisive acoustic + n-gram decision.\"\"\"\n    dec = [normalized_ngram_score(h) for h in nbest_result]\n    if lam == 0 or not llm_scores or len(llm_scores) != len(dec):\n        return int(np.argmax(dec))\n    combined = _standardize(dec) + lam * _standardize(llm_scores)\n    return int(np.argmax(combined))\n\n\n# --- Margin-ADAPTIVE fusion weight (replaces a single global lambda) ------\n# LLM_FUSION_WEIGHT (0.75) is now a FIXED *base* weight, not re-tuned via grid\n# search. Instead of applying it uniformly to every non-gated utterance, scale\n# it down as the decoder's own top1-vs-runnerup margin grows:\n#   - margin ~ 0            (genuinely ambiguous)      -> full base weight\n#   - margin -> gate margin threshold (fairly confident,\n#     just not confident enough to be gated outright)   -> weight -> 0\n# This lets the LLM matter most exactly where the acoustic+n-gram decoder is\n# least sure, and matter least where it's already fairly sure - without\n# touching the two fixed hyperparameters (lm_weight, LLM_FUSION_WEIGHT) or\n# running any grid search.\ndef adaptive_lambda(nbest_result, base_lambda=None):\n    \"\"\"Per-utterance effective fusion weight, scaled by decoder confidence\n    margin. Falls back to the flat `base_lambda` (or LLM_FUSION_WEIGHT) if the\n    gate margin threshold hasn't been calibrated yet.\"\"\"\n    base_lambda = LLM_FUSION_WEIGHT if base_lambda is None else base_lambda\n    if base_lambda == 0:\n        return 0.0\n    if LLM_GATE_MARGIN_THRESHOLD in (None, 0) or not np.isfinite(LLM_GATE_MARGIN_THRESHOLD):\n        return base_lambda\n    margin = top1_margin(nbest_result)\n    if not np.isfinite(margin):\n        return 0.0  # single-hypothesis result -> nothing to rerank\n    scale = 1.0 - min(margin / LLM_GATE_MARGIN_THRESHOLD, 1.0)\n    return base_lambda * scale\n\n\ndef calibrate_llm_gate_threshold(decoder_results, percentile=None, margin_percentile=None):\n    \"\"\"Set BOTH gate thresholds from this set's own score distribution:\n      - LLM_GATE_THRESHOLD: low percentile of top-1 normalized scores\n        (catches incoherent/random-looking utterances).\n      - LLM_GATE_MARGIN_THRESHOLD: high percentile of top1-vs-runnerup margins\n        (catches utterances the decoder is already confident about).\"\"\"\n    global LLM_GATE_THRESHOLD, LLM_GATE_MARGIN_THRESHOLD\n    percentile = CONFIG['llm_gate_percentile'] if percentile is None else percentile\n    margin_percentile = CONFIG['llm_gate_margin_percentile'] if margin_percentile is None else margin_percentile\n\n    top1_scores = [normalized_ngram_score(r[0]) for r in decoder_results if r]\n    LLM_GATE_THRESHOLD = float(np.percentile(top1_scores, percentile))\n\n    margins = [top1_margin(r) for r in decoder_results if r and np.isfinite(top1_margin(r))]\n    LLM_GATE_MARGIN_THRESHOLD = float(np.percentile(margins, margin_percentile)) if margins else float('inf')\n\n    print(f\"LLM gate calibrated on {len(top1_scores)} utterances:\\n\"\n          f\"  low-score cutoff ({percentile}th pct of top-1 score): {LLM_GATE_THRESHOLD:.4f}\\n\"\n          f\"  confidence-margin cutoff ({margin_percentile}th pct of top1-runnerup gap): \"\n          f\"{LLM_GATE_MARGIN_THRESHOLD:.4f}\")\n    return LLM_GATE_THRESHOLD, LLM_GATE_MARGIN_THRESHOLD\n\n\ndef _is_gated(nbest_result):\n    \"\"\"True if this utterance should skip the LLM entirely (kept at decoder\n    top-1): either it looks incoherent/random, or the decoder is already\n    confident. Used both by the tuner (to decide which utterances need an LLM\n    forward pass) and by gated_rescore() at inference.\"\"\"\n    if not nbest_result:\n        return True\n    low_score = LLM_GATE_THRESHOLD is not None and normalized_ngram_score(nbest_result[0]) < LLM_GATE_THRESHOLD\n    confident = LLM_GATE_MARGIN_THRESHOLD is not None and top1_margin(nbest_result) > LLM_GATE_MARGIN_THRESHOLD\n    return bool(low_score or confident)\n\n\ndef gated_rescore(nbest_result, lam=None, prefix=VERBATIM_PREFIX):\n    \"\"\"Pick the final transcript for one utterance's n-best list.\n    Keeps the decoder's top-1 when the utterance is gated (see _is_gated);\n    otherwise scores every hypothesis with the verbatim-conditioned LLM and\n    fuses those scores with the decoder's own scores (fuse_pick) - never a\n    pure LLM override. `lam` defaults to the margin-ADAPTIVE weight\n    (adaptive_lambda), not a flat LLM_FUSION_WEIGHT - pass lam explicitly to\n    override (e.g. lam=0 to force decoder top-1, or a flat scalar for\n    comparison against the old behaviour).\"\"\"\n    if not nbest_result:\n        return \"\"\n    hyps = [\" \".join(h.words) if h.words else \"\" for h in nbest_result]\n    if not any(hyps):\n        return \"\"\n    if _is_gated(nbest_result):\n        return hyps[0]\n    lam = adaptive_lambda(nbest_result) if lam is None else lam\n    if lam == 0:\n        return hyps[0]\n    llm_scores = compute_llm_scores(hyps, batch_size=len(hyps), prefix=prefix)\n    return hyps[fuse_pick(nbest_result, llm_scores, lam)]\n","metadata":{"papermill":{"duration":173.752452,"end_time":"2026-08-29T04:18:44.502+00:00","exception":false,"start_time":"2026-08-29T04:15:50.749548+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a72c0a57","cell_type":"markdown","source":"### Block 5.4 · Word / character / phoneme error rate on validation\n\nThree metrics, computed and plotted separately:\n\n1. **WER** and **CER**, via beam search + n-gram LM (never greedy). Two variants of each:\n   - **Beam + n-gram only** (fast check, no LLM) — next cell.\n   - **Beam + n-gram + LLM score fusion** (matches `submission.csv` exactly) — later cell. This is the number to report, since it is the exact submission pipeline.\n2. **PER**, via the fast CTC-greedy decode against ground-truth phoneme ids (no lexicon, no beam search). PER is identical with and without the LLM — rescoring only re-ranks *word* hypotheses and never touches the acoustic model's phoneme output.\n","metadata":{"papermill":{"duration":0.026785,"end_time":"2026-08-29T04:18:44.554735+00:00","exception":false,"start_time":"2026-08-29T04:18:44.52795+00:00","status":"completed"},"tags":[]}},{"id":"aab6ed14","cell_type":"code","source":"# ============================================================================\n# WORD / CHARACTER ERROR RATE ON VALIDATION SET (beam + n-gram, no LLM) + PER\n# ============================================================================\n# WER and CER are computed with jiwer instead of a hand-rolled DP\n# edit-distance pass. PER is computed separately (greedy CTC decode vs.\n# ground-truth phoneme ids) since it doesn't go through the beam-search /\n# lexicon / LLM pipeline at all - see the markdown above for why.\n\n# ============================================================================\n# TEXT NORMALIZATION FOR WER/CER\n# ============================================================================\n# The decoder's output is plain lowercase words with no punctuation, but the\n# ground-truth reference sentences keep original case and punctuation (e.g.\n# \"Up in New England where I'm from.\" vs \"up in new england where i'm from\").\n# jiwer.wer()/cer() do an exact string/word match, so \"New\" vs \"new\" and a\n# trailing \".\" vs nothing each count as a full substitution/deletion -\n# inflating WER/CER with errors that aren't real recognition mistakes.\n# Normalizing both sides (lowercase + strip punctuation + collapse\n# whitespace) before scoring removes this false-positive error and gives the\n# actual WER/CER of the pipeline.\nimport re\n\n# This is the EXACT normalization function used by the competition organizers\n# (Neuroprosthetics-Lab/nejm-brain-to-text, evaluate_model_helpers.py ->\n# remove_punctuation()) to clean both the ground truth and the predicted\n# sentence before computing WER for the official leaderboard. Using the same\n# rule here means our local validation WER/CER matches what Kaggle actually\n# scores, instead of a generic lowercase+strip-all-punctuation approach which\n# would also destroy apostrophes (\"isn't\" -> \"isnt\") that the official scorer\n# keeps.\ndef normalize_text(sentence):\n    \"\"\"Official brain-to-text '25 text normalization: keep letters, spaces,\n    hyphens and apostrophes; lowercase; collapse stray hyphens/spaces.\"\"\"\n    sentence = re.sub(r'[^a-zA-Z\\- \\']', '', sentence)\n    sentence = sentence.replace('- ', ' ').lower()\n    sentence = sentence.replace('--', '').lower()\n    sentence = sentence.replace(\" '\", \"'\").lower()\n    sentence = sentence.strip()\n    sentence = ' '.join([word for word in sentence.split() if word != ''])\n    return sentence\n\n\ndef corpus_wer(refs, hyps):\n    \"\"\"Standard corpus-level WER: total word edits / total reference words.\n    Both sides are normalized (lowercase, no punctuation) first so case and\n    punctuation differences don't get counted as recognition errors.\"\"\"\n    refs_n = [normalize_text(r) for r in refs]\n    hyps_n = [normalize_text(h) for h in hyps]\n    return jiwer.wer(refs_n, hyps_n)\n\n\ndef corpus_cer(refs, hyps):\n    \"\"\"Standard corpus-level CER: total character edits / total reference characters.\n    Normalized the same way as corpus_wer, for the same reason.\"\"\"\n    refs_n = [normalize_text(r) for r in refs]\n    hyps_n = [normalize_text(h) for h in hyps]\n    return jiwer.cer(refs_n, hyps_n)\n\n\ndef utterance_wer(ref, hyp):\n    return jiwer.wer(normalize_text(ref), normalize_text(hyp))\n\n\ndef utterance_cer(ref, hyp):\n    return jiwer.cer(normalize_text(ref), normalize_text(hyp))\n\n\ndef extract_emissions_from_loader(model, loader, device):\n    \"\"\"Same idea as extract_emissions() but reads directly from a DataLoader\n    that also carries reference sentences and day indices (used for val, which\n    has ground truth; the test set uses the sample-list version above).\"\"\"\n    model.eval()\n    all_emissions, all_lengths, all_refs, all_days = [], [], [], []\n    with torch.no_grad():\n        for batch in tqdm(loader, desc='Extracting Val Emissions'):\n            neural = batch['neural'].to(device)\n            lengths = batch['lengths']\n            day_idx = batch['day_idx'].to(device)\n\n            with autocast(enabled=CONFIG['use_amp']):\n                log_probs, output_lengths = model(neural, lengths, day_idx)\n            log_probs = log_probs.transpose(0, 1).float().cpu()  # [B, T, V]\n\n            all_emissions.append(log_probs)\n            all_lengths.append(output_lengths.cpu())\n            all_refs.extend(batch['sentences'])\n            all_days.extend(day_idx.cpu().tolist())\n\n    max_T = max(e.shape[1] for e in all_emissions)\n    padded = [F.pad(e, (0, 0, 0, max_T - e.shape[1])) for e in all_emissions]\n    return torch.cat(padded, dim=0), torch.cat(all_lengths, dim=0), all_refs, all_days\n\n\nval_emissions, val_lengths, val_refs, val_days = extract_emissions_from_loader(model, val_loader, CONFIG['device'])\n\nprint(\"Decoding validation set (top-1 beam, no LLM rescoring - fast WER/CER check)...\")\nval_hyps = []\nDECODE_BATCH = 16\nfor i in tqdm(range(0, len(val_emissions), DECODE_BATCH), desc=\"Val beam search\"):\n    emissions_chunk = val_emissions[i:i + DECODE_BATCH]\n    lengths_chunk = val_lengths[i:i + DECODE_BATCH]\n    results = decoder(emissions_chunk, lengths_chunk)\n    for sample_result in results:\n        top = sample_result[0] if sample_result else None\n        val_hyps.append(\" \".join(top.words) if top and top.words else \"\")\n\n# Guard against empty references/hypotheses.\npairs = [(r, h) for r, h in zip(val_refs, val_hyps) if r and r.strip()]\nrefs_clean = [r for r, h in pairs]\nhyps_clean = [h if h.strip() else \"<empty>\" for r, h in pairs]\n\noverall_wer = corpus_wer(refs_clean, hyps_clean)\noverall_cer = corpus_cer(refs_clean, hyps_clean)\nprint(f\"\\nValidation WER (lexicon + n-gram decode, no LLM rescoring): {overall_wer * 100:.2f}%\")\nprint(f\"Validation CER (lexicon + n-gram decode, no LLM rescoring): {overall_cer * 100:.2f}%\")\nprint(\"(Run this same decode + LLM rescoring step, like the test pipeline, for the numbers you'd actually submit.)\")\n\n# ---- PER: independent of the above, straight CTC-greedy vs. ground-truth phonemes ----\nprint(\"\\nComputing PER (CTC-greedy decode vs. ground-truth phoneme ids)...\")\nper_day_edits = {d: 0 for d in set(val_days)}\nper_day_counts = {d: 0 for d in set(val_days)}\ntotal_phon_edits, total_phon_count = 0, 0\nper_per_utt = []  # aligned 1:1 with refs_clean / hyps_clean (same \"ref non-empty\" filter, same loader order)\n\nmodel.eval()\nwith torch.no_grad():\n    for batch in tqdm(val_loader, desc=\"Greedy PER\"):\n        neural = batch['neural'].to(CONFIG['device'])\n        lengths = batch['lengths']\n        day_idx_batch = batch['day_idx']\n        sentences = batch['sentences']\n        targets = batch['target']\n        target_lengths = batch['target_lengths']\n\n        with autocast(enabled=CONFIG['use_amp']):\n            log_probs, output_lengths = model(neural, lengths, day_idx_batch.to(CONFIG['device']))\n        decoded_phonemes = greedy_decode_phonemes(log_probs, output_lengths)\n\n        for b, phon_ids in enumerate(decoded_phonemes):\n            ref_phon_ids = targets[b, :target_lengths[b]].tolist()\n            if len(ref_phon_ids) == 0:\n                continue\n            edits = editdistance.eval(phon_ids, ref_phon_ids)\n            d = int(day_idx_batch[b])\n            per_day_edits[d] += edits\n            per_day_counts[d] += len(ref_phon_ids)\n            total_phon_edits += edits\n            total_phon_count += len(ref_phon_ids)\n\n            ref = sentences[b]\n            if ref and ref.strip():\n                per_per_utt.append(100.0 * edits / max(len(ref_phon_ids), 1))\n\noverall_per = total_phon_edits / max(total_phon_count, 1)\nprint(f\"Validation PER (CTC-greedy decode): {overall_per * 100:.2f}%\")\n\nwith open(os.path.join(CONFIG['checkpoint_dir'], 'val_wer.json'), 'w') as f:\n    json.dump({'wer_beam_ngram_only': overall_wer, 'cer_beam_ngram_only': overall_cer,\n               'per_greedy': overall_per}, f, indent=2)\n","metadata":{"papermill":{"duration":281.232595,"end_time":"2026-08-29T04:23:25.813032+00:00","exception":false,"start_time":"2026-08-29T04:18:44.580437+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"13fe3f0d","cell_type":"code","source":"# ============================================================================\n# WER / CER / PER PLOTS: by-day breakdown (three separate figures)\n# ============================================================================\nclean_days = [d for r, d in zip(val_refs, val_days) if r and r.strip()]\n\nidx2session = {v: k for k, v in session2idx.items()}\nday_refs, day_hyps = {}, {}\nfor r, h, d in zip(refs_clean, hyps_clean, clean_days):\n    day_refs.setdefault(d, []).append(r)\n    day_hyps.setdefault(d, []).append(h)\n\ndays_sorted = sorted(day_refs.keys())\nsession_names = [idx2session[d] for d in days_sorted]\n\nwer_by_day = [corpus_wer(day_refs[d], day_hyps[d]) * 100 for d in days_sorted]\ncer_by_day = [corpus_cer(day_refs[d], day_hyps[d]) * 100 for d in days_sorted]\nper_by_day = [100.0 * per_day_edits[d] / per_day_counts[d] if per_day_counts.get(d, 0) > 0 else np.nan\n              for d in days_sorted]\n\n\ndef _plot_final_metric_by_day(values, ylabel, title, color, filename):\n    fig, ax = plt.subplots(figsize=(max(10, len(days_sorted) * 0.4), 5))\n    ax.bar(range(len(days_sorted)), values, color=color)\n    ax.set_xticks(range(len(days_sorted)))\n    ax.set_xticklabels(session_names, rotation=90, fontsize=7)\n    ax.set_ylabel(ylabel)\n    ax.set_title(title)\n    ax.axhline(np.nanmean(values), color='black', linestyle='--', linewidth=1,\n               label=f'Mean = {np.nanmean(values):.1f}%')\n    ax.legend()\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, filename), dpi=300, bbox_inches='tight')\n    plt.show()\n\n\n_plot_final_metric_by_day(wer_by_day, 'Word Error Rate (%)',\n                           'Validation WER by Recording Session/Day (beam + n-gram)',\n                           'tab:purple', 'wer_by_day.png')\n\n_plot_final_metric_by_day(cer_by_day, 'Character Error Rate (%)',\n                           'Validation CER by Recording Session/Day (beam + n-gram)',\n                           'tab:blue', 'cer_by_day.png')\n\n_plot_final_metric_by_day(per_by_day, 'Phoneme Error Rate (%)',\n                           'Validation PER by Recording Session/Day (CTC-greedy)',\n                           'tab:green', 'per_by_day.png')\n\n# Qualitative examples table - useful for a \"sample predictions\" table in the paper.\n# PER column reuses per_per_utt from the previous cell, which is aligned 1:1 with\n# refs_clean/hyps_clean (same loader order, same \"non-empty ref\" filter).\nexamples = pd.DataFrame({\n    'Reference': refs_clean[:10],\n    'Prediction': hyps_clean[:10],\n    'WER': [f'{utterance_wer(r, h) * 100:.1f}%' for r, h in zip(refs_clean[:10], hyps_clean[:10])],\n    'CER': [f'{utterance_cer(r, h) * 100:.1f}%' for r, h in zip(refs_clean[:10], hyps_clean[:10])],\n    'PER': [f'{v:.1f}%' for v in per_per_utt[:10]],\n})\nprint(\"\\nSample predictions:\")\nexamples\n","metadata":{"papermill":{"duration":3.295542,"end_time":"2026-08-29T04:23:29.143883+00:00","exception":false,"start_time":"2026-08-29T04:23:25.848341+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"109eb154","cell_type":"code","source":"# ============================================================================\n# FULL-PIPELINE VALIDATION: beam + n-gram + gated, verbatim-conditioned LLM\n# with MARGIN-ADAPTIVE SCORE FUSION (quick pre-sweep baseline)\n# ============================================================================\n# Pipeline: beam search (n-best, current default lm_weight/LM_WEIGHT) -> gate\n# out random/confident utterances -> score the rest with the verbatim-\n# conditioned LLM (Qwen2.5-7B) -> FUSE the LLM score with the decoder's\n# acoustic+n-gram score using a per-utterance ADAPTIVE weight derived from\n# LLM_FUSION_WEIGHT -> pick the fused-best hypothesis.\n#\n# NOTE: this cell uses whatever LM_WEIGHT / LLM_FUSION_WEIGHT are currently\n# set to (the starting defaults from Block 5.3, before any sweep). It gives a\n# quick number to sanity-check the pipeline is wired up correctly. The actual\n# best combo is found by sweeping LM_WEIGHT_GRID = [1.0..3.0] and\n# LAMBDA_GRID = [0.5..1.5] in Blocks 5.3d/5.3e below (RUN_HYPERPARAM_SWEEPS in\n# Block 5.3), which overwrite `decoder`, `LLM_FUSION_WEIGHT`, `full_pipeline_wer`\n# and `full_pipeline_cer` with the swept-and-tuned result - that's the number\n# that should actually be reported / used for submission.csv.\n#\n# PER is unchanged (LLM only re-ranks word hypotheses, never touches phonemes).\n\nprint(f\"Decoding validation set (n-best beam, lm_weight={LM_WEIGHT}) then applying \"\n      f\"margin-adaptive, verbatim-conditioned LLM score fusion \"\n      f\"(base LLM_FUSION_WEIGHT={LLM_FUSION_WEIGHT}, pre-sweep baseline)...\")\n\n# ---- 1. Beam search over the whole validation set, keep raw n-best results ----\nDECODE_BATCH = 16\nval_decoder_results = []\nfor i in tqdm(range(0, len(val_emissions), DECODE_BATCH), desc=\"Val beam search (n-best)\"):\n    emissions_chunk = val_emissions[i:i + DECODE_BATCH]\n    lengths_chunk = val_lengths[i:i + DECODE_BATCH]\n    val_decoder_results.extend(decoder(emissions_chunk, lengths_chunk))\n\n# ---- 2. Calibrate the gate thresholds from this set's own score distribution ----\ncalibrate_llm_gate_threshold(val_decoder_results)\n\n# ---- 3. Compute + CACHE LLM scores once for every non-gated utterance ----\n# For gated utterances we already know the answer (decoder top-1), so we don't\n# spend an LLM forward pass on them.\nval_hyps_cache = []       # per-utterance: list of hypothesis strings (\"\" placeholder if none)\nval_llm_scores_cache = [] # per-utterance: list of LLM scores, or None if gated/empty\nn_gated_low_score = 0\nn_gated_confident = 0\nfor sample_result in tqdm(val_decoder_results, desc=\"LLM scoring (non-gated only)\"):\n    if not sample_result:\n        val_hyps_cache.append([\"\"])\n        val_llm_scores_cache.append(None)\n        continue\n    hyps = [\" \".join(h.words) if h.words else \"\" for h in sample_result]\n    val_hyps_cache.append(hyps)\n\n    # Track WHY an utterance was gated, for the printed skip-rate.\n    low_score = normalized_ngram_score(sample_result[0]) < LLM_GATE_THRESHOLD\n    confident = (not low_score) and top1_margin(sample_result) > LLM_GATE_MARGIN_THRESHOLD\n    if low_score:\n        n_gated_low_score += 1\n    elif confident:\n        n_gated_confident += 1\n\n    if (not any(hyps)) or _is_gated(sample_result):\n        val_llm_scores_cache.append(None)          # gated -> no LLM pass\n    else:\n        val_llm_scores_cache.append(compute_llm_scores(hyps, batch_size=len(hyps)))\n\nn_gated_total = n_gated_low_score + n_gated_confident\nprint(f\"Skipped LLM rescoring on {n_gated_total}/{len(val_decoder_results)} \"\n      f\"utterances ({100 * n_gated_total / max(len(val_decoder_results), 1):.1f}%): \"\n      f\"{n_gated_low_score} low-score (likely random/incoherent), \"\n      f\"{n_gated_confident} high-confidence (decoder already sure).\")\n\n\n# ---- 4. Apply MARGIN-ADAPTIVE fusion (fixed base weight, no grid search) ----\ndef _predict_adaptive(decoder_results=None, hyps_cache=None, llm_scores_cache=None,\n                       base_lambda=None):\n    \"\"\"Same role as the old _predict_with_lambda(), but the fusion weight used\n    for each utterance comes from adaptive_lambda() instead of one flat\n    scalar shared by the whole set.\"\"\"\n    decoder_results = val_decoder_results if decoder_results is None else decoder_results\n    hyps_cache = val_hyps_cache if hyps_cache is None else hyps_cache\n    llm_scores_cache = val_llm_scores_cache if llm_scores_cache is None else llm_scores_cache\n    preds, lambdas_used = [], []\n    for res, hyps, llm_s in zip(decoder_results, hyps_cache, llm_scores_cache):\n        if not res or not any(hyps):\n            preds.append(\"\")\n            continue\n        if llm_s is None:                      # gated -> decoder top-1\n            preds.append(hyps[0])\n            continue\n        lam = adaptive_lambda(res, base_lambda=base_lambda)\n        lambdas_used.append(lam)\n        preds.append(hyps[0] if lam == 0 else hyps[fuse_pick(res, llm_s, lam)])\n    return preds, lambdas_used\n\n\ndef _wer_for_preds(preds, refs=None):\n    refs = val_refs if refs is None else refs\n    pr = [(r, h) for r, h in zip(refs, preds) if r and r.strip()]\n    return corpus_wer([r for r, _ in pr], [h if h.strip() else \"<empty>\" for _, h in pr])\n\n\nval_preds, val_lambdas_used = _predict_adaptive()\nfull_pipeline_wer = _wer_for_preds(val_preds)\n_pairs = [(r, h) for r, h in zip(val_refs, val_preds) if r and r.strip()]\n_refs_c = [r for r, _ in _pairs]\n_hyps_c = [h if h.strip() else \"<empty>\" for _, h in _pairs]\nfull_pipeline_cer = corpus_cer(_refs_c, _hyps_c)\n\nLLM_FUSION_WEIGHT = 0.75          # pre-sweep baseline value; Block 5.3e overwrites this\nCONFIG['llm_fusion_weight'] = LLM_FUSION_WEIGHT          # with the swept-and-tuned best lambda\n\nprint(f\"\\nAdaptive lambda actually applied on non-gated utterances: \"\n      f\"mean={np.mean(val_lambdas_used):.3f}  \"\n      f\"(base LLM_FUSION_WEIGHT={LLM_FUSION_WEIGHT}, ranges 0..base by decoder confidence)\")\nprint(f\"Validation WER - beam + n-gram (lm_weight={LM_WEIGHT}) + margin-adaptive, \"\n      f\"verbatim-conditioned LLM fusion: {full_pipeline_wer * 100:.2f}%\")\nprint(f\"Validation CER - same pipeline: {full_pipeline_cer * 100:.2f}%\")\nprint(f\"For reference, beam + n-gram only (no LLM) was \"\n      f\"WER={overall_wer * 100:.2f}%, CER={overall_cer * 100:.2f}%.\")\nprint(f\"PER (CTC-greedy, unaffected by LLM rescoring): {overall_per * 100:.2f}%\")\n\nwith open(os.path.join(CONFIG['checkpoint_dir'], 'val_wer.json'), 'w') as f:\n    json.dump(\n        {\n            'wer_beam_ngram_only': overall_wer,\n            'cer_beam_ngram_only': overall_cer,\n            'wer_full_pipeline': full_pipeline_wer,\n            'cer_full_pipeline': full_pipeline_cer,\n            'per_greedy': overall_per,\n            'lm_weight': LM_WEIGHT,\n            'llm_fusion_weight_base': LLM_FUSION_WEIGHT,\n            'mean_adaptive_lambda': float(np.mean(val_lambdas_used)) if val_lambdas_used else None,\n        },\n        f, indent=2,\n    )\n","metadata":{"papermill":{"duration":2462.630682,"end_time":"2026-08-29T05:04:31.812512+00:00","exception":false,"start_time":"2026-08-29T04:23:29.18183+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3bbafc9a","cell_type":"markdown","source":"### Block 5.3b · Rescoring-LLM comparison (Qwen2.5-7B vs Llama-3.1-8B vs Qwen3-4B vs Qwen3-14B vs Mistral-7B-v0.3)\n\nReuses `val_decoder_results` from the cell above (beam search doesn't depend on the LLM, so it isn't re-run) and, for each candidate, reloads the LLM, recomputes LLM scores over the same cached hypotheses, then re-tunes lambda with `tune_and_evaluate`. Expensive (one full LLM load + scoring pass per candidate) - drop candidates from `LLM_CANDIDATES` above if you want a faster subset.","metadata":{"papermill":{"duration":0.077044,"end_time":"2026-08-29T05:04:31.967658+00:00","exception":false,"start_time":"2026-08-29T05:04:31.890614+00:00","status":"completed"},"tags":[]}},{"id":"c6a4cd60","cell_type":"code","source":"# ============================================================================\n# Block 5.3b (rescoring-LLM comparison)\n# ============================================================================\n# Runs whenever RUN_HYPERPARAM_SWEEPS=True (Block 5.3). Each candidate LLM is\n# scored against the current n-best list and re-tuned with tune_and_evaluate()\n# over LAMBDA_GRID, rather than reusing one pinned lambda for every model.\nif not RUN_HYPERPARAM_SWEEPS:\n    print(\"Skipping Block 5.3b (rescoring-LLM comparison) - RUN_HYPERPARAM_SWEEPS=False. \"\n          \"Set RUN_HYPERPARAM_SWEEPS=True in Block 5.3 to re-enable.\")\nelse:\n    # ============================================================================\n    # RESCORING-LLM COMPARISON: same n-best hypotheses, different scoring model\n    # ============================================================================\n    comparison_results = {}\n\n    for key, model_name in LLM_CANDIDATES.items():\n        print(f\"\\n===== {key} ({model_name}) =====\")\n        try:\n            load_llm(model_name)\n        except Exception as e:\n            print(f\"  SKIPPED - failed to load ({e}). \"\n                  f\"(Llama-3.1-8B needs a HF token with license access.)\")\n            continue\n\n        cmp_hyps_cache, cmp_llm_scores_cache = [], []\n        for sample_result, hyps in tqdm(list(zip(val_decoder_results, val_hyps_cache)),\n                                         desc=f\"LLM scoring ({key})\"):\n            cmp_hyps_cache.append(hyps)\n            if (not any(hyps)) or _is_gated(sample_result):\n                cmp_llm_scores_cache.append(None)   # same gate mask as the default run\n            else:\n                cmp_llm_scores_cache.append(compute_llm_scores(hyps, batch_size=len(hyps)))\n\n        lam, wer, cer, _ = tune_and_evaluate(\n            decoder_results=val_decoder_results,\n            hyps_cache=cmp_hyps_cache,\n            llm_scores_cache=cmp_llm_scores_cache,\n            label=key,\n        )\n        comparison_results[key] = {\"model\": model_name, \"best_lambda\": lam, \"wer\": wer, \"cer\": cer}\n\n    print(\"\\n===== SUMMARY =====\")\n    print(f\"{'model':<18} {'lambda':>8} {'WER%':>8} {'CER%':>8}\")\n    for key, r in sorted(comparison_results.items(), key=lambda kv: kv[1][\"wer\"]):\n        print(f\"{key:<18} {r['best_lambda']:>8} {r['wer']*100:>8.2f} {r['cer']*100:>8.2f}\")\n\n    # Reload whichever model you want to keep using for the rest of the notebook\n    # (defaults back to the original choice; change LLM_KEY above to switch).\n    llm_model, tokenizer = load_llm(LLM_CANDIDATES[LLM_KEY])","metadata":{"papermill":{"duration":2461.144161,"end_time":"2026-08-29T05:45:33.191115+00:00","exception":false,"start_time":"2026-08-29T05:04:32.046954+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"56af7c9e","cell_type":"markdown","source":"### Block 5.3c · Verbatim-prefix A/B test\n\nSame idea as the model comparison above, but holding the LLM fixed and swapping `VERBATIM_PREFIX` for `VERBATIM_PREFIX_ALT` to see whether the exact wording of the disfluency framing changes which hypothesis wins fusion.","metadata":{"papermill":{"duration":0.115401,"end_time":"2026-08-29T05:45:33.42491+00:00","exception":false,"start_time":"2026-08-29T05:45:33.309509+00:00","status":"completed"},"tags":[]}},{"id":"95a2c016","cell_type":"code","source":"# ============================================================================\n# Block 5.3c (verbatim-prefix A/B test)\n# ============================================================================\n# Runs whenever RUN_HYPERPARAM_SWEEPS=True (Block 5.3). Each prefix variant is\n# re-tuned with tune_and_evaluate() over LAMBDA_GRID rather than reusing one\n# pinned lambda for both.\nif not RUN_HYPERPARAM_SWEEPS:\n    print(\"Skipping Block 5.3c (verbatim-prefix A/B test) - RUN_HYPERPARAM_SWEEPS=False. \"\n          \"Set RUN_HYPERPARAM_SWEEPS=True in Block 5.3 to re-enable.\")\nelse:\n    # ============================================================================\n    # VERBATIM-PREFIX A/B TEST (same LLM, two different conditioning prefixes)\n    # ============================================================================\n    prefix_variants = {\"current\": VERBATIM_PREFIX, \"alt\": VERBATIM_PREFIX_ALT}\n    prefix_results = {}\n\n    for name, prefix in prefix_variants.items():\n        print(f\"\\n===== prefix: {name} =====\")\n        ab_llm_scores_cache = []\n        for sample_result, hyps in tqdm(list(zip(val_decoder_results, val_hyps_cache)),\n                                         desc=f\"LLM scoring (prefix={name})\"):\n            if (not any(hyps)) or _is_gated(sample_result):\n                ab_llm_scores_cache.append(None)\n            else:\n                ab_llm_scores_cache.append(compute_llm_scores(hyps, batch_size=len(hyps), prefix=prefix))\n\n        lam, wer, cer, _ = tune_and_evaluate(\n            decoder_results=val_decoder_results,\n            hyps_cache=val_hyps_cache,\n            llm_scores_cache=ab_llm_scores_cache,\n            label=f\"prefix={name}\",\n        )\n        prefix_results[name] = {\"best_lambda\": lam, \"wer\": wer, \"cer\": cer}\n\n    print(\"\\n===== SUMMARY =====\")\n    for name, r in prefix_results.items():\n        print(f\"prefix={name:<8} lambda={r['best_lambda']:<5} WER={r['wer']*100:.2f}%  CER={r['cer']*100:.2f}%\")","metadata":{"papermill":{"duration":4002.000184,"end_time":"2026-08-29T06:52:15.537247+00:00","exception":false,"start_time":"2026-08-29T05:45:33.537063+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"75d6997c","cell_type":"markdown","source":"### Block 5.3d · N-gram `lm_weight` sweep (most expensive - rebuilds the decoder + re-runs beam search each time)\n\nThe acoustic/n-gram balance changes how much genuine ambiguity is left in the n-best list for the LLM to resolve, so it interacts with the fusion weight. This re-runs beam search from `val_emissions` for each `lm_weight` (cheap relative to the LLM pass, but not free), then reuses the current `llm_model` to score and fuse. Run this last, and consider trimming `LM_WEIGHT_GRID` if compute is tight.","metadata":{"papermill":{"duration":0.192354,"end_time":"2026-08-29T06:52:16.024164+00:00","exception":false,"start_time":"2026-08-29T06:52:15.83181+00:00","status":"completed"},"tags":[]}},{"id":"81165d74","cell_type":"code","source":"# ============================================================================\n# Block 5.3d (n-gram lm_weight sweep)\n# ============================================================================\n# lm_weight and lambda are swept (not fixed) - set RUN_HYPERPARAM_SWEEPS = False\n# in Block 5.3 if you want to skip this and reuse the last pinned values instead.\nif not RUN_HYPERPARAM_SWEEPS:\n    print(\"Skipping Block 5.3d (n-gram lm_weight sweep) - RUN_HYPERPARAM_SWEEPS=False. \"\n          \"Set RUN_HYPERPARAM_SWEEPS=True in Block 5.3 to re-enable.\")\nelse:\n    # ============================================================================\n    # N-GRAM lm_weight SWEEP (interacts with fusion weight - run this last)\n    # ============================================================================\n    LM_WEIGHT_GRID = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5]   # full [0.5, 6.5] sweep\n    lm_weight_results = {}\n\n    for lw in LM_WEIGHT_GRID:\n        print(f\"\\n===== lm_weight={lw} =====\")\n        sweep_decoder = build_decoder(lm_weight=lw)\n\n        sweep_decoder_results = []\n        for i in tqdm(range(0, len(val_emissions), DECODE_BATCH), desc=f\"Beam search (lm_weight={lw})\"):\n            emissions_chunk = val_emissions[i:i + DECODE_BATCH]\n            lengths_chunk = val_lengths[i:i + DECODE_BATCH]\n            sweep_decoder_results.extend(sweep_decoder(emissions_chunk, lengths_chunk))\n\n        # Gate thresholds are score-distribution-dependent, so recalibrate per lm_weight\n        # rather than reusing the lm_weight=1.5 thresholds from earlier.\n        calibrate_llm_gate_threshold(sweep_decoder_results)\n\n        sweep_hyps_cache, sweep_llm_scores_cache = [], []\n        for sample_result in tqdm(sweep_decoder_results, desc=f\"LLM scoring (lm_weight={lw})\"):\n            if not sample_result:\n                sweep_hyps_cache.append([\"\"])\n                sweep_llm_scores_cache.append(None)\n                continue\n            hyps = [\" \".join(h.words) if h.words else \"\" for h in sample_result]\n            sweep_hyps_cache.append(hyps)\n            if (not any(hyps)) or _is_gated(sample_result):\n                sweep_llm_scores_cache.append(None)\n            else:\n                sweep_llm_scores_cache.append(compute_llm_scores(hyps, batch_size=len(hyps)))\n\n        lam, wer, cer, _ = tune_and_evaluate(\n            decoder_results=sweep_decoder_results,\n            hyps_cache=sweep_hyps_cache,\n            llm_scores_cache=sweep_llm_scores_cache,\n            label=f\"lm_weight={lw}\",\n        )\n        lm_weight_results[lw] = {\"best_lambda\": lam, \"wer\": wer, \"cer\": cer}\n\n    print(\"\\n===== SUMMARY =====\")\n    print(f\"{'lm_weight':>10} {'lambda':>8} {'WER%':>8} {'CER%':>8}\")\n    best_lw = min(lm_weight_results, key=lambda k: lm_weight_results[k][\"wer\"])\n    for lw, r in lm_weight_results.items():\n        marker = \"  <-- best\" if lw == best_lw else \"\"\n        print(f\"{lw:>10} {r['best_lambda']:>8} {r['wer']*100:>8.2f} {r['cer']*100:>8.2f}{marker}\")\n\n    print(f\"\\nBest combo: lm_weight={best_lw}, lambda={lm_weight_results[best_lw]['best_lambda']} \"\n          f\"-> WER={lm_weight_results[best_lw]['wer']*100:.2f}%. \"\n          f\"If this beats the default LM_WEIGHT=1.5 pipeline above, rebuild `decoder = \"\n          f\"build_decoder(lm_weight={best_lw})`, re-run the Block 5.3b/5.4 cells with it, \"\n          f\"and update LLM_FUSION_WEIGHT to {lm_weight_results[best_lw]['best_lambda']} before \"\n          f\"generating submission.csv.\")","metadata":{"papermill":{"duration":19558.737574,"end_time":"2026-08-29T12:18:14.955996+00:00","exception":false,"start_time":"2026-08-29T06:52:16.218422+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e6799030","cell_type":"markdown","source":"### Block 5.3e · Apply best lm_weight/lambda combo (fixes Block 6 to use the actual best model)\n\nThe sweep above only tests candidates locally - it does **not** update the `decoder` / `LLM_FUSION_WEIGHT` globals that Block 6 reuses. This cell rebuilds those globals from the sweep's winning combo, so `submission.csv` is generated from the best-found pipeline instead of the untouched lm_weight=1.5 default.","metadata":{"papermill":{"duration":0.533974,"end_time":"2026-08-29T12:18:16.114074+00:00","exception":false,"start_time":"2026-08-29T12:18:15.5801+00:00","status":"completed"},"tags":[]}},{"id":"183084c1","cell_type":"code","source":"# ============================================================================\n# Block 5.3e (apply best lm_weight/lambda from the sweep)\n# ============================================================================\n# Runs whenever RUN_HYPERPARAM_SWEEPS=True (Block 5.3). Takes whichever\n# lm_weight in LM_WEIGHT_GRID (Block 5.3d) had the lowest WER, rebuilds the\n# global `decoder` with it, and re-tunes lambda over LAMBDA_GRID for that\n# specific decoder - so Block 6 (submission.csv) uses the actual best-found\n# combo instead of a stale pinned point estimate.\nif not RUN_HYPERPARAM_SWEEPS:\n    print(\"Skipping Block 5.3e (apply best lm_weight/lambda from the sweep) - RUN_HYPERPARAM_SWEEPS=False. \"\n          \"Set RUN_HYPERPARAM_SWEEPS=True in Block 5.3 to re-enable.\")\nelse:\n    # ============================================================================\n    # APPLY BEST lm_weight / lambda FROM THE SWEEP (updates the globals that\n    # Block 6 / submission.csv actually reads: decoder, LLM_FUSION_WEIGHT,\n    # LLM_GATE_THRESHOLD, LLM_GATE_MARGIN_THRESHOLD)\n    # ============================================================================\n    # WHY THIS CELL EXISTS:\n    # The lm_weight sweep above only built a LOCAL `sweep_decoder` for each\n    # candidate lm_weight - it never touched the global `decoder` or\n    # `LLM_FUSION_WEIGHT` that Block 6 (submission.csv) actually reads. Without\n    # this cell, Block 6 would still silently use the DEFAULT lm_weight=1.5\n    # pipeline (worse WER) instead of the best combo found by the sweep.\n    # Gate thresholds are also score-distribution-dependent on lm_weight, so\n    # they're recalibrated here too, not just copy-pasted from the lm_weight=1.5 run.\n\n    best_lw = min(lm_weight_results, key=lambda k: lm_weight_results[k][\"wer\"])\n    print(f\"Rebuilding decoder with best lm_weight={best_lw} \"\n          f\"(sweep WER={lm_weight_results[best_lw]['wer']*100:.2f}%)...\")\n\n    decoder = build_decoder(lm_weight=best_lw)     # <-- overwrites the global `decoder`\n\n    # Re-run the exact Block 5.4 'FULL-PIPELINE VALIDATION' logic with this new\n    # decoder so the gate thresholds AND lambda are re-tuned for it, not reused\n    # from the old lm_weight=1.5 run.\n    DECODE_BATCH = 16\n    val_decoder_results = []\n    for i in tqdm(range(0, len(val_emissions), DECODE_BATCH), desc=\"Val beam search (best lm_weight)\"):\n        emissions_chunk = val_emissions[i:i + DECODE_BATCH]\n        lengths_chunk = val_lengths[i:i + DECODE_BATCH]\n        val_decoder_results.extend(decoder(emissions_chunk, lengths_chunk))\n\n    calibrate_llm_gate_threshold(val_decoder_results)   # updates LLM_GATE_THRESHOLD / MARGIN globally\n\n    val_hyps_cache, val_llm_scores_cache = [], []\n    n_gated_low_score = 0\n    n_gated_confident = 0\n    for sample_result in tqdm(val_decoder_results, desc=\"LLM scoring (best lm_weight)\"):\n        if not sample_result:\n            val_hyps_cache.append([\"\"])\n            val_llm_scores_cache.append(None)\n            continue\n        hyps = [\" \".join(h.words) if h.words else \"\" for h in sample_result]\n        val_hyps_cache.append(hyps)\n\n        low_score = normalized_ngram_score(sample_result[0]) < LLM_GATE_THRESHOLD\n        confident = (not low_score) and top1_margin(sample_result) > LLM_GATE_MARGIN_THRESHOLD\n        if low_score:\n            n_gated_low_score += 1\n        elif confident:\n            n_gated_confident += 1\n\n        val_llm_scores_cache.append(\n            None if (not any(hyps)) or _is_gated(sample_result)\n            else compute_llm_scores(hyps, batch_size=len(hyps))\n        )\n\n    n_gated_total = n_gated_low_score + n_gated_confident\n    print(f\"Skipped LLM rescoring on {n_gated_total}/{len(val_decoder_results)} \"\n          f\"utterances ({100 * n_gated_total / max(len(val_decoder_results), 1):.1f}%): \"\n          f\"{n_gated_low_score} low-score, {n_gated_confident} high-confidence.\")\n\n    best_lambda, full_pipeline_wer, full_pipeline_cer, _ = tune_and_evaluate(\n        decoder_results=val_decoder_results,\n        hyps_cache=val_hyps_cache,\n        llm_scores_cache=val_llm_scores_cache,\n        label=f\"FINAL (lm_weight={best_lw})\",\n    )\n\n    LLM_FUSION_WEIGHT = best_lambda                 # <-- overwrites global fusion weight used by Block 6\n    CONFIG['llm_fusion_weight'] = best_lambda\n\n    print(f\"\\n✓ decoder now uses lm_weight={best_lw}\")\n    print(f\"✓ LLM_FUSION_WEIGHT now = {LLM_FUSION_WEIGHT}\")\n    print(f\"✓ Confirmed WER={full_pipeline_wer*100:.2f}%  CER={full_pipeline_cer*100:.2f}%\")\n    print(\"Block 6 (submission.csv) will now use this exact decoder + fusion weight.\")\n","metadata":{"papermill":{"duration":2053.515111,"end_time":"2026-08-29T12:52:30.165061+00:00","exception":false,"start_time":"2026-08-29T12:18:16.64995+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0c9e181c","cell_type":"markdown","source":"### Block 5.5 · Final summary — WER vs CER vs PER\n","metadata":{"papermill":{"duration":0.579616,"end_time":"2026-08-29T12:52:31.327953+00:00","exception":false,"start_time":"2026-08-29T12:52:30.748337+00:00","status":"completed"},"tags":[]}},{"id":"07490334","cell_type":"code","source":"# ============================================================================\n# FINAL SUMMARY: WER vs CER vs PER, side-by-side\n# ============================================================================\n# One overview chart pulling together every number computed above:\n# - WER and CER each have two bars (beam+n-gram only, vs. +LLM rescoring)\n# - PER has a single bar (CTC-greedy; unaffected by the beam-search/LLM stages)\nmetrics_summary = {\n    'WER': {'Beam+NGram': overall_wer * 100, 'Beam+NGram+LLM': full_pipeline_wer * 100},\n    'CER': {'Beam+NGram': overall_cer * 100, 'Beam+NGram+LLM': full_pipeline_cer * 100},\n    'PER': {'CTC-Greedy': overall_per * 100},\n}\n\nwith open(os.path.join(CONFIG['checkpoint_dir'], 'val_wer.json'), 'r') as f:\n    _saved = json.load(f)\n_saved['summary'] = metrics_summary\nwith open(os.path.join(CONFIG['checkpoint_dir'], 'val_wer.json'), 'w') as f:\n    json.dump(_saved, f, indent=2)\n\nfig, ax = plt.subplots(figsize=(9, 6))\ngroup_labels = ['WER\\n(Beam+NGram)', 'WER\\n(+LLM fusion)', 'CER\\n(Beam+NGram)', 'CER\\n(+LLM fusion)', 'PER\\n(CTC-Greedy)']\nvalues = [\n    metrics_summary['WER']['Beam+NGram'],\n    metrics_summary['WER']['Beam+NGram+LLM'],\n    metrics_summary['CER']['Beam+NGram'],\n    metrics_summary['CER']['Beam+NGram+LLM'],\n    metrics_summary['PER']['CTC-Greedy'],\n]\ncolors = ['tab:purple', 'tab:pink', 'tab:blue', 'tab:cyan', 'tab:green']\n\nbars = ax.bar(group_labels, values, color=colors)\nfor bar, val in zip(bars, values):\n    ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height(),\n            f'{val:.1f}%', ha='center', va='bottom')\nax.set_ylabel('Error Rate (%)')\nax.set_title('Final Validation Error Rates: WER vs CER vs PER')\nax.grid(True, axis='y', alpha=0.3)\nfig.tight_layout()\nfig.savefig(os.path.join(figures_dir, 'metrics_summary.png'), dpi=300, bbox_inches='tight')\nplt.show()\n\nprint(\"Final validation numbers:\")\nfor metric, variants in metrics_summary.items():\n    for variant, val in variants.items():\n        print(f\"  {metric} [{variant}]: {val:.2f}%\")\n","metadata":{"papermill":{"duration":1.089148,"end_time":"2026-08-29T12:52:33.084993+00:00","exception":false,"start_time":"2026-08-29T12:52:31.995845+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"60ac78fa","cell_type":"markdown","source":"### Block 5.6 · Paper-ready export — CSV tables + LaTeX summary\n\nEvery number computed above, pulled into plain files — no hand-copying from logs.\n","metadata":{"papermill":{"duration":0.58363,"end_time":"2026-08-29T12:52:34.344415+00:00","exception":false,"start_time":"2026-08-29T12:52:33.760785+00:00","status":"completed"},"tags":[]}},{"id":"6792b8c2","cell_type":"code","source":"# ============================================================================\n# PAPER-READY EXPORT: CSV tables + a LaTeX-friendly summary string\n# ============================================================================\n# Pulls together every number already computed above into a few small files\n# under CONFIG['checkpoint_dir'] (next to figures/) - convenient for copying\n# straight into a paper/thesis, no manual transcription from printed logs.\n\n# 1. Overall metrics table\noverall_df = pd.DataFrame([\n    {'Metric': 'WER', 'Beam+NGram (%)': overall_wer * 100, 'Beam+NGram+LLM (%)': full_pipeline_wer * 100},\n    {'Metric': 'CER', 'Beam+NGram (%)': overall_cer * 100, 'Beam+NGram+LLM (%)': full_pipeline_cer * 100},\n    {'Metric': 'PER', 'Beam+NGram (%)': overall_per * 100, 'Beam+NGram+LLM (%)': overall_per * 100},\n])\noverall_csv_path = os.path.join(CONFIG['checkpoint_dir'], 'results_overall.csv')\noverall_df.to_csv(overall_csv_path, index=False)\nprint(\"Overall metrics:\")\nprint(overall_df.to_string(index=False))\n\n# 2. Per-day breakdown table (WER / CER / PER together, one row per session)\nby_day_df = pd.DataFrame({\n    'Session': session_names,\n    'WER (%)': wer_by_day,\n    'CER (%)': cer_by_day,\n    'PER (%)': per_by_day,\n})\nby_day_csv_path = os.path.join(CONFIG['checkpoint_dir'], 'results_by_day.csv')\nby_day_df.to_csv(by_day_csv_path, index=False)\n\n# 3. Sample predictions table (already built two cells above as `examples`)\nexamples_csv_path = os.path.join(CONFIG['checkpoint_dir'], 'results_sample_predictions.csv')\nexamples.to_csv(examples_csv_path, index=False)\n\n# 4. A ready-to-paste LaTeX table for the overall numbers\nlatex_table = overall_df.to_latex(index=False, float_format=\"%.2f\",\n                                   caption=\"Validation WER, CER and PER for the proposed model.\",\n                                   label=\"tab:wer_cer_per\")\nlatex_path = os.path.join(CONFIG['checkpoint_dir'], 'results_overall_table.tex')\nwith open(latex_path, 'w') as f:\n    f.write(latex_table)\n\nprint(f\"\\nSaved:\\n  {overall_csv_path}\\n  {by_day_csv_path}\\n  {examples_csv_path}\\n  {latex_path}\")\nprint(f\"\\nAll figures are in: {figures_dir}\")\nprint(\"Figures:\", sorted(os.listdir(figures_dir)) if os.path.isdir(figures_dir) else \"(none yet)\")\n","metadata":{"papermill":{"duration":0.831115,"end_time":"2026-08-29T12:52:35.755649+00:00","exception":false,"start_time":"2026-08-29T12:52:34.924534+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"09acd7b0","cell_type":"markdown","source":"### Block 5.7 · Step-by-step pipeline ablation — paper-ready plots & insights\n\nEvery number below was already computed earlier in the notebook (Blocks 5.1-5.6) -\nthis block just turns the *cascade* of the pipeline (greedy CTC -> beam+n-gram ->\n+ margin-adaptive LLM fusion) into figures and a short auto-written insights\nsection, so nothing needs to be re-run or re-swept to get paper-ready material.\n\nFigures produced (all saved to `figures_dir` at 300 dpi):\n1. **Error-rate cascade** - PER -> WER(beam+n-gram) -> WER(final) and the matching\n   CER cascade, each stage's *relative* improvement over the previous one.\n2. **lm_weight sweep** (recorded from the Block 5.3d run - not re-swept here).\n3. **Rescoring-LLM comparison** (recorded from the Block 5.3b run) - why Qwen2.5-7B.\n4. **Decoder-confidence margin distribution** with the gate thresholds and the\n   adaptive-lambda curve overlaid - explains *why* each utterance got the fusion\n   weight it got.\n5. **Gating breakdown** - how many utterances were skip-gated vs. actually rescored.\n\n...plus an auto-generated bullet-point insights block at the end.","metadata":{"papermill":{"duration":0.584677,"end_time":"2026-08-29T12:52:36.924421+00:00","exception":false,"start_time":"2026-08-29T12:52:36.339744+00:00","status":"completed"},"tags":[]}},{"id":"66f60db1","cell_type":"code","source":"# ============================================================================\n# STEP-BY-STEP PIPELINE ABLATION: cascade plots + sweep-history plots +\n# confidence/gating plots + auto-written insights (paper-ready)\n# ============================================================================\n\n# ---- 1. ERROR-RATE CASCADE (PER -> WER beam+n-gram -> WER final) ----------\ncascade_stages = ['CTC-Greedy\\n(PER)', 'Beam+N-gram\\n(WER)', '+Adaptive LLM\\n(WER)']\ncascade_values = [overall_per * 100, overall_wer * 100, full_pipeline_wer * 100]\n\nfig, ax = plt.subplots(figsize=(7, 5))\nbars = ax.bar(cascade_stages, cascade_values,\n              color=['tab:green', 'tab:purple', 'tab:red'])\nfor b, v in zip(bars, cascade_values):\n    ax.text(b.get_x() + b.get_width() / 2, v + 0.15, f'{v:.2f}%',\n            ha='center', fontsize=10, fontweight='bold')\nfor i in range(1, len(cascade_values)):\n    rel = 100 * (cascade_values[i - 1] - cascade_values[i]) / cascade_values[i - 1]\n    ax.annotate(f'-{rel:.1f}%', xy=(i - 0.5, (cascade_values[i - 1] + cascade_values[i]) / 2),\n                ha='center', fontsize=9, color='dimgray', style='italic')\nax.set_ylabel('Error rate (%)')\nax.set_title('Pipeline error-rate cascade (validation set)')\nax.set_ylim(0, max(cascade_values) * 1.25)\nax.grid(True, axis='y', alpha=0.3)\nfig.tight_layout()\nfig.savefig(os.path.join(figures_dir, 'cascade_error_rate.png'), dpi=300, bbox_inches='tight')\nplt.show()\n\n# ---- 1b. Same cascade, but WER vs. CER side-by-side (beam+n-gram -> final) --\nfig, ax = plt.subplots(figsize=(7, 5))\nx = np.arange(2)\nwidth = 0.35\nwer_vals = [overall_wer * 100, full_pipeline_wer * 100]\ncer_vals = [overall_cer * 100, full_pipeline_cer * 100]\nax.bar(x - width / 2, wer_vals, width, label='WER', color='tab:purple')\nax.bar(x + width / 2, cer_vals, width, label='CER', color='tab:blue')\nfor xi, v in zip(x - width / 2, wer_vals):\n    ax.text(xi, v + 0.1, f'{v:.2f}%', ha='center', fontsize=9)\nfor xi, v in zip(x + width / 2, cer_vals):\n    ax.text(xi, v + 0.1, f'{v:.2f}%', ha='center', fontsize=9)\nax.set_xticks(x)\nax.set_xticklabels(['Beam+N-gram\\n(no LLM)', '+Adaptive LLM\\nfusion (final)'])\nax.set_ylabel('Error rate (%)')\nax.set_title('WER vs. CER before/after LLM fusion')\nax.legend()\nax.grid(True, axis='y', alpha=0.3)\nfig.tight_layout()\nfig.savefig(os.path.join(figures_dir, 'wer_vs_cer_before_after_llm.png'), dpi=300, bbox_inches='tight')\nplt.show()\n\n# ---- 2. lm_weight SWEEP (live results from Block 5.3d, if it ran this session) ----\nif 'lm_weight_results' in globals() and lm_weight_results:\n    _lmw_keys = sorted(lm_weight_results.keys())\n    _lmw_wer = [lm_weight_results[k]['wer'] * 100 for k in _lmw_keys]\n    _best_lmw = min(lm_weight_results, key=lambda k: lm_weight_results[k]['wer'])\n\n    fig, ax = plt.subplots(figsize=(7, 5))\n    ax.plot(_lmw_keys, _lmw_wer, marker='o', color='tab:red', linewidth=2)\n    for k, w in zip(_lmw_keys, _lmw_wer):\n        ax.annotate(f'{w:.2f}%', (k, w), textcoords='offset points', xytext=(0, 8), ha='center', fontsize=9)\n    ax.axvline(_best_lmw, color='black', linestyle='--', linewidth=1,\n               label=f'Best found (lm_weight={_best_lmw})')\n    ax.set_xlabel('n-gram lm_weight')\n    ax.set_ylabel('Validation WER (%)')\n    ax.set_title('WER vs. n-gram LM weight (fusion lambda re-tuned at each point)')\n    ax.legend()\n    ax.grid(True, alpha=0.3)\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'lm_weight_sweep.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n    if _best_lmw == max(_lmw_keys):\n        print(f\"NOTE: WER is still falling at lm_weight={_best_lmw}, the edge of the grid \"\n              f\"that was swept ({min(_lmw_keys)}-{max(_lmw_keys)}) - the true optimum may sit \"\n              f\"above {_best_lmw}. Extend LM_WEIGHT_GRID (Block 5.3d) further if you want to chase this.\")\nelse:\n    print(\"Skipping lm_weight-sweep plot - lm_weight_results is not available \"\n          \"(RUN_HYPERPARAM_SWEEPS was False for this run, or Block 5.3d hasn't been executed yet).\")\n\n# ---- 3. RESCORING-LLM COMPARISON (live results from Block 5.3b, if it ran) ----\nif 'comparison_results' in globals() and comparison_results:\n    _llm_names = list(comparison_results.keys())\n    _llm_wer = [comparison_results[k]['wer'] * 100 for k in _llm_names]\n    _order = np.argsort(_llm_wer)\n    _llm_names = [_llm_names[i] for i in _order]\n    _llm_wer = [_llm_wer[i] for i in _order]\n    _colors = ['tab:green' if n == LLM_KEY else 'tab:gray' for n in _llm_names]\n\n    fig, ax = plt.subplots(figsize=(7, 5))\n    bars = ax.barh(_llm_names, _llm_wer, color=_colors)\n    for b, v in zip(bars, _llm_wer):\n        ax.text(v + 0.02, b.get_y() + b.get_height() / 2, f'{v:.2f}%', va='center', fontsize=9)\n    ax.set_xlabel(f'Validation WER (%) @ lm_weight={LM_WEIGHT} (comparison run)')\n    ax.set_title('Rescoring-LLM comparison (green = currently selected)')\n    ax.invert_yaxis()\n    ax.grid(True, axis='x', alpha=0.3)\n    fig.tight_layout()\n    fig.savefig(os.path.join(figures_dir, 'llm_comparison.png'), dpi=300, bbox_inches='tight')\n    plt.show()\n    print(\"NOTE: llama3.1-8b (gated repo, needs HF auth) and qwen3-14b (OOM in 4-bit on this GPU) \"\n          \"could not be benchmarked and are omitted rather than shown as failures.\")\nelse:\n    print(\"Skipping LLM-comparison plot - comparison_results is not available \"\n          \"(RUN_HYPERPARAM_SWEEPS was False for this run, or Block 5.3b hasn't been executed yet).\")\n\n# ---- 4. DECODER-CONFIDENCE MARGIN DISTRIBUTION + adaptive-lambda curve ----\n_margins = np.array([top1_margin(r) for r in val_decoder_results if r and np.isfinite(top1_margin(r))])\n\nfig, ax1 = plt.subplots(figsize=(8, 5))\nax1.hist(_margins, bins=40, color='tab:blue', alpha=0.6, label='Utterances (top1-runnerup margin)')\nax1.axvline(LLM_GATE_MARGIN_THRESHOLD, color='black', linestyle='--', linewidth=1,\n            label=f'Gate threshold ({LLM_GATE_MARGIN_THRESHOLD:.2f})')\nax1.set_xlabel('Top1 - runner-up normalized decoder score margin')\nax1.set_ylabel('Number of utterances')\n\nax2 = ax1.twinx()\n_m_grid = np.linspace(0, LLM_GATE_MARGIN_THRESHOLD * 1.3, 200)\n_lam_grid = [LLM_FUSION_WEIGHT * (1 - min(m / LLM_GATE_MARGIN_THRESHOLD, 1.0)) for m in _m_grid]\nax2.plot(_m_grid, _lam_grid, color='tab:red', linewidth=2, label='Adaptive $\\\\lambda$(margin)')\nax2.set_ylabel('Effective LLM fusion weight ($\\\\lambda$)', color='tab:red')\nax2.tick_params(axis='y', labelcolor='tab:red')\n\nlines1, labels1 = ax1.get_legend_handles_labels()\nlines2, labels2 = ax2.get_legend_handles_labels()\nax1.legend(lines1 + lines2, labels1 + labels2, loc='upper right', fontsize=8)\nax1.set_title('Decoder confidence margin vs. adaptive LLM fusion weight')\nfig.tight_layout()\nfig.savefig(os.path.join(figures_dir, 'margin_vs_adaptive_lambda.png'), dpi=300, bbox_inches='tight')\nplt.show()\n\n# ---- 5. GATING BREAKDOWN --------------------------------------------------\n_n_total = len(val_decoder_results)\n_n_rescored = _n_total - n_gated_low_score - n_gated_confident\n_gate_labels = ['Rescored\\n(LLM fusion)', 'Gated: decoder\\nalready confident', 'Gated: low-score\\n(incoherent)']\n_gate_vals = [_n_rescored, n_gated_confident, n_gated_low_score]\n_gate_pcts = [100 * v / max(_n_total, 1) for v in _gate_vals]\n\nfig, ax = plt.subplots(figsize=(7, 5))\nbars = ax.bar(_gate_labels, _gate_pcts, color=['tab:red', 'tab:gray', 'tab:orange'])\nfor b, v, n in zip(bars, _gate_pcts, _gate_vals):\n    ax.text(b.get_x() + b.get_width() / 2, v + 0.5, f'{v:.1f}%\\n(n={n})', ha='center', fontsize=9)\nax.set_ylabel('% of validation utterances')\nax.set_title('Where validation-set compute went (gating breakdown)')\nax.set_ylim(0, max(_gate_pcts) * 1.3)\nax.grid(True, axis='y', alpha=0.3)\nfig.tight_layout()\nfig.savefig(os.path.join(figures_dir, 'gating_breakdown.png'), dpi=300, bbox_inches='tight')\nplt.show()\n\n# ---- 6. AUTO-WRITTEN INSIGHTS (paper discussion-section material) ---------\n_rel_beam_vs_greedy = 100 * (overall_per * 100 - overall_wer * 100) / (overall_per * 100) if overall_per > 0 else float('nan')\n_rel_llm_vs_beam = 100 * (overall_wer - full_pipeline_wer) / overall_wer if overall_wer > 0 else float('nan')\n_best_day_i = int(np.nanargmin(wer_by_day))\n_worst_day_i = int(np.nanargmax(wer_by_day))\n\nif 'lm_weight_results' in globals() and lm_weight_results:\n    _lmw_keys_sorted = sorted(lm_weight_results.keys())\n    _lmw_wer_pairs = ', '.join(f'{k}:{lm_weight_results[k][\"wer\"]*100:.2f}%' for k in _lmw_keys_sorted)\n    _best_lmw_i = min(lm_weight_results, key=lambda k: lm_weight_results[k]['wer'])\n    if _best_lmw_i == max(_lmw_keys_sorted):\n        _lmw_insight_line = (f\"The n-gram lm_weight sweep ({_lmw_wer_pairs}) was still improving at its \"\n                             f\"upper edge (lm_weight={_best_lmw_i}) - the true optimum for this axis has \"\n                             f\"not been confirmed and may lie beyond {_best_lmw_i}.\")\n    else:\n        _lmw_insight_line = (f\"The n-gram lm_weight sweep ({_lmw_wer_pairs}) found its best WER at \"\n                             f\"lm_weight={_best_lmw_i}, inside the swept range.\")\nelse:\n    _lmw_insight_line = \"The n-gram lm_weight sweep was not run this session (RUN_HYPERPARAM_SWEEPS=False).\"\n\ninsights = f\"\"\"\nPAPER-READY INSIGHTS (validation set, {len(refs_clean)} utterances)\n{'=' * 70}\n1. Lexicon + n-gram beam search alone (lm_weight={LM_WEIGHT}) reduces PER \"\n   ({overall_per*100:.2f}%) to a beam+n-gram WER of {overall_wer*100:.2f}% \"\n   / CER of {overall_cer*100:.2f}%.\n2. Margin-adaptive {LLM_KEY} fusion (base weight={LLM_FUSION_WEIGHT}, scaled per-\n   utterance by decoder confidence) further cuts WER to {full_pipeline_wer*100:.2f}%\n   ({_rel_llm_vs_beam:.1f}% relative reduction over beam+n-gram alone) and CER to\n   {full_pipeline_cer*100:.2f}%.\n3. Of {_n_total} validation utterances, {_n_rescored} ({100*_n_rescored/max(_n_total,1):.1f}%)\n   were actually sent through the LLM; {n_gated_confident} ({100*n_gated_confident/max(_n_total,1):.1f}%)\n   were skipped because the decoder was already confident, and {n_gated_low_score}\n   ({100*n_gated_low_score/max(_n_total,1):.1f}%) were skipped as likely incoherent/random trials.\n4. Mean adaptive lambda actually applied on rescored utterances: \n   {np.mean(val_lambdas_used):.3f} out of a base of {LLM_FUSION_WEIGHT} - i.e. the LLM's\n   average effective influence is scaled to about \n   {100*np.mean(val_lambdas_used)/LLM_FUSION_WEIGHT:.0f}% of its maximum allowed weight,\n   concentrated on the more ambiguous utterances by design.\n5. Per-session WER ranges from {wer_by_day[_best_day_i]:.2f}% ({session_names[_best_day_i]})\n   to {wer_by_day[_worst_day_i]:.2f}% ({session_names[_worst_day_i]}), a\n   {wer_by_day[_worst_day_i]-wer_by_day[_best_day_i]:.2f}-point spread - consistent with\n   day-to-day neural signal drift despite the day-adaptive input layer.\n6. {_lmw_insight_line}\n\"\"\"\nprint(insights)\nwith open(os.path.join(CONFIG['checkpoint_dir'], 'paper_insights.txt'), 'w') as f:\n    f.write(insights)\nprint(f\"Saved insights text -> {os.path.join(CONFIG['checkpoint_dir'], 'paper_insights.txt')}\")\nprint(f\"All figures saved under: {figures_dir}\")\n","metadata":{"papermill":{"duration":3.339687,"end_time":"2026-08-29T12:52:40.939886+00:00","exception":false,"start_time":"2026-08-29T12:52:37.600199+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e129ea45","cell_type":"markdown","source":"### Block 6 · [Optional] Generate Kaggle submission from the test set\n\nNot needed for the validation numbers above — skip unless you want `submission.csv`. Reloads the competition test set and reuses `model`, `decoder`, `llm_model`, `tokenizer`, `compute_llm_scores`, and the validation-tuned `LLM_FUSION_WEIGHT`, so run it only after the cells above.\n","metadata":{"papermill":{"duration":0.577491,"end_time":"2026-08-29T12:52:42.195896+00:00","exception":false,"start_time":"2026-08-29T12:52:41.618405+00:00","status":"completed"},"tags":[]}},{"id":"73e7fbbb","cell_type":"code","source":"# ============================================================================\n# OPTIONAL: GENERATE KAGGLE SUBMISSION FROM THE ACTUAL TEST SET\n# ============================================================================\ndef block_load_test_data(data_dir, session2idx, feat_mean, feat_std, clip=5.0):\n    pattern = f'{data_dir}/**/data_test.hdf5'\n    files = sorted(glob(pattern, recursive=True))\n    all_samples = []\n    sample_id = 0\n    for filepath in tqdm(files, desc=\"Loading Test HDF5\"):\n        session = Path(filepath).parent.name\n        with h5py.File(filepath, 'r') as f:\n            # Competition requires submission rows in chronological order\n            # (session date, then block, then trial number) - session order\n            # is already correct (sorted() on the date-like folder names),\n            # but trials within a file must be sorted explicitly by\n            # block_num/trial_num, since f.keys() order is alphabetical over\n            # the key *string*, not numeric over the trial index (e.g.\n            # \"trial_10\" would otherwise sort before \"trial_2\").\n            trial_keys = [k for k in f.keys() if 'trial' in k.lower()]\n            entries = []\n            for trial_key in trial_keys:\n                trial = f[trial_key]\n                if 'input_features' not in trial:\n                    continue\n                block_num = int(trial.attrs.get('block_num', 0))\n                trial_num = int(trial.attrs.get('trial_num', 0))\n                entries.append((block_num, trial_num, trial_key))\n            entries.sort(key=lambda e: (e[0], e[1]))\n\n            for block_num, trial_num, trial_key in entries:\n                trial = f[trial_key]\n                features = trial['input_features'][:trial.attrs['n_time_steps']]\n                features = torch.FloatTensor(features)\n                features = (features - feat_mean) / feat_std       # same stats as train/val\n                features = torch.clamp(features, -clip, clip)\n                all_samples.append({\n                    'id': sample_id, 'session': session, 'day_idx': session2idx[session],\n                    'block_num': block_num, 'trial_num': trial_num,\n                    'trial_key': trial_key, 'features': features\n                })\n                sample_id += 1\n    return all_samples\n\n\ndef extract_emissions(model, samples, device):\n    model.eval()\n    all_emissions, all_lengths, all_ids = [], [], []\n    with torch.no_grad():\n        for i in tqdm(range(0, len(samples), 32), desc='Extracting Test Emissions'):\n            batch = samples[i:i + 32]\n            features = [s['features'] for s in batch]\n            lengths = torch.LongTensor([len(f) for f in features])\n            day_idx = torch.LongTensor([s['day_idx'] for s in batch]).to(device)\n\n            features_padded = torch.nn.utils.rnn.pad_sequence(features, batch_first=True).to(device)\n            sorted_lengths, sorted_idx = lengths.sort(descending=True)\n\n            with autocast(enabled=CONFIG['use_amp']):\n                log_probs, output_lengths = model(features_padded[sorted_idx], sorted_lengths, day_idx[sorted_idx])\n            log_probs = log_probs.transpose(0, 1).float().cpu()  # [B, T, V] for torchaudio's decoder\n\n            unsorted = torch.empty_like(log_probs)\n            unsorted_lengths = torch.empty_like(output_lengths.cpu())\n            unsorted[sorted_idx] = log_probs\n            unsorted_lengths[sorted_idx] = output_lengths.cpu()\n\n            all_emissions.append(unsorted)\n            all_lengths.append(unsorted_lengths)\n            all_ids.extend([s['id'] for s in batch])\n\n    # Each batch was padded to ITS OWN max trial length, so the T (time) dim\n    # can differ batch-to-batch (e.g. 323 vs 432) - pad every batch's\n    # emissions out to a shared max_T before concatenating, exactly like\n    # extract_emissions_from_loader() does for the validation set. Without\n    # this, torch.cat(..., dim=0) fails because dim=1 (T) doesn't match.\n    max_T = max(e.shape[1] for e in all_emissions)\n    all_emissions = [F.pad(e, (0, 0, 0, max_T - e.shape[1])) for e in all_emissions]\n    return torch.cat(all_emissions, dim=0), torch.cat(all_lengths, dim=0), all_ids\n\n\nprint(\"Loading test set...\")\ntest_samples = block_load_test_data(CONFIG['data_dir'], session2idx, feat_mean, feat_std)\nprint(f\"Test samples: {len(test_samples)}\")\n\ntest_emissions, test_lengths, test_ids = extract_emissions(model, test_samples, CONFIG['device'])\n\nprint(\"Running batched beam search on test set...\")\ntest_decoder_results = []  # list of per-utterance n-best CTCHypothesis lists\nDECODE_BATCH = 16\nfor i in tqdm(range(0, len(test_emissions), DECODE_BATCH), desc=\"Beam search\"):\n    emissions_chunk = test_emissions[i:i + DECODE_BATCH]\n    lengths_chunk = test_lengths[i:i + DECODE_BATCH]\n    test_decoder_results.extend(decoder(emissions_chunk, lengths_chunk))\n\n# Reuse the SAME gate threshold calibrated on the validation set (in the\n# \"FULL-PIPELINE ... ON VALIDATION\" cell) rather than recalibrating on the\n# test set - the test set has no ground truth to sanity-check a fresh\n# calibration against, and the decoder's score scale doesn't change between\n# val and test. If that cell hasn't been run yet, fall back to calibrating on\n# the test set's own scores now (with a warning) so this cell still works\n# standalone.\nif LLM_GATE_THRESHOLD is None or LLM_GATE_MARGIN_THRESHOLD is None:\n    print(\"WARNING: no validation-calibrated gate thresholds found - \"\n          \"calibrating from the test set's own scores instead. Run the \"\n          \"validation 'FULL-PIPELINE' cell first if you want the thresholds \"\n          \"chosen against ground-truth-verified data.\")\n    calibrate_llm_gate_threshold(test_decoder_results)\n\nprint(f\"Using score-fusion LLM rescoring with LLM_FUSION_WEIGHT=\"\n      f\"{LLM_FUSION_WEIGHT} (tuned on validation; lambda=0 would disable the LLM).\")\n\nFINAL_PREDICTIONS = []\nn_gated_low_score = 0\nn_gated_confident = 0\nprint(\"Rescoring n-best hypotheses with the LLM (confidence-gated)...\")\nfor sample_result in tqdm(test_decoder_results, desc=\"Gated LLM rescoring\"):\n    if sample_result:\n        if normalized_ngram_score(sample_result[0]) < LLM_GATE_THRESHOLD:\n            n_gated_low_score += 1\n        elif top1_margin(sample_result) > LLM_GATE_MARGIN_THRESHOLD:\n            n_gated_confident += 1\n    FINAL_PREDICTIONS.append(gated_rescore(sample_result))\n\nn_gated_total = n_gated_low_score + n_gated_confident\nprint(f\"Skipped LLM rescoring on {n_gated_total}/{len(test_decoder_results)} \"\n      f\"test utterances ({100 * n_gated_total / max(len(test_decoder_results), 1):.1f}%): \"\n      f\"{n_gated_low_score} low-score (likely random/incoherent), \"\n      f\"{n_gated_confident} high-confidence (decoder already sure).\")\n\n# Match whatever columns the competition actually expects instead of assuming\n# 'id'/'text' - a silent column-name mismatch against sample_submission.csv is\n# a common reason a submission \"doesn't work\" even though the notebook itself\n# ran fine.\nsample_sub_path = None\nfor pattern in [os.path.join(os.path.dirname(CONFIG['data_dir']), '**', 'sample_submission.csv'),\n                '/kaggle/input/**/sample_submission.csv']:\n    matches = glob(pattern, recursive=True)\n    if matches:\n        sample_sub_path = matches[0]\n        break\n\nif sample_sub_path:\n    sample_df = pd.read_csv(sample_sub_path)\n    id_col, text_col = sample_df.columns[0], sample_df.columns[1]\n    print(f\"Found sample_submission.csv at {sample_sub_path} -> using columns \"\n          f\"'{id_col}', '{text_col}'\")\nelse:\n    id_col, text_col = 'id', 'text'\n    print(\"No sample_submission.csv found automatically - defaulting to columns \"\n          \"'id', 'text'. Check the competition's Data tab and adjust id_col/text_col \"\n          \"above if it expects something else.\")\n\ndf = pd.DataFrame({id_col: test_ids, text_col: FINAL_PREDICTIONS})\ndf = df.sort_values(id_col).reset_index(drop=True)\ndf[id_col] = range(len(df))\ndf.to_csv('submission.csv', index=False)\n\nprint('\\n✓ Finished! Predictions written to submission.csv')\ndf.head(10)\n","metadata":{"papermill":{"duration":2616.844557,"end_time":"2026-08-29T13:36:19.631763+00:00","exception":false,"start_time":"2026-08-29T12:52:42.787206+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5371f762","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.713343,"end_time":"2026-08-29T13:36:20.974592+00:00","exception":false,"start_time":"2026-08-29T13:36:20.261249+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2d2aedd6","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.716124,"end_time":"2026-08-29T13:36:22.314747+00:00","exception":false,"start_time":"2026-08-29T13:36:21.598623+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2bbcfe09","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.631635,"end_time":"2026-08-29T13:36:23.562787+00:00","exception":false,"start_time":"2026-08-29T13:36:22.931152+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"49502122","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.616747,"end_time":"2026-08-29T13:36:24.898341+00:00","exception":false,"start_time":"2026-08-29T13:36:24.281594+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ed03f41f","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.622218,"end_time":"2026-08-29T13:36:26.248853+00:00","exception":false,"start_time":"2026-08-29T13:36:25.626635+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9f285771","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.738365,"end_time":"2026-08-29T13:36:27.612873+00:00","exception":false,"start_time":"2026-08-29T13:36:26.874508+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"1d36b218","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.651624,"end_time":"2026-08-29T13:36:28.902131+00:00","exception":false,"start_time":"2026-08-29T13:36:28.250507+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"831c4fb3","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.622827,"end_time":"2026-08-29T13:36:30.252505+00:00","exception":false,"start_time":"2026-08-29T13:36:29.629678+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"25f9f0eb","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.627399,"end_time":"2026-08-29T13:36:31.608665+00:00","exception":false,"start_time":"2026-08-29T13:36:30.981266+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"28aa435a","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.710218,"end_time":"2026-08-29T13:36:32.932121+00:00","exception":false,"start_time":"2026-08-29T13:36:32.221903+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"aa653ecb","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.723978,"end_time":"2026-08-29T13:36:34.273818+00:00","exception":false,"start_time":"2026-08-29T13:36:33.54984+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3c3c5f94","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.720285,"end_time":"2026-08-29T13:36:35.609534+00:00","exception":false,"start_time":"2026-08-29T13:36:34.889249+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b79ca778","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.621491,"end_time":"2026-08-29T13:36:36.850924+00:00","exception":false,"start_time":"2026-08-29T13:36:36.229433+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0ead237c","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.621957,"end_time":"2026-08-29T13:36:38.186573+00:00","exception":false,"start_time":"2026-08-29T13:36:37.564616+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"09f0bbf9","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.724981,"end_time":"2026-08-29T13:36:39.559924+00:00","exception":false,"start_time":"2026-08-29T13:36:38.834943+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4f59c2ca","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.719876,"end_time":"2026-08-29T13:36:40.898347+00:00","exception":false,"start_time":"2026-08-29T13:36:40.178471+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0b3413bc","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.623351,"end_time":"2026-08-29T13:36:42.144366+00:00","exception":false,"start_time":"2026-08-29T13:36:41.521015+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8259541e","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.616186,"end_time":"2026-08-29T13:36:43.476885+00:00","exception":false,"start_time":"2026-08-29T13:36:42.860699+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e4e139b0","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.620242,"end_time":"2026-08-29T13:36:44.815889+00:00","exception":false,"start_time":"2026-08-29T13:36:44.195647+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"163b538d","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.61871,"end_time":"2026-08-29T13:36:46.148552+00:00","exception":false,"start_time":"2026-08-29T13:36:45.529842+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"81c89f2b","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.720556,"end_time":"2026-08-29T13:36:47.502149+00:00","exception":false,"start_time":"2026-08-29T13:36:46.781593+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"30453a74","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.661654,"end_time":"2026-08-29T13:36:48.780634+00:00","exception":false,"start_time":"2026-08-29T13:36:48.11898+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f8ee83bc","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.617808,"end_time":"2026-08-29T13:36:50.111005+00:00","exception":false,"start_time":"2026-08-29T13:36:49.493197+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2de6328d","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.622084,"end_time":"2026-08-29T13:36:51.449051+00:00","exception":false,"start_time":"2026-08-29T13:36:50.826967+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"87ddae48","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.71687,"end_time":"2026-08-29T13:36:52.78858+00:00","exception":false,"start_time":"2026-08-29T13:36:52.07171+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"4722809b","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.719958,"end_time":"2026-08-29T13:36:54.123502+00:00","exception":false,"start_time":"2026-08-29T13:36:53.403544+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"2543b08c","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.720589,"end_time":"2026-08-29T13:36:55.470233+00:00","exception":false,"start_time":"2026-08-29T13:36:54.749644+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"04a5e656","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.626107,"end_time":"2026-08-29T13:36:56.711217+00:00","exception":false,"start_time":"2026-08-29T13:36:56.08511+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"7cf03548","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.620837,"end_time":"2026-08-29T13:36:58.057708+00:00","exception":false,"start_time":"2026-08-29T13:36:57.436871+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"fc42b91e","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.713492,"end_time":"2026-08-29T13:36:59.433169+00:00","exception":false,"start_time":"2026-08-29T13:36:58.719677+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b4ffcc71","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.732839,"end_time":"2026-08-29T13:37:00.805504+00:00","exception":false,"start_time":"2026-08-29T13:37:00.072665+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a485bf3d","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.616889,"end_time":"2026-08-29T13:37:02.033342+00:00","exception":false,"start_time":"2026-08-29T13:37:01.416453+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9781fdbb","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.629878,"end_time":"2026-08-29T13:37:03.383333+00:00","exception":false,"start_time":"2026-08-29T13:37:02.753455+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9ffe2925","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.614313,"end_time":"2026-08-29T13:37:04.715771+00:00","exception":false,"start_time":"2026-08-29T13:37:04.101458+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ad01def9","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.618187,"end_time":"2026-08-29T13:37:06.0485+00:00","exception":false,"start_time":"2026-08-29T13:37:05.430313+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9f57e964","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.723499,"end_time":"2026-08-29T13:37:07.39599+00:00","exception":false,"start_time":"2026-08-29T13:37:06.672491+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"bfb64092","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.648693,"end_time":"2026-08-29T13:37:08.673109+00:00","exception":false,"start_time":"2026-08-29T13:37:08.024416+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"152b96e9","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.617021,"end_time":"2026-08-29T13:37:10.000226+00:00","exception":false,"start_time":"2026-08-29T13:37:09.383205+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"0849e6a2","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.616742,"end_time":"2026-08-29T13:37:11.324208+00:00","exception":false,"start_time":"2026-08-29T13:37:10.707466+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b0af7758","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.704681,"end_time":"2026-08-29T13:37:12.642332+00:00","exception":false,"start_time":"2026-08-29T13:37:11.937651+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5ea81a61","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.711329,"end_time":"2026-08-29T13:37:13.972068+00:00","exception":false,"start_time":"2026-08-29T13:37:13.260739+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c884efb6","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.714505,"end_time":"2026-08-29T13:37:15.299397+00:00","exception":false,"start_time":"2026-08-29T13:37:14.584892+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3296f1d2","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.626186,"end_time":"2026-08-29T13:37:16.544813+00:00","exception":false,"start_time":"2026-08-29T13:37:15.918627+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8de06491","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.62006,"end_time":"2026-08-29T13:37:17.882467+00:00","exception":false,"start_time":"2026-08-29T13:37:17.262407+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"035e9cd4","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.724267,"end_time":"2026-08-29T13:37:19.267618+00:00","exception":false,"start_time":"2026-08-29T13:37:18.543351+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3b563fe0","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.714447,"end_time":"2026-08-29T13:37:20.600234+00:00","exception":false,"start_time":"2026-08-29T13:37:19.885787+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5c6a49e5","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.612517,"end_time":"2026-08-29T13:37:21.836616+00:00","exception":false,"start_time":"2026-08-29T13:37:21.224099+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"33a57831","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.627157,"end_time":"2026-08-29T13:37:23.17724+00:00","exception":false,"start_time":"2026-08-29T13:37:22.550083+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"65b88aa5","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.617376,"end_time":"2026-08-29T13:37:24.51781+00:00","exception":false,"start_time":"2026-08-29T13:37:23.900434+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"151fdf32","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.613171,"end_time":"2026-08-29T13:37:25.848427+00:00","exception":false,"start_time":"2026-08-29T13:37:25.235256+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"48a887cf","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.720078,"end_time":"2026-08-29T13:37:27.191119+00:00","exception":false,"start_time":"2026-08-29T13:37:26.471041+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"983acd82","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.665873,"end_time":"2026-08-29T13:37:28.473751+00:00","exception":false,"start_time":"2026-08-29T13:37:27.807878+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"b31e46a6","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.620196,"end_time":"2026-08-29T13:37:29.824029+00:00","exception":false,"start_time":"2026-08-29T13:37:29.203833+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"5ff2eca5","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.62083,"end_time":"2026-08-29T13:37:31.164045+00:00","exception":false,"start_time":"2026-08-29T13:37:30.543215+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"8c588895","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.715924,"end_time":"2026-08-29T13:37:32.493062+00:00","exception":false,"start_time":"2026-08-29T13:37:31.777138+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"673e7368","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.7394,"end_time":"2026-08-29T13:37:33.867164+00:00","exception":false,"start_time":"2026-08-29T13:37:33.127764+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"3ef01837","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.723682,"end_time":"2026-08-29T13:37:35.210767+00:00","exception":false,"start_time":"2026-08-29T13:37:34.487085+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"72f65e34","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.630112,"end_time":"2026-08-29T13:37:36.462536+00:00","exception":false,"start_time":"2026-08-29T13:37:35.832424+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"e290f93d","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.614962,"end_time":"2026-08-29T13:37:37.801433+00:00","exception":false,"start_time":"2026-08-29T13:37:37.186471+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c1ba8742","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.729826,"end_time":"2026-08-29T13:37:39.168895+00:00","exception":false,"start_time":"2026-08-29T13:37:38.439069+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"f1733dee","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.719573,"end_time":"2026-08-29T13:37:40.515924+00:00","exception":false,"start_time":"2026-08-29T13:37:39.796351+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"36eba144","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.61712,"end_time":"2026-08-29T13:37:41.752588+00:00","exception":false,"start_time":"2026-08-29T13:37:41.135468+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"60f716d8","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.614528,"end_time":"2026-08-29T13:37:43.078015+00:00","exception":false,"start_time":"2026-08-29T13:37:42.463487+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"a0b1ef49","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.621821,"end_time":"2026-08-29T13:37:44.425186+00:00","exception":false,"start_time":"2026-08-29T13:37:43.803365+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"9cd6f23e","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.61775,"end_time":"2026-08-29T13:37:45.764748+00:00","exception":false,"start_time":"2026-08-29T13:37:45.146998+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"500b84ee","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.720227,"end_time":"2026-08-29T13:37:47.103843+00:00","exception":false,"start_time":"2026-08-29T13:37:46.383616+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"c8aed1fd","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.621398,"end_time":"2026-08-29T13:37:48.340414+00:00","exception":false,"start_time":"2026-08-29T13:37:47.719016+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"d41522f8","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.616852,"end_time":"2026-08-29T13:37:49.69877+00:00","exception":false,"start_time":"2026-08-29T13:37:49.081918+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"ad142936","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.618733,"end_time":"2026-08-29T13:37:51.038976+00:00","exception":false,"start_time":"2026-08-29T13:37:50.420243+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"abb6ab9e","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.709781,"end_time":"2026-08-29T13:37:52.367743+00:00","exception":false,"start_time":"2026-08-29T13:37:51.657962+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"50418578","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.718633,"end_time":"2026-08-29T13:37:53.70405+00:00","exception":false,"start_time":"2026-08-29T13:37:52.985417+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"df59ee72","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.723413,"end_time":"2026-08-29T13:37:55.051886+00:00","exception":false,"start_time":"2026-08-29T13:37:54.328473+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"017a9779","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.617497,"end_time":"2026-08-29T13:37:56.293906+00:00","exception":false,"start_time":"2026-08-29T13:37:55.676409+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"831075fc","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.62264,"end_time":"2026-08-29T13:37:57.641999+00:00","exception":false,"start_time":"2026-08-29T13:37:57.019359+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null},{"id":"61c746e9","cell_type":"code","source":"","metadata":{"papermill":{"duration":0.740911,"end_time":"2026-08-29T13:37:59.010263+00:00","exception":false,"start_time":"2026-08-29T13:37:58.269352+00:00","status":"completed"},"tags":[],"trusted":true},"outputs":[],"execution_count":null}]}