All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog.
RemoteModelWrapper(textattack.models.wrappers.RemoteModelWrapper): query a model served behind a remote HTTP API instead of running it locally. Request/response handling is adaptable to different endpoint schemas viarequest_fn/response_fn.- New attack recipe
bad-characters(BadCharacters2021, seetextattack/attack_recipes/bad_characters_2021.py) implementing Bad Characters: Imperceptible NLP Attacks: perturbations invisible on some rendering systems (invisible characters, homoglyphs, reorderings, deletions of zero-width characters), combining a newdifferential-evolutionsearch method (WordSwapDifferentialEvolutiontransformation family) with newLogitSum/NamedEntityRecognition/TargetedStrict/TargetedBonusgoal functions (#817). - New attack recipe
leap(LEAP2023, seetextattack/attack_recipes/leap_2023.py) implementing LEAP: Efficient and Automated Test Method for NLP Software (arXiv:2308.11284). LEAP is a Levy-flight/adaptive-inertia variant of the Particle Swarm Optimization search already used by thepsorecipe (PSOZang2020); seetextattack/search_methods/particle_swarm_optimization_leap.py(ParticleSwarmOptimizationLEAP, subclassingParticleSwarmOptimization) for the algorithmic relationship between the two. tests/test_attack_recipes.py: structural and functional tests comparingLEAP2023againstPSOZang2020, its closest existing recipe.tests/benchmark_leap_vs_pso.py: a manual (not CI-run) benchmark script comparingLEAP2023against a WordNet-transformation variant of vanillaParticleSwarmOptimization, isolating the search algorithm from the candidate-word transformation. Results (cnn-ag-news, AG News test set) are documented in the "Benchmark" section ofLEAP2023's docstring: under an unrestricted query budget, both hit ~95% success (n=20), with LEAP using ~6% fewer queries and running ~2.25x faster; under a 2000-query budget (n=100), success collapses to ~23-24% for both, with LEAP still modestly ahead on every metric but by a much smaller margin.traincommand:--dataset-from-filesupport, matching whatattack/evalalready had. Point it at a Python module exposingtrain_dataset/eval_dataset(each atextattack.datasets.Dataset), optionallypath.py^prefixforprefix_train_dataset/prefix_eval_dataset(#625).HuggingFaceModelWrapper: an optionalmax_lengthconstructor argument, forwarded to.generate()for rawtransformersencoder-decoder generation models. Left unset by default so a checkpoint's owngeneration_configisn't overridden.- Docs: the single-example
Attack.attack(text, label)API (already existed, wasn't documented) (#673); a working example attacking a rawtransformers.BartForConditionalGeneration/T5ForConditionalGenerationviaSeq2SickCheng2018BlackBox(#772); a "Multi-lingual attacks" section listing the French/Spanish/Chinese recipes (#423).
- Modernized CI: bumped GitHub Actions (
actions/checkoutv2->v4,actions/setup-pythonv2->v5,github/codeql-action/*v1->v3), fixed the pre-existing lint errors this surfaced, and re-enabledpytestexecution in CI (it had been disabled) (#822). ChineseWordSwapHowNet: cache HowNet replacement-word lookups to speed up repeated candidate generation, without changing results (#786).- Refactored
ParticleSwarmOptimization.perform_search(shared base class) to expose its per-iteration deltas as overridable hook methods (_initialize_velocities,_pre_iteration_setup,_compute_omega,_compute_turn_prob,_compute_change_ratio), matching the hook patternGeneticAlgorithm/AlzantotGeneticAlgorithmalready use elsewhere in this codebase.ParticleSwarmOptimizationLEAPnow overrides only these hooks and_perturb, instead of duplicating a ~155-line copy ofperform_search; the standalone_greedy_perturbmethod was folded into a_perturboverride, so LEAP's mutation step can no longer silently fall back to the parent's probabilistic mutation by mistake. - Replaced LEAP's hand-rolled
softmaxwithscipy.special.softmax, and cached (functools.lru_cache) the alpha-invariant constants in its Levy-flight sampler (sigmax/K/C), sincealphais always1.5in this module -- both were previously recomputed from scratch on every call. The Levy-flight sampling algorithm itself (Mantegna's method, matching the authors' reference implementation) was intentionally left as-is rather than swapped forscipy.stats.levy_stable, since that would change the search's statistical behavior, not just its implementation. - Renamed the
gammaparameter oflevy()toscale, since it shadowed the module-levelfrom scipy.special import gamma as gammaimport.
EDAaugmentation recipe (textattack augmentCLI with--recipe eda): didn't accept the default arguments defined on theAugmentationsuperclass, since it didn't forward**kwargsto its componentAugmenterobjects.HuggingFaceDataset:shuffle()(andshuffle=Trueat construction) had no effect, sincedatasets.Dataset.shuffle()returns a newDatasetrather than shuffling in place, and the return value wasn't being assigned back (#791).WordSwapChangeNumber._alter_number: raisedValueError: high is out of bounds for int64on negative numbers, sinceint(num * self.max_change) + 1can compute a negativechangethat flips therandrange/randintbounds (#741).sentence_encoder.py'sget_angular_sim: clampcos_simto[-1, 1]beforetorch.acos, since floating-point error can push an equal pair of embeddings' cosine similarity slightly above1(e.g.1.00004), makingacosreturnNaNinstead of1.CompositeTransformation: iterated its sub-transformations' results through aset, making output order (and therefore downstream behavior relying on it) non-deterministic across runs; switched to a list-based dedup that preserves order.- Typo fix in
composite_transformation.py's docstring ("optoins" -> "options").
Several correctness issues found while porting LEAP against its authors'
reference implementation, some of which also affect the pre-existing pso
recipe since ParticleSwarmOptimizationLEAP shares code with
ParticleSwarmOptimization:
- LEAP's mutation step now calls its greedy mutation instead of silently
falling back to the inherited, probabilistic
_perturb, and computeschange_ratioagainst each particle's own local elite instead of the original input, matching the reference implementation. - Guarded LEAP's per-iteration adaptive inertia-weight interpolation against
a zero/negative denominator (
fit_ave/fit_minare frozen at the initial population's statistics, so a particle's score can drift belowfit_minin later iterations). - Fixed
ParticleSwarmOptimization.perform_search(shared base class, also used bypso) lettingglobal_elite/local_elitesalias the samePopulationMemberobjects held inpopulation-- bothglobal_elite = max(population, ...)andlocal_elites = copy.copy(population)only copied the list, not its elements, so a particle never reassigned to a new object by_turnduring an iteration (a real possibility whenever neither of that iteration's two random turn-probability checks fire) stayed aliased to its tracked elite. A later in-place mutation (_perturb) would then silently corrupt that elite. Every population member and both elites are now copied individually at initialization, in addition to the existing fix on_turn's constraint-failure return path. - Reset LEAP's per-iteration
omega(inertia weight) list at the start of each iteration instead of accumulating it across the whole search; it was being indexed by particle position, so every iteration after the first was silently reading back iteration-0's stale values. - Capped the retry count in LEAP's Levy-flight rejection sampling
(
get_one_levy/ velocity initialization), which previously used an unboundedwhile Trueloop with no fallback. - Fixed
docs/api/search_methods.rst'sParticleSwarmOptimizationLEAPsection heading underline, which was shorter than the title.
A round of fixes for long-standing issues found during an issue-triage pass:
BERTAttackLi2020: defaultmax_candidates48->8, since48makes the masked-LM candidate search explode combinatorially on multi-subword tokens, causing multi-hour runtimes (#586).AttackedText.generate_new_attacked_text: fixed corruption of the<SPLIT>join token when a replaced word (e.g. "I") is itself a character substring of"<SPLIT>"and directly follows it in the joined text (#631).WordSwapInflections: restored matching against flair's current"upos-fast"tags (NOUN/VERB/ADJ/PROPN/AUX), whichAttackedText.pos_of_word_indexhad switched to emitting directly a while back, leaving the transformation silently returning zero candidates for ordinary words (#713, #727).textattack.shared.utils.flair_tag: cached one tagger pertag_typeinstead of a single global slot, which silently reused whichever tagger (POS or NER) loaded first for every later call regardless of the requestedtag_type-- corrupting POS/NER results for whichever came second in the same process.GreedyWordSwapWIR: actually implementedtruncate_words_to(a2t's recipe had been passing it since an earlier PR, but the constructor never accepted it, crashing on init) (#754); forwir_method="gradient", the truncation now also bounds the expensiveget_gradcall itself (not just the cheap post-hoc index-scoring loop), and sortsindices_to_orderfirst since it can arrive in non-ascending order from aset-derived source.textattack/shared/validators.py: the model-compatibility regex for classification models only matched the pre-4.xtransformers.modeling_<model>layout; now also matchestransformers.models.<model>.modeling_<model>(#722). Also added a matching entry for rawtransformersencoder-decoder generation classes (T5ForConditionalGeneration,BartForConditionalGeneration, ...), not just TextAttack's ownT5ForTextToTexthelper, which used to print a spurious compatibility warning for every attack against one (#771).AttackArgs: warn (rather than silently drop) whennum_examplesis explicitly set alongsidenum_successful_examples, since the latter overrides the former and users combining both had no way to tell whynum_examplescame backNone(#728).AttackedText.words_diff_ratio: comparing two Python lists with!=yields a single bool, not an elementwise mask, so this always returned0or1regardless of how many words actually differed; fixed by comparing as numpy arrays (#787).Augmenter.augment: bound-retry the outer sampling loop so a transformation with limited output diversity for a given input (e.g.BackTranslationAugmenter's random language chaining colliding on short sentences) doesn't silently return fewer thantransformations_per_exampleunique augmentations (#800); a later version of that same fix madehigh_yield=Truemode plateau at roughly half its previous output instead of scaling withtransformations_per_example(a single outer pass can add several results at once in that mode), and its final downsampling step calledrandom.sample()on aset, which Python 3.11+ no longer accepts.words_from_text: strip allowed marks (quotes/hyphens/etc.) from both ends of a word, not just the leading end, so a quoted word like"'CCC'"doesn't keep its trailing quote (#723).HuggingFaceModelWrapper: route encoder-decoder generation models (e.g. a rawBartForConditionalGenerationloaded directly fromtransformers) through.generate()+ decode instead of a plain forward pass, which only returns logits and breaks text-to-text goal functions expecting strings (#771). Routing prefersmodel.can_generate()overhasattr(model, "generate"), since ontransformersversions predatingcan_generate(), everyPreTrainedModelexposed.generateregardless of whether it had a generation-capable head, risking misrouting a seq2seq-backbone classification model.Attack.cuda_/cpu_: skip re-placing atransformers.PreTrainedModelthat already has anhf_device_map(i.e. was loaded withdevice_map=...across multiple GPUs viaaccelerate), since forcing it onto a single device breaks that placement (#798);cpu_was missing this guard entirely, and the guard is now scoped totransformers.PreTrainedModelspecifically rather than anytorch.nn.Module, since this visitor also traverses non-HuggingFace models reachable from aConstraint/GoalFunction/Transformation.Trainer.training_step/evaluate_step: pad to the longest sequence in the batch (padding=True) instead of the tokenizer's static max length, avoiding wasted compute on shorter batches (#737).