Posts文章
Gen AI生成式 AI1 July 2026 · 23 min read2026年7月1日 · 閱讀約 44 分鐘

LLM Fine-Tuning: Teaching a Small Model to Analyze 10-K Risk DisclosuresLLM 微調:教小型模型分析 10-K 風險揭露

A practical guide to supervised fine-tuning, PEFT, LoRA, and QLoRA—covering dataset design, loss masking, leakage-resistant evaluation, and a hands-on experiment with SEC 10-K risk disclosures.一份關於監督式微調、PEFT、LoRA 與 QLoRA 的實用指南,涵蓋資料集設計、損失遮罩、可防資料外洩的評估方法,以及一項以 SEC 10-K 風險揭露為題的實作實驗。

LLM Fine-Tuning: Teaching a Small Model to Analyze 10-K Risk Disclosures

Fine-tuning adapts a pretrained model to reproduce one specific behavior. The hard part isn't starting the training job — it's deciding what that behavior should be, building examples that express it consistently, and proving the resulting adapter actually beats a well-prompted baseline.

This article is organized into four parts:

  1. Part I: Fine-Tuning Overview
  2. Part II: LoRA and QLoRA
  3. Part III: Evaluating a Fine-Tuned Model
  4. Part IV: Hands-On SEC 10-K Risk-Analysis Case

This article assumes familiarity with decoder-only Transformers — see LLM Pretrain: Anatomy of a Decoder-Only Transformer for background.

Part I: Fine-Tuning Overview

Fine-tuning is useful when a desired behavior is repeated, specific, and difficult to obtain reliably through prompting alone. It is an optimization step, not an automatic first step.

Is Fine-Tuning the Right Tool?

Before investing compute and time into training, it is crucial to establish a strong baseline using few-shot prompting or Retrieval-Augmented Generation (RAG). Prompting injects facts and context, whereas fine-tuning alters a model's fundamental tone, structure, and reasoning pathways.

You should consider moving from prompting to fine-tuning when:

  • You are consistently maxing out your context window with complex, highly specific few-shot examples.
  • You require rigid, guaranteed output formats (such as a strict JSON schema mapping out 10-K risk severities) that prompting alone cannot reliably enforce.
  • Cost or latency constraints dictate replacing a massive proprietary model with a smaller, specialized open-weights model (e.g., an 8B parameter model).
RequirementRecommended Approach
Improve a one-off instructionPrompt engineering
Access current or private documentsRetrieval-augmented generation (RAG)
Add frequently changing financial factsRetrieval-augmented generation (RAG), not model memory
Reproduce a stable tone, taxonomy, or output schemaFine-tuning
Teach consistent decisions from labeled examplesFine-tuning
Reproduce a stable tone, taxonomy, or JSON schemaFine-tuning

These approaches are complementary; in our 10-K experiment, retrieval supplies the relevant SEC disclosure, while fine-tuning teaches the model exactly how to analyze and format it.

The Supervised Fine-Tuning (SFT) Objective

Fine-tuning encompasses several distinct objectives. A model might undergo continued pretraining for domain terminology, SFT for task behavior, and preference tuning (like DPO or RLHF) to refine its output choices.

Fine-tuning is not a single training objective; it encompasses several distinct methods that teach different kinds of adaptation.

MethodWhat it teaches
Continued pretrainingDomain language, terminology, and document patterns through next-token prediction
Supervised fine-tuning (SFT)Desired input-to-output behaviour from labelled demonstrations
Preference tuningWhich of two responses is preferable, using methods such as Direct Preference Optimisation (DPO)
Reinforcement learningBehaviour that maximises a reward signal, using approaches such as RLHF or RLAIF

For our 10-K experiment, the primary objective is SFT because our desired output can be expressed as labeled prompt-completion pairs.

The causal language-modeling objective does not fundamentally change from pretraining: the model still predicts the next token. What changes is the data distribution. While raw documents teach the model to merely continue text, curated SFT demonstrations teach it to respond to a specific request in a consistent, structural way.

Chat Templates: The Training Contract

A conversation represented as role and content objects must eventually become one token sequence. A chat template inserts the control tokens that mark system, user, assistant, and message boundaries:

Structured messages
        ↓ model-specific chat template
[system control tokens   ]  instruction
[user control tokens     ]  request
[assistant control tokens]  response
        ↓ tokenizer
Training token IDs

Different model families use entirely different control tokens. For example, LLaMA-3 structures its prompts using <|start_header_id|> and <|eot_id|>, while Mistral uses [INST] and [/INST]. Training and inference must use the exact template associated with the selected tokenizer. Manually reproducing another model's format will materially degrade response quality.

The transformers library exposes this conversion through tokenizer.apply_chat_template:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1")
chat = [
  {"role": "system", "content": "You are a financial assistant."},
  {"role": "user", "content": "Analyze this 10-K excerpt."},
  {"role": "assistant", "content": "{\"risks\": [...]}"}
]

# This outputs the properly formatted flat string with control tokens
tokenizer.apply_chat_template(chat, tokenize=False)

Hugging Face chat-template guide

Loss Masking: Completion-Only Loss

Once the text is formatted, we must tell the optimizer what to learn. During completion-only training, prompt tokens provide necessary context, but they should not contribute to the loss. We only want to penalize the model for errors in the assistant's response.

We achieve this using a label mask. The ignore value -100 tells the PyTorch cross-entropy implementation not to score those positions:

Tokens: [system instruction] [user prompt] [assistant response]
Labels: [       -100      ] [   -100    ] [ compute loss here ]

By masking the prompt, we concentrate the learning signal entirely on the behavior the model must produce. In current versions of the TRL (Transformer Reinforcement Learning) library, prompt-completion datasets use completion-only loss by default. Conversational datasets can trigger this behavior using assistant_only_loss=True, provided the chat template supplies a valid generation mask. (For implementation details, refer to the TRL SFTTrainer loss-masking documentation)

Training on the full sequence is also a legitimate objective in other settings, so response-only masking should be selected deliberately rather than treated as a universal rule.

Full Fine-Tuning vs. PEFT

Full fine-tuning updates every parameter in the model, demanding massive memory for gradients and optimizer states. Every specialized model requires storing a complete set of multi-gigabyte weights.

Parameter-efficient fine-tuning (PEFT) freezes most of the base model and trains a fraction of the parameters. The industry standard taxonomy divides PEFT into three high-level strategies:

PEFT StrategyMechanismRepresentative Methods
AdditiveFreezes the base model and introduces new trainable parameters.Adapter modules, prompt tuning, prefix tuning, P-tuning
SelectiveUpdates only a chosen subset of existing parameters.BitFit, LayerNorm tuning, partial layer unfreezing
ReparameterisationExpresses weight updates through a compact mathematical structure.LoRA, AdaLoRA, DoRA

LoRA belongs to the reparameterization family, and QLoRA simply pairs LoRA with a quantized base model. While PEFT drastically reduces compute costs, it does not compensate for poor data; your adaptation is only as good as your labels.

This taxonomy is presented in Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning.

Part II: LoRA and QLoRA


LoRA visualization

LoRA: Low-Rank Adaptation

LoRA observes that the useful parameter change for a downstream task—like adapting a general model to parse complex SEC 10-K filings— often has a much lower intrinsic dimension than the original weight matrix.

For a frozen weight matrix W0W_0, LoRA learns an update:

W=W0+ΔWW' = W_0 + \Delta W

Instead of training a massive dense matrix, the update is factorized into two smaller matrices:

ΔW=αrBA\Delta W = \frac{\alpha}{r}BA

Where ARr×kA \in \mathbb{R}^{r \times k} and BRd×rB \in \mathbb{R}^{d \times r} are trainable, rr is the adapter rank, and α\alpha controls the scale. By keeping rr smaller than dd and kk, trainable parameters drop drastically from dkdk to approximately r(d+k)r(d+k).

For example, a square 768×768768 \times 768 update contains 589,824 parameters. A rank-16 LoRA factorisation contains:

16(768+768)=24,57616(768+768)=24{,}576

