From ad18d38374cec34b2a643fdfe93b060fe7b8f4e2 Mon Sep 17 00:00:00 2001 From: 51616 Date: Sun, 28 Sep 2025 15:46:09 +0000 Subject: [PATCH] iclr 2026 (3) --- data/gutenburg_sample.txt | 387 +++++++++++++++++++++++ run_eval.py | 5 + src/ctx_to_lora/data/definitions.py | 61 +++- src/ctx_to_lora/data/preprocessing_fn.py | 77 +++++ src/ctx_to_lora/data/processing.py | 78 +++-- src/ctx_to_lora/eval_utils.py | 92 +++++- src/ctx_to_lora/metrics.py | 223 ++++++++++++- token_length_distributions.pdf | Bin 0 -> 22208 bytes webui/app.py | 46 ++- webui/templates/visualize.html | 91 +++++- 10 files changed, 996 insertions(+), 64 deletions(-) create mode 100644 data/gutenburg_sample.txt create mode 100644 token_length_distributions.pdf diff --git a/data/gutenburg_sample.txt b/data/gutenburg_sample.txt new file mode 100644 index 0000000..12cde16 --- /dev/null +++ b/data/gutenburg_sample.txt @@ -0,0 +1,387 @@ +The Project Gutenberg eBook, Addison, by William John Courthope + + +This eBook is for the use of anyone anywhere at no cost and with +almost no restrictions whatsoever. You may copy it, give it away or +re-use it under the terms of the Project Gutenberg License included +with this eBook or online at www.gutenberg.org + + + + + +Title: Addison + + +Author: William John Courthope + + + +Release Date: November 27, 2012 [eBook #41496] + +Language: English + +Character set encoding: ISO-8859-1 + + +***START OF THE PROJECT GUTENBERG EBOOK ADDISON*** + + +E-text prepared by the Online Distributed Proofreading Team +(http://www.pgdp.net) from page images generously made available by +Internet Archive (http://archive.org) + + + +Note: Images of the original pages are available through + Internet Archive. See + http://archive.org/details/addison_00cour + + +Transcriber's note: + + Text enclosed by underscores is in italics (_italics_). + + Text enclosed by curly brackets is superscripted + (example: y{e}). + + + + + +English Men of Letters + +Edited by John Morley + +ADDISON + +by + +W. J. COURTHOPE + + + + + + + +Harper & Brothers Publishers +New York and London +1902 + + * * * * * + +ENGLISH MEN OF LETTERS. + +EDITED BY JOHN MORLEY. + + JOHNSON Leslie Stephen. + GIBBON J. C. Morison. + SCOTT R. H. Hutton. + SHELLEY J. A. Symonds. + HUME T. H. Huxley. + GOLDSMITH William Black. + DEFOE William Minto. + BURNS J. C. Shairp. + SPENSER R. W. Church. + THACKERAY Anthony Trollope. + BURKE John Morley. + MILTON Mark Pattison. + HAWTHORNE Henry James, Jr. + SOUTHEY E. Dowden. + CHAUCER A. W. Ward. + BUNYAN J. A. Froude. + COWPER Goldwin Smith. + POPE Leslie Stephen. + BYRON John Nichol. + LOCKE Thomas Fowler. + WORDSWORTH F. Myers. + DRYDEN G. Saintsbury. + LANDOR Sidney Colvin. + DE QUINCEY David Masson. + LAMB Alfred Ainger. + BENTLEY R. C. Jebb. + DICKENS A. W. Ward. + GRAY E. W. Gosse. + SWIFT Leslie Stephen. + STERNE H. D. Traill. + MACAULAY J. Cotter Morison. + FIELDING Austin Dobson. + SHERIDAN Mrs. Oliphant. + ADDISON W. J. Courthope. + BACON R. W. Church. + COLERIDGE H. D. Traill. + SIR PHILIP SIDNEY J. A. Symonds. + KEATS Sidney Colvin. + CARLYLE John Nichol. + +12mo, Cloth, 75 cents per volume. + +_Other volumes in preparation._ + +PUBLISHED BY HARPER & BROTHERS, NEW YORK. + +_Any of the above works will be sent by mail, postage prepaid, to any part +of the United States, Canada, or Mexico, on receipt of the price._ + + * * * * * + + + +CONTENTS. + + + PAGE + + CHAPTER I. + THE STATE OF ENGLISH SOCIETY AND LETTERS + AFTER THE RESTORATION 1 + + CHAPTER II. + ADDISON'S FAMILY AND EDUCATION 21 + + CHAPTER III. + ADDISON ON HIS TRAVELS 38 + + CHAPTER IV. + HIS EMPLOYMENT IN AFFAIRS OF STATE 53 + + CHAPTER V. + THE "TATLER" AND "SPECTATOR" 78 + + CHAPTER VI. + "CATO" 110 + + CHAPTER VII. + ADDISON'S QUARREL WITH POPE 125 + + CHAPTER VIII. + THE LAST YEARS OF HIS LIFE 139 + + CHAPTER IX. + THE GENIUS OF ADDISON 153 + + + + +ADDISON. + + + + +CHAPTER I. + +THE STATE OF ENGLISH SOCIETY AND LETTERS AFTER THE RESTORATION. + + +Of the four English men of letters whose writings most fully embody the +spirit of the eighteenth century, the one who provides the biographer with +the scantiest materials is Addison. In his _Journal to Stella_, his social +verses, and his letters to his friends, we have a vivid picture of those +relations with women and that protracted suffering which invest with such +tragic interest the history of Swift. Pope, by the publication of his own +correspondence, has enabled us, in a way that he never intended, to +understand the strange moral twist which distorted a nature by no means +devoid of noble instincts. Johnson was fortunate in the companionship of +perhaps the best biographer who ever lived. But of the real life and +character of Addison scarcely any contemporary record remains. The formal +narrative prefixed to his works by Tickell is, by that writer's own +admission, little more than a bibliography. Steele, who might have told us +more than any man about his boyhood and his manner of life in London, had +become estranged from his old friend before his death. No writer has +taken the trouble to preserve any account of the wit and wisdom that +enlivened the "little senate" at Button's. His own letters are, as a rule, +compositions as finished as his papers in the _Spectator_. Those features +in his character which excite the greatest interest have been delineated +by the hand of an enemy--an enemy who possessed an unrivalled power of +satirical portrait-painting, and was restrained by no regard for truth +from creating in the public mind such impressions about others as might +serve to heighten the favourable opinion of himself. + +This absence of dramatic incident in Addison's life would lead us +naturally to conclude that he was deficient in the energy and passion +which cause a powerful nature to leave a mark upon its age. Yet such a +judgment would certainly be erroneous. Shy and reserved as he was, the +unanimous verdict of his most illustrious contemporaries is decisive as to +the respect and admiration which he excited among them. The man who could +exert so potent an influence over the mercurial Steele, who could +fascinate the haughty and cynical intellect of Swift, whose conversation, +by the admission of his satirist Pope, had in it something more charming +than that of any other man; of whom it was said that he might have been +chosen king if he wished it; such a man, though to the coarse perception +of Mandeville he might have seemed no more than "a parson in a tye-wig," +can hardly have been deficient in force of character. + +Nor would it have been possible for a writer distinguished by mere +elegance and refinement to leave a lasting impress on the literature and +society of his country. In one generation after another, men representing +opposing elements of rank, class, interest, and taste, have agreed in +acknowledging Addison's extraordinary merits. "Whoever wishes," says +Johnson--at the end of a biography strongly coloured with the +prepossessions of a semi-Jacobite Tory--"whoever wishes to attain an +English style, familiar but not coarse, and elegant but not ostentatious, +must give his days and nights to the volumes of Addison." "Such a mark of +national respect," says Macaulay, the best representative of middle-class +opinion in the present century, speaking of the statue erected to Addison +in Westminster Abbey, "was due to the unsullied statesman, to the +accomplished scholar, to the master of pure English eloquence, to the +consummate painter of life and manners. It was due, above all, to the +great satirist who alone knew how to use ridicule without abusing it; who, +without inflicting a wound, effected a great social reform, and who +reconciled wit and virtue after a long and disastrous separation, during +which wit had been led astray by profligacy, and virtue by fanaticism." + +This verdict of a great critic is accepted by an age to which the grounds +of it are, perhaps, not very apparent. The author of any ideal creation--a +poem, a drama, or a novel--has an imprescriptible property in the fame of +his work. But to harmonise conflicting social elements, to bring order out +of chaos in the sphere of criticism, to form right ways of thinking about +questions of morals, taste, and breeding, are operations of which the +credit, though it is certainly to be ascribed to particular individuals, +is generally absorbed by society itself. Macaulay's eulogy is as just as +it is eloquent, but the pages of the _Spectator_ alone will hardly show +the reader why Addison should be so highly praised for having reconciled +wit with virtue. Nor, looking at him as a critic, will it appear a great +achievement to have pointed out to English society the beauties of +_Paradise Lost_, unless it be remembered that the taste of the preceding +generation still influenced Addison's contemporaries, and that in that +generation Cowley was accounted a greater poet than Milton. + +To estimate Addison at his real value we must regard him as the chief +architect of Public Opinion in the eighteenth century. But here again we +are met by an initial difficulty, because it has become almost a +commonplace of contemporary criticism to represent the eighteenth century +as a period of sheer destruction. It is tacitly assumed by a school of +distinguished philosophical writers that we have arrived at a stage in the +world's history in which it is possible to take a positive and scientific +view of human affairs. As it is of course necessary that from such a +system all belief in the supernatural shall be jealously excluded, it has +not seemed impossible to write the history of Thought itself in the +eighteenth century. And in tracing the course of this supposed continuous +stream it is natural that all the great English writers of the period +should be described as in one way or another helping to pull down, or +vainly to strengthen, the theological barriers erected by centuries of +bigotry against the irresistible tide of enlightened progress. + +It would be of course entirely out of place to discuss here the merits of +this new school of history. Those who consider that, whatever glimpses we +may obtain of the law and order of the universe, man is, as he always has +been and always will be, a mystery to himself, will hardly allow that the +operations of the human spirit can be traced in the dissecting-room. But +it is, in any case, obvious that to treat the great _imaginative_ writers +of any age as if they were only mechanical agents in an evolution of +thought is to do them grave injustice. Such writers are, above all things, +creative. Their first aim is to "show the very age and body of the time +his form and pressure." No work of the eighteenth century, composed in a +consciously destructive spirit, has taken its place among the acknowledged +classics of the language. Even the _Tale of a Tub_ is to be regarded as a +satire upon the aberrations of theologians from right reason, not upon the +principles of Christianity itself. The _Essay on Man_ has, no doubt, +logically a tendency towards Deism, but nobody ever read the poem for the +sake of its philosophy; and it is well known that Pope was much alarmed +when it was pointed out to him that his conclusions might be represented +as incompatible with the doctrines of revealed religion. + +The truth indeed seems to be the exact converse of what is alleged by the +scientific historians. So far from the eighteenth century in England being +an age of destructive analysis, its energies were chiefly devoted to +political, social, and literary reconstruction. Whatever revolution in +faith and manners the English nation had undergone had been the work of +the two preceding centuries, and though the historic foundations of +society remained untouched, the whole form of the superstructure had been +profoundly modified. + + "So tenacious are we," said Burke, towards the close of the last + century, "of our old ecclesiastical modes and fashions of institution + that very little change has been made in them since the fourteenth or + fifteenth centuries, adhering in this particular as in all else to our + old settled maxim never entirely nor at once to depart from antiquity. + We found these institutions on the whole favourable to morality and + discipline, and we thought they were susceptible of amendment without + altering the ground. We thought they were capable of receiving and + meliorating, and, above all, of preserving the accessories of science + and literature as the order of Providence should successively produce + them. And after all, with this Gothic and monkish education (for such + it is the groundwork), we may put in our claim to as ample and early + a share in all the improvements in science, in arts, and in literature + which have illuminated the modern world as any other nation in Europe. + We think one main cause of this improvement was our not despising the + patrimony of knowledge which was left us by our forefathers." + +All this is, in substance, true of our political as well as our +ecclesiastical institutions. And yet, when Burke wrote, the great feudal +and mediæval structure of England had been so transformed by the Wars of +the Roses, the Reformation, the Rebellion, and the Revolution, that its +ancient outlines were barely visible. In so far, therefore, as his words +seem to imply that the social evolution he describes was produced by an +imperceptible and almost mechanical process of national instinct, the +impression they tend to create is entirely erroneous. + +If we have been hitherto saved from such corruption as undermined the +republics of Italy, from the religious wars that so long enfeebled and +divided Germany, and from the Revolution that has severed modern France +from her ancient history, thanks for this are due partly, no doubt, to +favouring conditions of nature and society, but quite as much to the +genius of great individuals who prepared the mind of the nation for the +gradual assimilation of new ideas. Thus Langland and Wycliffe and their +numerous followers, long before the Reformation, had so familiarised the +minds of the people with their ideas of the Christian religion that the +Sovereign was able to assume the Headship of the Church without the shock +of a social convulsion. Fresh feelings and instincts grew up in the hearts +of whole classes of the nation without at first producing any change in +outward habits of life, and even without arousing a sense of their logical +incongruity. These mixed ideas were constantly brought before the +imagination in the works of the poets. Shakespeare abounds with passages +in which, side by side with the old feudal, monarchical, catholic, and +patriotic instincts of Englishmen, we find the sentiments of the Italian +Renaissance. Spenser conveys Puritan doctrines sometimes by the mouth of +shepherds, whose originals he had found in Theocritus and Virgil; +sometimes under allegorical forms derived from books of chivalry and the +ceremonial of the Catholic Church. Milton, the most rigidly Calvinistic of +all the English poets in his opinions, is also the most severely classical +in his style. + +It was the task of Addison to carry on the reconciling traditions of our +literature. It is his praise to have accomplished his task under +conditions far more difficult than any that his predecessors had +experienced. What they had done was to give instinctive and characteristic +expression to the floating ideas of the society about them; what Addison +and his contemporaries did was to found a public opinion by a conscious +effort of reason and persuasion. Before the Civil Wars there had been at +least no visible breach in the principle of Authority in Church and State. +At the beginning of the eighteenth century constituted authority had been +recently overthrown; one king had been beheaded, another had been +expelled; the Episcopalian form of Church Government had been violently +displaced in favour of the Presbyterian, and had been with almost equal +violence restored. Whole classes of the population had been drawn into +opposing camps during the Civil War, and still stood confronting each +other with all the harsh antagonism of sentiment inherited from that +conflict. Such a bare summary alone is sufficient to indicate the nature +of the difficulties Addison had to encounter in his efforts to harmonise +public opinion; but a more detailed examination of the state of society +after the Restoration is required to place in its full light the +extraordinary merits of the success that he achieved. + +There was, to begin with, a vehement opposition between town and country. +In the country the old ideas of Feudalism, modified by circumstances, but +vigorous and deep-rooted, still prevailed. True, the military system of +land-tenure had disappeared with the Restoration, but it was not so with +the relations of life, and the habits of thought and feeling which the +system had created. The features of surviving Feudalism have been +inimitably preserved for us in the character of Sir Roger de Coverley. +Living in the patriarchal fashion, in the midst of tenants and retainers, +who looked up to him as their chief, and for whose welfare and protection +he considered himself responsible, the country gentleman valued above all +things the principle of Loyalty. To the moneyed classes in the towns he +was instinctively opposed; he regarded their interests, both social and +commercial, as contrary to his own; he looked with dislike and suspicion +on the economical principles of government and conduct on which these +classes naturally rely. Even the younger sons of county families had in +Addison's day abandoned the custom, common enough in the feudal times, of +seeking their fortune in trade. Many a Will Wimble now spent his whole +life in the country, training dogs for his neighbours, fishing their +streams, making whips for their young heirs, and even garters for their +wives and daughters.[1] + + + diff --git a/run_eval.py b/run_eval.py index 5e3f390..a6dc5bf 100644 --- a/run_eval.py +++ b/run_eval.py @@ -149,6 +149,11 @@ if __name__ == "__main__": type=float, default=1.0, ) + parser.add_argument( + "--flip_ctx_inp", + action="store_true", + help="Flip the order of context and input", + ) cli_args = vars(parser.parse_args()) # setup_logging(output_dir, debug=os.getenv("DEBUG", False)) diff --git a/src/ctx_to_lora/data/definitions.py b/src/ctx_to_lora/data/definitions.py index 92f8f99..fa20dd5 100644 --- a/src/ctx_to_lora/data/definitions.py +++ b/src/ctx_to_lora/data/definitions.py @@ -202,6 +202,18 @@ DS_KWARGS = { split="train[180:]", ), ), + "squad_negative": dict( + test=dict(path="data/raw_datasets/squad", split="validation"), + ), + "squad_assistant_ctx": dict( + test=dict(path="data/raw_datasets/squad", split="validation"), + ), + "squad_negative_no_passage": dict( + test=dict(path="data/raw_datasets/squad", split="validation"), + ), + "squad_assistant_ctx_no_passage": dict( + test=dict(path="data/raw_datasets/squad", split="validation"), + ), "fw_qa_xl": dict( train=dict( path="parquet", @@ -569,6 +581,30 @@ DS_KWARGS = { # split="train", # ), # ), + "gsm8k_assistant_ctx": dict( + train=dict( + path="openai/gsm8k", + name="main", + split="train[100:]", + ), + validation=dict( + path="openai/gsm8k", + name="main", + split="train[:100]", + ), + test=dict( + path="openai/gsm8k", + name="main", + split="test", + ), + ), + "gsm8k_negative": dict( + test=dict( + path="openai/gsm8k", + name="main", + split="test", + ), + ), "gsm8k": dict( train=dict( path="openai/gsm8k", @@ -603,6 +639,16 @@ DS_KWARGS = { split="test", ), ), + "metaicl": dict( + validation=dict( + path="SakanaAI/metaicl", + split="train[:100]", + ), + test=dict( + path="SakanaAI/metaicl", + split="train", + ), + ), "openmathintx-2": dict( train=dict( path="nvidia/OpenMathInstruct-2", @@ -766,6 +812,10 @@ CLOSED_QA_DATASETS = { "longbench/musique", "hotpot_qa", "squad", + "squad_negative", + "squad_assistant_ctx", + "squad_negative_no_passage", + "squad_assistant_ctx_no_passage", "triviaqa_retrieved", "negative_nq", "ropes", @@ -780,6 +830,10 @@ MULTI_ANSWER_DATASETS = { "longbench/2wikimqa", "longbench/musique", "squad", + "squad_negative", + "squad_assistant_ctx", + "squad_negative_no_passage", + "squad_assistant_ctx_no_passage", "drop", } @@ -793,7 +847,8 @@ for ds_name in list(MULTI_ANSWER_DATASETS): if ds_name.startswith("longbench/"): MULTI_ANSWER_DATASETS.add(f"{ds_name}_e") -GSM8K_DATASETS = {"gsm8k", "gsm8k_fewshot"} +GSM8K_DATASETS = {"gsm8k", "gsm8k_fewshot", "gsm8k_assistant_ctx", "gsm8k_negative"} +MULTI_CHOICE_DATASETS = {"metaicl"} # for training closed qa datasets, e.g., hotpot_qa, squad, etc. CLOSED_QA_INTX_TEMPLATES = [ @@ -829,6 +884,10 @@ EVAL_INTX_TEMPLATES = { "drop": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", # short-ctx extractive qa "squad": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", + "squad_negative": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", + "squad_assistant_ctx": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", + "squad_negative_no_passage": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", + "squad_assistant_ctx_no_passage": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", # retrieved noisy ctx? "triviaqa_retrieved": "Answer the following question. Output only the answer and do not output any other words.\n\nQuestion: {input}", # doc w/ distractors qa diff --git a/src/ctx_to_lora/data/preprocessing_fn.py b/src/ctx_to_lora/data/preprocessing_fn.py index f8973fe..7f816c5 100644 --- a/src/ctx_to_lora/data/preprocessing_fn.py +++ b/src/ctx_to_lora/data/preprocessing_fn.py @@ -148,6 +148,46 @@ def get_preprocessing_fn( "response": sample["answers"]["text"][0], } + elif ds_name == "squad_assistant_ctx": + + def f(sample): + return { + "context": "You are a useful AI assistant.", + "prompt": sample["context"] + "\n\n" + sample["question"], + "response": sample["answers"]["text"][0], + } + + elif ds_name == "squad_negative": + with open("data/gutenburg_sample.txt") as f: + gutenburg_sample = f.read() + + def f(sample): + return { + "context": gutenburg_sample, + "prompt": sample["context"] + "\n\n" + sample["question"], + "response": sample["answers"]["text"][0], + } + + elif ds_name == "squad_negative_no_passage": + with open("data/gutenburg_sample.txt") as f: + gutenburg_sample = f.read() + + def f(sample): + return { + "context": gutenburg_sample, + "prompt": sample["question"], + "response": sample["answers"]["text"][0], + } + + elif ds_name == "squad_assistant_ctx_no_passage": + + def f(sample): + return { + "context": "You are a useful AI assistant.", + "prompt": sample["question"], + "response": sample["answers"]["text"][0], + } + elif ds_name == "drop": def f(sample): @@ -264,6 +304,30 @@ def get_preprocessing_fn( # "prompt": sample["question"] + "\n" + instruction_prompt, # "response": sample["answer"], # } + elif ds_name == "gsm8k_negative": + with open("data/gutenburg_sample.txt") as f: + gutenburg_sample = f.read() + + def f(sample): + return { + "prompt": sample["question"], + "context": gutenburg_sample, + "response": sample["answer"], + } + + elif ds_name == "gsm8k_assistant_ctx": + + def f(sample): + return { + "prompt": sample["question"], + "context": "You are a useful AI assistant.", + "response": sample["answer"], + } + # return { + # "context": sample["question"], + # "prompt": sample["question"] + "\n" + instruction_prompt, + # "response": sample["answer"], + # } elif ds_name == "gsm8k_fewshot": N_FEW_SHOT_EXAMPLES = 5 @@ -306,6 +370,19 @@ def get_preprocessing_fn( "context": few_shot_examples_txt, "response": sample["answer"], } + elif ds_name == "metaicl": + + def f(sample): + prompt = f"You are a classifier. Reply must be of the following format: 'Answer: $LETTER' (without quotes) where LETTER is one of {sample['options']['options']}.\nInput:\n{sample['input']}" + context = "Examples\n" + for example in sample["examples"]: + context += f"Input: {example['input']}\nAnswer: {example['output']}\n\n" + + return { + "context": context, + "prompt": prompt, + "response": sample["output"], + } elif "opencoder-edu" in ds_name: diff --git a/src/ctx_to_lora/data/processing.py b/src/ctx_to_lora/data/processing.py index aa8e404..d94ac9b 100644 --- a/src/ctx_to_lora/data/processing.py +++ b/src/ctx_to_lora/data/processing.py @@ -77,7 +77,7 @@ def load_answers(ds_name, split): def extract_ans(sample): return {"answers": sample["answers"]} - elif ds_name == "squad": + elif "squad" in ds_name: def extract_ans(sample): return {"answers": sample["answers"]["text"]} @@ -102,35 +102,36 @@ def get_ds_kwargs(ds_name: str, split: str) -> dict[str, Any]: skip = slice.split(":")[0] take = slice.split(":")[1] - if ds_name.startswith("self_gen/"): - if ds_name.endswith(".parquet"): - # ds_name is a glob pattern - files = glob(f"{RAW_DATA_DIR}/{ds_name}") - if not files: - raise FileNotFoundError( - f"The provided pattern does not match any files: {RAW_DATA_DIR}/{ds_name}" - ) - else: - # e.g., "self_gen/google/gemma-2-2b-it/pwc" - base_model_name = "/".join(ds_name.split("/")[1:3]) - base_ds = "/".join(ds_name.split("/")[3:]) - if ("[" in split) and split.endswith("]"): - kwargs["split"], slice = split.split("[") - slice = slice.strip("]") - skip = slice.split(":")[0] - if skip: - kwargs["skip"] = int(skip) - take = slice.split(":")[1] - if take: - kwargs["take"] = int(take) - files = glob( - f"{SELF_GEN_DATA_DIR}/{base_model_name}/{base_ds}/{split}/*.parquet" + if ds_name.endswith(".parquet"): + # ds_name is a glob pattern + files = glob(f"{RAW_DATA_DIR}/{ds_name}") + if not files: + raise FileNotFoundError( + f"The provided pattern does not match any files: {RAW_DATA_DIR}/{ds_name}" + ) + kwargs = dict(path="parquet", data_files=files, split="train") + + elif ds_name.startswith("self_gen/"): + # e.g., "self_gen/google/gemma-2-2b-it/pwc" + base_model_name = "/".join(ds_name.split("/")[1:3]) + base_ds = "/".join(ds_name.split("/")[3:]) + if ("[" in split) and split.endswith("]"): + kwargs["split"], slice = split.split("[") + slice = slice.strip("]") + skip = slice.split(":")[0] + if skip: + kwargs["skip"] = int(skip) + take = slice.split(":")[1] + if take: + kwargs["take"] = int(take) + files = glob( + f"{SELF_GEN_DATA_DIR}/{base_model_name}/{base_ds}/{split}/*.parquet" + ) + if not files: + raise FileNotFoundError( + f"No self-gen files found for base model {base_model_name} " + f"in {SELF_GEN_DATA_DIR}/{base_model_name}/{base_ds}/" ) - if not files: - raise FileNotFoundError( - f"No self-gen files found for base model {base_model_name} " - f"in {SELF_GEN_DATA_DIR}/{base_model_name}/{base_ds}/" - ) kwargs = dict(path="parquet", data_files=files, split="train") elif (ds_name not in DS_KWARGS) or (split not in DS_KWARGS[ds_name]): kwargs = dict(path=ds_name, split=split) @@ -220,6 +221,7 @@ def get_tokenized_dataset( set_format: str | None = None, truncate_if_too_long_inp: bool = False, truncate_if_too_long_ctx: bool = False, + flip_ctx_inp: bool = False, ) -> dict[str, Any]: if max_qas_len > 0: assert max_qas_len <= base_model_max_len, ( @@ -282,6 +284,23 @@ def get_tokenized_dataset( "is not present in the dataset." ) logger.info(f"Constructing and tokenizing {ds_name} with {split} split...") + if flip_ctx_inp: + + def squeeze(sample, column): + first_id = sample[column][0] + if check_is_iterable(first_id): + sample[column] = first_id + return sample + + def unsqueeze(sample, column): + sample[column] = [sample[column]] + return sample + + ds = ds.rename_column("context", "context_temp") + ds = ds.rename_column("prompts", "context") + ds = ds.rename_column("context_temp", "prompts") + ds = ds.map(squeeze, fn_kwargs={"column": "context"}) + ds = ds.map(unsqueeze, fn_kwargs={"column": "prompts"}) tokenized_ds = construct_and_tokenize_ctx_qa( ds=ds, @@ -304,6 +323,7 @@ def get_tokenized_dataset( tokenized_ds = tokenized_ds.remove_columns( ["logprobs_vals", "logprobs_indices"] ) + return tokenized_ds diff --git a/src/ctx_to_lora/eval_utils.py b/src/ctx_to_lora/eval_utils.py index 00e64b6..307ab8b 100644 --- a/src/ctx_to_lora/eval_utils.py +++ b/src/ctx_to_lora/eval_utils.py @@ -14,7 +14,7 @@ import numpy as np import pandas as pd import torch import yaml -from datasets import disable_caching +from datasets import disable_caching, load_dataset from peft import get_peft_model from transformers import ( PreTrainedModel, @@ -33,13 +33,19 @@ from ctx_to_lora.data.definitions import ( LONGBENCH_E_TASKS, LONGBENCH_TASKS, MULTI_ANSWER_DATASETS, + MULTI_CHOICE_DATASETS, +) +from ctx_to_lora.data.processing import ( + get_ds_kwargs, + get_tokenized_dataset, + load_answers, ) -from ctx_to_lora.data.processing import get_tokenized_dataset, load_answers from ctx_to_lora.data.self_gen_template import SELF_QA_INTX from ctx_to_lora.metrics import ( LENGTH_BINS, Evaluator, compute_gsm8k_acc, + compute_macro_f1_score, compute_metrics, compute_per_token_acc, compute_perplexity, @@ -195,16 +201,20 @@ def split_string(s: str) -> list[str]: return [x for x in out if x] # remove empty spaces -def f1_score(prediction: str, ground_truth: str) -> float: - """Compute F1 score between prediction and ground truth strings.""" +def f1_score(prediction: str, ground_truth: str) -> tuple[float, float, float]: + """Compute F1 score, precision, and recall between prediction and ground truth strings.""" common = Counter(prediction) & Counter(ground_truth) num_same = sum(common.values()) if num_same == 0: - return 0 - precision = 1.0 * num_same / len(prediction) - recall = 1.0 * num_same / len(ground_truth) - f1 = (2 * precision * recall) / (precision + recall) - return f1 + return 0, 0, 0 + precision = 1.0 * num_same / len(prediction) if len(prediction) > 0 else 0 + recall = 1.0 * num_same / len(ground_truth) if len(ground_truth) > 0 else 0 + f1 = ( + (2 * precision * recall) / (precision + recall) + if (precision + recall) > 0 + else 0 + ) + return f1, precision, recall def compute_qa_f1_score( @@ -214,17 +224,35 @@ def compute_qa_f1_score( Word-level F1 score for evaluating question answering systems. Order of the words does not matter. """ - res = [] + f1_scores = [] + precisions = [] + recalls = [] + for prediction, answers in zip(pred_texts, answers_list): normalized_prediction = normalize_answer(prediction) prediction_words = split_string(normalized_prediction) - score = 0 + best_f1 = 0 + best_precision = 0 + best_recall = 0 + for answer in answers: normalized_label = normalize_answer(answer) label_words = split_string(normalized_label) - score = max(score, f1_score(prediction_words, label_words)) - res.append(score) - return dict(qa_f1_score=np.mean(res)), dict(qa_f1_score=res) + f1, precision, recall = f1_score(prediction_words, label_words) + if f1 > best_f1: + best_f1 = f1 + best_precision = precision + best_recall = recall + + f1_scores.append(best_f1) + precisions.append(best_precision) + recalls.append(best_recall) + + return dict( + qa_f1_score=np.mean(f1_scores), + qa_precision=np.mean(precisions), + qa_recall=np.mean(recalls), + ), dict(qa_f1_score=f1_scores, qa_precision=precisions, qa_recall=recalls) def add_longbench_tasks(ds_names: list[str]) -> None: @@ -251,11 +279,12 @@ def save_generated_text( os.makedirs(split_dir, exist_ok=True) metric_keys = list(per_sample_metric.keys()) - assert len(metric_keys) == 1 + # assert len(metric_keys) == 1 metric_name = metric_keys[0] with open(f"{output_dir}/{split}_generated_text.jsonl", "w") as f: for sample, metric_val in zip(samples, per_sample_metric[metric_name]): - sample[f"{metric_name}"] = metric_val + for metric_name in metric_keys: + sample[f"{metric_name}"] = metric_val f.write(json.dumps(sample) + "\n") @@ -576,6 +605,7 @@ def eval_generation( tokenizer, ctx_tokenizer, datasets, + original_datasets, answers, split, remove_context, @@ -623,6 +653,18 @@ def eval_generation( ) for k, v in gsm8k_acc_metric.items(): eval_result.metrics[f"{split_name}_{k}"] = v + + elif ds_name in MULTI_CHOICE_DATASETS: + print("Computing Multi-Choice Accuracy") + original_ds = original_datasets[ds_name] + multi_choice_acc_metric, per_sample_metric = compute_macro_f1_score( + pred_texts, + label_texts, + [x["options"] for x in original_ds["options"]], + original_ds["task"], + ) + for k, v in multi_choice_acc_metric.items(): + eval_result.metrics[f"{split_name}_{k}"] = v else: rouge_metrics, per_sample_metric = compute_rouge(pred_texts, label_texts) for k, v in rouge_metrics.items(): @@ -912,9 +954,11 @@ def evaluate( add_self_distill_template=use_cd, # only for eval truncate_if_too_long_inp=args.truncate_if_too_long_inp, # only for eval truncate_if_too_long_ctx=args.truncate_if_too_long_ctx, # only for eval + flip_ctx_inp=args.flip_ctx_inp, # only for eval ) datasets = dict() + original_datasets = dict() answers = dict() ds_names = args.val_ds_names if split == "validation" else args.test_ds_names add_longbench_tasks(ds_names) @@ -923,6 +967,11 @@ def evaluate( # handling cases where there are multiple answers if ds_name in MULTI_ANSWER_DATASETS: answers[ds_name] = load_answers(ds_name, split) + if ds_name in MULTI_CHOICE_DATASETS: + ds_kwargs = get_ds_kwargs(ds_name, split) + original_datasets[ds_name] = load_dataset( + **ds_kwargs, trust_remote_code=True + ) print(f"Datasets: {datasets}") print(f"Answers: {answers}") @@ -935,6 +984,10 @@ def evaluate( datasets[ds_name] = ds.select(val_indices) if ds_name in answers: answers[ds_name] = answers[ds_name].select(val_indices) + if ds_name in original_datasets: + original_datasets[ds_name] = original_datasets[ds_name].select( + val_indices + ) max_test_samples_per_ds = getattr(args, "max_test_samples_per_ds", 0) if split == "test" and max_test_samples_per_ds > 0: @@ -944,6 +997,10 @@ def evaluate( datasets[ds_name] = ds.select(test_indices) if ds_name in answers: answers[ds_name] = answers[ds_name].select(test_indices) + if ds_name in original_datasets: + original_datasets[ds_name] = original_datasets[ds_name].select( + test_indices + ) print(f"Datasets: {datasets}") print(f"Answers: {answers}") @@ -1032,6 +1089,7 @@ def evaluate( tokenizer, ctx_tokenizer, {ds_name: ds}, + original_datasets, answers, split, args.remove_context, @@ -1074,6 +1132,7 @@ def run_eval( add_ctx_to_input: bool = False, truncate_if_too_long_inp: bool = False, truncate_if_too_long_ctx: bool = False, + flip_ctx_inp: bool = False, gen_lora_scaling: float = 1, ) -> None: """Run evaluation with the specified parameters.""" @@ -1155,6 +1214,7 @@ def run_eval( args.gen_lora_scaling = gen_lora_scaling args.truncate_if_too_long_inp = truncate_if_too_long_inp args.truncate_if_too_long_ctx = truncate_if_too_long_ctx + args.flip_ctx_inp = flip_ctx_inp setup_logging(args.logging_dir) logger.debug(f"CMD: {' '.join(os.sys.argv)}") diff --git a/src/ctx_to_lora/metrics.py b/src/ctx_to_lora/metrics.py index 00c4d3d..6961ea6 100644 --- a/src/ctx_to_lora/metrics.py +++ b/src/ctx_to_lora/metrics.py @@ -35,10 +35,6 @@ def get_length_bin(length: int): def compute_gsm8k_acc(pred_texts, label_texts): out = defaultdict(list) for pred_text, label_text in zip(pred_texts, label_texts): - if "Answer" not in pred_text: - out["gsm8k_acc"].append(0) - continue - label_ans = float(label_text.split("####")[-1].strip().replace(",", "")) # find all the numbers (including decimals) in the string numbers = re.findall(r"\d+\.?\d*", pred_text) @@ -60,6 +56,225 @@ def compute_gsm8k_acc(pred_texts, label_texts): return out_mean, out +def compute_macro_f1_score( + pred_texts: list[str], + label_texts: list[str], + choices: list[list[str]], # per-sample choices + tasks: list[str], # per-sample task name +): + """ + Compute macro-averaged precision/recall/F1 for multi-class classification. + - Parses labels/preds using: + * Prefer token immediately following case-insensitive "Answer:" / "Answer:" + * Otherwise exact token match of any provided choice (case-insensitive, token-boundary) + - Invalid predictions count as errors for accuracy and as FN for the true class. + - Macro metrics exclude zero-support classes *within each task*. + - Final macro scores are the simple average across tasks (each task has equal weight). + Returns: + out_mean: dict with task-averaged macro metrics + per-task metrics like macro_f1_ + out_details: dict with: + - "accuracy": per-sample correctness (0/1) + - "per_task": raw per-task metric dicts + """ + import re + from collections import defaultdict + + import numpy as np + + assert len(pred_texts) == len(label_texts) == len(choices) == len(tasks), ( + "All input lists must have the same length." + ) + + # --- helpers --- + def _norm_label(text: str) -> str: + m = re.search(r"answer\s*[::]\s*([^\n\r]+)", text, flags=re.I) + cand = (m.group(1) if m else text).strip().rstrip(".") + return cand + + def _extract_choice(text: str, per_sample_choices: list[str]): + # 1) token after "Answer:" + m = re.search(r"answer\s*[::]\s*([A-Za-z0-9\-\._]+)", text, flags=re.I) + if m: + raw = m.group(1).strip().rstrip(".") + for ch in per_sample_choices: + if raw.lower() == ch.lower(): + return ch + # 2) exact token match (case-insensitive) with boundaries + for ch in per_sample_choices: + pattern = rf"(? int: + if val is None: + return -1 + for i, ch in enumerate(choice_list): + if val.lower() == ch.lower(): + return i + return -1 + + def _slugify(name: str) -> str: + # lowercase, replace non-alnum with underscores, collapse repeats, trim edges + s = re.sub(r"[^A-Za-z0-9]+", "_", name.strip().lower()) + s = re.sub(r"_+", "_", s).strip("_") + return s or "task" + + # Normalize ground-truth labels + norm_labels = [_norm_label(lbl) for lbl in label_texts] + + # Group indices by task + by_task = defaultdict(list) + for i, tname in enumerate(tasks): + by_task[tname].append(i) + + per_sample_correct = [0] * len(pred_texts) + task_metrics = {} # name -> dict + + # Compute per-task metrics + for tname, idxs in by_task.items(): + # Union of classes across samples in this task + class_set = [] + seen = set() + for i in idxs: + for ch in choices[i]: + key = ch.lower() + if key not in seen: + seen.add(key) + class_set.append(ch) + + y_true_idx, y_pred_idx = [], [] + for i in idxs: + t_idx = _choice_to_idx(norm_labels[i], class_set) + pred_choice = _extract_choice(pred_texts[i], choices[i]) + p_idx = _choice_to_idx(pred_choice, class_set) + if p_idx == -1: + # debug logging if desired + print( + f"Extracted {pred_choice} from {pred_texts[i]} which is not in {class_set}" + ) + + if t_idx == -1: + print(f"True label {norm_labels[i]} not in class set {class_set}") + + y_true_idx.append(t_idx) + y_pred_idx.append(p_idx) + + per_sample_correct[i] = int(t_idx != -1 and p_idx == t_idx) + + # Per-class stats + n_classes = len(class_set) + f1s, precs, recs, supports = [], [], [], [] + for ci in range(n_classes): + tp = sum(1 for t, p in zip(y_true_idx, y_pred_idx) if t == ci and p == ci) + fp = sum( + 1 + for t, p in zip(y_true_idx, y_pred_idx) + if p == ci and t != ci and p != -1 + ) + fn = sum(1 for t, p in zip(y_true_idx, y_pred_idx) if t == ci and p != ci) + support = sum(1 for t in y_true_idx if t == ci) + supports.append(support) + + precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0 + recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0 + f1 = ( + (2 * precision * recall / (precision + recall)) + if (precision + recall) > 0 + else 0.0 + ) + + precs.append(precision) + recs.append(recall) + f1s.append(f1) + + present = [i for i, s in enumerate(supports) if s > 0] + if present: + task_macro_f1 = float(np.mean([f1s[i] for i in present])) + task_macro_prec = float(np.mean([precs[i] for i in present])) + task_macro_rec = float(np.mean([recs[i] for i in present])) + else: + task_macro_f1 = task_macro_prec = task_macro_rec = 0.0 + + task_acc = ( + float( + np.mean( + [ + 1 if (t != -1 and p == t) else 0 + for t, p in zip(y_true_idx, y_pred_idx) + ] + ) + ) + if idxs + else 0.0 + ) + + # --- valid sample ratios (both label and prediction valid) --- + valid_both = [ + 1 if (t != -1 and p != -1) else 0 for t, p in zip(y_true_idx, y_pred_idx) + ] + task_valid_sample_ratio = float(np.mean(valid_both)) if idxs else 0.0 + + task_metrics[tname] = { + "macro_f1": task_macro_f1, + "macro_precision": task_macro_prec, + "macro_recall": task_macro_rec, + "accuracy": task_acc, + "valid_sample_ratio": task_valid_sample_ratio, + "n_samples": len(idxs), + "n_classes_present": len(present), + } + + # Aggregate across tasks (equal weight per task) + task_names = list(task_metrics.keys()) + if task_names: + macro_f1_over_tasks = float( + np.mean([task_metrics[t]["macro_f1"] for t in task_names]) + ) + macro_prec_over_tasks = float( + np.mean([task_metrics[t]["macro_precision"] for t in task_names]) + ) + macro_rec_over_tasks = float( + np.mean([task_metrics[t]["macro_recall"] for t in task_names]) + ) + acc_over_tasks = float( + np.mean([task_metrics[t]["accuracy"] for t in task_names]) + ) + valid_ratio_task_avg = float( + np.mean([task_metrics[t]["valid_sample_ratio"] for t in task_names]) + ) + else: + macro_f1_over_tasks = macro_prec_over_tasks = macro_rec_over_tasks = ( + acc_over_tasks + ) = valid_ratio_task_avg = 0.0 + + out_mean = { + # Task-averaged (equal task weights) + "macro_f1": macro_f1_over_tasks, + "macro_precision": macro_prec_over_tasks, + "macro_recall": macro_rec_over_tasks, + "accuracy_task_avg": acc_over_tasks, + "valid_sample_ratio_task_avg": valid_ratio_task_avg, + } + + # --- add per-task metrics to out_mean with slugified keys --- + for tname, metrics in task_metrics.items(): + slug = _slugify(tname) + out_mean[f"macro_f1_{slug}"] = metrics["macro_f1"] + # out_mean[f"macro_precision_{slug}"] = metrics["macro_precision"] + # out_mean[f"macro_recall_{slug}"] = metrics["macro_recall"] + # out_mean[f"accuracy_{slug}"] = metrics["accuracy"] + out_mean[f"valid_sample_ratio_{slug}"] = metrics["valid_sample_ratio"] + # out_mean[f"n_samples_{slug}"] = metrics["n_samples"] + # out_mean[f"n_classes_present_{slug}"] = metrics["n_classes_present"] + + out_details = { + "accuracy": per_sample_correct, # per-sample 0/1 + } + + return out_mean, out_details + + def compute_rouge(pred_texts, label_texts): out = defaultdict(list) scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=True) diff --git a/token_length_distributions.pdf b/token_length_distributions.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a591e67738b40879a7d04f2ae8ed1b383f9838cb GIT binary patch literal 22208 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

3`J zIK4#n*QQVE%buKAA^-tHAs_)^AaD#30p5r~pnz951$qa%P_saJIAD%75d?$L&;t1P zcMg5J3Ba5taM}uRpb7fH1CC-*Z~@LIL7&C)&lxa(6_E(ZJJ{OV04J%Sk4`ZLq4x-R zx>;BOr@8>+b3-4o;$&ls`kD1`u(L-2r>FowoxA{HSh+eoTY!Xtcerjg9uBV7AYmZl z=x5{MN+RUy0t^!6;|ly|?_mSvZGq!ifd3u5fxIWczmTVmw~Y%($OhAo`4?y$R1=otRU z9pLOA2P+vDJ0}}}aUmU)jk6v)qh{fY84)lbVd%e)-}wua8G{gD@c$o)Q zFn|`F&~5yc;4lbaCujkP3Ihpf2q_8@0W1>%JTY*fa}jicLqSj^oCHXK4I@BsIM4|2 z2#WxZg@`D?02s)luju>07g7k|Y$Fk%KDtdL;1Lm`CuQg>W-u@j67&_#1bR^Dga8Z< zT^9}v{G<(l7U&m~2SNZ)5m8Kniy$#qB!DjvKsf>e1qOK1uQ1Rr9GG8VNOV165FEIQ z0>VOHF$s#MLtun}zM}ibC=D$@fCoUJBstFj_=tw$VdF zUx9?t2fDkHL<0Y*v%i}<$p8$VGyw%h3{-=oxdJ#xS4W${U+Vhn3aAW-f}cnU(}J)F zKm>fch-M#c2Yp66VPp;1 z1==S5l`!^zwu!$I##VmX1lk_{ozQ$?egkcwZR4+m9ytOG2XqMVinakP2`LKb1Z@|9 zYx{Tl%O?K1qJ;wJodk15=l_*3x<0v&rujRe?Eb=sRmm4w>JP3hH2$L9s!UnrqBv7@}D`3>j6fLVdKQJ z0prC`Sp)WSlCuE;69cB~WO4f`9 z?Y4o36jL4J$WMGB=A{ClhMyiA{f3WWGM-V+D_%!C+b(5L|T))7PFg?9Fs9MH6R{%KSA|0D$P%0FcA|C0!*P=kz|8mOc5&so0SB;@?}JwaCwJ6;kY0GwKTSplc#{kxi* zwJpdJ4dGm@|7rpt#dP4{>LQOmXYhi&7!(YJgON}u7zq;viyHC%OD4V^Hnt=H7yy$1 zZu;jRU|ewE8MX!eU519l=s%Fl&oU?&jy9lQ${+yd{o`im(6GlH^zm@^}#(yjO ztv!GOZTbJH3kJi`lhuFA05A4ydtfjW3_KLS&>?`C`M+flQ8-$a|Dl73!oa_d4FUj} zUwMHb;pk`dKXt*vBIw!jmoic0uW|vy(aV)z=-_blJow+ba1j8l{91*zu=D|dwcqGO&{3CP>WaWcf8#|&iM55fw(eumAEV3P(>|zm$o9 zf8zxSMz0wDONWG@XV+iKpvd3L(1`L^Iwaa{{--Vk3LNsbADam9eaNqLBGBLL3V34B@a{jn z0COj>>iN42 {% endblock %} @@ -651,10 +666,23 @@ + +

+ + +
+ +
+ + +

A single scalar applied to bias; independent of the number of contexts.

+
+
@@ -806,14 +834,24 @@ let formData = new FormData(); formData.append('message', message); - // Check if we're using hypernetwork and include contexts if (document.getElementById('chat-model-name').textContent.includes('Hypernetwork')) { const contextInputs = document.querySelectorAll('.context-input'); - - // Add all contexts to the form data Array.from(contextInputs).forEach(input => { formData.append('contexts[]', input.value); }); + + const scalerInputs = document.querySelectorAll('.context-scaler'); + Array.from(scalerInputs).forEach(input => { + const v = (input.value || '').trim(); + formData.append('scalers[]', v.length ? v : '1.0'); + }); + + // Include singular bias scaler + const biasScalerInput = document.getElementById('bias-scaler'); + if (biasScalerInput) { + const bv = (biasScalerInput.value || '').trim(); + formData.append('bias_scaler', bv.length ? bv : '1.0'); + } } fetch('/chat', { @@ -845,6 +883,43 @@ }); } + // Utility to wire up slider-number sync inside a context-field + function attachScalerSync(fieldEl) { + const slider = fieldEl.querySelector('.context-scaler-slider'); + const number = fieldEl.querySelector('.context-scaler'); + + if (!slider || !number) return; + + // Keep slider within [-2,2], but allow number to be arbitrary + const clampToSlider = (x) => { + const min = parseFloat(slider.min || '-2'); + const max = parseFloat(slider.max || '2'); + if (isNaN(x)) return 1.0; + return Math.min(Math.max(x, min), max); + }; + + slider.addEventListener('input', () => { + number.value = slider.value; + }); + + number.addEventListener('input', () => { + const parsed = parseFloat(number.value); + if (isNaN(parsed)) { + number.value = '1.0'; + slider.value = '1.0'; + } else { + // do not clamp number; only clamp slider representation + slider.value = clampToSlider(parsed); + } + }); + } + + // Bind initial scaler sync + document.addEventListener('DOMContentLoaded', function () { + const firstField = document.querySelector('.context-fields .context-field'); + if (firstField) attachScalerSync(firstField); + }); + // Handle hypernetwork checkpoint loading document.addEventListener('DOMContentLoaded', function () { const applyHypernetworkButton = document.getElementById('apply-hypernetwork-button'); @@ -1113,10 +1188,18 @@ + +
+ + +
`; contextFieldsContainer.appendChild(newField); + // Wire up slider-number sync for this field + attachScalerSync(newField); + // Enable the remove button if we have more than one context if (contextCount > 1) { removeContextButton.disabled = false; @@ -1128,8 +1211,6 @@ if (contextCount > 1) { contextFieldsContainer.removeChild(contextFieldsContainer.lastChild); contextCount--; - - // Disable the remove button if we're back to just one context if (contextCount === 1) { removeContextButton.disabled = true; }