jpad commited on
Commit
1a1b460
·
verified ·
1 Parent(s): a6914f9

Add README.md

Browse files
Files changed (1) hide show
  1. README.md +152 -142
README.md CHANGED
@@ -26,7 +26,7 @@ extra_gated_fields:
26
 
27
  # NOPE Edge Mini - Crisis Classification Model
28
 
29
- A fine-tuned model for detecting crisis signals in text - suicidal ideation, self-harm, abuse, violence, and other safety-critical content. Designed for integration into safety pipelines, content moderation systems, and mental health applications.
30
 
31
  > **License:** [NOPE Edge Community License v1.0](LICENSE.md) - Free for research, academic, nonprofit, and evaluation use. Commercial production requires a separate license. See [nope.net/edge](https://nope.net/edge) for details.
32
 
@@ -34,10 +34,10 @@ A fine-tuned model for detecting crisis signals in text - suicidal ideation, sel
34
 
35
  ## Model Variants
36
 
37
- | Model | Parameters | Accuracy | Latency | Use Case |
38
- |-------|------------|----------|---------|----------|
39
- | **[nope-edge](https://huggingface.co/nopenet/nope-edge)** | 4B | **90.6%** | ~750ms | Maximum accuracy |
40
- | **[nope-edge-mini](https://huggingface.co/nopenet/nope-edge-mini)** | 1.7B | 85.9% | ~260ms | High-volume, cost-sensitive |
41
 
42
  This is **nope-edge-mini (1.7B)**.
43
 
@@ -60,6 +60,7 @@ pip install torch transformers accelerate
60
  ```python
61
  from transformers import AutoModelForCausalLM, AutoTokenizer
62
  import torch
 
63
 
64
  model_id = "nopenet/nope-edge-mini"
65
 
@@ -71,7 +72,7 @@ model = AutoModelForCausalLM.from_pretrained(
71
  )
72
 
73
  def classify(message: str) -> str:
74
- """Returns 'type|severity|subject' or 'none'."""
75
  input_ids = tokenizer.apply_chat_template(
76
  [{"role": "user", "content": message}],
77
  tokenize=True,
@@ -80,198 +81,218 @@ def classify(message: str) -> str:
80
  ).to(model.device)
81
 
82
  with torch.no_grad():
83
- output = model.generate(input_ids, max_new_tokens=30, do_sample=False)
84
 
85
  return tokenizer.decode(
86
  output[0][input_ids.shape[1]:],
87
  skip_special_tokens=True
88
  ).strip()
89
 
90
- classify("I want to end it all") # -> "suicide|high|self"
91
- classify("Great day at work!") # -> "none"
92
- classify("My friend said she wants to kill herself") # -> "suicide|high|other"
 
 
 
 
 
 
 
 
93
  ```
94
 
95
  ---
96
 
97
  ## Output Format
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  **Crisis detected:**
 
 
 
 
100
  ```
101
- {type}|{severity}|{subject}
 
 
 
102
  ```
103
 
104
- | Field | Values | Description |
105
- |-------|--------|-------------|
106
- | type | `suicide`, `self_harm`, `self_neglect`, `violence`, `abuse`, `sexual_violence`, `exploitation`, `stalking`, `neglect` | Risk category |
107
- | severity | `mild`, `moderate`, `high`, `critical` | Urgency level |
108
- | subject | `self`, `other` | Who is at risk |
109
 
110
- **No crisis:** `none`
 
 
 
 
 
 
111
 
112
  ### Subject Attribution
113
 
114
  | Subject | Meaning | Example |
115
  |---------|---------|---------|
116
- | `self` | The speaker is at risk or is the victim | "I want to kill myself", "My partner hits me" |
117
- | `other` | The speaker is reporting concern about someone else | "My friend said she wants to die" |
118
 
119
  ### Parsing Example
120
 
121
  ```python
 
 
 
 
 
 
 
 
 
 
 
 
122
  def parse_output(output: str) -> dict:
123
- output = output.strip().lower()
124
- if output == "none":
125
- return {"is_crisis": False}
126
-
127
- parts = output.split("|")
128
- return {
129
- "is_crisis": True,
130
- "type": parts[0] if len(parts) > 0 else None,
131
- "severity": parts[1] if len(parts) > 1 else None,
132
- "subject": parts[2] if len(parts) > 2 else None,
133
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  ```
135
 
136
  ---
137
 
138
- ## Input Best Practices
139
-
140
- ### Text Preprocessing
141
-
142
- **Preserve natural prose.** The model was trained on real conversations with authentic expression. Emotional signals matter:
143
-
144
- | Keep | Why |
145
- |------|-----|
146
- | Emojis | `💀` in "kms 💀" signals irony; `😭` signals distress intensity |
147
- | Punctuation intensity | "I can't do this!!!" conveys more urgency than "I can't do this" |
148
- | Casual spelling | "im so done" vs "I'm so done" — both valid, don't normalize |
149
- | Slang/algospeak | "kms", "unalive", "catch the bus" — model understands these |
150
-
151
- **Only remove:**
152
-
153
- | Remove | Example |
154
- |--------|---------|
155
- | Zero-width/invisible Unicode | `hello\u200bworld` → `helloworld` |
156
- | Decorative Unicode fonts | `ℐ 𝓌𝒶𝓃𝓉 𝓉𝑜 𝒹𝒾𝑒` → `I want to die` |
157
- | Newlines (single messages) | `I can't\ndo this` → `I can't do this` |
158
 
159
- **Keep newlines** when they provide turn structure (see Multi-Turn Conversations below).
160
 
161
- **Examples:**
 
 
 
 
 
 
162
 
163
- ```python
164
- # KEEP - emotional signal matters
165
- "I can't do this anymore 😭😭😭" # Keep emojis - signals distress
166
- "i want to die!!!!!!!" # Keep punctuation - signals intensity
167
- "kms lmao 💀" # Keep all - irony/context signal
168
-
169
- # NORMALIZE - only structural/invisible issues
170
- "ℐ 𝓌𝒶𝓃𝓉 𝓉𝑜 𝒹𝒾𝑒" → "I want to die" # Fancy Unicode fonts
171
- "I can't\ndo this\nanymore" → "I can't do this anymore" # Single message
172
- "hello\u200bworld" → "helloworld" # Zero-width chars
173
  ```
174
 
175
- **Minimal preprocessing function:**
176
 
177
- ```python
178
- import re
179
- import unicodedata
 
 
180
 
181
- def preprocess(text: str) -> str:
182
- # Normalize decorative Unicode fonts to ASCII (NFKC)
183
- text = unicodedata.normalize('NFKC', text)
 
 
184
 
185
- # Remove zero-width and invisible characters
186
- text = re.sub(r'[\u200b-\u200f\u2028-\u202f\u2060-\u206f\ufeff]', '', text)
187
 
188
- # Flatten newlines to spaces (for single messages only)
189
- text = re.sub(r'\n+', ' ', text)
190
 
191
- # Collapse multiple spaces
192
- text = re.sub(r' +', ' ', text)
193
 
194
- return text.strip()
195
 
196
- # NOTE: Do NOT remove emojis, punctuation, or "normalize" spelling
197
- ```
 
 
 
 
198
 
199
- **Language considerations:**
200
- - Model is English-primary but handles multilingual input
201
- - Keep native scripts (Chinese, Arabic, Korean, etc.) intact
202
- - Preserve natural punctuation and expression in all languages
203
 
204
  ### Multi-Turn Conversations
205
 
206
- **The model was trained on pre-serialized transcripts, not native multi-turn chat format.**
207
-
208
- When classifying conversations, serialize into a single user message:
209
 
210
  ```python
211
- # CORRECT - serialize conversation into single message
212
  conversation = """User: How are you?
213
  Assistant: I'm here to help. How are you feeling?
214
  User: Not great. I've been thinking about ending it all."""
215
 
216
  messages = [{"role": "user", "content": conversation}]
217
-
218
- # WRONG - don't use multiple role/content pairs
219
- messages = [
220
- {"role": "user", "content": "How are you?"},
221
- {"role": "assistant", "content": "I'm here to help..."},
222
- {"role": "user", "content": "Not great..."}
223
- ] # Model was NOT trained this way
224
- ```
225
-
226
- **Why serialization matters:**
227
- - Model treats all content equally (no user/assistant distinction)
228
- - Trained on pre-serialized transcripts for consistent attention patterns
229
- - Native multi-turn format causes the model to "chat" instead of classify
230
-
231
- **Flexible format - these all work:**
232
-
233
- ```python
234
- # Simple newlines
235
- "User: message 1\nAssistant: message 2\nUser: message 3"
236
-
237
- # Markdown-style
238
- "**User:** message 1\n**Assistant:** message 2"
239
-
240
- # Labeled
241
- "{user}: message 1\n{assistant}: message 2"
242
-
243
- # XML-style
244
- "<user>message 1</user>\n<assistant>message 2</assistant>"
245
  ```
246
 
247
- The model is robust to formatting variations. Consistency matters more than specific format choice.
248
-
249
- ### Input Length
250
-
251
- - **Single messages:** No preprocessing needed beyond character cleanup
252
- - **Conversations:** For very long conversations (20+ turns), consider:
253
- - Classifying a sliding window (last 10-15 turns)
254
- - The model's attention may not span extremely long contexts effectively
255
- - Deep needle detection (crisis buried in turn 3 of 25) is a known limitation
256
-
257
  ---
258
 
259
  ## Production Deployment
260
 
261
- For high-throughput production use, deploy with vLLM or SGLang:
262
 
263
  ```bash
 
 
 
 
 
 
264
  # vLLM
265
  pip install vllm
266
  python -m vllm.entrypoints.openai.api_server \
267
  --model nopenet/nope-edge-mini \
268
  --dtype bfloat16 --max-model-len 2048 --port 8000
269
-
270
- # SGLang
271
- pip install sglang
272
- python -m sglang.launch_server \
273
- --model nopenet/nope-edge-mini \
274
- --dtype bfloat16 --port 8000
275
  ```
276
 
277
  Then call as OpenAI-compatible API:
@@ -282,15 +303,10 @@ curl http://localhost:8000/v1/chat/completions \
282
  -d '{
283
  "model": "nopenet/nope-edge-mini",
284
  "messages": [{"role": "user", "content": "I want to end it all"}],
285
- "max_tokens": 30, "temperature": 0
286
  }'
287
  ```
288
 
289
- | Setup | Throughput | Latency (p50) |
290
- |-------|-----------|---------------|
291
- | transformers | ~8 req/sec | ~180ms |
292
- | vLLM / SGLang | 50-100+ req/sec | ~50ms |
293
-
294
  ---
295
 
296
  ## Model Details
@@ -340,12 +356,6 @@ This model is free for research, academic, nonprofit, and evaluation use.
340
  - Email: support@nope.net
341
  - Website: https://nope.net/edge
342
 
343
- Commercial licenses include:
344
- - Production deployment rights
345
- - Priority support
346
- - Custom fine-tuning options
347
- - SLA guarantees
348
-
349
  ---
350
 
351
  ## About NOPE
 
26
 
27
  # NOPE Edge Mini - Crisis Classification Model
28
 
29
+ A fine-tuned model for detecting crisis signals in text - suicidal ideation, self-harm, abuse, violence, and other safety-critical content. Features chain-of-thought reasoning that explains its classifications.
30
 
31
  > **License:** [NOPE Edge Community License v1.0](LICENSE.md) - Free for research, academic, nonprofit, and evaluation use. Commercial production requires a separate license. See [nope.net/edge](https://nope.net/edge) for details.
32
 
 
34
 
35
  ## Model Variants
36
 
37
+ | Model | Parameters | Use Case |
38
+ |-------|------------|----------|
39
+ | **[nope-edge](https://huggingface.co/nopenet/nope-edge)** | 4B | Maximum accuracy |
40
+ | **[nope-edge-mini](https://huggingface.co/nopenet/nope-edge-mini)** | 1.7B | High-volume, cost-sensitive |
41
 
42
  This is **nope-edge-mini (1.7B)**.
43
 
 
60
  ```python
61
  from transformers import AutoModelForCausalLM, AutoTokenizer
62
  import torch
63
+ import re
64
 
65
  model_id = "nopenet/nope-edge-mini"
66
 
 
72
  )
73
 
74
  def classify(message: str) -> str:
75
+ """Returns XML with reflection and risk classification."""
76
  input_ids = tokenizer.apply_chat_template(
77
  [{"role": "user", "content": message}],
78
  tokenize=True,
 
81
  ).to(model.device)
82
 
83
  with torch.no_grad():
84
+ output = model.generate(input_ids, max_new_tokens=300, do_sample=False)
85
 
86
  return tokenizer.decode(
87
  output[0][input_ids.shape[1]:],
88
  skip_special_tokens=True
89
  ).strip()
90
 
91
+ # Example
92
+ result = classify("I want to end it all tonight")
93
+ print(result)
94
+ ```
95
+
96
+ **Output:**
97
+ ```xml
98
+ <reflection>The user directly expresses intent to end their life with a specific timeline ("tonight"), indicating acute suicidal ideation with imminent risk.</reflection>
99
+ <risks>
100
+ <risk subject="self" type="suicide" severity="high" imminence="urgent"/>
101
+ </risks>
102
  ```
103
 
104
  ---
105
 
106
  ## Output Format
107
 
108
+ The model outputs XML with two components:
109
+
110
+ ### 1. Reflection (Chain-of-Thought)
111
+
112
+ ```xml
113
+ <reflection>Reasoning about the input...</reflection>
114
+ ```
115
+
116
+ The model explains its classification, including:
117
+ - What signals it detected
118
+ - Why it chose the risk type and severity
119
+ - Any contextual factors considered
120
+
121
+ ### 2. Risk Classification
122
+
123
  **Crisis detected:**
124
+ ```xml
125
+ <risks>
126
+ <risk subject="self" type="suicide" severity="high" imminence="urgent" features="active_ideation,intent_stated"/>
127
+ </risks>
128
  ```
129
+
130
+ **No crisis:**
131
+ ```xml
132
+ <risks/>
133
  ```
134
 
135
+ ### Risk Attributes
 
 
 
 
136
 
137
+ | Attribute | Values | Description |
138
+ |-----------|--------|-------------|
139
+ | `subject` | `self`, `other` | Who is at risk |
140
+ | `type` | `suicide`, `self_harm`, `self_neglect`, `violence`, `abuse`, `sexual_violence`, `exploitation`, `stalking`, `neglect` | Risk category |
141
+ | `severity` | `mild`, `moderate`, `high`, `critical` | Urgency level |
142
+ | `imminence` | `chronic`, `acute`, `urgent`, `emergency` | Time sensitivity |
143
+ | `features` | comma-separated list | Specific indicators detected |
144
 
145
  ### Subject Attribution
146
 
147
  | Subject | Meaning | Example |
148
  |---------|---------|---------|
149
+ | `self` | The speaker is at risk | "I want to kill myself" |
150
+ | `other` | Reporting concern about someone else | "My friend said she wants to die" |
151
 
152
  ### Parsing Example
153
 
154
  ```python
155
+ import re
156
+ from dataclasses import dataclass
157
+ from typing import Optional
158
+
159
+ @dataclass
160
+ class Risk:
161
+ subject: str
162
+ type: str
163
+ severity: str
164
+ imminence: Optional[str] = None
165
+ features: Optional[list] = None
166
+
167
  def parse_output(output: str) -> dict:
168
+ """Parse model output into structured data."""
169
+ result = {
170
+ "reflection": None,
171
+ "risks": [],
172
+ "is_crisis": False
 
 
 
 
 
173
  }
174
+
175
+ # Extract reflection
176
+ reflection_match = re.search(r'<reflection>(.*?)</reflection>', output, re.DOTALL)
177
+ if reflection_match:
178
+ result["reflection"] = reflection_match.group(1).strip()
179
+
180
+ # Check for empty risks (no crisis)
181
+ if '<risks/>' in output or '<risks />' in output:
182
+ return result
183
+
184
+ # Extract risk elements
185
+ risk_pattern = r'<risk\s+([^>]+)/?\s*>'
186
+ for match in re.finditer(risk_pattern, output):
187
+ attrs = {}
188
+ for attr_match in re.finditer(r'(\w+)="([^"]*)"', match.group(1)):
189
+ attrs[attr_match.group(1)] = attr_match.group(2)
190
+
191
+ if attrs:
192
+ risk = Risk(
193
+ subject=attrs.get("subject", "self"),
194
+ type=attrs.get("type"),
195
+ severity=attrs.get("severity"),
196
+ imminence=attrs.get("imminence"),
197
+ features=attrs.get("features", "").split(",") if attrs.get("features") else None
198
+ )
199
+ result["risks"].append(risk)
200
+ result["is_crisis"] = True
201
+
202
+ return result
203
+
204
+ # Usage
205
+ output = classify("I want to end it all tonight")
206
+ parsed = parse_output(output)
207
+ print(f"Crisis: {parsed['is_crisis']}")
208
+ print(f"Reasoning: {parsed['reflection']}")
209
+ for risk in parsed['risks']:
210
+ print(f"Risk: {risk.type}/{risk.severity} ({risk.subject})")
211
  ```
212
 
213
  ---
214
 
215
+ ## Examples
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ ### Crisis Detection
218
 
219
+ **Input:** "I want to end it all tonight"
220
+ ```xml
221
+ <reflection>The user directly expresses intent to end their life with a specific timeline ("tonight"), indicating acute suicidal ideation with imminent risk.</reflection>
222
+ <risks>
223
+ <risk subject="self" type="suicide" severity="high" imminence="urgent"/>
224
+ </risks>
225
+ ```
226
 
227
+ **Input:** "My friend told me she's been cutting herself"
228
+ ```xml
229
+ <reflection>The user is reporting concern about a friend engaging in self-harm behavior. This is third-party disclosure requiring attention.</reflection>
230
+ <risks>
231
+ <risk subject="other" type="self_harm" severity="moderate" imminence="chronic"/>
232
+ </risks>
 
 
 
 
233
  ```
234
 
235
+ ### No Crisis (Correctly Ignored)
236
 
237
+ **Input:** "kms lmao this exam is killing me"
238
+ ```xml
239
+ <reflection>The user is using hyperbolic internet slang ("kms" = "kill myself") to express frustration about an exam. The "lmao" and casual context indicate this is not genuine suicidal ideation.</reflection>
240
+ <risks/>
241
+ ```
242
 
243
+ **Input:** "I used to be suicidal but therapy helped me recover"
244
+ ```xml
245
+ <reflection>The user is sharing a recovery narrative about past suicidal ideation. They explicitly state therapy helped and they have recovered. No current crisis indicators.</reflection>
246
+ <risks/>
247
+ ```
248
 
249
+ ---
 
250
 
251
+ ## Input Best Practices
 
252
 
253
+ ### Text Preprocessing
 
254
 
255
+ **Preserve natural prose.** The model was trained on real conversations with authentic expression:
256
 
257
+ | Keep | Why |
258
+ |------|-----|
259
+ | Emojis | Emotional signals matter |
260
+ | Punctuation intensity | "I can't do this!!!" vs "I can't do this" |
261
+ | Slang/algospeak | "kms", "unalive", "catch the bus", "graped" |
262
+ | Casual spelling | "im so done" - don't normalize |
263
 
264
+ **Only remove:** Zero-width Unicode, decorative fonts, excessive whitespace.
 
 
 
265
 
266
  ### Multi-Turn Conversations
267
 
268
+ Serialize into a single user message:
 
 
269
 
270
  ```python
 
271
  conversation = """User: How are you?
272
  Assistant: I'm here to help. How are you feeling?
273
  User: Not great. I've been thinking about ending it all."""
274
 
275
  messages = [{"role": "user", "content": conversation}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  ```
277
 
 
 
 
 
 
 
 
 
 
 
278
  ---
279
 
280
  ## Production Deployment
281
 
282
+ For high-throughput use, deploy with vLLM or SGLang:
283
 
284
  ```bash
285
+ # SGLang (recommended)
286
+ pip install sglang
287
+ python -m sglang.launch_server \
288
+ --model nopenet/nope-edge-mini \
289
+ --dtype bfloat16 --port 8000
290
+
291
  # vLLM
292
  pip install vllm
293
  python -m vllm.entrypoints.openai.api_server \
294
  --model nopenet/nope-edge-mini \
295
  --dtype bfloat16 --max-model-len 2048 --port 8000
 
 
 
 
 
 
296
  ```
297
 
298
  Then call as OpenAI-compatible API:
 
303
  -d '{
304
  "model": "nopenet/nope-edge-mini",
305
  "messages": [{"role": "user", "content": "I want to end it all"}],
306
+ "max_tokens": 300, "temperature": 0
307
  }'
308
  ```
309
 
 
 
 
 
 
310
  ---
311
 
312
  ## Model Details
 
356
  - Email: support@nope.net
357
  - Website: https://nope.net/edge
358
 
 
 
 
 
 
 
359
  ---
360
 
361
  ## About NOPE