trainable parameters—24 times fewer for that matrix. The saving depends on the matrix shape, chosen rank, and number of modules adapted.

The low-rank hypothesis is not that the original weight matrix is low rank. It is that the task-specific change ΔW\Delta W can often be expressed through a much smaller number of useful directions. LoRA learns those directions while leaving W0W_0 frozen.

LoRA is commonly applied to attention projection layers and sometimes to the feed-forward projections as well. A higher rank provides more adaptation capacity but increases memory, storage, and the risk of fitting noise. Rank is therefore a model-selection parameter, not a quality setting that should always be maximised.

Initialization, Scaling, and Target Modules

LoRA must begin without disturbing the pretrained model. A common initialization gives one factor non-zero random values and initializes the other to zero. The standard Hugging Face PEFT initialization sets AA with random values and BB to zero. Because BA=0BA=0, the first forward pass behaves exactly like the base model.

Rank (rr) dictates the adapter's learning capacity and parameter count, while the ratio α/r\alpha/r scales the update. There is no universally optimal ratio, so both parameters require empirical tuning for your specific dataset.

While early LoRA research focused strictly on attention projections, modern LLM recipes often target a wider array of layers, including query, key, value, and output projections, as well as the gate, up, and down projections in the feed-forward network. For our 10-K case study, setting target_modules="all-linear" ensures we target both attention and feed-forward projections without needing to hard-code model-specific layer names.

After training, you must decide how to deploy the LoRA update:

  • Dynamic Swapping (Keep Separate): Highly memory-efficient if one base model serves several different tasks (e.g., swapping between a 10-K risk adapter and a general summarization adapter on the fly).
  • Weight Merging (Fuse to Base): Merging the adapter into the base weights removes the separate adapter computation during inference. This is ideal when deploying a single, fixed variant dedicated purely to financial analysis.

The original method is detailed in LoRA: Low-Rank Adaptation of Large Language Models. The current PEFT implementation exposes rank, scaling, dropout, and target modules through LoraConfig (see the Hugging Face PEFT LoRA guide)

QLoRA: Reducing the Frozen Model's Memory

While LoRA reduces trainable parameters, the massive frozen base model must still be loaded into VRAM. QLoRA bridges this gap, making single-accelerator fine-tuning practical by loading the base model in 4-bit precision while training higher-precision LoRA adapters through the quantised model.

The vital distinctions are:

  • Pretrained base weights are quantized to 4-bit and remain frozen.
  • The LoRA adapter parameters remain trainable.
  • Matrix multiplication computations occur in higher precision (e.g., BF16).

Core Mechanisms

QLoRA can make single-accelerator experiments practical, but it is not free. Quantisation introduces representation error, backend compatibility matters, and the final model must still be evaluated on the target task. The Transformers documentation recommends NF4 for training 4-bit base models.

QLoRA achieves these extreme memory savings through three specific innovations:

  1. Normal Float 4-bit (NF4): A 4-bit data type mathematically optimized to match the normal distribution of pretrained neural network weights. More values near 0, and fewer values at extremes.
  2. Double quantisation: Quantizing the scale constants used by the first quantisation step, further reducing metadata overhead.
  3. Paged optimisers: Leveraging unified memory paging to absorb temporary optimizer memory spikes-automatically pages optimizer states to CPU RAM when GPU runs out, preventing out-of-memory crashes.

The original QLoRA paper demonstrated fine-tuning a 65-billion-parameter model on a single 48 GB GPU. That result demonstrates the method's potential, not a universal hardware guarantee. Total training memory also includes quantisation metadata, activations, LoRA parameters, optimiser state, temporary workspaces, and a workload-dependent context length; it cannot be inferred from the raw 4-bit weight size alone.

See:
QLoRA: Efficient Finetuning of Quantized LLMs
Hugging Face bitsandbytes guide

Part III: Evaluating a Fine-Tuned Model

Evaluation asks a much broader question than whether training loss decreased: did the adaptation produce a reliable, practical improvement on unseen data? Answering this requires rigorous baselines, protected test sets, task-level metrics, and error analysis.

Establishing Meaningful Baselines

Before declaring the fine-tuning a success, at minimum, compare the fine-tuned model with the unchanged model using the same test examples and decoding settings. The unchanged model should receive a strong prompt containing the task definition, output format, and, where appropriate, a small number of demonstrations.

A useful comparison can include:

  1. The base model with a zero-shot prompt.
  2. The base model with the strongest few-shot prompt.
  3. The fine-tuned model with its intended inference prompt.

This isolation proves whether your costly training process actually generated new capabilities, or if the same result could have been achieved through clever prompting alone.

Preventing Data Leakage

Training, validation, and test data have different purposes. Training examples produce parameter updates, validation examples guide model and hyperparameter selection, and the test set provides the final estimate after those decisions have been made.

A random train/test split is dangerous when documents contain duplicates, revisions, or repeated templates. Split by the unit that could leak—for example, document, customer, company, or time period—and keep near-duplicates in the same split. The test set should also represent the conditions under which the model will be used, including difficult and negative examples.

If you split your dataset randomly by paragraph, your test set will contain near-duplicates of your training data.

  • Split by Entity: Group your splits by company ticker or CIK. This ensures the model is evaluated on entirely unseen organizations.
  • Preserve Distributions: Ensure your test set represents the actual production environment, including difficult edge cases and negative examples (documents where no material risks are present).

Measuring the Application Contract

Perplexity and validation loss measure token prediction; it does not measure if the application works safely. Evaluation metrics should reflect the behaviour that users depend on:

Evaluation DimensionExample Measures
Task AccuracyPrecision, recall, F1, exact match, or field-level accuracy
Output ContractParse rate, schema validity, and allowed-value compliance
GroundingEvidence validity, factual consistency, and unsupported-claim rate
AbstentionAccuracy when evidence is missing, irrelevant, or ambiguous
RobustnessPerformance by input length, domain, difficulty, and unseen source
General capabilityRegression tests for instruction following and unrelated tasks
Operational efficiencyLatency, throughput, memory use, and adapter storage

Not every task needs every metric. A classifier may be judged primarily by precision, recall, and calibration; a structured extraction system also needs schema and evidence checks; an open-ended assistant requires a carefully defined human-review rubric.

Deterministic checks (like JSON parsing and exact-string matching for evidence) should be fully automated. However, semantic properties—such as completeness, faithfulness, clarity, and appropriate uncertainty often require a carefully defined human-review rubric or a validated LLM-as-a-judge pipeline.

Human comparisons should use a written rubric and anonymised outputs so that reviewers do not know which system produced each response. Report results by important slices rather than only as one average, and inspect representative false positives, false negatives, hallucinations, and formatting failures.

Ultimately, a fine-tuned model is successful only when its improvement over the baseline justifies its training, evaluation, storage, and serving costs.

Part IV: Hands-On SEC 10-K Risk-Analysis Case

The analysis notebooks can be found here.

1. Problem statement, objectives, and task

Risk disclosures in Item 1A of a 10-K are valuable to analysts, compliance teams, and risk managers, but they are lengthy, repetitive, and expressed in unstructured language. Similar risks can be described differently across companies, while manual categorisation and summarisation may be inconsistent.

The proposed system is an analytical assistant rather than an investment or materiality engine. It converts a supplied disclosure passage into a structured, reviewable record. Its objectives are to:

  1. reduce the effort needed to identify disclosed risks;
  2. normalise those risks into a consistent schema;
  3. preserve exact evidence for traceability; and
  4. determine whether fine-tuning improves on a well-prompted base model.

The model receives a 10-K excerpt:

A substantial portion of our cash is held outside our home market. 
Restrictions on transferring those funds or adverse currency movements 
could reduce the amount available for domestic operations.

and outputs:

{
  "risks": [
    {
      "category": "Financial",
      "summary": "Transfer restrictions and currency movements may reduce cash available for domestic operations.",
      "evidence": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations."
    }
  ],
  "insufficient_evidence": false
}

It must identify zero or more risks, assign an experiment-specific category, summarise each risk, copy supporting evidence, and abstain when the passage does not support an extraction. It must not use outside facts or predict whether the risk will occur. Comparing disclosures between years is a possible downstream task, but it is deliberately excluded from this first experiment.

