File size: 5,697 Bytes
d018134
 
5ddd730
d018134
5ddd730
d018134
 
 
 
5ddd730
d018134
 
5ddd730
d018134
 
5ddd730
 
 
 
8282424
5ddd730
 
 
 
d018134
5ddd730
d018134
5ddd730
 
 
 
 
d018134
 
5ddd730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d018134
 
 
5ddd730
 
d018134
 
5ddd730
 
d018134
 
8282424
d018134
 
5ddd730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d018134
5ddd730
 
d018134
5ddd730
 
d018134
 
5ddd730
 
 
 
 
 
 
 
 
 
 
 
 
 
d018134
 
 
5ddd730
 
 
 
d018134
5ddd730
d018134
5ddd730
d018134
5ddd730
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
---
language: en
license: mit
library_name: tensorflow
pipeline_tag: text-generation
tags:
  - character-level
  - text-generation
  - gru
  - rnn
  - tensorflow
  - keras
  - news
---

# Character-Level News Writer (GRU)

A small GRU language model (1M parameters) that writes **one character at a time**. It was never given a word list: it learned spelling, spacing, punctuation and the shape of a news story from ~2,700 Pakistani business and sports news articles.

**Try it:** [Character-level News Writer](https://huggingface.co/spaces/Ismailalshofi/char-news-writer), a Space where the model runs in your browser (the GRU re-implemented in plain JavaScript, checked against TensorFlow) · **Code:** [GitHub](https://github.com/ismailalshofi/character-based-language-model)

## Samples

Real outputs, temperature 0.5 and 0.8:

> **HONG KONG:** Asian stocks started on the final over as dealers could be able to see if they will allow the first day of the last settlement. The country's cricket team has recovered from a more than half the second set to beat South Africa...

> **KARACHI:** Former West Indies Bailey delivery immossed the winner in 2052, and then been planned to the statement on Friday despite himself to consolidate their production cuts in a new target of 127 for six...

The style is right: datelines, prices, scores, team names, "said on Monday". The facts are not, and business and sports get mixed in the same sentence, because the model only ever saw letters, never meaning.

## Model

| Component | Details |
|---|---|
| Embedding | 106 characters → 128-dim vectors |
| GRU | 512 units, dropout 0.2 |
| Dense | 512 → 106 logits |
| Parameters | 1,054,058 (4 MB) |

## Training

| | |
|---|---|
| Data | 2,692 news articles (1,284 business, 1,408 sports), 4.48M characters after cleaning |
| Cleaning | HTML and URLs removed, smart quotes and dashes normalized, whitespace collapsed. Case, digits and punctuation kept |
| Sequences | 100 characters, 90/10 train/validation split |
| Optimizer | Adam, batch 128, 30 epochs |
| Final validation loss | **1.096** (≈ 1.58 bits per character), accuracy 67.4% |

The validation loss was still going down at epoch 30, so early stopping never triggered: more epochs would likely help.

![Training curves](training_plot.png)

## Usage

The model is a subclassed Keras model, so loading it needs the `CharModel` class. This is the complete code (TensorFlow 2.15):

```python
import json

import tensorflow as tf
from huggingface_hub import hf_hub_download

REPO_ID = "Ismailalshofi/char-based-language-model"


class CharModel(tf.keras.Model):
    def __init__(self, vocab_size, embedding_dim, rnn_units, dropout_rate=0.2):
        super().__init__()
        self.embedding = tf.keras.layers.Embedding(vocab_size, embedding_dim)
        self.dropout = tf.keras.layers.Dropout(dropout_rate)
        self.gru = tf.keras.layers.GRU(
            rnn_units, return_sequences=True, return_state=True, dropout=dropout_rate, recurrent_dropout=0.0
        )
        self.dense = tf.keras.layers.Dense(vocab_size)

    def call(self, inputs, states=None, return_state=False, training=False):
        x = self.embedding(inputs, training=training)
        x = self.dropout(x, training=training)
        if states is None:
            states = self.gru.get_initial_state(x)
        x, states = self.gru(x, initial_state=states, training=training)
        x = self.dense(x, training=training)
        return (x, states) if return_state else x


# local_dir keeps the original file names (on Windows the hub cache uses symlinks TensorFlow can't read)
files = {name: hf_hub_download(REPO_ID, name, local_dir="model") for name in ("model.keras", "vocab.json")}
with open(files["vocab.json"], encoding="utf-8") as f:
    vocab = json.load(f)
get_ids = tf.keras.layers.StringLookup(vocabulary=vocab, mask_token=None)
get_chars = tf.keras.layers.StringLookup(vocabulary=get_ids.get_vocabulary(), invert=True, mask_token=None)
model = tf.keras.models.load_model(files["model.keras"], custom_objects={"CharModel": CharModel}, compile=False)

# Never sample the [UNK] token
unk_mask = tf.one_hot(get_ids(["[UNK]"])[0], len(get_ids.get_vocabulary()), on_value=-1e9, off_value=0.0)


def generate(seed, length=300, temperature=0.8):
    ids = get_ids(tf.strings.unicode_split([seed], "UTF-8")).to_tensor()
    states, text = None, seed
    for _ in range(length):
        logits, states = model(ids, states=states, return_state=True)
        ids = tf.random.categorical(logits[:, -1, :] / temperature + unk_mask, num_samples=1)
        text += get_chars(ids)[0, 0].numpy().decode("utf-8")
    return text


print(generate("KARACHI:"))
```

**Temperature:** around 0.5 is safe and repetitive, 0.8 is a good balance, above 1.0 the model starts inventing words.

## Limitations

- **Style, not facts.** It writes text that looks like news. Every name, number and event in it is made up.
- **Narrow domain.** Pakistani business and sports news from one source. Other topics or seeds produce weaker text.
- **Encoding artifacts.** Some apostrophes in the source data were stored as `´` (e.g. `Lord´s`), and the model learned them.
- **English only**, 106 characters. Anything outside that vocabulary becomes `[UNK]`.

## Related

This model is the starting point of a [controlled study of character vs subword tokenization](https://github.com/ismailalshofi/tokenization-study), which trains the same Transformer with both tokenizers and compares them fairly in bits per character.

## Author

**Ismail Alshofi** · [GitHub](https://github.com/ismailalshofi) · [Kaggle](https://www.kaggle.com/ismailalshofi) · [Medium](https://medium.com/@ialshofi)