danielsebesta commited on
Commit
f2f82b7
·
verified ·
1 Parent(s): 9ce8733

Model card and inference code

Browse files
Files changed (2) hide show
  1. README.md +80 -0
  2. kecal.py +112 -0
README.md ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - cs
4
+ license: cc-by-nc-sa-4.0
5
+ library_name: pytorch
6
+ pipeline_tag: text-generation
7
+ tags:
8
+ - czech
9
+ - chat
10
+ - steganography
11
+ - small-language-model
12
+ - encrypt.click
13
+ datasets:
14
+ - CZLC/CNC_oral_ortofon
15
+ - CZLC/fb_sentiment_balanced
16
+ - lang-uk/Reddit-MultiGEC
17
+ ---
18
+
19
+ # Kecal
20
+
21
+ A small language model that writes casual Czech chat messages: one person sending a few messages in a row to a friend, the way people actually type on Messenger, WhatsApp or Discord.
22
+
23
+ Kecal was made for the **Chat cipher** on [encrypt.click](https://encrypt.click), where it turns encrypted data into ordinary looking chat messages and back. It is a tool for that job, not a chatbot or an assistant.
24
+
25
+ ```
26
+ to je
27
+ ale teď mě napadá
28
+ nechceš se náhodou někdy vidět
29
+ mohli bychom si pokecat
30
+ na pivo
31
+ a já ti povím
32
+ jak mi ta hra šla
33
+ ```
34
+
35
+ ## Model
36
+
37
+ | | |
38
+ |---|---|
39
+ | Parameters | 37.5 M |
40
+ | Architecture | decoder-only transformer, 8 layers, 8 heads, width 512 |
41
+ | Tokens | 24,000 words (lowercase, without diacritics) plus `<pad>`, `<eol>`, `<unk>`, `<turn>` |
42
+ | Context | 128 words |
43
+
44
+ Words are stored folded (lowercase, no diacritics); `vocab.json` maps each one back to its most common written form. `<eol>` ends a message, `<turn>` marks a change of writer.
45
+
46
+ ## Usage
47
+
48
+ ```python
49
+ from kecal import Kecal
50
+
51
+ kecal = Kecal.load('.') # this folder: config.json, model.safetensors, vocab.json
52
+ print(kecal.generate(60))
53
+ ```
54
+
55
+ Requires `torch` and `safetensors`.
56
+
57
+ ## Training
58
+
59
+ 1. Pretraining on Czech subtitles (OpenSubtitles, about 800 M words).
60
+ 2. Fine-tuning on conversational Czech: transcribed spoken Czech, Facebook comments and Reddit posts.
61
+ 3. Further fine-tuning on synthetic chat messages, refined for casual one-person writing.
62
+
63
+ ## Limitations
64
+
65
+ - Output has no punctuation or capital letters, and diacritics follow the most common written form of each word.
66
+ - Built for natural sounding filler text, not facts. It invents things and can drift between topics.
67
+ - Trained on informal language, so it can produce slang and swearing.
68
+
69
+ ## Credits
70
+
71
+ Kecal builds on these resources. Thank you to their authors:
72
+
73
+ - **ORTOFON** – corpus of informal spoken Czech, M. Kopřivová et al., Institute of the Czech National Corpus, Faculty of Arts, Charles University, Prague ([korpus.cz](https://www.korpus.cz)). Via [CZLC/CNC_oral_ortofon](https://huggingface.co/datasets/CZLC/CNC_oral_ortofon) (CC BY-NC-SA 4.0).
74
+ - **Czech Facebook sentiment corpus** – I. Habernal, T. Ptáček, J. Steinberger (2013), *Sentiment Analysis in Czech Social Media Using Supervised Machine Learning*; University of West Bohemia, [corpora.kiv.zcu.cz/sentiment](https://corpora.kiv.zcu.cz/sentiment/). Via [CZLC/fb_sentiment_balanced](https://huggingface.co/datasets/CZLC/fb_sentiment_balanced) (CC BY-NC-SA 4.0).
75
+ - **Reddit-MultiGEC** – [lang-uk/Reddit-MultiGEC](https://huggingface.co/datasets/lang-uk/Reddit-MultiGEC) (MIT).
76
+ - **OpenSubtitles** – P. Lison and J. Tiedemann (2016), *OpenSubtitles2016: Extracting Large Parallel Corpora from Movie and TV Subtitles*, via [OPUS](https://opus.nlpl.eu/), with subtitles from [opensubtitles.org](https://www.opensubtitles.org/).
77
+
78
+ ## License
79
+
80
+ [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), following the licenses of the training data: attribution, non-commercial use, share alike.
kecal.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kecal: a small word-level language model for casual Czech chat messages.
2
+
3
+ from kecal import Kecal
4
+ kecal = Kecal.load('.') # folder with config.json, model.safetensors, vocab.json
5
+ print(kecal.generate(60))
6
+
7
+ Needs torch and safetensors.
8
+ """
9
+ import json
10
+ import math
11
+ import os
12
+ from dataclasses import dataclass
13
+
14
+ import torch
15
+ import torch.nn as nn
16
+ import torch.nn.functional as F
17
+ from safetensors.torch import load_file
18
+
19
+ PAD, EOL, UNK, TURN = 0, 1, 2, 3
20
+
21
+
22
+ @dataclass
23
+ class Config:
24
+ vocab: int = 24_000
25
+ context: int = 128
26
+ layers: int = 8
27
+ heads: int = 8
28
+ dim: int = 512
29
+
30
+
31
+ class Block(nn.Module):
32
+ def __init__(self, c: Config):
33
+ super().__init__()
34
+ self.heads = c.heads
35
+ self.norm1 = nn.LayerNorm(c.dim)
36
+ self.qkv = nn.Linear(c.dim, 3 * c.dim, bias=False)
37
+ self.proj = nn.Linear(c.dim, c.dim, bias=False)
38
+ self.norm2 = nn.LayerNorm(c.dim)
39
+ self.up = nn.Linear(c.dim, 4 * c.dim, bias=False)
40
+ self.down = nn.Linear(4 * c.dim, c.dim, bias=False)
41
+
42
+ def forward(self, x):
43
+ b, t, d = x.shape
44
+ q, k, v = self.qkv(self.norm1(x)).split(d, dim=2)
45
+ q, k, v = (z.view(b, t, self.heads, d // self.heads).transpose(1, 2) for z in (q, k, v))
46
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
47
+ x = x + self.proj(y.transpose(1, 2).reshape(b, t, d))
48
+ return x + self.down(F.gelu(self.up(self.norm2(x))))
49
+
50
+
51
+ class GPT(nn.Module):
52
+ def __init__(self, c: Config):
53
+ super().__init__()
54
+ self.config = c
55
+ self.embed = nn.Embedding(c.vocab, c.dim)
56
+ self.position = nn.Embedding(c.context, c.dim)
57
+ self.blocks = nn.ModuleList(Block(c) for _ in range(c.layers))
58
+ self.norm = nn.LayerNorm(c.dim)
59
+
60
+ def forward(self, idx):
61
+ x = self.embed(idx) + self.position(torch.arange(idx.shape[1], device=idx.device))
62
+ for block in self.blocks:
63
+ x = block(x)
64
+ return self.norm(x) @ self.embed.weight.T
65
+
66
+
67
+ class Kecal:
68
+ def __init__(self, model, words, display):
69
+ self.model = model.eval()
70
+ self.words = words
71
+ self.display = display
72
+ self.index = {w: i for i, w in enumerate(words)}
73
+
74
+ @classmethod
75
+ def load(cls, folder='.'):
76
+ config = Config(**json.load(open(os.path.join(folder, 'config.json'), encoding='utf-8'))['model'])
77
+ model = GPT(config)
78
+ model.load_state_dict(load_file(os.path.join(folder, 'model.safetensors')))
79
+ vocab = json.load(open(os.path.join(folder, 'vocab.json'), encoding='utf-8'))
80
+ return cls(model, vocab['words'], vocab['display'])
81
+
82
+ @torch.no_grad()
83
+ def next_token_logits(self, context):
84
+ """Logits for the next token, with a change of writer folded into a new message by the same writer."""
85
+ logits = self.model(torch.tensor([context[-self.model.config.context:]]))[0, -1].float()
86
+ logits[EOL] = torch.logaddexp(logits[EOL], logits[TURN])
87
+ logits[[PAD, UNK, TURN]] = -math.inf
88
+ return logits
89
+
90
+ def generate(self, length=60, top_k=40, seed=None):
91
+ generator = torch.Generator().manual_seed(seed) if seed is not None else None
92
+ context = [TURN]
93
+ for _ in range(length):
94
+ values, indices = torch.topk(self.next_token_logits(context), top_k)
95
+ context.append(indices[torch.multinomial(F.softmax(values, dim=0), 1, generator=generator)].item())
96
+ return self.render(context[1:])
97
+
98
+ def render(self, tokens):
99
+ lines, line = [], []
100
+ for token in tokens:
101
+ if token == EOL:
102
+ lines.append(' '.join(line))
103
+ line = []
104
+ else:
105
+ word = self.words[token]
106
+ line.append(self.display.get(word, word))
107
+ lines.append(' '.join(line))
108
+ return '\n'.join(l for l in lines if l)
109
+
110
+
111
+ if __name__ == '__main__':
112
+ print(Kecal.load(os.path.dirname(os.path.abspath(__file__))).generate(80))