2. The data problem

The modelling task begins with a data-engineering problem. A production pipeline would retrieve filings from SEC EDGAR, identify Item 1A, remove filing markup, split the section into coherent disclosure passages, and retain enough provenance to trace every passage back to its source. At minimum, a record should preserve the company identifier, accession number, filing date, source URL, section, and source text.

SEC EDGAR API documentation

This experiment uses synthetic disclosures so that the workflow is reproducible and redistributable. The synthetic data is suitable for testing the training and evaluation pipeline, but it cannot establish performance on real filings.

The source of truth is a canonical annotation record rather than a model-specific prompt. A simplified record looks like this:

{
  "example_id": "train_001",
  "split": "train",
  "issuer_id": "issuer_01",
  "filing_year": 2023,
  "source_text": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations.",
  "risks": [
    {
      "category": "Financial",
      "summary": "Transfer restrictions and currency movements may reduce cash available for domestic operations.",
      "evidence": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations."
    }
  ],
  "insufficient_evidence": false
}

The risk taxonomy is deliberately small and operational. It standardises the expected output without pretending to be a universal classification of 10-K risks:

CategoryExamples of disclosed risk
FinancialLiquidity, credit, currency, interest-rate, or funding exposure
OperationalSupply-chain, manufacturing, capacity, or business-continuity disruption
MarketDemand, pricing, competition, or macroeconomic exposure
RegulatoryLaws, investigations, compliance obligations, or policy changes
CybersecuritySecurity incidents, system compromise, or data exposure
StrategicAcquisitions, concentration, execution, or business-model risk
PeopleTalent retention, labour availability, leadership, or workforce risk

The dataset contains 68 examples:

SplitExamplesDesign
Training402022–2023 disclosures from development issuers
Validation122024 disclosures from the same issuers
Test162025 disclosures from entirely held-out issuers

All splits contain positive, negative, and multi-risk examples. Negative examples teach abstention; multi-risk examples prevent the model from assuming that every passage contains exactly one label. Evidence must be copied exactly from the source text, which makes grounding mechanically verifiable.

The split is designed to reduce leakage. Development issuers appear only in the training and validation sets, while test issuers are held out completely. Normalised passages are also compared across splits so that near-duplicate language cannot turn evaluation into memorisation.

Only after these checks is each canonical record converted into the conversational prompt-completion structure required for SFT:

{
  "prompt": [
    {"role": "system", "content": "Extract only risks supported by the supplied 10-K excerpt..."},
    {"role": "user", "content": "<10-K excerpt>"}
  ],
  "completion": [
    {"role": "assistant", "content": "{\"risks\": [...], \"insufficient_evidence\": false}"}
  ]
}

Keeping canonical annotations separate from rendered prompts makes it possible to change a chat template or model family without rewriting the labels. Before training, the pipeline validates the schema, category values, exact evidence spans, abstention logic, split membership, near-duplicate controls, and token lengths.

3. Methodology: a controlled LoRA–QLoRA comparison

The experiment starts from Qwen/Qwen3-0.6B. It is an instruction-following causal language model small enough for an accessible demonstration while still supporting the chat-template and structured-generation workflow used by larger models. Starting from an instruction model is intentional: the experiment is specialising an existing assistant for a constrained extraction behaviour, not trying to teach a base model how to follow instructions from a small dataset.

Two adapters are trained against the same checkpoint and data. The ordinary LoRA run keeps the frozen base weights in BF16 or FP16. The QLoRA run stores those frozen weights in four-bit NF4 and performs adapter computation at a higher precision. In both cases, the trainable update is LoRA.

SettingValue
TaskGrounded multi-risk extraction to JSON
Base checkpointQwen/Qwen3-0.6B
Training objectiveCompletion-only causal language modelling
LoRA rank and alphar=16, alpha=32
Target modulesAll linear attention and feed-forward projections
Context limit1,024 tokens
Evaluation systemsBase zero-shot, base few-shot, LoRA, and QLoRA

The intended experimental difference is therefore how the frozen base model is stored during training:

Training armFrozen base modelTrainable parameters
LoRABF16 or FP16LoRA adapter
QLoRAFour-bit NF4 with double quantisationLoRA adapter

To make the comparison meaningful, both arms use the same dataset splits, prompt format, maximum sequence length, rank, scaling, target modules, optimiser settings, random seed, and evaluation protocol. The LoRA configuration is:

lora_config = LoraConfig(
    task_type="CAUSAL_LM", # The model performs causal next-token generation
    r=16,                  # Capacity and size of the low-rank update
    lora_alpha=32,         # Scale applied to that update
    lora_dropout=0.05,     # Regularisation
    target_modules="all-linear", # Model layers receiving LoRA adapters, generally including: attention and feed-forward network
    bias="none",           # Whether existing bias parameters are trained
)

target_modules="all-linear" applies adapters to the model's linear attention and feed-forward projections. It is convenient for this experiment because it avoids hard-coding architecture-specific module names, although a production study should still inspect the matched modules and report them.

This is not a comparison between two different adapter algorithms. QLoRA still trains LoRA parameters; quantisation reduces the memory required to keep the frozen base model available during training. The expected trade-off is lower peak memory, potentially accompanied by a small change in training dynamics.

The controlled question is therefore whether QLoRA reduces peak training memory without materially reducing extraction accuracy, grounding, or schema compliance relative to ordinary LoRA.

4. Training with completion-only SFT

Each training arm uses the same prepared prompt-completion records and LoRA configuration. The experiment is controlled by a TRAINING_METHOD flag; only the frozen base-model loading path changes.

For QLoRA, we dynamically inject a 4-bit quantization configuration:

if TRAINING_METHOD == "qlora":
    load_kwargs["quantization_config"] = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_use_double_quant=True,
        bnb_4bit_compute_dtype=compute_dtype,
    )

Configuring the Trainer

The tutorial configuration uses five epochs, a learning rate of 2e-4, an adapter rank of 16, and memory-saving techniques like gradient accumulation and checkpointing. A maximum sequence length of 1024 accommodates the SEC excerpts. These are experiment settings rather than universal defaults.

The trainer receives the conversational records and is explicitly instructed to use completion-only loss:

training_args = SFTConfig(
    learning_rate=2e-4,
    num_train_epochs=5,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    gradient_checkpointing=True,
    max_length=1024,
    completion_only_loss=True,
    eval_strategy="epoch",
    save_strategy="epoch",
)

trainer = SFTTrainer(
    model=model,
    args=training_args,
    processing_class=tokenizer,
    train_dataset=dataset["train"],
    eval_dataset=dataset["validation"],
)

Passing processing_class=tokenizer is critical: it tells the trainer how to apply the chat template, render the strings, and convert them into token IDs.

Understanding the Training Batch Tensors

Behind the scenes, trainer.get_train_dataloader() constructs the batches that enter the model. Each batch is composed of three aligned integer matrices, typically with the shape [batch_size, sequence_length].

TensorPurpose
input_idsVocabulary index integers representing the rendered system message, user excerpt, and assistant completion.
attention_maskContains 1 for real tokens and 0 for padding, telling the attention mechanism which positions to ignore.e
labelsThe target tokens used for gradient calculation. Prompt and padding positions are set to -100.

Here is a conceptual look at a single training row showing how completion-only loss masks the prompt:

PositionContentinput_idsattention_masklabels (Target)
0System1011-100 (Ignored)
1User2051-100 (Ignored)
2User3011-100 (Ignored)
3Assistant4011401 (Supervised)
4Assistant4021402 (Supervised)
5Padding00-100 (Ignored)

