Commit ·
a028f12
0
Parent(s):
Duplicate from cfahlgren1/enron-autocomplete
Browse files- .gitattributes +36 -0
- README.md +111 -0
- best.pt +3 -0
- hero.png +3 -0
- model.onnx +3 -0
- model_quint8.onnx +3 -0
- report.json +548 -0
- train.py +557 -0
- vocab.json +9 -0
.gitattributes
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
hero.png filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: onnx
|
| 3 |
+
pipeline_tag: text-generation
|
| 4 |
+
datasets:
|
| 5 |
+
- corbt/enron-emails
|
| 6 |
+
language:
|
| 7 |
+
- en
|
| 8 |
+
tags:
|
| 9 |
+
- autocomplete
|
| 10 |
+
- gru
|
| 11 |
+
- onnxruntime-web
|
| 12 |
+
- in-browser
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
# enron-autocomplete
|
| 16 |
+
|
| 17 |
+
[](https://huggingface.co/spaces/cfahlgren1/enron-autocomplete)
|
| 18 |
+
|
| 19 |
+
A tiny email autocomplete that runs in your browser. It learned to write from Enron's emails.
|
| 20 |
+
|
| 21 |
+
**[Try the demo](https://huggingface.co/spaces/cfahlgren1/enron-autocomplete)**
|
| 22 |
+
|
| 23 |
+
| | |
|
| 24 |
+
|---|---|
|
| 25 |
+
| Size | 944K parameters, 0.98 MB |
|
| 26 |
+
| Speed | about 4 ms per suggestion, on CPU in Chrome |
|
| 27 |
+
| Training | 14 minutes on one A10G with Hugging Face Jobs, 35 cents |
|
| 28 |
+
| Data | 119,859 Enron emails |
|
| 29 |
+
|
| 30 |
+
## What it does
|
| 31 |
+
|
| 32 |
+
It finishes the word or short phrase you're typing.
|
| 33 |
+
|
| 34 |
+
| You type | It suggests |
|
| 35 |
+
|---|---|
|
| 36 |
+
| Please accept my resig | nation |
|
| 37 |
+
| Please accept my resignation. I will be o | ut of the office |
|
| 38 |
+
| The books are fine. Please call me i | f you have any |
|
| 39 |
+
| Enron N | orth America Corp. |
|
| 40 |
+
|
| 41 |
+
## How sure it has to be
|
| 42 |
+
|
| 43 |
+
The model knows how sure it is of each guess. You pick a cutoff, and it only suggests when it's at least that sure. A higher cutoff means fewer suggestions, and more of them right.
|
| 44 |
+
|
| 45 |
+
| Cutoff | How often it suggests | How often it's right |
|
| 46 |
+
|---|---|---|
|
| 47 |
+
| 20% | 30% of the time | 48% |
|
| 48 |
+
| 35% | 18% of the time | 66% |
|
| 49 |
+
| 50% | 11% of the time | 80% |
|
| 50 |
+
| 70% | 7% of the time | 90% |
|
| 51 |
+
|
| 52 |
+
The demo uses 40%.
|
| 53 |
+
|
| 54 |
+
These numbers come from 5,000 places in emails from people the model never saw in training. "Right" means the suggestion exactly matched what the person typed next. Right suggestions are about 6 characters long on average.
|
| 55 |
+
|
| 56 |
+
## Files
|
| 57 |
+
|
| 58 |
+
| File | What it is |
|
| 59 |
+
|---|---|
|
| 60 |
+
| `model_quint8.onnx` | The model to use. 0.98 MB. |
|
| 61 |
+
| `model.onnx` | The same model before shrinking. 3.8 MB. |
|
| 62 |
+
| `best.pt` | The PyTorch checkpoint. |
|
| 63 |
+
| `vocab.json` | The character ids. |
|
| 64 |
+
| `report.json` | The full training and test results. |
|
| 65 |
+
| `train.py` | The training script. |
|
| 66 |
+
|
| 67 |
+
I also tried a bigger version with 3.46M parameters and 3.5 MB. It wasn't better. When both models suggest equally often, 9% of the time, the small one is right about 85% of the time and the big one 82%. The big one got the same training settings and may need different ones to do well.
|
| 68 |
+
|
| 69 |
+
## Use it
|
| 70 |
+
|
| 71 |
+
The model takes one character and its memory of the text so far. It returns its guess for the next character and an updated memory. So each key press is one small step.
|
| 72 |
+
|
| 73 |
+
```js
|
| 74 |
+
import * as ort from "onnxruntime-web";
|
| 75 |
+
|
| 76 |
+
const session = await ort.InferenceSession.create("model_quint8.onnx", {
|
| 77 |
+
executionProviders: ["wasm"],
|
| 78 |
+
});
|
| 79 |
+
let state = new ort.Tensor("float32", new Float32Array(512), [1, 512]);
|
| 80 |
+
|
| 81 |
+
async function step(id) {
|
| 82 |
+
const input_id = new ort.Tensor("int64", BigInt64Array.of(BigInt(id)), [1]);
|
| 83 |
+
const { logits, next_state } = await session.run({ input_id, state });
|
| 84 |
+
state = next_state;
|
| 85 |
+
return logits.data;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
let logits = await step(1); // 1 starts a new email
|
| 89 |
+
for (const char of "Let me kn") logits = await step(char.charCodeAt(0) - 29);
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
A character's id is its character code minus 29. A new line is 2 and the end of an email is 1. Only English keyboard characters are supported.
|
| 93 |
+
|
| 94 |
+
To build a suggestion, keep taking the most likely next character and multiply the probabilities together as you go. Stop at a new line, or when that running product drops below your cutoff. Show the text up to the last finished word.
|
| 95 |
+
|
| 96 |
+
## Training
|
| 97 |
+
|
| 98 |
+
It read 40 million characters from 119,859 emails sent from enron.com addresses, 30 times over. Typos were added to what it read, so it keeps predicting when you make one. The run took 14 minutes on one A10G with Hugging Face Jobs and cost 35 cents.
|
| 99 |
+
|
| 100 |
+
[`train.py`](train.py) is the whole thing: it downloads the emails, trains, tests and exports the model. To train your own:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
hf jobs uv run --flavor a10g-large --secrets HF_TOKEN train.py --repo your-name/your-model
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
## Limits
|
| 107 |
+
|
| 108 |
+
- It only knows office email from 2001. Outside that it has little to say.
|
| 109 |
+
- With no cutoff it rambles. "Dear Santa," becomes "I am attaching a copy of the agreement to the Enron Corp."
|
| 110 |
+
- It learned real names and sign-offs from the emails, so keep suggestions short.
|
| 111 |
+
- English only.
|
best.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8f2a663ad7a697a00ebf6bd15942824fcefa46975f9935c7ac6dd1eceedf3a36
|
| 3 |
+
size 3780287
|
hero.png
ADDED
|
Git LFS Details
|
model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1ccc402ffaa27e6382114ad0d44a2e5a01e9beabe9d81e3c3ae0ad2f9676627b
|
| 3 |
+
size 3780016
|
model_quint8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:079424a9d2a72b9e5b2d6002c6466b836023c088780b7d292748f37564dc143b
|
| 3 |
+
size 981294
|
report.json
ADDED
|
@@ -0,0 +1,548 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"args": {
|
| 3 |
+
"hidden": 512,
|
| 4 |
+
"epochs": 30,
|
| 5 |
+
"batch": 256,
|
| 6 |
+
"seq": 256,
|
| 7 |
+
"burn_in": 32,
|
| 8 |
+
"lr": 0.0015,
|
| 9 |
+
"typo_rate": 0.08,
|
| 10 |
+
"limit_frac": 1.0,
|
| 11 |
+
"eval_positions": 5000,
|
| 12 |
+
"context": 128,
|
| 13 |
+
"horizon": 48,
|
| 14 |
+
"repo": "cfahlgren1/email-autocomplete-gru",
|
| 15 |
+
"run_name": "gru-512",
|
| 16 |
+
"no_push": false
|
| 17 |
+
},
|
| 18 |
+
"device": "cuda",
|
| 19 |
+
"parameters": 944354,
|
| 20 |
+
"data": {
|
| 21 |
+
"train": {
|
| 22 |
+
"emails": 119859,
|
| 23 |
+
"chars": 40136658
|
| 24 |
+
},
|
| 25 |
+
"val": {
|
| 26 |
+
"emails": 8252,
|
| 27 |
+
"chars": 2775980
|
| 28 |
+
},
|
| 29 |
+
"test": {
|
| 30 |
+
"emails": 6922,
|
| 31 |
+
"chars": 2710169
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"history": [
|
| 35 |
+
{
|
| 36 |
+
"epoch": 1,
|
| 37 |
+
"train_loss": 1.7093,
|
| 38 |
+
"val_loss": 1.3716,
|
| 39 |
+
"seconds": 23.9,
|
| 40 |
+
"chars_per_sec": 1678766
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"epoch": 2,
|
| 44 |
+
"train_loss": 1.2792,
|
| 45 |
+
"val_loss": 1.2911,
|
| 46 |
+
"seconds": 23.6,
|
| 47 |
+
"chars_per_sec": 1701572
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"epoch": 3,
|
| 51 |
+
"train_loss": 1.2231,
|
| 52 |
+
"val_loss": 1.2582,
|
| 53 |
+
"seconds": 23.6,
|
| 54 |
+
"chars_per_sec": 1700904
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"epoch": 4,
|
| 58 |
+
"train_loss": 1.195,
|
| 59 |
+
"val_loss": 1.2396,
|
| 60 |
+
"seconds": 23.6,
|
| 61 |
+
"chars_per_sec": 1702926
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"epoch": 5,
|
| 65 |
+
"train_loss": 1.1768,
|
| 66 |
+
"val_loss": 1.2254,
|
| 67 |
+
"seconds": 23.5,
|
| 68 |
+
"chars_per_sec": 1703932
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
"epoch": 6,
|
| 72 |
+
"train_loss": 1.1639,
|
| 73 |
+
"val_loss": 1.2171,
|
| 74 |
+
"seconds": 23.6,
|
| 75 |
+
"chars_per_sec": 1701905
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"epoch": 7,
|
| 79 |
+
"train_loss": 1.1538,
|
| 80 |
+
"val_loss": 1.2087,
|
| 81 |
+
"seconds": 23.6,
|
| 82 |
+
"chars_per_sec": 1701450
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"epoch": 8,
|
| 86 |
+
"train_loss": 1.1456,
|
| 87 |
+
"val_loss": 1.2043,
|
| 88 |
+
"seconds": 23.6,
|
| 89 |
+
"chars_per_sec": 1702601
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"epoch": 9,
|
| 93 |
+
"train_loss": 1.1388,
|
| 94 |
+
"val_loss": 1.1969,
|
| 95 |
+
"seconds": 23.5,
|
| 96 |
+
"chars_per_sec": 1703175
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"epoch": 10,
|
| 100 |
+
"train_loss": 1.1327,
|
| 101 |
+
"val_loss": 1.193,
|
| 102 |
+
"seconds": 23.6,
|
| 103 |
+
"chars_per_sec": 1701059
|
| 104 |
+
},
|
| 105 |
+
{
|
| 106 |
+
"epoch": 11,
|
| 107 |
+
"train_loss": 1.1276,
|
| 108 |
+
"val_loss": 1.1895,
|
| 109 |
+
"seconds": 23.6,
|
| 110 |
+
"chars_per_sec": 1702827
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"epoch": 12,
|
| 114 |
+
"train_loss": 1.1228,
|
| 115 |
+
"val_loss": 1.1856,
|
| 116 |
+
"seconds": 23.6,
|
| 117 |
+
"chars_per_sec": 1700650
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"epoch": 13,
|
| 121 |
+
"train_loss": 1.1186,
|
| 122 |
+
"val_loss": 1.1824,
|
| 123 |
+
"seconds": 23.5,
|
| 124 |
+
"chars_per_sec": 1703353
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"epoch": 14,
|
| 128 |
+
"train_loss": 1.1141,
|
| 129 |
+
"val_loss": 1.1791,
|
| 130 |
+
"seconds": 23.6,
|
| 131 |
+
"chars_per_sec": 1702586
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"epoch": 15,
|
| 135 |
+
"train_loss": 1.1108,
|
| 136 |
+
"val_loss": 1.1765,
|
| 137 |
+
"seconds": 23.6,
|
| 138 |
+
"chars_per_sec": 1702570
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"epoch": 16,
|
| 142 |
+
"train_loss": 1.1075,
|
| 143 |
+
"val_loss": 1.175,
|
| 144 |
+
"seconds": 23.6,
|
| 145 |
+
"chars_per_sec": 1702686
|
| 146 |
+
},
|
| 147 |
+
{
|
| 148 |
+
"epoch": 17,
|
| 149 |
+
"train_loss": 1.1041,
|
| 150 |
+
"val_loss": 1.1718,
|
| 151 |
+
"seconds": 23.5,
|
| 152 |
+
"chars_per_sec": 1703957
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"epoch": 18,
|
| 156 |
+
"train_loss": 1.1015,
|
| 157 |
+
"val_loss": 1.17,
|
| 158 |
+
"seconds": 23.6,
|
| 159 |
+
"chars_per_sec": 1701778
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"epoch": 19,
|
| 163 |
+
"train_loss": 1.0985,
|
| 164 |
+
"val_loss": 1.1678,
|
| 165 |
+
"seconds": 23.6,
|
| 166 |
+
"chars_per_sec": 1702612
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"epoch": 20,
|
| 170 |
+
"train_loss": 1.0959,
|
| 171 |
+
"val_loss": 1.1659,
|
| 172 |
+
"seconds": 23.6,
|
| 173 |
+
"chars_per_sec": 1702960
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"epoch": 21,
|
| 177 |
+
"train_loss": 1.0935,
|
| 178 |
+
"val_loss": 1.1642,
|
| 179 |
+
"seconds": 23.6,
|
| 180 |
+
"chars_per_sec": 1701776
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"epoch": 22,
|
| 184 |
+
"train_loss": 1.0913,
|
| 185 |
+
"val_loss": 1.1632,
|
| 186 |
+
"seconds": 23.6,
|
| 187 |
+
"chars_per_sec": 1702317
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"epoch": 23,
|
| 191 |
+
"train_loss": 1.0896,
|
| 192 |
+
"val_loss": 1.1624,
|
| 193 |
+
"seconds": 23.6,
|
| 194 |
+
"chars_per_sec": 1701215
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"epoch": 24,
|
| 198 |
+
"train_loss": 1.0874,
|
| 199 |
+
"val_loss": 1.1613,
|
| 200 |
+
"seconds": 23.6,
|
| 201 |
+
"chars_per_sec": 1701278
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"epoch": 25,
|
| 205 |
+
"train_loss": 1.0863,
|
| 206 |
+
"val_loss": 1.1606,
|
| 207 |
+
"seconds": 23.6,
|
| 208 |
+
"chars_per_sec": 1700749
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"epoch": 26,
|
| 212 |
+
"train_loss": 1.085,
|
| 213 |
+
"val_loss": 1.1596,
|
| 214 |
+
"seconds": 23.6,
|
| 215 |
+
"chars_per_sec": 1701575
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"epoch": 27,
|
| 219 |
+
"train_loss": 1.0839,
|
| 220 |
+
"val_loss": 1.1592,
|
| 221 |
+
"seconds": 23.6,
|
| 222 |
+
"chars_per_sec": 1699705
|
| 223 |
+
},
|
| 224 |
+
{
|
| 225 |
+
"epoch": 28,
|
| 226 |
+
"train_loss": 1.083,
|
| 227 |
+
"val_loss": 1.1588,
|
| 228 |
+
"seconds": 23.6,
|
| 229 |
+
"chars_per_sec": 1700990
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"epoch": 29,
|
| 233 |
+
"train_loss": 1.0823,
|
| 234 |
+
"val_loss": 1.1588,
|
| 235 |
+
"seconds": 23.6,
|
| 236 |
+
"chars_per_sec": 1702216
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"epoch": 30,
|
| 240 |
+
"train_loss": 1.0821,
|
| 241 |
+
"val_loss": 1.1586,
|
| 242 |
+
"seconds": 23.5,
|
| 243 |
+
"chars_per_sec": 1704907
|
| 244 |
+
}
|
| 245 |
+
],
|
| 246 |
+
"best_val_loss": 1.1586,
|
| 247 |
+
"test_positions": 5000,
|
| 248 |
+
"held_out_sender_suggestions": {
|
| 249 |
+
"0.05": {
|
| 250 |
+
"coverage": 0.624,
|
| 251 |
+
"precision": 0.2064,
|
| 252 |
+
"avg_chars_when_correct": 6.93,
|
| 253 |
+
"chars_saved_per_position": 0.892
|
| 254 |
+
},
|
| 255 |
+
"0.1": {
|
| 256 |
+
"coverage": 0.4622,
|
| 257 |
+
"precision": 0.3124,
|
| 258 |
+
"avg_chars_when_correct": 6.41,
|
| 259 |
+
"chars_saved_per_position": 0.925
|
| 260 |
+
},
|
| 261 |
+
"0.2": {
|
| 262 |
+
"coverage": 0.2974,
|
| 263 |
+
"precision": 0.4808,
|
| 264 |
+
"avg_chars_when_correct": 6.03,
|
| 265 |
+
"chars_saved_per_position": 0.863
|
| 266 |
+
},
|
| 267 |
+
"0.35": {
|
| 268 |
+
"coverage": 0.1796,
|
| 269 |
+
"precision": 0.6559,
|
| 270 |
+
"avg_chars_when_correct": 5.8,
|
| 271 |
+
"chars_saved_per_position": 0.683
|
| 272 |
+
},
|
| 273 |
+
"0.5": {
|
| 274 |
+
"coverage": 0.1114,
|
| 275 |
+
"precision": 0.7953,
|
| 276 |
+
"avg_chars_when_correct": 5.64,
|
| 277 |
+
"chars_saved_per_position": 0.5
|
| 278 |
+
},
|
| 279 |
+
"0.7": {
|
| 280 |
+
"coverage": 0.0694,
|
| 281 |
+
"precision": 0.8991,
|
| 282 |
+
"avg_chars_when_correct": 5.4,
|
| 283 |
+
"chars_saved_per_position": 0.337
|
| 284 |
+
}
|
| 285 |
+
},
|
| 286 |
+
"exports": {
|
| 287 |
+
"float": {
|
| 288 |
+
"size_mb": 3.78,
|
| 289 |
+
"native_cpu_single_thread_step": {
|
| 290 |
+
"p50_ms": 0.114,
|
| 291 |
+
"p95_ms": 0.125
|
| 292 |
+
},
|
| 293 |
+
"suggestions_1000_positions": {
|
| 294 |
+
"0.05": {
|
| 295 |
+
"coverage": 0.638,
|
| 296 |
+
"precision": 0.2132,
|
| 297 |
+
"avg_chars_when_correct": 6.7,
|
| 298 |
+
"chars_saved_per_position": 0.911
|
| 299 |
+
},
|
| 300 |
+
"0.1": {
|
| 301 |
+
"coverage": 0.496,
|
| 302 |
+
"precision": 0.3085,
|
| 303 |
+
"avg_chars_when_correct": 6.3,
|
| 304 |
+
"chars_saved_per_position": 0.964
|
| 305 |
+
},
|
| 306 |
+
"0.2": {
|
| 307 |
+
"coverage": 0.312,
|
| 308 |
+
"precision": 0.4583,
|
| 309 |
+
"avg_chars_when_correct": 5.94,
|
| 310 |
+
"chars_saved_per_position": 0.849
|
| 311 |
+
},
|
| 312 |
+
"0.35": {
|
| 313 |
+
"coverage": 0.202,
|
| 314 |
+
"precision": 0.604,
|
| 315 |
+
"avg_chars_when_correct": 5.77,
|
| 316 |
+
"chars_saved_per_position": 0.704
|
| 317 |
+
},
|
| 318 |
+
"0.5": {
|
| 319 |
+
"coverage": 0.119,
|
| 320 |
+
"precision": 0.7479,
|
| 321 |
+
"avg_chars_when_correct": 5.58,
|
| 322 |
+
"chars_saved_per_position": 0.497
|
| 323 |
+
},
|
| 324 |
+
"0.7": {
|
| 325 |
+
"coverage": 0.078,
|
| 326 |
+
"precision": 0.8718,
|
| 327 |
+
"avg_chars_when_correct": 5.21,
|
| 328 |
+
"chars_saved_per_position": 0.354
|
| 329 |
+
}
|
| 330 |
+
}
|
| 331 |
+
},
|
| 332 |
+
"quint8": {
|
| 333 |
+
"size_mb": 0.98,
|
| 334 |
+
"native_cpu_single_thread_step": {
|
| 335 |
+
"p50_ms": 0.059,
|
| 336 |
+
"p95_ms": 0.084
|
| 337 |
+
},
|
| 338 |
+
"suggestions_1000_positions": {
|
| 339 |
+
"0.05": {
|
| 340 |
+
"coverage": 0.655,
|
| 341 |
+
"precision": 0.2,
|
| 342 |
+
"avg_chars_when_correct": 6.83,
|
| 343 |
+
"chars_saved_per_position": 0.895
|
| 344 |
+
},
|
| 345 |
+
"0.1": {
|
| 346 |
+
"coverage": 0.492,
|
| 347 |
+
"precision": 0.311,
|
| 348 |
+
"avg_chars_when_correct": 6.35,
|
| 349 |
+
"chars_saved_per_position": 0.971
|
| 350 |
+
},
|
| 351 |
+
"0.2": {
|
| 352 |
+
"coverage": 0.318,
|
| 353 |
+
"precision": 0.4686,
|
| 354 |
+
"avg_chars_when_correct": 5.97,
|
| 355 |
+
"chars_saved_per_position": 0.89
|
| 356 |
+
},
|
| 357 |
+
"0.35": {
|
| 358 |
+
"coverage": 0.198,
|
| 359 |
+
"precision": 0.6414,
|
| 360 |
+
"avg_chars_when_correct": 5.66,
|
| 361 |
+
"chars_saved_per_position": 0.719
|
| 362 |
+
},
|
| 363 |
+
"0.5": {
|
| 364 |
+
"coverage": 0.123,
|
| 365 |
+
"precision": 0.7642,
|
| 366 |
+
"avg_chars_when_correct": 5.53,
|
| 367 |
+
"chars_saved_per_position": 0.52
|
| 368 |
+
},
|
| 369 |
+
"0.7": {
|
| 370 |
+
"coverage": 0.081,
|
| 371 |
+
"precision": 0.8889,
|
| 372 |
+
"avg_chars_when_correct": 5.42,
|
| 373 |
+
"chars_saved_per_position": 0.39
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
}
|
| 377 |
+
},
|
| 378 |
+
"prompt_samples": {
|
| 379 |
+
"Hi John,\n\nThanks for ": {
|
| 380 |
+
"continuation": "the info. I will be out of the office tomorrow a",
|
| 381 |
+
"suggestion_at_0.2": "the"
|
| 382 |
+
},
|
| 383 |
+
"Please let me know if ": {
|
| 384 |
+
"continuation": "you have any questions.\n\nThanks,\n\nKayThe follow",
|
| 385 |
+
"suggestion_at_0.2": "you have any"
|
| 386 |
+
},
|
| 387 |
+
"I will send you ": {
|
| 388 |
+
"continuation": "a copy of the attached document. I will be out o",
|
| 389 |
+
"suggestion_at_0.2": "a"
|
| 390 |
+
},
|
| 391 |
+
"Let me kn": {
|
| 392 |
+
"continuation": "ow if you have any questions.\n\nThanks,\n\nKayThe ",
|
| 393 |
+
"suggestion_at_0.2": "ow if you have"
|
| 394 |
+
},
|
| 395 |
+
"Thnaks for the ": {
|
| 396 |
+
"continuation": "info. I will be out of the office tomorrow and w",
|
| 397 |
+
"suggestion_at_0.2": ""
|
| 398 |
+
},
|
| 399 |
+
"I look forward to ": {
|
| 400 |
+
"continuation": "seeing you there. I will be out of the office to",
|
| 401 |
+
"suggestion_at_0.2": ""
|
| 402 |
+
},
|
| 403 |
+
"Can we schedule a ": {
|
| 404 |
+
"continuation": "meeting to discuss the attached spreadsheet for ",
|
| 405 |
+
"suggestion_at_0.2": "meeting"
|
| 406 |
+
},
|
| 407 |
+
"Sorry for the dela": {
|
| 408 |
+
"continuation": "y. I will be out of the office tomorrow and we w",
|
| 409 |
+
"suggestion_at_0.2": "y."
|
| 410 |
+
},
|
| 411 |
+
"Plese find attached ": {
|
| 412 |
+
"continuation": "the following contract for the Enron Corp. Gas P",
|
| 413 |
+
"suggestion_at_0.2": "the"
|
| 414 |
+
},
|
| 415 |
+
"We need to discuss ": {
|
| 416 |
+
"continuation": "the attached spreadsheet for the following dates",
|
| 417 |
+
"suggestion_at_0.2": "the"
|
| 418 |
+
}
|
| 419 |
+
},
|
| 420 |
+
"held_out_examples": [
|
| 421 |
+
{
|
| 422 |
+
"context": "ou trading a book yet? Playing any poker? Bingham was here f",
|
| 423 |
+
"truth": "or Q-School last week. Didn't go so well.",
|
| 424 |
+
"suggestion_at_0.2": "or"
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"context": "Please use this model. I had a few change",
|
| 428 |
+
"truth": "s after talking to the guys in the west. Thanks!",
|
| 429 |
+
"suggestion_at_0.2": "s to"
|
| 430 |
+
},
|
| 431 |
+
{
|
| 432 |
+
"context": "not open the attachment. Please, send it as a MS Word docume",
|
| 433 |
+
"truth": "nt or a .pdf file.",
|
| 434 |
+
"suggestion_at_0.2": "nt."
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"context": "n I do to help. I would also like to visit on status of e*p ",
|
| 438 |
+
"truth": "group and mariner.",
|
| 439 |
+
"suggestion_at_0.2": ""
|
| 440 |
+
},
|
| 441 |
+
{
|
| 442 |
+
"context": " complete hourly schedule for June 1, including IBM and inco",
|
| 443 |
+
"truth": "rporating all known changes confirmed by that ti",
|
| 444 |
+
"suggestion_at_0.2": "me"
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"context": " delivery rights at SoCal/Topock for at least a pro rata por",
|
| 448 |
+
"truth": "tion of its released capacity.",
|
| 449 |
+
"suggestion_at_0.2": "tion of"
|
| 450 |
+
},
|
| 451 |
+
{
|
| 452 |
+
"context": "i talked to chris a li",
|
| 453 |
+
"truth": "ttle bit and he is going to call back tonight at",
|
| 454 |
+
"suggestion_at_0.2": "ttle"
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
"context": " I might be missing something about the way you guys do thes",
|
| 458 |
+
"truth": "e deals in Canada so I thought I would run this ",
|
| 459 |
+
"suggestion_at_0.2": "e"
|
| 460 |
+
},
|
| 461 |
+
{
|
| 462 |
+
"context": "Traci,\n\nThanks. This is the best solution given all the unce",
|
| 463 |
+
"truth": "rtainties we face.",
|
| 464 |
+
"suggestion_at_0.2": "rtainty"
|
| 465 |
+
},
|
| 466 |
+
{
|
| 467 |
+
"context": "\nAquila,s strategy comes as little surprise to Stuebi. As mu",
|
| 468 |
+
"truth": "ch success as the company has had, he said he th",
|
| 469 |
+
"suggestion_at_0.2": "ch"
|
| 470 |
+
},
|
| 471 |
+
{
|
| 472 |
+
"context": "Jim,\n\nWhat about 2 p.m., my time, on Friday, Au",
|
| 473 |
+
"truth": "gust 17. If this works for you, I shall try to i",
|
| 474 |
+
"suggestion_at_0.2": "gust"
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"context": "'s and CDD's for Philedelphia, Allentown Washington DC, Balt",
|
| 478 |
+
"truth": "imore, Pittsburg, Newark, New York City, Cinncin",
|
| 479 |
+
"suggestion_at_0.2": "imore"
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"context": " release.\n\nThe company will offer derivatives to hedge volat",
|
| 483 |
+
"truth": "ile electricity prices.",
|
| 484 |
+
"suggestion_at_0.2": "ility"
|
| 485 |
+
},
|
| 486 |
+
{
|
| 487 |
+
"context": "day, I need access to West deals in Sitara. At a mininum I s",
|
| 488 |
+
"truth": "till need access to all Chicago/Midwest deals al",
|
| 489 |
+
"suggestion_at_0.2": "ent"
|
| 490 |
+
},
|
| 491 |
+
{
|
| 492 |
+
"context": "on, attached for your review is a draft of the First Amendme",
|
| 493 |
+
"truth": "nt to Master Agreement between Enron North Ameri",
|
| 494 |
+
"suggestion_at_0.2": "nt to"
|
| 495 |
+
},
|
| 496 |
+
{
|
| 497 |
+
"context": "I have provided all the info that Tami Scudder ",
|
| 498 |
+
"truth": "requested today.",
|
| 499 |
+
"suggestion_at_0.2": "is"
|
| 500 |
+
},
|
| 501 |
+
{
|
| 502 |
+
"context": "ontana Power Company (MPC) System Border for Thursday and Fr",
|
| 503 |
+
"truth": "iday (July 12, 13) @ $67.85/MWh. Thanks, John",
|
| 504 |
+
"suggestion_at_0.2": "iday"
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"context": "e EnBank contracts physical or financial? It seems to me we ",
|
| 508 |
+
"truth": "should use the same customer profiles - that is ",
|
| 509 |
+
"suggestion_at_0.2": ""
|
| 510 |
+
},
|
| 511 |
+
{
|
| 512 |
+
"context": "I have an appointment at 1:00 today and should be ",
|
| 513 |
+
"truth": "back to the office between 2 - 2:30.",
|
| 514 |
+
"suggestion_at_0.2": ""
|
| 515 |
+
},
|
| 516 |
+
{
|
| 517 |
+
"context": "I'm glad everythi",
|
| 518 |
+
"truth": "ng worked out for you. Hope you have a great tim",
|
| 519 |
+
"suggestion_at_0.2": "ng is"
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
"context": "o's Line 1100 from Eunice Station to Pecos River Station wil",
|
| 523 |
+
"truth": "l be out of service for 16 hours Saturday beginn",
|
| 524 |
+
"suggestion_at_0.2": "l be"
|
| 525 |
+
},
|
| 526 |
+
{
|
| 527 |
+
"context": "rty\" in the second line of paragraph 1. Otherwise they look ",
|
| 528 |
+
"truth": "great to me.",
|
| 529 |
+
"suggestion_at_0.2": ""
|
| 530 |
+
},
|
| 531 |
+
{
|
| 532 |
+
"context": "on Houston in the next day or so.\n\nPlease call me if you hav",
|
| 533 |
+
"truth": "e any questions.",
|
| 534 |
+
"suggestion_at_0.2": "e any questions."
|
| 535 |
+
},
|
| 536 |
+
{
|
| 537 |
+
"context": "s not guarantee any continuity of support for you.\n\nPlease, ",
|
| 538 |
+
"truth": "let me know what you decide to do over the next ",
|
| 539 |
+
"suggestion_at_0.2": ""
|
| 540 |
+
},
|
| 541 |
+
{
|
| 542 |
+
"context": " tend to agree with you, but Nora Dobin is the lawyer who kn",
|
| 543 |
+
"truth": "ows the most about the DealBench system - you sh",
|
| 544 |
+
"suggestion_at_0.2": "ows"
|
| 545 |
+
}
|
| 546 |
+
],
|
| 547 |
+
"total_seconds": 831.5
|
| 548 |
+
}
|
train.py
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# /// script
|
| 2 |
+
# requires-python = ">=3.11,<3.14"
|
| 3 |
+
# dependencies = [
|
| 4 |
+
# "torch>=2.6,<2.8",
|
| 5 |
+
# "datasets>=3.0",
|
| 6 |
+
# "numpy<3",
|
| 7 |
+
# "onnx>=1.17",
|
| 8 |
+
# "onnxruntime>=1.20",
|
| 9 |
+
# "huggingface_hub>=0.30",
|
| 10 |
+
# ]
|
| 11 |
+
# ///
|
| 12 |
+
"""Train a character-level GRU for in-browser email autocomplete on Enron."""
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import hashlib
|
| 16 |
+
import json
|
| 17 |
+
import os
|
| 18 |
+
import re
|
| 19 |
+
import time
|
| 20 |
+
from collections import Counter
|
| 21 |
+
from pathlib import Path
|
| 22 |
+
|
| 23 |
+
import numpy as np
|
| 24 |
+
import onnxruntime as ort
|
| 25 |
+
import torch
|
| 26 |
+
import torch.nn as nn
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
from datasets import load_dataset
|
| 29 |
+
from huggingface_hub import HfApi
|
| 30 |
+
from onnxruntime.quantization import QuantType, quantize_dynamic
|
| 31 |
+
|
| 32 |
+
# Vocabulary: PAD, EOS, newline, then printable ASCII.
|
| 33 |
+
PAD, EOS, NEWLINE, FIRST_PRINTABLE = 0, 1, 2, 3
|
| 34 |
+
VOCAB_SIZE = FIRST_PRINTABLE + (127 - 32)
|
| 35 |
+
IGNORE = -100
|
| 36 |
+
|
| 37 |
+
ENCODE = np.full(256, 255, dtype=np.uint8)
|
| 38 |
+
ENCODE[10] = NEWLINE
|
| 39 |
+
ENCODE[32:127] = np.arange(FIRST_PRINTABLE, VOCAB_SIZE, dtype=np.uint8)
|
| 40 |
+
DECODE = {NEWLINE: "\n", **{FIRST_PRINTABLE + i: chr(32 + i) for i in range(95)}}
|
| 41 |
+
|
| 42 |
+
LETTER = np.zeros(VOCAB_SIZE, dtype=bool)
|
| 43 |
+
for char in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
| 44 |
+
LETTER[ENCODE[ord(char)]] = True
|
| 45 |
+
|
| 46 |
+
KEYBOARD_NEIGHBORS = {
|
| 47 |
+
"q": "wa", "w": "qes", "e": "wrd", "r": "etf", "t": "ryg", "y": "tuh", "u": "yij",
|
| 48 |
+
"i": "uok", "o": "ipl", "p": "ol", "a": "qsz", "s": "awdx", "d": "serf", "f": "drgv",
|
| 49 |
+
"g": "fthb", "h": "gyjn", "j": "hukm", "k": "jil", "l": "kop", "z": "asx", "x": "zsc",
|
| 50 |
+
"c": "xdv", "v": "cfb", "b": "vgn", "n": "bhm", "m": "njk",
|
| 51 |
+
}
|
| 52 |
+
NEIGHBOR_TABLE = np.tile(np.arange(VOCAB_SIZE, dtype=np.uint8)[:, None], (1, 4))
|
| 53 |
+
for key, neighbors in KEYBOARD_NEIGHBORS.items():
|
| 54 |
+
for slot in range(4):
|
| 55 |
+
NEIGHBOR_TABLE[ENCODE[ord(key)], slot] = ENCODE[ord(neighbors[slot % len(neighbors)])]
|
| 56 |
+
|
| 57 |
+
CUT_REPLY = re.compile(
|
| 58 |
+
r"(?im)^\s*(?:-+\s*original message|-+\s*forwarded by|-+\s*inline attachment|from:\s|to:\s|sent:\s"
|
| 59 |
+
r"|subject:\s|cc:\s|.*\bwrote:\s*$|_{5,}|-{5,}|={5,}|\*{5,}|\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2})"
|
| 60 |
+
)
|
| 61 |
+
SENSITIVE = re.compile(
|
| 62 |
+
r"(?i)https?://|www\.|\S+@\S+|\(?\b\d{3}\)?[-. ]\d{3}[-. ]\d{4}\b|\b\d{3}-\d{2}-\d{4}\b"
|
| 63 |
+
)
|
| 64 |
+
BULK_SENDER = re.compile(r"(?i)announce|newsletter|mailer|outlook\.team|^all[._]|general\.|chairman|enron\.(?:on|net)")
|
| 65 |
+
SMART_CHARS = str.maketrans({"‘": "'", "’": "'", "“": '"', "”": '"', "–": "-", "—": "-", "\t": " "})
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def clean_paragraphs(body: str) -> list[str]:
|
| 69 |
+
text = body.replace("\r\n", "\n").replace("=20", " ").replace("=\n", "").translate(SMART_CHARS)
|
| 70 |
+
text = "\n".join(line for line in text.split("\n") if not line.lstrip().startswith(">"))
|
| 71 |
+
cut = CUT_REPLY.search(text)
|
| 72 |
+
if cut:
|
| 73 |
+
text = text[: cut.start()]
|
| 74 |
+
text = re.sub(r"(?m)^-- *\n[\s\S]*", "", text)
|
| 75 |
+
|
| 76 |
+
paragraphs = []
|
| 77 |
+
for block in re.split(r"\n\s*\n", text):
|
| 78 |
+
lines = [re.sub(r" +", " ", line).strip() for line in block.split("\n") if line.strip()]
|
| 79 |
+
if not lines:
|
| 80 |
+
continue
|
| 81 |
+
# Enron bodies are hard-wrapped near 70 columns; only keep breaks after short lines.
|
| 82 |
+
joined = lines[0]
|
| 83 |
+
for previous, line in zip(lines, lines[1:]):
|
| 84 |
+
joined += (" " if len(previous) >= 50 else "\n") + line
|
| 85 |
+
letters = sum(char.isalpha() for char in joined)
|
| 86 |
+
if SENSITIVE.search(joined) or letters < 0.6 * len(joined):
|
| 87 |
+
continue
|
| 88 |
+
paragraphs.append(joined)
|
| 89 |
+
return paragraphs
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def load_emails(limit_frac: float) -> dict[str, list[np.ndarray]]:
|
| 93 |
+
dataset = load_dataset("corbt/enron-emails", split="train").select_columns(["from", "body"])
|
| 94 |
+
if limit_frac < 1:
|
| 95 |
+
dataset = dataset.shuffle(seed=0).select(range(int(len(dataset) * limit_frac)))
|
| 96 |
+
|
| 97 |
+
parsed: list[tuple[str, list[str]]] = []
|
| 98 |
+
for row in dataset:
|
| 99 |
+
sender = (row["from"] or "").lower()
|
| 100 |
+
if "@enron.com" not in sender or BULK_SENDER.search(sender):
|
| 101 |
+
continue
|
| 102 |
+
paragraphs = clean_paragraphs(row["body"] or "")
|
| 103 |
+
if paragraphs:
|
| 104 |
+
parsed.append((sender, paragraphs))
|
| 105 |
+
|
| 106 |
+
counts = Counter(p.lower() for _, paragraphs in parsed for p in paragraphs if len(p) >= 60)
|
| 107 |
+
|
| 108 |
+
seen: set[bytes] = set()
|
| 109 |
+
splits: dict[str, list[np.ndarray]] = {"train": [], "val": [], "test": []}
|
| 110 |
+
for sender, paragraphs in parsed:
|
| 111 |
+
text = "\n\n".join(p for p in paragraphs if counts[p.lower()] < 5)
|
| 112 |
+
if not 30 <= len(text) <= 6000:
|
| 113 |
+
continue
|
| 114 |
+
digest = hashlib.sha1(re.sub(r"\s+", " ", text.lower()).encode()).digest()
|
| 115 |
+
if digest in seen:
|
| 116 |
+
continue
|
| 117 |
+
seen.add(digest)
|
| 118 |
+
bucket = int(hashlib.sha1(sender.encode()).hexdigest(), 16) % 100
|
| 119 |
+
split = "test" if bucket < 5 else "val" if bucket < 10 else "train"
|
| 120 |
+
ids = ENCODE[np.frombuffer(text.encode("ascii", "ignore"), dtype=np.uint8)]
|
| 121 |
+
splits[split].append(np.append(ids[ids != 255], EOS).astype(np.uint8))
|
| 122 |
+
return splits
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def concat_shuffled(emails: list[np.ndarray], rng: np.random.Generator) -> np.ndarray:
|
| 126 |
+
return np.concatenate([emails[i] for i in rng.permutation(len(emails))])
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def add_typos(clean: np.ndarray, word_rate: float, rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]:
|
| 130 |
+
"""Corrupt the input stream while the targets keep predicting the clean next character."""
|
| 131 |
+
x = clean[:-1].copy()
|
| 132 |
+
y = clean[1:].astype(np.int16)
|
| 133 |
+
is_letter = LETTER[x]
|
| 134 |
+
starts = np.flatnonzero(is_letter[1:] & ~is_letter[:-1]) + 1
|
| 135 |
+
ends = np.flatnonzero(is_letter[:-1] & ~is_letter[1:]) + 1
|
| 136 |
+
if len(ends) and len(starts) and ends[0] <= starts[0]:
|
| 137 |
+
ends = ends[1:]
|
| 138 |
+
count = min(len(starts), len(ends))
|
| 139 |
+
starts, ends = starts[:count], ends[:count]
|
| 140 |
+
|
| 141 |
+
lengths = ends - starts
|
| 142 |
+
chosen = (lengths >= 3) & (rng.random(count) < word_rate)
|
| 143 |
+
word_starts, word_lengths = starts[chosen], lengths[chosen]
|
| 144 |
+
position = word_starts + 1 + (rng.random(len(word_starts)) * (word_lengths - 2)).astype(np.int64)
|
| 145 |
+
position = position[position + 1 < len(x)]
|
| 146 |
+
ops = rng.integers(0, 4, size=len(position))
|
| 147 |
+
neighbor = NEIGHBOR_TABLE[x[position], rng.integers(0, 4, size=len(position))]
|
| 148 |
+
|
| 149 |
+
substitute, swap, insert, delete = (position[ops == op] for op in range(4))
|
| 150 |
+
x[substitute] = neighbor[ops == 0]
|
| 151 |
+
first, second = x[swap].copy(), x[swap + 1].copy()
|
| 152 |
+
x[swap], x[swap + 1], y[swap] = second, first, IGNORE
|
| 153 |
+
|
| 154 |
+
repeats = np.ones(len(x), dtype=np.int64)
|
| 155 |
+
repeats[delete] = 0
|
| 156 |
+
repeats[insert] = 2
|
| 157 |
+
inserted_at = np.cumsum(repeats)[insert] - 1
|
| 158 |
+
x_out, y_out = np.repeat(x, repeats), np.repeat(y, repeats)
|
| 159 |
+
x_out[inserted_at] = neighbor[ops == 2]
|
| 160 |
+
return x_out, y_out
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def make_batches(x, y, batch_size, seq_len, rng):
|
| 164 |
+
offset = int(rng.integers(seq_len))
|
| 165 |
+
starts = rng.permutation(np.arange(offset, len(x) - seq_len, seq_len))
|
| 166 |
+
span = np.arange(seq_len)
|
| 167 |
+
for index in range(0, len(starts) - batch_size + 1, batch_size):
|
| 168 |
+
positions = starts[index : index + batch_size][:, None] + span
|
| 169 |
+
yield torch.from_numpy(x[positions].astype(np.int64)), torch.from_numpy(y[positions].astype(np.int64))
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class CharGRU(nn.Module):
|
| 173 |
+
def __init__(self, hidden: int, embedding: int = 64):
|
| 174 |
+
super().__init__()
|
| 175 |
+
self.embedding = nn.Embedding(VOCAB_SIZE, embedding, padding_idx=PAD)
|
| 176 |
+
self.gru = nn.GRU(embedding, hidden, batch_first=True)
|
| 177 |
+
self.head = nn.Linear(hidden, VOCAB_SIZE)
|
| 178 |
+
|
| 179 |
+
def forward(self, ids, state=None):
|
| 180 |
+
output, state = self.gru(self.embedding(ids), state)
|
| 181 |
+
return self.head(output), state
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
def sequence_loss(logits, targets, burn_in):
|
| 185 |
+
return F.cross_entropy(logits[:, burn_in:].flatten(0, 1), targets[:, burn_in:].flatten(), ignore_index=IGNORE)
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@torch.no_grad()
|
| 189 |
+
def evaluate_loss(model, emails, args, device, max_chars=5_000_000):
|
| 190 |
+
rng = np.random.default_rng(1)
|
| 191 |
+
clean = concat_shuffled(emails, rng)[:max_chars]
|
| 192 |
+
x, y = clean[:-1], clean[1:].astype(np.int16)
|
| 193 |
+
total, batches = 0.0, 0
|
| 194 |
+
for inputs, targets in make_batches(x, y, args.batch, args.seq, rng):
|
| 195 |
+
logits, _ = model(inputs.to(device))
|
| 196 |
+
total += sequence_loss(logits, targets.to(device), args.burn_in).item()
|
| 197 |
+
batches += 1
|
| 198 |
+
return total / max(batches, 1)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def decode(ids) -> str:
|
| 202 |
+
return "".join(DECODE.get(int(i), "") for i in ids)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def sample_positions(emails, count, context, horizon, rng):
|
| 206 |
+
contexts, truths = [], []
|
| 207 |
+
attempts = 0
|
| 208 |
+
while len(contexts) < count and attempts < count * 200:
|
| 209 |
+
attempts += 1
|
| 210 |
+
email = emails[int(rng.integers(len(emails)))]
|
| 211 |
+
if len(email) < 64:
|
| 212 |
+
continue
|
| 213 |
+
cursor = int(rng.integers(16, len(email) - 16))
|
| 214 |
+
previous, following = int(email[cursor - 1]), int(email[cursor])
|
| 215 |
+
if not (LETTER[previous] or previous == ENCODE[32]) or not LETTER[following]:
|
| 216 |
+
continue
|
| 217 |
+
window = np.full(context, EOS, dtype=np.int64)
|
| 218 |
+
prefix = email[max(0, cursor - context) : cursor]
|
| 219 |
+
window[-len(prefix):] = prefix
|
| 220 |
+
truth = email[cursor : cursor + horizon]
|
| 221 |
+
ending = np.flatnonzero((truth == EOS) | (truth == NEWLINE))
|
| 222 |
+
truths.append(decode(truth[: ending[0]] if len(ending) else truth))
|
| 223 |
+
contexts.append(window)
|
| 224 |
+
return np.stack(contexts), truths
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
def build_suggestion(ids, probabilities, threshold, max_words=6):
|
| 228 |
+
text, confidence, best, words = "", 1.0, "", 0
|
| 229 |
+
for token, probability in zip(ids, probabilities):
|
| 230 |
+
token = int(token)
|
| 231 |
+
if token in (EOS, NEWLINE, PAD):
|
| 232 |
+
break
|
| 233 |
+
char = DECODE[token]
|
| 234 |
+
if char == " ":
|
| 235 |
+
if confidence >= threshold:
|
| 236 |
+
best = text
|
| 237 |
+
words += 1
|
| 238 |
+
if words >= max_words:
|
| 239 |
+
break
|
| 240 |
+
confidence *= float(probability)
|
| 241 |
+
if confidence < threshold:
|
| 242 |
+
break
|
| 243 |
+
text += char
|
| 244 |
+
if char in ".,!?":
|
| 245 |
+
best = text
|
| 246 |
+
return best
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
def score_suggestions(generated, probabilities, truths, thresholds):
|
| 250 |
+
report = {}
|
| 251 |
+
for threshold in thresholds:
|
| 252 |
+
shown, correct, saved = 0, 0, 0
|
| 253 |
+
for ids, probs, truth in zip(generated, probabilities, truths):
|
| 254 |
+
suggestion = build_suggestion(ids, probs, threshold)
|
| 255 |
+
if len(suggestion) < 4:
|
| 256 |
+
continue
|
| 257 |
+
shown += 1
|
| 258 |
+
if truth.startswith(suggestion):
|
| 259 |
+
correct += 1
|
| 260 |
+
saved += len(suggestion)
|
| 261 |
+
report[str(threshold)] = {
|
| 262 |
+
"coverage": round(shown / len(truths), 4),
|
| 263 |
+
"precision": round(correct / shown, 4) if shown else None,
|
| 264 |
+
"avg_chars_when_correct": round(saved / correct, 2) if correct else None,
|
| 265 |
+
"chars_saved_per_position": round(saved / len(truths), 3),
|
| 266 |
+
}
|
| 267 |
+
return report
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
@torch.no_grad()
|
| 271 |
+
def greedy_torch(model, contexts, steps, device, chunk=2048):
|
| 272 |
+
all_ids, all_probs = [], []
|
| 273 |
+
for index in range(0, len(contexts), chunk):
|
| 274 |
+
ids = torch.from_numpy(contexts[index : index + chunk]).to(device)
|
| 275 |
+
logits, state = model(ids)
|
| 276 |
+
step_logits = logits[:, -1]
|
| 277 |
+
batch_ids, batch_probs = [], []
|
| 278 |
+
for _ in range(steps):
|
| 279 |
+
probabilities = step_logits.softmax(-1)
|
| 280 |
+
confidence, token = probabilities.max(-1)
|
| 281 |
+
batch_ids.append(token.cpu().numpy())
|
| 282 |
+
batch_probs.append(confidence.cpu().numpy())
|
| 283 |
+
logits, state = model(token[:, None], state)
|
| 284 |
+
step_logits = logits[:, -1]
|
| 285 |
+
all_ids.append(np.stack(batch_ids, 1))
|
| 286 |
+
all_probs.append(np.stack(batch_probs, 1))
|
| 287 |
+
return np.concatenate(all_ids), np.concatenate(all_probs)
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def greedy_onnx(session, contexts, steps, hidden):
|
| 291 |
+
state = np.zeros((len(contexts), hidden), dtype=np.float32)
|
| 292 |
+
logits = None
|
| 293 |
+
for column in range(contexts.shape[1]):
|
| 294 |
+
logits, state = session.run(None, {"input_id": contexts[:, column], "state": state})
|
| 295 |
+
ids, confidences = [], []
|
| 296 |
+
for _ in range(steps):
|
| 297 |
+
probabilities = np.exp(logits - logits.max(-1, keepdims=True))
|
| 298 |
+
probabilities /= probabilities.sum(-1, keepdims=True)
|
| 299 |
+
token = probabilities.argmax(-1)
|
| 300 |
+
ids.append(token)
|
| 301 |
+
confidences.append(probabilities[np.arange(len(token)), token])
|
| 302 |
+
logits, state = session.run(None, {"input_id": token.astype(np.int64), "state": state})
|
| 303 |
+
return np.stack(ids, 1), np.stack(confidences, 1)
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
class MatMulLinear(nn.Module):
|
| 307 |
+
"""Linear layer that exports as MatMul so ONNX Runtime dynamic quantization applies."""
|
| 308 |
+
|
| 309 |
+
def __init__(self, weight: torch.Tensor, bias: torch.Tensor):
|
| 310 |
+
super().__init__()
|
| 311 |
+
self.weight = nn.Parameter(weight.t().clone())
|
| 312 |
+
self.bias = nn.Parameter(bias.clone())
|
| 313 |
+
|
| 314 |
+
def forward(self, inputs):
|
| 315 |
+
return inputs.matmul(self.weight) + self.bias
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
class SingleStep(nn.Module):
|
| 319 |
+
"""One-character GRU update with weights copied from the trained nn.GRU."""
|
| 320 |
+
|
| 321 |
+
def __init__(self, model: CharGRU, hidden: int):
|
| 322 |
+
super().__init__()
|
| 323 |
+
self.embedding = model.embedding
|
| 324 |
+
input_weight, hidden_weight = model.gru.weight_ih_l0.detach(), model.gru.weight_hh_l0.detach()
|
| 325 |
+
input_bias, hidden_bias = model.gru.bias_ih_l0.detach(), model.gru.bias_hh_l0.detach()
|
| 326 |
+
layers = []
|
| 327 |
+
for gate in range(3):
|
| 328 |
+
row = slice(gate * hidden, (gate + 1) * hidden)
|
| 329 |
+
layers.append(MatMulLinear(input_weight[row], input_bias[row]))
|
| 330 |
+
layers.append(MatMulLinear(hidden_weight[row], hidden_bias[row]))
|
| 331 |
+
self.gates = nn.ModuleList(layers)
|
| 332 |
+
self.head = MatMulLinear(model.head.weight.detach(), model.head.bias.detach())
|
| 333 |
+
|
| 334 |
+
def forward(self, input_id, state):
|
| 335 |
+
embedded = self.embedding(input_id)
|
| 336 |
+
reset = torch.sigmoid(self.gates[0](embedded) + self.gates[1](state))
|
| 337 |
+
update = torch.sigmoid(self.gates[2](embedded) + self.gates[3](state))
|
| 338 |
+
proposal = torch.tanh(self.gates[4](embedded) + reset * self.gates[5](state))
|
| 339 |
+
next_state = (1 - update) * proposal + update * state
|
| 340 |
+
return self.head(next_state), next_state
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def verify_single_step(model, step, hidden):
|
| 344 |
+
ids = torch.randint(3, VOCAB_SIZE, (4, 32))
|
| 345 |
+
logits, _ = model(ids)
|
| 346 |
+
state = torch.zeros(4, hidden)
|
| 347 |
+
outputs = []
|
| 348 |
+
for column in range(ids.shape[1]):
|
| 349 |
+
step_logits, state = step(ids[:, column], state)
|
| 350 |
+
outputs.append(step_logits)
|
| 351 |
+
difference = (torch.stack(outputs, 1) - logits).abs().max().item()
|
| 352 |
+
if difference > 1e-3:
|
| 353 |
+
raise RuntimeError(f"Exported single-step GRU does not match nn.GRU: max difference {difference}")
|
| 354 |
+
return difference
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
def measure_latency(path, hidden, runs=3000):
|
| 358 |
+
options = ort.SessionOptions()
|
| 359 |
+
options.intra_op_num_threads = 1
|
| 360 |
+
session = ort.InferenceSession(str(path), options, providers=["CPUExecutionProvider"])
|
| 361 |
+
state = np.zeros((1, hidden), dtype=np.float32)
|
| 362 |
+
timings = []
|
| 363 |
+
for run in range(runs):
|
| 364 |
+
token = np.array([FIRST_PRINTABLE + run % 90], dtype=np.int64)
|
| 365 |
+
started = time.perf_counter()
|
| 366 |
+
_, state = session.run(None, {"input_id": token, "state": state})
|
| 367 |
+
timings.append((time.perf_counter() - started) * 1000)
|
| 368 |
+
return {"p50_ms": round(float(np.percentile(timings, 50)), 3), "p95_ms": round(float(np.percentile(timings, 95)), 3)}
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def export_and_score(model, args, output, contexts, truths, thresholds):
|
| 372 |
+
model = model.cpu().eval()
|
| 373 |
+
step = SingleStep(model, args.hidden).eval()
|
| 374 |
+
difference = verify_single_step(model, step, args.hidden)
|
| 375 |
+
print(f"single-step GRU matches nn.GRU (max difference {difference:.2e})", flush=True)
|
| 376 |
+
|
| 377 |
+
float_path = output / "model.onnx"
|
| 378 |
+
torch.onnx.export(
|
| 379 |
+
step,
|
| 380 |
+
(torch.zeros(1, dtype=torch.long), torch.zeros(1, args.hidden)),
|
| 381 |
+
float_path,
|
| 382 |
+
input_names=["input_id", "state"],
|
| 383 |
+
output_names=["logits", "next_state"],
|
| 384 |
+
dynamic_axes={"input_id": {0: "batch"}, "state": {0: "batch"}, "logits": {0: "batch"}, "next_state": {0: "batch"}},
|
| 385 |
+
opset_version=17,
|
| 386 |
+
dynamo=False,
|
| 387 |
+
)
|
| 388 |
+
quantized_path = output / "model_quint8.onnx"
|
| 389 |
+
quantize_dynamic(float_path, quantized_path, weight_type=QuantType.QUInt8, op_types_to_quantize=["MatMul"])
|
| 390 |
+
|
| 391 |
+
subset = min(1000, len(contexts))
|
| 392 |
+
result = {}
|
| 393 |
+
for name, path in (("float", float_path), ("quint8", quantized_path)):
|
| 394 |
+
session = ort.InferenceSession(str(path), providers=["CPUExecutionProvider"])
|
| 395 |
+
ids, probabilities = greedy_onnx(session, contexts[:subset], args.horizon, args.hidden)
|
| 396 |
+
result[name] = {
|
| 397 |
+
"size_mb": round(path.stat().st_size / 1e6, 2),
|
| 398 |
+
"native_cpu_single_thread_step": measure_latency(path, args.hidden),
|
| 399 |
+
"suggestions_1000_positions": score_suggestions(ids, probabilities, truths[:subset], thresholds),
|
| 400 |
+
}
|
| 401 |
+
return result
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def show_samples(model, device):
|
| 405 |
+
prompts = [
|
| 406 |
+
"Hi John,\n\nThanks for ", "Please let me know if ", "I will send you ", "Let me kn", "Thnaks for the ",
|
| 407 |
+
"I look forward to ", "Can we schedule a ", "Sorry for the dela", "Plese find attached ", "We need to discuss ",
|
| 408 |
+
]
|
| 409 |
+
samples = {}
|
| 410 |
+
for prompt in prompts:
|
| 411 |
+
window = np.full(128, EOS, dtype=np.int64)
|
| 412 |
+
encoded = ENCODE[np.frombuffer(prompt.encode("ascii"), dtype=np.uint8)]
|
| 413 |
+
window[-len(encoded):] = encoded
|
| 414 |
+
ids, probabilities = greedy_torch(model, window[None], 48, device)
|
| 415 |
+
samples[prompt] = {
|
| 416 |
+
"continuation": decode(ids[0]),
|
| 417 |
+
"suggestion_at_0.2": build_suggestion(ids[0], probabilities[0], 0.2),
|
| 418 |
+
}
|
| 419 |
+
print(f"{prompt!r} -> {samples[prompt]}", flush=True)
|
| 420 |
+
return samples
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
def parse_args():
|
| 424 |
+
parser = argparse.ArgumentParser()
|
| 425 |
+
parser.add_argument("--hidden", type=int, default=512)
|
| 426 |
+
parser.add_argument("--epochs", type=int, default=30)
|
| 427 |
+
parser.add_argument("--batch", type=int, default=256)
|
| 428 |
+
parser.add_argument("--seq", type=int, default=256)
|
| 429 |
+
parser.add_argument("--burn-in", type=int, default=32)
|
| 430 |
+
parser.add_argument("--lr", type=float, default=1.5e-3)
|
| 431 |
+
parser.add_argument("--typo-rate", type=float, default=0.08)
|
| 432 |
+
parser.add_argument("--limit-frac", type=float, default=1.0)
|
| 433 |
+
parser.add_argument("--eval-positions", type=int, default=5000)
|
| 434 |
+
parser.add_argument("--context", type=int, default=128)
|
| 435 |
+
parser.add_argument("--horizon", type=int, default=48)
|
| 436 |
+
parser.add_argument("--repo", default="cfahlgren1/enron-autocomplete")
|
| 437 |
+
parser.add_argument("--no-push", action="store_true")
|
| 438 |
+
return parser.parse_args()
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def main():
|
| 442 |
+
args = parse_args()
|
| 443 |
+
torch.manual_seed(0)
|
| 444 |
+
rng = np.random.default_rng(0)
|
| 445 |
+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 446 |
+
print(f"device={device} {torch.cuda.get_device_name(0) if device.type == 'cuda' else ''} args={vars(args)}", flush=True)
|
| 447 |
+
output = Path("outputs")
|
| 448 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 449 |
+
|
| 450 |
+
started = time.time()
|
| 451 |
+
splits = load_emails(args.limit_frac)
|
| 452 |
+
data_stats = {name: {"emails": len(e), "chars": int(sum(map(len, e)))} for name, e in splits.items()}
|
| 453 |
+
print(f"data ready in {time.time() - started:.0f}s: {data_stats}", flush=True)
|
| 454 |
+
if not splits["train"] or not splits["val"] or not splits["test"]:
|
| 455 |
+
raise RuntimeError(f"A split is empty: {data_stats}")
|
| 456 |
+
|
| 457 |
+
model = CharGRU(args.hidden).to(device)
|
| 458 |
+
parameters = sum(p.numel() for p in model.parameters())
|
| 459 |
+
print(f"parameters={parameters:,}", flush=True)
|
| 460 |
+
|
| 461 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=1e-5)
|
| 462 |
+
steps_per_epoch = max(data_stats["train"]["chars"] // (args.batch * args.seq), 1)
|
| 463 |
+
total_steps = args.epochs * steps_per_epoch
|
| 464 |
+
scheduler = torch.optim.lr_scheduler.LambdaLR(
|
| 465 |
+
optimizer, lambda step: 0.02 + 0.98 * 0.5 * (1 + np.cos(np.pi * min(step / total_steps, 1)))
|
| 466 |
+
)
|
| 467 |
+
|
| 468 |
+
history = []
|
| 469 |
+
best_loss = float("inf")
|
| 470 |
+
best_path = output / "best.pt"
|
| 471 |
+
for epoch in range(args.epochs):
|
| 472 |
+
model.train()
|
| 473 |
+
epoch_started = time.time()
|
| 474 |
+
x, y = add_typos(concat_shuffled(splits["train"], rng), args.typo_rate, rng)
|
| 475 |
+
running, batches, chars = 0.0, 0, 0
|
| 476 |
+
for inputs, targets in make_batches(x, y, args.batch, args.seq, rng):
|
| 477 |
+
logits, _ = model(inputs.to(device, non_blocking=True))
|
| 478 |
+
loss = sequence_loss(logits, targets.to(device, non_blocking=True), args.burn_in)
|
| 479 |
+
optimizer.zero_grad(set_to_none=True)
|
| 480 |
+
loss.backward()
|
| 481 |
+
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
| 482 |
+
optimizer.step()
|
| 483 |
+
scheduler.step()
|
| 484 |
+
running += loss.item()
|
| 485 |
+
batches += 1
|
| 486 |
+
chars += inputs.numel()
|
| 487 |
+
if batches == 0:
|
| 488 |
+
raise RuntimeError("No training batches; reduce --batch or --seq for this data size")
|
| 489 |
+
|
| 490 |
+
model.eval()
|
| 491 |
+
val_loss = evaluate_loss(model, splits["val"], args, device)
|
| 492 |
+
seconds = time.time() - epoch_started
|
| 493 |
+
record = {
|
| 494 |
+
"epoch": epoch + 1,
|
| 495 |
+
"train_loss": round(running / batches, 4),
|
| 496 |
+
"val_loss": round(val_loss, 4),
|
| 497 |
+
"seconds": round(seconds, 1),
|
| 498 |
+
"chars_per_sec": int(chars / seconds),
|
| 499 |
+
}
|
| 500 |
+
history.append(record)
|
| 501 |
+
print(json.dumps(record), flush=True)
|
| 502 |
+
if val_loss < best_loss:
|
| 503 |
+
best_loss = val_loss
|
| 504 |
+
torch.save({"model": model.state_dict(), "args": vars(args)}, best_path)
|
| 505 |
+
|
| 506 |
+
model.load_state_dict(torch.load(best_path, map_location=device)["model"])
|
| 507 |
+
model.eval()
|
| 508 |
+
|
| 509 |
+
thresholds = [0.05, 0.1, 0.2, 0.35, 0.5, 0.7]
|
| 510 |
+
eval_rng = np.random.default_rng(42)
|
| 511 |
+
contexts, truths = sample_positions(splits["test"], args.eval_positions, args.context, args.horizon, eval_rng)
|
| 512 |
+
ids, probabilities = greedy_torch(model, contexts, args.horizon, device)
|
| 513 |
+
suggestions = score_suggestions(ids, probabilities, truths, thresholds)
|
| 514 |
+
print(json.dumps({"held_out_sender_suggestions": suggestions}, indent=1), flush=True)
|
| 515 |
+
|
| 516 |
+
examples = []
|
| 517 |
+
for index in range(min(25, len(contexts))):
|
| 518 |
+
examples.append({
|
| 519 |
+
"context": decode(contexts[index])[-60:],
|
| 520 |
+
"truth": truths[index],
|
| 521 |
+
"suggestion_at_0.2": build_suggestion(ids[index], probabilities[index], 0.2),
|
| 522 |
+
})
|
| 523 |
+
samples = show_samples(model, device)
|
| 524 |
+
exported = export_and_score(model, args, output, contexts, truths, thresholds)
|
| 525 |
+
print(json.dumps(exported, indent=1), flush=True)
|
| 526 |
+
|
| 527 |
+
report = {
|
| 528 |
+
"args": vars(args),
|
| 529 |
+
"device": str(device),
|
| 530 |
+
"parameters": parameters,
|
| 531 |
+
"data": data_stats,
|
| 532 |
+
"history": history,
|
| 533 |
+
"best_val_loss": round(best_loss, 4),
|
| 534 |
+
"test_positions": len(contexts),
|
| 535 |
+
"held_out_sender_suggestions": suggestions,
|
| 536 |
+
"exports": exported,
|
| 537 |
+
"prompt_samples": samples,
|
| 538 |
+
"held_out_examples": examples,
|
| 539 |
+
"total_seconds": round(time.time() - started, 1),
|
| 540 |
+
}
|
| 541 |
+
(output / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 542 |
+
(output / "vocab.json").write_text(json.dumps({
|
| 543 |
+
"pad": PAD, "eos": EOS, "newline": NEWLINE, "first_printable_id": FIRST_PRINTABLE,
|
| 544 |
+
"first_printable_codepoint": 32, "size": VOCAB_SIZE, "hidden": args.hidden,
|
| 545 |
+
}, indent=2) + "\n")
|
| 546 |
+
|
| 547 |
+
if args.no_push:
|
| 548 |
+
print(f"Skipping upload; artifacts in {output}", flush=True)
|
| 549 |
+
return
|
| 550 |
+
api = HfApi(token=os.environ["HF_TOKEN"])
|
| 551 |
+
api.create_repo(args.repo, repo_type="model", private=True, exist_ok=True)
|
| 552 |
+
api.upload_folder(folder_path=str(output), repo_id=args.repo, repo_type="model")
|
| 553 |
+
print(f"Uploaded to https://huggingface.co/{args.repo}", flush=True)
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
if __name__ == "__main__":
|
| 557 |
+
main()
|
vocab.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"pad": 0,
|
| 3 |
+
"eos": 1,
|
| 4 |
+
"newline": 2,
|
| 5 |
+
"first_printable_id": 3,
|
| 6 |
+
"first_printable_codepoint": 32,
|
| 7 |
+
"size": 98,
|
| 8 |
+
"hidden": 512
|
| 9 |
+
}
|