SmolLM3-3B-banking77

HuggingFaceTB/SmolLM3-3B fine-tuned to sort online-banking customer messages into the 77 intents of BANKING77. Made as a final project of the Hugging Face smol-course.

Results

All 3080 messages of the BANKING77 test split, which training never used. The model writes the intent name, and an answer that isn't exactly one of the 77 names counts as wrong.

Model Accuracy Macro precision Macro recall Macro F1 Valid intent name
Untrained 46.9% 64.0% 46.9% 46.2% 98.9%
This model (SFT) 93.3% 93.5% 93.3% 93.3% 100.0%

DPO was also tried after SFT but made validation accuracy worse, so it was dropped and never used on the test (see Training). The published model is the SFT model.

Change in accuracy compared with the untrained model: +46.3% (95% range about ±1.8%).

The untrained model already saw the full list of intent names in its prompt, so this compares the same prompt before and after training.

Test results before and after fine-tuning

Most common mix-ups

Right intent What the model said Times
beneficiary_not_allowed failed_transfer 6
pending_transfer balance_not_updated_after_bank_transfer 5
reverted_card_payment declined_card_payment 5
card_arrival card_delivery_estimate 4
card_delivery_estimate card_arrival 4
top_up_reverted top_up_failed 4
card_payment_not_recognised compromised_card 4
topping_up_by_card pending_top_up 4

The 8 hardest intents on the test

Training

  • Data: 6992 training messages for SFT, 2000 for DPO and 1000 for validation, all from the BANKING77 training split. Messages repeated in the test split were removed.
  • Step 1, SFT + LoRA (r=16, alpha=32, all linear layers): only the intent name counts towards the loss. It stopped at its 3.5-hour time limit, after 1,619 of 1,748 planned updates, while validation was still improving slightly. The version with the best validation accuracy (update 1,600) was kept.
  • Step 2, DPO + LoRA on 2000 pairs. The rejected answer was the SFT model's own mistake (147 pairs) or, when it was right, the intent most often confused with the right one (1853 pairs).
  • DPO decision: Not kept. On validation it newly got 10 messages right and 52 wrong compared with SFT (-4.2%, ±1.5%), which isn't a clear gain. The published model is the SFT model.
  • The best versions were chosen using validation only, and thinking mode was off.

Validation accuracy during SFT and DPO

Validation accuracy rose from 44.7% to 93.7% during SFT. DPO then lowered it to 85.8%, so DPO was dropped. DPO's own loss kept falling while its accuracy fell, which is why real answers were scored at every check.

SFT training and validation loss

Limits

  • It knows only these 77 intents, for one online bank's kind of messages, in English. A message about anything else still gets one of the 77.
  • The prompt must list the intents, as in the example below.
  • BANKING77 is licensed CC BY 4.0.

How to use it

from transformers import AutoModelForCausalLM, AutoTokenizer

tokenizer = AutoTokenizer.from_pretrained("BasselNashaat/SmolLM3-3B-banking77")
model = AutoModelForCausalLM.from_pretrained("BasselNashaat/SmolLM3-3B-banking77", device_map="auto")
intents = ["activate_my_card", "age_limit", "apple_pay_or_google_pay", "atm_support", "automatic_top_up", "balance_not_updated_after_bank_transfer", "balance_not_updated_after_cheque_or_cash_deposit", "beneficiary_not_allowed", "cancel_transfer", "card_about_to_expire", "card_acceptance", "card_arrival", "card_delivery_estimate", "card_linking", "card_not_working", "card_payment_fee_charged", "card_payment_not_recognised", "card_payment_wrong_exchange_rate", "card_swallowed", "cash_withdrawal_charge", "cash_withdrawal_not_recognised", "change_pin", "compromised_card", "contactless_not_working", "country_support", "declined_card_payment", "declined_cash_withdrawal", "declined_transfer", "direct_debit_payment_not_recognised", "disposable_card_limits", "edit_personal_details", "exchange_charge", "exchange_rate", "exchange_via_app", "extra_charge_on_statement", "failed_transfer", "fiat_currency_support", "get_disposable_virtual_card", "get_physical_card", "getting_spare_card", "getting_virtual_card", "lost_or_stolen_card", "lost_or_stolen_phone", "order_physical_card", "passcode_forgotten", "pending_card_payment", "pending_cash_withdrawal", "pending_top_up", "pending_transfer", "pin_blocked", "receiving_money", "Refund_not_showing_up", "request_refund", "reverted_card_payment?", "supported_cards_and_currencies", "terminate_account", "top_up_by_bank_transfer_charge", "top_up_by_card_charge", "top_up_by_cash_or_cheque", "top_up_failed", "top_up_limits", "top_up_reverted", "topping_up_by_card", "transaction_charged_twice", "transfer_fee_charged", "transfer_into_account", "transfer_not_received_by_recipient", "transfer_timing", "unable_to_verify_identity", "verify_my_identity", "verify_source_of_funds", "verify_top_up", "virtual_card_not_working", "visa_or_mastercard", "why_verify_identity", "wrong_amount_of_cash_received", "wrong_exchange_rate_for_cash_withdrawal"]
message = "I still haven't received my new card"
question = ("Customer message: " + message + "\n\nWhich one of these intents does the message have?\n"
            + "\n".join(intents) + "\n\nReply with the intent name only, exactly as written.")
text = tokenizer.apply_chat_template([{"role": "user", "content": question}], tokenize=False,
                                     add_generation_prompt=True, enable_thinking=False)
inputs = tokenizer(text, return_tensors="pt", add_special_tokens=False).to(model.device)
output = model.generate(**inputs, max_new_tokens=20, do_sample=False)
print(tokenizer.decode(output[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True))
Downloads last month
173
Safetensors
Model size
3B params
Tensor type
F16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for BasselNashaat/SmolLM3-3B-banking77

Adapter
(78)
this model

Dataset used to train BasselNashaat/SmolLM3-3B-banking77