While the visual representation above is simplified, understanding the mechanics of these tensors is essential for debugging:

  • The Causal Shift: Notice that at supervised positions (3 and 4), the labels ID is identical to the input_ids value. This is because PyTorch's causal language modeling loss internally shifts the labels by one position. The model's prediction at position tt is evaluated against the label at position t+1t+1.

  • Dynamic Padding: With per_device_train_batch_size=1, each matrix has exactly one row. If the batch size increases, shorter examples are dynamically padded to match the longest sequence in that specific batch. The sequence length is capped at 1,024, but most batches will be narrower than that.

  • Indices vs. Embeddings: The input_ids matrix does not contain dense vector embeddings. It contains simple vocabulary indices that the model uses to look up the actual embeddings, which then produce a hidden-state tensor of shape [batch_size, sequence_length, d_model].

Finally, each run records the model identifier, random seed, LoRA settings, trainable parameters, peak GPU memory, and training duration. While validation loss is logged to monitor optimization health, it does not replace the rigorous task-level evaluation detailed in Part III.

Each run records the model identifier, random seed, LoRA settings, library versions, trainable and total parameters, training duration, peak GPU memory, and trainer metrics. Validation loss monitors optimisation; it does not replace task-level evaluation.

5. Evaluation and base-model comparison

The evaluation protocol is designed to compare four systems under the same held-out data, output limit, and deterministic decoding policy:

  1. the unchanged base model with a zero-shot instruction;
  2. the unchanged base model with representative positive and negative examples;
  3. the LoRA adapter; and
  4. the QLoRA adapter.

The completed run used Qwen/Qwen3-0.6B and all 16 held-out synthetic examples from eight issuers. Inference ran in FP16 on Apple MPS. Both adapters were loaded separately onto the same higher-precision base checkpoint used by the baselines. The comparison therefore measures the behaviour learned by each adapter rather than comparing FP16 and 4-bit inference.

MetricWhat it tests
JSON and schema validityWhether downstream code can safely consume the response.
Risk precision, recall, and F1Whether labelled risks are extracted without extras or omissions.
Category macro F1Whether performance extends beyond the most frequent category.
Evidence validityWhether each quoted span explicitly occurs in the supplied excerpt.
Unsupported-evidence rateHow often generated evidence is hallucinated.
Abstention accuracyWhether non-risk passages are rejected correctly.
Latency and tokens per secondRuntime cost under the declared test setup.
Peak training memory and durationTraining-resource footprint (comparable only on identical hardware).
Adapter sizePer-task storage requirement.

Note: Invalid JSON, unsupported evidence, and missed risks are explicitly decoupled. A schema failure is a formatting error, not automatically a hallucinated claim. The automated metrics below precede our blinded human-review rubric for summary faithfulness.

Quality Results

SystemJSON validSchema validRisk precisionRisk recallRisk F1Category macro F1Evidence validAbstention accuracy
Base, zero-shot100.0%56.3%11.1%6.7%8.3%2.4%100.0%43.8%
Base, few-shot100.0%100.0%33.3%20.0%25.0%20.0%100.0%81.3%
LoRA adapter100.0%100.0%63.6%46.7%53.8%76.2%100.0%100.0%
QLoRA adapter100.0%100.0%58.3%46.7%51.9%70.1%91.7%93.8%

Few-shot prompting was already a much stronger baseline than the zero-shot instruction. Relative to that stronger baseline, Both adapters maintained perfect schema validity. LoRA significantly outperformed the strong few-shot baseline, boosting risk F1 by 28.8 points and abstention accuracy by 18.8 points. QLoRA followed closely. While LoRA retained a slight edge in precision and grounding over QLoRA, the small sample size (n=16n=16) means these figures serve to demonstrate our pipeline's analytical rigor rather than proving general algorithmic superiority.

Efficiency Results

SystemMean latencyOutput tokens/sAdapter sizePeak training memoryTraining time
Base, zero-shot1.47 s58.90
Base, few-shot0.72 s54.30
LoRA adapter1.19 s36.6749.46 MiBNot recorded63.63 s
QLoRA adapter1.25 s36.4149.46 MiB1.63 GiB84.84 s

Latency was measured sequentially on Apple MPS and includes varying output lengths and unmerged PEFT overhead. Consequently, these figures serve as an observational baseline rather than a hardware-independent benchmark. Both adapters predictably share an identical 49.46 MiB footprint; 4-bit quantization compresses the frozen base model during training, but does not alter the saved adapter's dimensions.

Similarly, the training times (63.63s for LoRA; 84.84s for QLoRA) and memory allocations (a 1.27 GiB end-of-run snapshot for LoRA versus a 1.63 GiB recorded peak for QLoRA) strictly reflect this specific MPS environment. Because the memory profiling captured different lifecycle phases, these values do not provide a controlled baseline for quantifying QLoRA's memory savings.

The Path to Production

Aggregate metrics only tell part of the story. Meaningful evaluation requires slicing errors across sectors, risk categories, negative examples, and multi-risk passages. While our 16-example synthetic dataset successfully validates the evaluation mechanics, proving true production readiness demands significantly more rigor, including:

  • An expansive, strictly held-out test set of manually reviewed EDGAR excerpts.
  • Systematic uncertainty estimation and blinded domain review.
  • Broad regression testing outside the core extraction task.

Ultimately, fine-tuning earns its place in the stack only if an adapter demonstrably outperforms the strongest reasonable prompting baseline, flawlessly preserves evidence grounding, and delivers a performance gain that explicitly justifies the added operational complexity.

微調(fine-tuning)是讓一個已經預訓練好的模型,去穩定重現某一種特定行為。真正困難的地方,從來不是啟動訓練工作,而是決定這個行為究竟該是什麼、建立能一致表達這個行為的範例,並且證明訓練出來的 adapter 真的勝過一個提示詞(prompt)寫得很好的基準模型。

本文分為四個部分:

  1. 第一部分:微調總覽
  2. 第二部分:LoRA 與 QLoRA
  3. 第三部分:評估微調後的模型
  4. 第四部分:實作案例——SEC 10-K 風險分析

閱讀本文前,建議先熟悉 decoder-only Transformer 的基本原理——可參考 LLM 預訓練:Decoder-Only Transformer 剖析 作為背景知識。

第一部分:微調總覽

當你想要的行為是重複出現、定義明確,而且光靠提示詞(prompting)難以穩定達成時,微調才會派上用場。它是一項優化手段,而不是預設的第一步。

微調是正確的工具嗎?

在投入運算資源與時間進行訓練之前,務必先用少樣本提示(few-shot prompting)或 檢索增強生成(RAG) 建立一個夠強的基準線。 提示詞注入的是事實與情境脈絡,而微調改變的則是模型根本的語氣、結構與推理路徑。

在以下情況,你可以考慮從提示詞轉向微調:

  • 你的上下文視窗(context window)持續被複雜且高度特定的少樣本範例塞滿。
  • 你需要嚴格、可保證的輸出格式(例如描述 10-K 風險嚴重程度的嚴格 JSON schema),而光靠提示詞無法可靠地強制達成。
  • 成本或延遲的限制,使你必須用一個更小、更專精的開放權重模型(例如 8B 參數的模型)取代龐大的專有模型。
需求建議做法
改善單次的指令表現提示工程(Prompt engineering)
存取即時或私有文件檢索增強生成(RAG)
加入頻繁變動的財務事實檢索增強生成(RAG),而非仰賴模型記憶
重現穩定的語氣、分類體系或輸出格式微調
從標註範例中學習一致的判斷微調
重現穩定的語氣、分類體系或 JSON schema微調

這些做法是互補的;在我們的 10-K 實驗中,檢索負責提供相關的 SEC 揭露內容,而微調則教會模型該如何分析並格式化這些內容。

監督式微調(SFT)的訓練目標

微調涵蓋好幾種不同的訓練目標。一個模型可能會先做持續預訓練(continued pretraining)來學習領域術語,接著用 SFT 學習任務行為,最後再透過偏好調整(例如 DPO 或 RLHF)來精煉它的輸出選擇。

微調並不是單一的訓練目標;它涵蓋好幾種不同的方法,分別教會模型不同種類的適應能力。

方法學到的內容
持續預訓練透過下一個 token 預測,學習領域語言、術語與文件模式
監督式微調(SFT)從標註範例中學習期望的輸入到輸出行為
偏好調整學習兩個回應中哪一個較好,常用方法如直接偏好優化(DPO)
強化學習學習能最大化獎勵訊號的行為,常用方法如 RLHF 或 RLAIF

