Text Generation
Transformers
Safetensors
Czech
gpt2
czech
chat
steganography
small-language-model
encrypt.click
text-generation-inference
Instructions to use danielsebesta/kecal with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use danielsebesta/kecal with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="danielsebesta/kecal")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("danielsebesta/kecal") model = AutoModelForCausalLM.from_pretrained("danielsebesta/kecal", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use danielsebesta/kecal with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "danielsebesta/kecal" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "danielsebesta/kecal", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/danielsebesta/kecal
- SGLang
How to use danielsebesta/kecal with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "danielsebesta/kecal" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "danielsebesta/kecal", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "danielsebesta/kecal" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "danielsebesta/kecal", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use danielsebesta/kecal with Docker Model Runner:
docker model run hf.co/danielsebesta/kecal
Model card and inference code
Browse files
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))
|