Fine-Tuning Fixes Mode Collapse and Over-Dispersion in LLMs
Organizations: Northwestern University · University of Chicago
Abstract
Recent work by Doshi and Hauser (2024), Bisbee et al. (2024), and Xie et al. (2026) raises concerns that outputs from large language models (LLMs) tend to be under-diverse: they repeat or resemble one another more often than responses from the population they are meant to represent, a phenomenon known as mode collapse. In this work, we show that whether mode-collapse, or its opposite, occurs depends on the specific model and dataset used. Further, with sufficient supervised fine-tuning (SFT) data, LLM output diversity converges toward that of the target distribution from which fine-tuning data are sampled. To quantify this comparison, we measure the probability that two responses sampled independently from the same fixed prompt coincide (collide), or their expected similarity under a kernel. We derive a bias-variance decomposition of the expected gap between the model's and target's collision probabilities, showing that SFT is not inherently biased toward mode collapse or its opposite: finite-sample SFT can leave a model either under- or over-dispersed, depending on the model and dataset. Finally, we show that the absolute gap is bounded by the square root of the Kullback-Leibler (KL) divergence from the target distribution to the model. Consequently, a model sufficiently close to optimal under population cross-entropy cannot exhibit arbitrarily miscalibrated diversity. We test the decomposition and the bound in three experiments: small transformers on synthetic languages, four LLMs fine-tuned on human surveys, and these LLMs fine-tuned on CodeNet, a dataset of human code solutions. More target data moves model diversity toward the human (or synthetic target) level in all experiments, consistent with our theoretical predictions. These results show that diversity miscalibration can arise from finite-sample error and shrink as SFT better approximates the target distribution.
Figures & tables
Appendix figures & tables16 assets
Supplementary material from the paper’s appendix.
Appendix
| Starting distribution | Diffuse (regime (a)) | Mode-aligned (regime (b)) |
|---|---|---|
| Survey | Model | Base ( ) | Max over grid (final) | Largest (final) |
|---|---|---|---|---|
| GSS | gemma-2-2b-it | |||
| GSS | Qwen3.5-2B | |||
| GSS | Qwen3.5-4B | |||
| GSS | gemma-3-4b-it | |||
| WVS | gemma-2-2b-it | |||
| WVS | Qwen3.5-2B |
| Survey | Respondents | Countries | Groups | Eval groups ( ) | Eval pairs | Questions |
|---|---|---|---|---|---|---|
| GSS | — (US) | |||||
| WVS | ||||||
| ANES | — (US) | |||||
| Examples (fine-tuning / validation), pooled (respondent, question) pairs: GSS ( / ); WVS ( / ); ANES ( / ). Minimum-eval threshold respondents per pair. The -grid is anchored per survey to the full fine-tuning pool and halves down to the smallest : GSS points ; WVS points ; ANES points (roughly six orders of magnitude on the GSS). | ||||||
| Item ( var ) | Graded answer options | Ex. |
|---|---|---|
| Death penalty for murder ( cappun ) | A) favor; B) oppose | A |
| General happiness ( happy ) | A) very happy; B) pretty happy; C) not too happy | B |
| Financial satisfaction ( satfin ) | A) pretty well satisfied; B) more or less satisfied; C) not satisfied at all | B |
| Political views ( polviews ) | A) extremely liberal; B) liberal; C) slightly liberal; D) moderate; E) slightly conservative; F) conservative; G) extremely conservative | D |
| Party identification ( partyid ) | A) strong democrat; B) not very strong democrat; C) independent, close to democrat; D) independent; E) independent, close to republican; F) not very strong republican; G) strong republican; H) other party | A |
| Item ( var ) | Graded answer options | Ex. |
|---|---|---|
| Interpersonal trust ( trust ) | A) most people can be trusted; B) need to be very careful | B |
| Feeling of happiness ( happy ) | A) very happy; B) rather happy; C) not very happy; D) not at all happy | B |
| Confidence in government ( conf_govt ) | A) a great deal; B) quite a lot; C) not very much; D) none at all | C |
| Justifiability of divorce ( divorce_just ) | A) 1 (never justifiable); B) 2; …; I) 9; J) 10 (always justifiable) | C |
| Left–right self-placement ( left_right ) | A) 1 (left); B) 2; …; I) 9; J) 10 (right) | E |
| Item ( var ) | Graded answer options | Ex. |
|---|---|---|
| Party identification ( VCF0301 ) | A) strong Democrat; B) weak Democrat; C) independent–Democrat; D) independent; E) independent–Republican; F) weak Republican; G) strong Republican | D |
| Liberal–conservative ( VCF0803 ) | A) extremely liberal; …; D) moderate; …; G) extremely conservative | D |
| Government health insurance ( VCF0806 ) | A) 1 (government insurance plan); B) 2; …; F) 6; G) 7 (private insurance plan) | D |
| Interest in elections ( VCF0310 ) | A) not much interested; B) somewhat interested; C) very much interested | B |
| Presidential approval ( VCF0450 ) | A) approve; B) disapprove | A |
| Hyperparameter | Value |
|---|---|
| LoRA rank | |
| LoRA | |
| LoRA dropout | |
| LoRA bias | none |
| Task type | causal language modeling |
| Base precision | bf16 |
| Model | LoRA targets | Trainable params | % of total |
|---|---|---|---|
| gemma-2-2b-it | 7 proj., all decoder layers | ||
| Qwen3.5-2B | 7 proj., all decoder layers | ||
| Qwen3.5-4B | 7 proj., all decoder layers | ||
| gemma-3-4b-it | 7 proj., language_model only |
| Code A: s613176273 ⬇ A , B = map ( int , input (). split ()) if B % A ==0: print ( A + B ) else : print ( B - A ) | Complete canonical AST A ⬇ root : Module body []: Assign targets []: Tuple elts []: Name : VAR_0_0 elts []: Name : VAR_0_1 value : Call func : Name : map args []: Name : int args []: Call func : Attribute : split value : Call func : Name : input body []: If test : Compare left : BinOp left : Name : VAR_0_1 op : Mod right : Name : VAR_0_0 ops []: Eq comparators []: Constant : INT body []: Expr value : Call func : Name : print args []: BinOp left : Name : VAR_0_0 op : Add right : Name : VAR_0_1 orelse []: Expr value : Call func : Name : print args []: BinOp left : Name : VAR_0_1 op : Sub right : Name : VAR_0_0 | Code B: s053364634 ⬇ a , b = map ( int , input (). split ()) if b % a == 0: print ( b + a ) else : print ( b - a ) | Complete canonical AST B ⬇ root : Module body []: Assign targets []: Tuple elts []: Name : VAR_0_0 elts []: Name : VAR_0_1 value : Call func : Name : map args []: Name : int args []: Call func : Attribute : split value : Call func : Name : input body []: If test : Compare left : BinOp left : Name : VAR_0_1 op : Mod right : Name : VAR_0_0 ops []: Eq comparators []: Constant : INT body []: Expr value : Call func : Name : print args []: BinOp left : Name : VAR_0_1 op : Add right : Name : VAR_0_0 orelse []: Expr value : Call func : Name : print args []: BinOp left : Name : VAR_0_1 op : Sub right : Name : VAR_0_0 |
| Code A: s227737564 ⬇ L = input () L = L . split () for i , val in enumerate ( L ): L [ i ] = int ( val ) t = L [0]/ L [1] t = format ( t , ’0.4f’ ) print ( t ) | Complete canonical AST A ⬇ root : Module body []: Assign targets []: Name : VAR_0_0 value : Call func : Name : input body []: Assign targets []: Name : VAR_0_0 value : Call func : Attribute : split value : Name : VAR_0_0 body []: For target : Tuple elts []: Name : VAR_0_1 elts []: Name : VAR_0_2 iter : Call func : Name : enumerate args []: Name : VAR_0_0 body []: Assign targets []: Subscript value : Name : VAR_0_0 slice : Name : VAR_0_1 value : Call func : Name : int args []: Name : VAR_0_2 body []: Assign targets []: Name : VAR_0_3 value : BinOp left : Subscript value : Name : VAR_0_0 slice : Constant : INT op : Div right : Subscript value : Name : VAR_0_0 slice : Constant : INT body []: Assign targets []: Name : VAR_0_3 value : Call func : Name : format args []: Name : VAR_0_3 args []: Constant : STRING body []: Expr value : Call func : Name : print args []: Name : VAR_0_3 | Code B: s089174134 ⬇ import sys def solve ( T : int , X : int ): print ( T / X ) return def main (): def iterate_tokens (): for line in sys . stdin : for word in line . split (): yield word tokens = iterate_tokens () T = int ( next ( tokens )) X = int ( next ( tokens )) solve ( T , X ) if name == ’main’ : main () | Complete canonical AST B ⬇ root : Module body []: Import names []: alias : sys body []: FunctionDef : FUNC_0 args : arguments args []: arg : VAR_1_0 annotation : Name : int args []: arg : VAR_1_1 annotation : Name : int body []: Expr value : Call func : Name : print args []: BinOp left : Name : VAR_1_0 op : Div right : Name : VAR_1_1 body []: Return body []: FunctionDef : FUNC_1 args : arguments body []: FunctionDef : FUNC_2 args : arguments body []: For target : Name : VAR_3_0 iter : Attribute : stdin value : Name : LIB_sys body []: For target : Name : VAR_3_1 iter : Call func : Attribute : split value : Name : VAR_3_0 body []: Expr value : Yield value : Name : VAR_3_1 body []: Assign targets []: Name : VAR_2_0 value : Call func : Name : FUNC_2 body []: Assign targets []: Name : VAR_2_1 value : Call func : Name : int args []: Call func : Name : next args []: Name : VAR_2_0 body []: Assign targets []: Name : VAR_2_2 value : Call func : Name : int args []: Call func : Name : next args []: Name : VAR_2_0 body []: Expr value : Call func : Name : FUNC_0 args []: Name : VAR_2_1 args []: Name : VAR_2_2 body []: If test : Compare left : Name : name ops []: Eq comparators []: Constant : STRING body []: Expr value : Call func : Name : FUNC_1 |