在我們的 10-K 實驗中,主要的訓練目標是 SFT,因為我們期望的輸出可以表達成標註好的 prompt-completion 配對。

因果語言建模(causal language modeling)這個目標本身,和預訓練階段相比並沒有根本上的改變:模型依然是在預測下一個 token。真正改變的是資料的分布。原始文件教會模型的只是「接續文字」,而經過整理的 SFT 示範範例,則教會模型以一致、結構化的方式回應特定類型的請求。

聊天樣板(Chat Template):訓練的契約

一段以 rolecontent 物件表示的對話,最終都必須被轉換成一串 token 序列。聊天樣板(chat template)的作用,就是插入用來標記 system、user、assistant 以及訊息邊界的控制符號(control token):

結構化訊息
        ↓ 模型專屬的聊天樣板
[system 控制符號]     指令
[user 控制符號]       請求
[assistant 控制符號]  回應
        ↓ 分詞器(tokenizer)
訓練用的 Token ID

不同的模型家族,使用的控制符號完全不同。舉例來說,LLaMA-3 用 <|start_header_id|><|eot_id|> 來組織提示詞的結構,而 Mistral 則使用 [INST][/INST]。訓練與推論階段,都必須使用與所選 tokenizer 完全對應的樣板;若手動模仿另一個模型的格式,會明顯降低回應品質。

transformers 函式庫透過 tokenizer.apply_chat_template 這個方法,將這個轉換過程開放給使用者:

from transformers import AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("mistralai/Mistral-7B-Instruct-v0.1")
chat = [
  {"role": "system", "content": "You are a financial assistant."},
  {"role": "user", "content": "Analyze this 10-K excerpt."},
  {"role": "assistant", "content": "{\"risks\": [...]}"}
]

# 這會輸出一個帶有控制符號、格式正確的扁平字串
tokenizer.apply_chat_template(chat, tokenize=False)

Hugging Face chat-template guide

損失遮罩(Loss Masking):只計算 Completion 的損失

文字格式化完成後,我們還必須告訴優化器該學什麼。在只計算 completion 損失(completion-only)的訓練方式中,prompt 的 token 提供必要的情境脈絡,但不應該對損失有任何貢獻。我們只想針對 assistant 回應中的錯誤來懲罰模型。

我們透過標籤遮罩(label mask)來達成這件事。忽略值 -100 會告訴 PyTorch 的交叉熵(cross-entropy)實作,不要對這些位置計分:

Token:  [系統指令]     [使用者提示]     [助理回應]
標籤:   [   -100   ]   [   -100   ]     [ 在此計算損失 ]

