commit 70c646b3fa8394a6b08eded4fa5ac85a30df7c03 Author: ModelHub XC Date: Sun Jun 21 16:32:17 2026 +0800 初始化项目,由ModelHub XC社区提供模型 Model: cs-552-2026-Flash-McQueenS-and-TheKing/safety_model Source: Original Platform diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..52373fe --- /dev/null +++ b/.gitattributes @@ -0,0 +1,36 @@ +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text +tokenizer.json filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md new file mode 100644 index 0000000..05158d6 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +--- +license: apache-2.0 +base_model: Qwen/Qwen3-1.7B +language: +- en +pipeline_tag: text-generation +library_name: transformers +tags: +- safety +- qwen3 +- sft +- lora +- multiple-choice +datasets: +- cs-552-2026-Flash-McQueenS-and-TheKing/safety_sft_data +--- + +# safety_model — Qwen3-1.7B fine-tuned for safety multiple-choice (non-thinking) + +This is the **safety** individual model for the CS-552 (Modern NLP, EPFL, Spring 2026) +course project. It is a supervised fine-tune of **Qwen/Qwen3-1.7B** specialised for the +safety benchmark, which is **multiple-choice, scored pass@1**. + +The model answers in **non-thinking mode**: it emits a one-sentence justification and then +the answer letter inside `\boxed{}`, with no `` reasoning block. The rationale is given +in [Why non-thinking, and no CoT distillation](#why-non-thinking-and-no-cot-distillation). + +> **Output contract.** Every answer ends with the option letter wrapped in `\boxed{...}`, +> e.g. `\boxed{C}`. The boxing instruction and the non-thinking switch are baked into the +> tokenizer's `chat_template.jinja`, so they apply even when the evaluator calls +> `apply_chat_template(messages, add_generation_prompt=True)` with no extra arguments. + +## Training data + +The full training set is released as a companion dataset: +[**cs-552-2026-Flash-McQueenS-and-TheKing/safety_sft_data**](https://huggingface.co/datasets/cs-552-2026-Flash-McQueenS-and-TheKing/safety_sft_data). + +It contains **3,250 English multiple-choice items** across the seven safety categories of +SafetyBench (Zhang et al., 2024). Each example pairs a **user message** (a question with +labelled options) with an **assistant message** (a one-sentence justification followed by the +answer letter in `\boxed{}`). Four categories are derived from established public datasets; +three smaller categories are LLM-generated to cover topics with no convenient public source. + +| Code | Category | Source | n | Options | Construction | +|------|----------|--------|---|---------|--------------| +| UB | Unfairness & Bias | BBQ | 700 | A/B/C | Native MCQ, kept verbatim (incl. "Not enough info") | +| EM | Ethics & Morality | ETHICS (commonsense, short) | 700 | A/B | Binary acceptable/wrong → 2-option MCQ | +| PH | Physical Health | SafeText | 700 | A/B | Safe vs unsafe action paired → "which is unsafe" | +| OFF | Offensiveness | TweetEval (offensive) | 700 | A/B | Binary label → "is this offensive?" Yes/No | +| MH | Mental Health | LLM-generated | 150 | A–D | Synthetic, SafetyBench augmentation recipe | +| IA | Illegal Activities | LLM-generated | 150 | A–D | Synthetic, SafetyBench augmentation recipe | +| PP | Privacy & Property | LLM-generated | 150 | A–D | Synthetic, SafetyBench augmentation recipe | + +### Processing pipeline + +- **Letter balancing.** Within each category the correct option is shuffled per item so the + answer key is not concentrated on one position. (The aggregate A/B skew is a structural + consequence of most categories being 2- or 3-option, not a per-item bias.) +- **Synthetic validation.** The three generated categories were filtered for validity, + deduplicated, self-consistency-checked, and letter-balanced before inclusion. +- **Decontamination.** Because four categories come from public datasets that the safety + benchmark may also draw on, every training row was checked against the SafetyBench English + test split using word 8-gram containment and sentence-embedding cosine similarity; + near-duplicate rows were dropped. +- **Format.** Each example is a single user→assistant turn. The user turn carries the question + and labelled options; the assistant target is a one-sentence justification followed by + `\boxed{}`. + +## Fine-tuning + +| Setting | Value | +|---|---| +| Base model | `Qwen/Qwen3-1.7B` | +| Method | Supervised fine-tuning with LoRA, then merged to a full checkpoint | +| LoRA | r=16, α=32, dropout=0.05, all linear projections | +| Loss | Completion-only (prompt and the empty `` block are masked) | +| Epochs | 3 | +| Learning rate | 1e-4, cosine schedule, 3% warmup | +| Effective batch | 16 (per-device 2 × grad-accum 8) | +| Max sequence length | 1024 | +| Precision | bf16/fp16 | +| Mode | Non-thinking (`enable_thinking = false`) | + +The LoRA adapter was merged back into the base weights so this repository is a standalone, +vLLM-loadable Qwen3 checkpoint (full `model.safetensors`, `config.json`, +`generation_config.json`, and a tokenizer carrying `chat_template.jinja`). + +### Train / inference consistency + +Each training example was built as +`apply_chat_template(user, add_generation_prompt=True, enable_thinking=False)` + completion + +`<|im_end|>`, with the prompt tokens masked from the loss. The model is therefore trained on +**exactly** the prefix the evaluator produces — including the empty `` block +that Qwen3 emits in non-thinking mode — so there is no train/test format drift. + +### Generation config + +Sampling defaults favour low variance, because the benchmark is **pass@1**: a single +completion is scored, so determinism is worth more than diversity. We use a low temperature +while keeping `do_sample=true` as the project requires. For a pass@k task the opposite choice +would be appropriate. + +## Why non-thinking, and no CoT distillation + +A reasonable first instinct is to distill chain-of-thought (CoT) from a stronger model and +train the student to reason before answering. We deliberately did **not** do this, for four +reasons specific to this task: + +1. **The benchmark is not reasoning-intensive.** The SafetyBench authors explicitly omit + CoT-based evaluation, noting the benchmark is less reasoning-intensive than capability + benchmarks such as MMLU. Safety MCQ is largely knowledge and norm-judgment, not multi-step + deduction. +2. **CoT mostly helps on math and symbolic tasks.** The meta-analysis of Sprague et al. (2024), + "To CoT or not to CoT?", finds the large gains from CoT concentrate on math, logic, and + symbolic reasoning, with little benefit on knowledge/judgment multiple-choice. On such + tasks, directly emitting the answer is about as accurate as reasoning first. +3. **CoT can hurt small models on classification-style tasks**, and a 1.7B model is firmly in + the size range where this risk is real. Long reasoning traces also add variance to a + single-shot (pass@1) prediction. +4. **Token budget and the boxing contract.** Under a capped generation budget, a long `` + trace risks consuming the budget before reaching `\boxed{}` — a failure mode that scores ~0 + despite a fluent answer. Non-thinking emits the justification and box immediately, so answer + extraction is reliable and fast. + +There is also a data-integrity argument: generating CoT with a teacher that already knows the +gold label tends to produce post-hoc *rationalisations* rather than genuine reasoning, which is +especially hazardous on bias/safety items (e.g. BBQ ambiguous-context questions) where a fluent +justification can be built for a stereotyped wrong answer. + +Note the model is **not** answer-only: the one-sentence justification before the box is itself a +lightweight, in-format rationale. We keep that, and skip the heavier `` block. + +## Intended use and limitations + +- **Intended use.** Answering English safety multiple-choice questions in the `\boxed{}` + format. This is a research/coursework artifact. +- **Option-count coverage.** Training items span 2–4 options; performance on items with many + more options is less certain. +- **Category imbalance.** The four public-derived categories (700 each) are far larger than the + three synthetic ones (150 each), so the model is expected to be stronger on the former. +- **Not a deployable safety system.** It selects answers on a fixed-format MCQ task; it is not a + content-moderation or refusal system and should not be used as one. + +## How to use + +```python \ No newline at end of file diff --git a/chat_template.jinja b/chat_template.jinja new file mode 100644 index 0000000..e8acaa5 --- /dev/null +++ b/chat_template.jinja @@ -0,0 +1,93 @@ +{%- set enable_thinking = false %} +{%- if messages[0]['role'] != 'system' %} +{%- set messages = [{'role':'system','content':'You are answering a multiple-choice question. Give a one-sentence justification, then put ONLY the letter of the correct option inside \\boxed{}. Example final line: \\boxed{B}'}] + messages %} +{%- endif %} +{%- if tools %} + {{- '<|im_start|>system\n' }} + {%- if messages[0].role == 'system' %} + {{- messages[0].content + '\n\n' }} + {%- endif %} + {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }} + {%- for tool in tools %} + {{- "\n" }} + {{- tool | tojson }} + {%- endfor %} + {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }} +{%- else %} + {%- if messages[0].role == 'system' %} + {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }} + {%- endif %} +{%- endif %} +{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %} +{%- for message in messages[::-1] %} + {%- set index = (messages|length - 1) - loop.index0 %} + {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('') and message.content.endswith('')) %} + {%- set ns.multi_step_tool = false %} + {%- set ns.last_query_index = index %} + {%- endif %} +{%- endfor %} +{%- for message in messages %} + {%- if message.content is string %} + {%- set content = message.content %} + {%- else %} + {%- set content = '' %} + {%- endif %} + {%- if (message.role == "user") or (message.role == "system" and not loop.first) %} + {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }} + {%- elif message.role == "assistant" %} + {%- set reasoning_content = '' %} + {%- if message.reasoning_content is string %} + {%- set reasoning_content = message.reasoning_content %} + {%- else %} + {%- if '' in content %} + {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') %} + {%- set content = content.split('')[-1].lstrip('\n') %} + {%- endif %} + {%- endif %} + {%- if loop.index0 > ns.last_query_index %} + {%- if loop.last or (not loop.last and reasoning_content) %} + {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- else %} + {{- '<|im_start|>' + message.role + '\n' + content }} + {%- endif %} + {%- if message.tool_calls %} + {%- for tool_call in message.tool_calls %} + {%- if (loop.first and content) or (not loop.first) %} + {{- '\n' }} + {%- endif %} + {%- if tool_call.function %} + {%- set tool_call = tool_call.function %} + {%- endif %} + {{- '\n{"name": "' }} + {{- tool_call.name }} + {{- '", "arguments": ' }} + {%- if tool_call.arguments is string %} + {{- tool_call.arguments }} + {%- else %} + {{- tool_call.arguments | tojson }} + {%- endif %} + {{- '}\n' }} + {%- endfor %} + {%- endif %} + {{- '<|im_end|>\n' }} + {%- elif message.role == "tool" %} + {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %} + {{- '<|im_start|>user' }} + {%- endif %} + {{- '\n\n' }} + {{- content }} + {{- '\n' }} + {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %} + {{- '<|im_end|>\n' }} + {%- endif %} + {%- endif %} +{%- endfor %} +{%- if add_generation_prompt %} + {{- '<|im_start|>assistant\n' }} + {%- if enable_thinking is defined and enable_thinking is false %} + {{- '\n\n\n\n' }} + {%- endif %} +{%- endif %} \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000..e31f9e1 --- /dev/null +++ b/config.json @@ -0,0 +1,63 @@ +{ + "architectures": [ + "Qwen3ForCausalLM" + ], + "attention_bias": false, + "attention_dropout": 0.0, + "bos_token_id": 151643, + "dtype": "float16", + "eos_token_id": 151645, + "head_dim": 128, + "hidden_act": "silu", + "hidden_size": 2048, + "initializer_range": 0.02, + "intermediate_size": 6144, + "layer_types": [ + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention", + "full_attention" + ], + "max_position_embeddings": 40960, + "max_window_layers": 28, + "model_type": "qwen3", + "num_attention_heads": 16, + "num_hidden_layers": 28, + "num_key_value_heads": 8, + "pad_token_id": null, + "rms_norm_eps": 1e-06, + "rope_parameters": { + "rope_theta": 1000000, + "rope_type": "default" + }, + "sliding_window": null, + "tie_word_embeddings": true, + "transformers_version": "5.10.2", + "use_cache": true, + "use_sliding_window": false, + "vocab_size": 151936 +} diff --git a/generation_config.json b/generation_config.json new file mode 100644 index 0000000..7ddb6e2 --- /dev/null +++ b/generation_config.json @@ -0,0 +1,13 @@ +{ + "bos_token_id": 151643, + "do_sample": true, + "eos_token_id": [ + 151645, + 151643 + ], + "pad_token_id": 151643, + "temperature": 0.3, + "top_k": 20, + "top_p": 0.9, + "transformers_version": "5.10.2" +} diff --git a/model.safetensors b/model.safetensors new file mode 100644 index 0000000..c2806b0 --- /dev/null +++ b/model.safetensors @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef4e81286c5f5ef9d66c4e2c699d1bffef18fa6e71aa4b58490338a24c1b5de5 +size 3441185296 diff --git a/tokenizer.json b/tokenizer.json new file mode 100644 index 0000000..c7afbed --- /dev/null +++ b/tokenizer.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be75606093db2094d7cd20f3c2f385c212750648bd6ea4fb2bf507a6a4c55506 +size 11422650 diff --git a/tokenizer_config.json b/tokenizer_config.json new file mode 100644 index 0000000..770e41d --- /dev/null +++ b/tokenizer_config.json @@ -0,0 +1,30 @@ +{ + "add_prefix_space": false, + "backend": "tokenizers", + "bos_token": null, + "clean_up_tokenization_spaces": false, + "eos_token": "<|im_end|>", + "errors": "replace", + "extra_special_tokens": [ + "<|im_start|>", + "<|im_end|>", + "<|object_ref_start|>", + "<|object_ref_end|>", + "<|box_start|>", + "<|box_end|>", + "<|quad_start|>", + "<|quad_end|>", + "<|vision_start|>", + "<|vision_end|>", + "<|vision_pad|>", + "<|image_pad|>", + "<|video_pad|>" + ], + "is_local": false, + "local_files_only": false, + "model_max_length": 131072, + "pad_token": "<|endoftext|>", + "split_special_tokens": false, + "tokenizer_class": "Qwen2Tokenizer", + "unk_token": null +}