透過遮罩 prompt,我們能把學習訊號完全集中在模型必須產生的行為上。在目前版本的 TRL(Transformer Reinforcement Learning)函式庫中,prompt-completion 格式的資料集預設就會使用 completion-only 損失。對話格式的資料集,則可以透過設定 assistant_only_loss=True 來啟用這個行為,前提是所使用的聊天樣板必須提供有效的 generation mask。 (實作細節請參考 TRL SFTTrainer loss-masking documentation

在其他情境下,對整個序列進行訓練同樣是合理的目標,因此是否採用「只針對回應遮罩」的做法,應該經過刻意選擇,而不是當成放諸四海皆準的規則。

全參數微調 vs. PEFT

全參數微調(full fine-tuning)會更新模型中的每一個參數,因此需要龐大的記憶體來儲存梯度與優化器狀態。每一個專精化後的模型,都必須完整儲存一整組動輒數 GB 的權重。

參數高效微調(Parameter-efficient fine-tuning, PEFT)則凍結大部分的基礎模型,只訓練其中一小部分參數。業界普遍採用的分類方式,把 PEFT 分成三種高階策略:

PEFT 策略運作機制代表方法
加法式(Additive)凍結基礎模型,並加入新的可訓練參數。Adapter 模組、prompt tuning、prefix tuning、P-tuning
選擇式(Selective)只更新既有參數中,經過挑選的一小部分。BitFit、LayerNorm tuning、部分層解凍
重參數化(Reparameterisation)用一個精簡的數學結構,來表達權重的更新。LoRA、AdaLoRA、DoRA

LoRA 屬於重參數化家族,而 QLoRA 其實就是把 LoRA 搭配一個量化過的基礎模型而已。雖然 PEFT 大幅降低了運算成本,但它並不能彌補品質不佳的資料;你的適應結果,終究取決於你的標註品質。

這套分類方式,出自 Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning

第二部分:LoRA 與 QLoRA


LoRA 概念示意圖

LoRA:低秩適應(Low-Rank Adaptation)

LoRA 的核心觀察是:對於一個下游任務(例如把一個通用模型改造成能解析複雜 SEC 10-K 文件)而言,真正有用的參數變化量,其內在維度(intrinsic dimension)往往遠低於原始權重矩陣的維度。

對於一個凍結的權重矩陣 W0W_0,LoRA 學習的是一個更新量:

W=W0+ΔWW' = W_0 + \Delta W

它不直接訓練一個龐大的密集矩陣,而是把這個更新量,分解成兩個較小的矩陣:

ΔW=αrBA\Delta W = \frac{\alpha}{r}BA

其中 ARr×kA \in \mathbb{R}^{r \times k}BRd×rB \in \mathbb{R}^{d \times r} 是可訓練的參數,rr 是 adapter 的秩(rank),而 α\alpha 則控制更新量的縮放比例。只要讓 rr 遠小於 ddkk,可訓練參數量就能從 dkdk 大幅降低到約 r(d+k)r(d+k)

舉例來說,一個 768×768768 \times 768 的方陣更新量,包含 589,824 個參數。而一個 rank-16 的 LoRA 分解,則只包含:

16(768+768)=24,57616(768+768)=24{,}576

個可訓練參數——對這個矩陣而言,足足少了 24 倍。實際節省的幅度,則取決於矩陣的形狀、選定的 rank,以及套用 adapter 的模組數量。

低秩假設(low-rank hypothesis)指的並不是原始權重矩陣本身是低秩的,而是任務特定的「變化量」ΔW\Delta W,往往可以只用少數幾個有用的方向來表達。LoRA 學的正是這些方向,同時讓 W0W_0 保持凍結。

LoRA 通常套用在注意力機制(attention)的投影層上,有時也會套用到前饋網路(feed-forward)的投影層。較高的 rank 能提供更多的適應能力,但也會增加記憶體用量、儲存空間,以及擬合雜訊的風險。因此 rank 是一個需要透過模型選擇來決定的參數,而不是一個「越高越好」的品質設定。

初始化、縮放與 Target Modules

LoRA 在一開始,必須完全不干擾預訓練模型的行為。常見的初始化方式,是讓其中一個矩陣使用非零的隨機值,另一個矩陣則初始化為零。Hugging Face PEFT 的標準初始化方式,是讓 AA 使用隨機值,BB 則初始化為零。由於 BA=0BA=0,第一次前向傳播(forward pass)的行為,會與基礎模型完全一致。

Rank(rr)決定了 adapter 的學習容量與參數量,而 α/r\alpha/r 這個比例則決定更新量的縮放幅度。並不存在一個放諸四海皆準的最佳比例,因此這兩個參數都需要針對你自己的資料集,透過實驗來調整。

早期的 LoRA 研究,幾乎只鎖定注意力機制的投影層,但現在常見的 LLM 訓練配方,往往會涵蓋更廣的一組層,包括 query、key、value 與 output 投影層,以及前饋網路中的 gate、up、down 投影層。在我們的 10-K 案例研究中,設定 target_modules="all-linear",能確保同時涵蓋注意力機制與前饋網路的投影層,而不需要針對特定模型手動寫死各層的名稱。

訓練完成後,你還必須決定如何部署這個 LoRA 更新量:

  • 動態交換(保持分離):如果同一個基礎模型要服務多個不同任務(例如即時在「10-K 風險 adapter」與「一般摘要 adapter」之間切換),這種方式在記憶體使用上會非常有效率。
  • 權重合併(融合回基礎模型):把 adapter 合併回基礎權重,可以在推論時省去額外的 adapter 運算。當你要部署的是單一、固定、專門用於財務分析的版本時,這是理想的做法。

原始方法的詳細內容,可參考 LoRA: Low-Rank Adaptation of Large Language Models。目前的 PEFT 實作,則透過 LoraConfig 開放 rank、縮放比例、dropout 與 target modules 等設定(詳見 Hugging Face PEFT LoRA guide)。

QLoRA:降低凍結模型的記憶體用量

雖然 LoRA 減少了可訓練參數的數量,但那個龐大的凍結基礎模型,依然必須完整載入 VRAM。QLoRA 補上了這個缺口:透過把基礎模型以 4-bit 精度載入,同時仍在這個量化後的模型上,以較高精度訓練 LoRA adapter,讓單一加速器(single-accelerator)上的微調,變得實際可行。

幾個關鍵的區別在於:

  • 預訓練的基礎權重被量化為 4-bit,並維持凍結狀態。
  • LoRA adapter 的參數仍然是可訓練的。
  • 矩陣乘法運算,則是以較高精度進行(例如 BF16)。

核心機制

QLoRA 能讓單一加速器上的實驗變得可行,但這並非沒有代價。量化會帶來表示誤差(representation error),後端的相容性也很重要,而且最終的模型,仍然必須在目標任務上接受評估。Transformers 官方文件建議,訓練 4-bit 基礎模型時使用 NF4。

QLoRA 之所以能達到如此極端的記憶體節省,靠的是三項具體的創新:

  1. Normal Float 4-bit(NF4): 一種在數學上經過優化的 4-bit 資料型別,能夠貼合預訓練神經網路權重的常態分布——數值集中在 0 附近的較多,極端值則較少。
  2. 雙重量化(Double quantisation): 把第一次量化步驟所使用的縮放常數,再進行一次量化,進一步降低 metadata 的額外開銷。
  3. 分頁優化器(Paged optimisers): 利用統一記憶體分頁(unified memory paging)機制,來吸收優化器暫時性的記憶體峰值——當 GPU 記憶體不足時,自動把優化器狀態分頁到 CPU RAM,避免記憶體不足導致的當機。

QLoRA 的原始論文,展示了在單張 48 GB GPU 上微調一個 650 億參數模型的成果。但這個結果,展現的是這個方法的潛力,而不是一個放諸四海皆準的硬體保證。訓練所需的總記憶體,還包括量化 metadata、activations、LoRA 參數、優化器狀態、暫時性的工作空間,以及依工作負載而定的上下文長度;這些都無法只從原始的 4-bit 權重大小推算出來。

見:
QLoRA: Efficient Finetuning of Quantized LLMs
Hugging Face bitsandbytes guide

第三部分:評估微調後的模型

評估所要回答的問題,遠比「訓練損失是否下降」廣泛得多:這次的適應,究竟有沒有在未見過的資料上,帶來可靠、實際的改善?要回答這個問題,需要嚴謹的基準模型、受保護的測試集、任務層級的指標,以及錯誤分析。

建立有意義的基準模型

在宣稱微調成功之前,至少要用相同的測試範例與解碼設定,把微調後的模型和未經修改的原始模型進行比較。未經修改的模型,應該搭配一個夠好的提示詞,內容包含任務定義、輸出格式,並在適當情況下附上少量示範範例。

一個有用的比較,可以包含:

  1. 使用 zero-shot 提示詞的基礎模型。
  2. 使用最強 few-shot 提示詞的基礎模型。
  3. 使用其預期推論提示詞的微調後模型。

這樣的拆解,能證明你這耗費成本的訓練過程,究竟是真的產生了新的能力,還是同樣的結果,光靠巧妙的提示詞設計就能達成。

防止資料外洩

訓練集、驗證集與測試集,各自有不同的用途。訓練範例用來產生參數更新,驗證範例用來引導模型與超參數的選擇,而測試集,則是在這些決策都底定之後,提供最終的效果估計。

當文件中存在重複內容、修訂版本或重複樣板時,隨機切分訓練/測試集是很危險的做法。應該依照可能造成外洩的單位(例如文件、客戶、公司或時間區間)來切分,並讓近似重複(near-duplicate)的內容留在同一個切分中。測試集也應該反映模型實際使用時的情境,包括困難案例與負面案例。

如果你依段落隨機切分資料集,測試集中就會出現與訓練資料高度相似的內容。

  • 依實體切分:依公司股票代號或 CIK 來分組。這能確保模型是在完全沒見過的公司上接受評估。
  • 保留分布:確保你的測試集能反映實際的生產環境,包括困難的邊界案例,以及負面案例(也就是不存在重大風險的文件)。

衡量應用程式的契約

困惑度(perplexity)與驗證損失衡量的是 token 預測的好壞,並不能衡量這個應用程式運作起來是否安全。評估指標,應該要反映使用者真正仰賴的行為:

評估面向範例指標
任務準確度精確率(Precision)、召回率(Recall)、F1、完全匹配(exact match),或欄位層級的準確度
輸出契約解析成功率、schema 有效性,以及允許值的合規性
事實根據(Grounding)證據有效性、事實一致性,以及缺乏依據的宣稱比例
拒答(Abstention)在證據缺失、不相關或含糊不清時的判斷準確度
穩健性依輸入長度、領域、難度與未見過的來源而定的表現
一般能力針對指令遵循能力與不相關任務的回歸測試
營運效率延遲、吞吐量、記憶體用量,以及 adapter 儲存空間

並非每一項任務都需要用到所有指標。一個分類器,可能主要依精確率、召回率與校準程度來評判;一個結構化擷取系統,還需要 schema 與證據檢查;而一個開放式的助理,則需要一套經過仔細定義的人工審查準則(rubric)。

決定性的檢查(例如 JSON 解析,以及針對證據的精確字串匹配),應該完全自動化。然而像完整性、忠實度、清晰度,以及適當程度的不確定性表達,這類語意層面的性質,往往需要一套仔細定義的人工審查準則,或是一套經過驗證的 LLM-as-a-judge 流程。

人工比較時,應該使用書面的審查準則,並將輸出結果匿名化,讓審查者無法得知每個回應是由哪個系統產生。結果的呈現方式,也應該依重要的切分維度分別報告,而不是只給一個整體平均值,同時也要檢視具代表性的偽陽性、偽陰性、幻覺內容,以及格式錯誤的案例。

說到底,一個微調後的模型是否算成功,取決於它相對於基準模型的改善幅度,是否值得付出訓練、評估、儲存與服務所需的成本。

第四部分:實作案例——SEC 10-K 風險分析

相關的分析 notebook 可以在這裡找到: https://github.com/SzuYaoC/llm-fine-tuning-10K-report/tree/master.

1. 問題定義、目標與任務

10-K 中 Item 1A 的風險揭露內容,對分析師、法遵團隊與風險管理人員而言相當有價值,但這些內容通常又長又重複,而且以非結構化的語言呈現。類似的風險,在不同公司之間可能會用完全不同的方式描述,而人工分類與摘要,也可能出現不一致的情況。

本文提出的系統,是一個分析輔助工具,而不是投資建議引擎或重大性(materiality)判斷引擎。它會把輸入的揭露段落,轉換成一筆結構化、可供審查的紀錄。它的目標是:

  1. 降低辨識揭露風險所需的人力;
  2. 把這些風險統一成一致的 schema;
  3. 保留精確的證據,以維持可追溯性;以及
  4. 判斷微調是否真的優於一個經過良好提示的基礎模型。

模型收到的輸入,是一段 10-K 摘錄:

A substantial portion of our cash is held outside our home market. 
Restrictions on transferring those funds or adverse currency movements 
could reduce the amount available for domestic operations.

輸出則是:

{
  "risks": [
    {
      "category": "Financial",
      "summary": "Transfer restrictions and currency movements may reduce cash available for domestic operations.",
      "evidence": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations."
    }
  ],
  "insufficient_evidence": false
}

模型必須辨識出零個或多個風險、指派一個本實驗自訂的分類、為每個風險撰寫摘要、複製支持該風險的證據,並在該段落無法支持任何擷取結果時選擇拒答(abstain)。它不能使用段落以外的事實,也不能預測該風險是否真的會發生。比較不同年度揭露內容之間的差異,是一個可能的後續任務,但在這第一個實驗中,刻意將其排除在外。

2. 資料問題

建模任務,首先要面對的是一個資料工程問題。一套正式的生產環境 pipeline,會從 SEC EDGAR 擷取文件、辨識出 Item 1A、移除文件標記語法(markup)、把該段落切分成一個個語意完整的揭露段落,並保留足夠的來源資訊,讓每個段落都能追溯回原始出處。每一筆紀錄,至少應該保留公司識別碼、accession number、申報日期、來源網址、所屬段落,以及原始文字。

SEC EDGAR API documentation

這個實驗使用的是合成(synthetic)的揭露內容,目的是讓整個工作流程可以重現、也可以自由散布。這份合成資料,適合用來測試訓練與評估的 pipeline,但無法用來確立模型在真實申報文件上的表現。

資料的真實來源(source of truth),是一筆標準化的標註紀錄,而不是針對特定模型設計的 prompt。一筆簡化後的紀錄大致長這樣:

{
  "example_id": "train_001",
  "split": "train",
  "issuer_id": "issuer_01",
  "filing_year": 2023,
  "source_text": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations.",
  "risks": [
    {
      "category": "Financial",
      "summary": "Transfer restrictions and currency movements may reduce cash available for domestic operations.",
      "evidence": "Restrictions on transferring those funds or adverse currency movements could reduce the amount available for domestic operations."
    }
  ],
  "insufficient_evidence": false
}

這套風險分類體系(taxonomy),刻意設計得精簡且具備可操作性。它的作用是統一預期輸出的格式,而不是宣稱自己是一套放諸四海皆準、涵蓋所有 10-K 風險的通用分類:

分類揭露風險範例
Financial(財務)流動性、信用、匯率、利率或資金曝險
Operational(營運)供應鏈、製造、產能或營運持續性中斷
Market(市場)需求、定價、競爭或總體經濟曝險
Regulatory(法規)法律、調查、合規義務或政策變動
Cybersecurity(資安)資安事件、系統遭入侵或資料外洩
Strategic(策略)併購、集中度、執行力或商業模式風險
People(人才)人才留任、勞動力供給、領導層或勞動力風險

整個資料集包含 68 筆範例:

切分範例數設計方式
訓練集40來自開發用發行人(issuer)的 2022–2023 年揭露內容
驗證集12來自相同發行人的 2024 年揭露內容
測試集16來自完全未見過的發行人的 2025 年揭露內容

每一個切分,都同時包含正例、負例與多風險範例。負例用來教模型學會拒答;多風險範例則能避免模型誤以為每個段落都恰好只對應一個標籤。證據必須是直接從原文複製而來,這讓 grounding(事實根據)可以用機械化的方式驗證。

這樣的切分設計,是為了降低外洩風險。開發用發行人只會出現在訓練集與驗證集中,而測試用發行人則被完全排除在訓練過程之外。系統也會針對正規化後的段落跨切分做比對,避免近似重複的用語,讓評估結果變成單純的記憶背誦。

只有在通過這些檢查之後,每一筆標準化紀錄,才會被轉換成 SFT 所需的對話式 prompt-completion 結構:

{
  "prompt": [
    {"role": "system", "content": "Extract only risks supported by the supplied 10-K excerpt..."},
    {"role": "user", "content": "<10-K excerpt>"}
  ],
  "completion": [
    {"role": "assistant", "content": "{\"risks\": [...], \"insufficient_evidence\": false}"}
  ]
}

讓標準化標註與實際渲染出的 prompt 保持分離,好處是即使要更換聊天樣板或模型家族,也不需要重寫標註內容。在訓練之前,pipeline 會驗證 schema、分類值、精確的證據範圍、拒答邏輯、切分歸屬、近似重複的控制機制,以及 token 長度。

3. 實驗方法:一場受控的 LoRA–QLoRA 比較

這個實驗以 Qwen/Qwen3-0.6B 為起點。這是一個具備指令遵循能力的因果語言模型,體積小到足以做成一個容易上手的示範,同時仍然支援大型模型所使用的聊天樣板與結構化生成工作流程。刻意選擇從一個指令模型出發,是有意為之的:這個實驗的目的,是把一個既有的助理,特化成執行一種受限的擷取行為,而不是嘗試用一個小資料集,從頭教會一個基礎模型如何遵循指令。

設定
任務有事實根據的多風險擷取,輸出為 JSON
基礎 checkpointQwen/Qwen3-0.6B
訓練目標Completion-only 的因果語言建模
LoRA rank 與 alphar=16alpha=32
Target modules所有線性的注意力與前饋網路投影層
上下文長度上限1,024 tokens
評估對象基礎模型 zero-shot、基礎模型 few-shot、LoRA、QLoRA

因此,這個實驗真正想觀察的差異,是訓練過程中凍結的基礎模型是以什麼方式儲存:

訓練分支凍結的基礎模型可訓練參數
LoRABF16 或 FP16LoRA adapter
QLoRA4-bit NF4,搭配雙重量化LoRA adapter

為了讓這個比較有意義,兩個分支使用完全相同的資料集切分、prompt 格式、最大序列長度、rank、縮放比例、target modules、優化器設定、隨機種子,以及評估流程。LoRA 的設定如下:

lora_config = LoraConfig(
    task_type="CAUSAL_LM", # 模型執行的是因果式的下一個 token 生成
    r=16,                  # 低秩更新量的容量與大小
    lora_alpha=32,         # 套用在該更新量上的縮放係數
    lora_dropout=0.05,     # 正則化(regularisation)
    target_modules="all-linear", # 套用 LoRA adapter 的模型層,一般包含注意力機制與前饋網路
    bias="none",           # 是否訓練既有的 bias 參數
)

target_modules="all-linear" 會把 adapter 套用到模型中所有線性的注意力與前饋網路投影層。這對本實驗來說相當方便,因為不需要針對特定架構手動寫死模組名稱,不過在正式的生產研究中,仍然應該檢查實際被匹配到的模組,並將其記錄下來。

這並不是在比較兩種不同的 adapter 演算法。QLoRA 訓練的,依然是 LoRA 參數;量化所降低的,是在訓練期間維持凍結基礎模型可用所需的記憶體。預期中的取捨,是較低的記憶體峰值,但可能伴隨訓練動態上的些微變化。

因此,這個受控實驗真正想回答的問題是:相較於一般的 LoRA,QLoRA 能否在不明顯犧牲擷取準確度、事實根據,或 schema 合規性的前提下,降低訓練期間的記憶體峰值。

4. 使用 Completion-Only SFT 進行訓練

每個訓練分支,都使用相同準備好的 prompt-completion 紀錄,以及相同的 LoRA 設定。整個實驗透過一個 TRAINING_METHOD 旗標來控制;唯一會改變的,是凍結基礎模型的載入方式。

在 QLoRA 的情況下,我們會動態注入一組 4-bit 量化設定:

if TRAINING_METHOD == "qlora":
    load_kwargs["quantization_config"] = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_use_double_quant=True,
        bnb_4bit_compute_dtype=compute_dtype,
    )

設定 Trainer

本教學使用的設定,包括 5 個 epoch、學習率 2e-4、adapter rank 為 16,並搭配梯度累積(gradient accumulation)與 checkpointing 等節省記憶體的技巧。最大序列長度設為 1024,以容納 SEC 的摘錄內容。這些是本次實驗所使用的設定,而不是放諸四海皆準的預設值。

Trainer 接收這些對話格式的紀錄,並被明確指示使用 completion-only 損失:

training_args = SFTConfig(
    learning_rate=2e-4,
    num_train_epochs=5,
    per_device_train_batch_size=1,
    gradient_accumulation_steps=4,
    gradient_checkpointing=True,
    max_length=1024,
    completion_only_loss=True,
    eval_strategy="epoch",
    save_strategy="epoch",
)

trainer = SFTTrainer(
    model=model,
    args=training_args,
    processing_class=tokenizer,
    train_dataset=dataset["train"],
    eval_dataset=dataset["validation"],
)

傳入 processing_class=tokenizer 這一步非常關鍵:它會告訴 trainer 該如何套用聊天樣板、將內容渲染成字串,並轉換成 token ID。

理解訓練批次中的 Tensor

在幕後,trainer.get_train_dataloader() 會建構出實際輸入模型的一個個 batch。每個 batch,都由三個對齊好的整數矩陣組成,形狀通常是 [batch_size, sequence_length]

Tensor用途
input_ids代表渲染後的 system 訊息、user 摘錄與 assistant completion 的詞彙索引整數。
attention_mask真正的 token 為 1,padding 則為 0,用來告訴注意力機制該忽略哪些位置。
labels用於梯度計算的目標 token。prompt 與 padding 的位置,都會被設為 -100

以下用概念性的方式,呈現單一筆訓練資料的樣貌,展示 completion-only 損失是如何遮罩 prompt 的:

位置內容input_idsattention_masklabels(目標)
0System1011-100(忽略)
1User2051-100(忽略)
2User3011-100(忽略)
3Assistant4011401(監督學習)
4Assistant4021402(監督學習)
5Padding00-100(忽略)

雖然上面的示意有經過簡化,但理解這些 tensor 背後的運作機制,對除錯來說相當重要:

  • 因果位移(Causal Shift):注意在受到監督的位置(3 與 4),labels 的 ID 其實與 input_ids 的值完全相同。這是因為 PyTorch 的因果語言建模損失,內部會把 labels 位移一個位置。模型在位置 tt 的預測,會拿去和位置 t+1t+1 的 label 做比對評估。

  • 動態 Padding:當 per_device_train_batch_size=1 時,每個矩陣恰好只有一列。如果 batch size 增加,較短的範例就會被動態 padding,補齊到該 batch 中最長序列的長度。序列長度的上限是 1,024,但大多數 batch 的實際寬度都會小於這個數字。

  • 索引 vs. Embeddinginput_ids 矩陣裡存放的,並不是密集向量形式的 embedding,而是單純的詞彙索引,模型會用這些索引去查表,取得實際的 embedding,再進一步產生形狀為 [batch_size, sequence_length, d_model] 的 hidden-state tensor。

最後,每一次訓練都會記錄模型識別碼、隨機種子、LoRA 設定、可訓練參數量、GPU 記憶體峰值,以及訓練所需時間。雖然驗證損失會被記錄下來,用以監控優化過程是否健康,但它並不能取代第三部分中詳述的、嚴謹的任務層級評估。

每一次訓練都會記錄模型識別碼、隨機種子、LoRA 設定、函式庫版本、可訓練與總參數量、訓練所需時間、GPU 記憶體峰值,以及 trainer 的各項指標。驗證損失用於監控優化過程,並不能取代任務層級的評估。

5. 評估與基礎模型比較

這套評估流程的設計目的,是在相同的保留資料、輸出長度限制,以及決定性(deterministic)解碼策略下,比較四個系統:

  1. 使用 zero-shot 指令的未修改基礎模型;
  2. 搭配具代表性正例與負例的未修改基礎模型;
  3. LoRA adapter;以及
  4. QLoRA adapter。

這次完整執行的實驗,使用的是 Qwen/Qwen3-0.6B,以及來自八家發行人、全部 16 筆保留下來的合成範例。推論階段在 Apple MPS 上以 FP16 執行。兩個 adapter 都分別載入到與基準模型相同、較高精度的基礎 checkpoint 上。因此,這個比較所衡量的,是每個 adapter 所學到的行為本身,而不是在比較 FP16 與 4-bit 推論的差異。

指標檢驗內容
JSON 與 schema 有效性下游程式碼,是否能安全地處理這個回應。
風險精確率、召回率與 F1標註的風險,是否在沒有多抓或漏抓的情況下被擷取出來。
分類 Macro F1表現是否不只侷限在出現頻率最高的分類上。
證據有效性每一段被引用的內容,是否確實出現在提供的摘錄之中。
無依據證據比例生成的證據內容,出現幻覺的頻率有多高。
拒答準確度不含風險的段落,是否被正確地拒答。
延遲與每秒 token 數在既定測試環境下的執行成本。
訓練記憶體峰值與所需時間訓練所耗費的資源(僅在相同硬體下才具可比性)。
Adapter 大小每個任務所需的儲存空間。

備註:無效的 JSON、無依據的證據,以及遺漏的風險,這三者被刻意區分開來。schema 失敗只是格式上的錯誤,不代表這必然是幻覺內容。以下的自動化指標,是我們針對摘要忠實度所做的盲測人工審查之前的步驟。

品質結果

系統JSON 有效Schema 有效風險精確率風險召回率風險 F1分類 Macro F1證據有效拒答準確度
基礎模型,zero-shot100.0%56.3%11.1%6.7%8.3%2.4%100.0%43.8%
基礎模型,few-shot100.0%100.0%33.3%20.0%25.0%20.0%100.0%81.3%
LoRA adapter100.0%100.0%63.6%46.7%53.8%76.2%100.0%100.0%
QLoRA adapter100.0%100.0%58.3%46.7%51.9%70.1%91.7%93.8%

Few-shot 提示詞本身,就已經是比 zero-shot 指令強得多的基準了。相對於這個較強的基準,兩個 adapter 都維持了完美的 schema 有效性。LoRA 的表現明顯優於這個強力的 few-shot 基準,風險 F1 提升了 28.8 個百分點,拒答準確度也提升了 18.8 個百分點。QLoRA 則緊追在後。雖然 LoRA 在精確率與事實根據上,對 QLoRA 仍保有些微優勢,但由於樣本數很小(n=16n=16),這些數字的作用,是展示我們這套分析流程的嚴謹程度,而不是要證明某種演算法在通則上更為優越。

效率結果

系統平均延遲輸出 tokens/sAdapter 大小訓練記憶體峰值訓練時間
基礎模型,zero-shot1.47 s58.90
基礎模型,few-shot0.72 s54.30
LoRA adapter1.19 s36.6749.46 MiB未記錄63.63 s
QLoRA adapter1.25 s36.4149.46 MiB1.63 GiB84.84 s

延遲是在 Apple MPS 上依序測量的,其中包含了不同的輸出長度,以及尚未合併的 PEFT 額外開銷。因此,這些數字的作用,是提供一個觀察性的基準,而不是一個與硬體無關的通用 benchmark。兩個 adapter 理所當然地擁有完全相同的 49.46 MiB 大小;4-bit 量化壓縮的是訓練期間的凍結基礎模型,並不會改變最終儲存下來的 adapter 本身的維度。

同樣地,訓練時間(LoRA 為 63.63 秒;QLoRA 為 84.84 秒)與記憶體配置(LoRA 在執行結束時的快照為 1.27 GiB,QLoRA 記錄到的峰值則為 1.63 GiB),都只反映了這個特定的 MPS 環境。由於記憶體剖析(profiling)所擷取的,是不同的生命週期階段,這些數值並不能作為量化 QLoRA 記憶體節省效果的受控基準。

通往生產環境之路

聚合指標只能說明部分的故事。有意義的評估,需要依產業別、風險分類、負例,以及多風險段落,去細看錯誤的分布。雖然我們這份包含 16 筆範例的合成資料集,成功驗證了整套評估機制的可行性,但要證明系統真正具備生產環境的可用性,還需要嚴謹得多的驗證,包括:

  • 一份規模夠大、嚴格保留、經過人工審查的 EDGAR 摘錄測試集。
  • 系統性的不確定性估計,以及盲測式的領域審查。
  • 涵蓋核心擷取任務以外、範圍更廣的回歸測試。

說到底,微調要在整個技術堆疊中真正站穩腳步,唯有當一個 adapter 能明顯勝過合理範圍內最強的提示詞基準、完美保留證據的事實根據,並帶來足以正當化其額外營運複雜度的效能提升,才算真正做到。