Symbiont Classifier
From word counting to a language model: you generate your training data with the simple method, fine-tune a BERT on it — and catch the Leech where word lists are blind: at the repeated idea.
In a nutshell
What: The word-list approach from the base part has two weaknesses — it does not recognise the stylistic Butterfly person, and the Leech only unreliably. For the Leech's signature is not a word but the repetition of someone else's idea. You push the method further: first fine-tune a language model, then catch the Leech with semantic similarity.
The core idea: weak supervision. Instead of labelling laboriously by hand, you let the simple word counter generate pseudo-labels automatically — a rough "teacher" — and train with them a much finer "student" model that understands meaning in context rather than merely counting words.
You need: anonymised chats (your own export files or the corpus in the repo), Python with
transformers, datasets and sentence-transformers, ideally on a Colab
GPU. The full guide with training data and notebook is in the folder kap07-master on
GitHub.
What it's about
In the base part you read five roles — Bee, Ant, Butterfly, Capybara, Leech — from words alone. That is a coarse tool: word lists are a hypothesis, not understanding. Someone can say "idea" without being creative, and be very creative without ever using the word. Better methods — those built on language models like BERT — understand meaning in context. Exactly one of them you build here.
And you solve the base part's most stubborn problem: the Leech. With Felix we got only a hint (the self-positioning words), but missed the actual mechanism — that his "ideas" are, in content, repetitions of others' ideas. To recognise that, you must measure meaning-similarity over time. Exactly this is what modern language models can do.
Before you start
This section is aimed at advanced readers with programming experience — in the Master course at MIT, HSLU and the University of Cologne this step is the central learning occasion. The fine-tuning needs a GPU; simplest is Google Colab (Runtime → GPU). On a CPU everything runs too, but takes much longer — then shrink the data volume and the epochs.
Step 1 — Generate training data with the simple method
We collect 100 to 500 anonymised chats, apply the word-list classifier and get pseudo-labels automatically. Not perfect, but good enough as a teacher model.
# five roles, each with a small word list (from the base part)
wordlists = {
"Bee": ["idea", "imagine", "what if", "could we", "proposal"],
"Ant": ["plan", "list", "done", "deadline", "who does", "organise"],
"Butterfly": ["beautiful", "feel", "wonderful", "dream", "mood", "aesthetic"],
"Capybara": ["fine", "no stress", "whatever", "relaxed", "all good", "chill"],
"Leech": ["my parents", "showed them", "impressed by", "as i said before"],
}
def wordlist_guess(text):
text = text.lower()
scores = {role: sum(w in text for w in ws) for role, ws in wordlists.items()}
best = max(scores, key=scores.get)
return best if scores[best] > 0 else None # None = uncertain -> drop
# messages = list of many anonymised chat lines (strings)
pseudo = [(m, wordlist_guess(m)) for m in messages]
pseudo = [(m, r) for m, r in pseudo if r is not None] # keep only sure labels
print("pseudo-labels:", len(pseudo))
Step 2 — Fine-tune a BERT model
With the pseudo-labels we train a pre-trained model (bert-base-uncased) on the symbiont
classification. Thanks to transformers that is about 30 lines.
from datasets import Dataset
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
roles = list(wordlists) # fixed order -> numbers
to_id = {r: i for i, r in enumerate(roles)}
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
def encode(b): return tok(b["text"], truncation=True, padding="max_length", max_length=64)
ds = Dataset.from_dict({"text": [m for m, _ in pseudo],
"label": [to_id[r] for _, r in pseudo]})
ds = ds.map(encode, batched=True).train_test_split(test_size=0.2)
model = AutoModelForSequenceClassification.from_pretrained(
"bert-base-uncased", num_labels=len(roles))
args = TrainingArguments("bert-symbiont", num_train_epochs=3,
per_device_train_batch_size=16, eval_strategy="epoch")
Trainer(model, args, train_dataset=ds["train"], eval_dataset=ds["test"]).train()
# the fine model now understands context, not only words
test = tok(["Should we think about this in a completely different way?"],
return_tensors="pt", truncation=True, padding=True)
print(roles[int(model(**test).logits.argmax())])
Step 3 — Catch the Leech with semantic similarity
In addition we compute for each message the semantic similarity to all earlier messages of other people (with Sentence-BERT). Whoever regularly produces highly similar posts without citing is a Leech candidate.
from sentence_transformers import SentenceTransformer, util
sbert = SentenceTransformer("all-MiniLM-L6-v2")
# chat = list of (author, text) in chronological order
vectors = sbert.encode([t for _, t in chat], convert_to_tensor=True)
for i, (author, text) in enumerate(chat):
earlier = [j for j in range(i) if chat[j][0] != author] # only OTHERS, only EARLIER
if not earlier:
continue
similar = util.cos_sim(vectors[i], vectors[earlier])[0]
highest = float(similar.max())
if highest > 0.70: # high similarity without citation -> suspicion
source = chat[earlier[int(similar.argmax())]][0]
print(f"Leech suspicion: {author} repeats {source} (similarity {highest:.2f})")
In Felix's case this method clearly flags his Monday-18:42 post as taking over Mira's Monday-14:03 idea — exactly the take-over the word list missed.
What you should see
The fine-tuned BERT hits the roles clearly more reliably than the word list — above all where the same intent sits in quite different words. And the similarity analysis brings the Leech out without a single "tell-tale" word being needed. Together they show what a language model has over mere counting: it reads meaning, not letters.
Worksheet
From counting to understanding
- Weak supervision: why may the coarse word counter train a better model, although it itself makes mistakes? Where is the limit of this idea?
- Find a message the word list classifies wrongly but the fine-tuned BERT correctly — and explain why the context makes the difference.
- Why do we compare each message only with earlier posts of others — not with all? What would otherwise go wrong?
- The 0.70 threshold is arbitrary. How does a higher or lower threshold change the number of Leech suspicions — and the share of false alarms?
- The core problem (Step 4): whoever has a thought independently looks to the model like a Leech. Can one tell take-over from independent convergence from behavioural data alone?
Show solution
1. Because the student model learns the reliable patterns over many examples and averages out the teacher's random errors. The limit: systematic errors of the teacher (always the same blind spot) are learned along — pseudo-labels inherit the word list's biases.
2. Individual. Typically: "Should we think about this in a completely different way?" contains no word-list word but is clearly a Bee idea. BERT recognises the intent from sentence structure and context; the word list sees only unknown words.
3. Only earlier: otherwise an idea would count as a copy of its own later repetition — cause and effect swapped. Only others: repeating yourself is not Leech behaviour but normal.
4. Higher threshold → fewer suspicions, but real take-overs are missed; lower threshold → more hits, but many harmless topic overlaps as false alarms. It is the usual trade-off between precision and recall.
5. From behavioural data alone, not for sure. High similarity plus temporal order is a strong hint, but no proof — two people can arrive at the same thing independently. The plagiarism-detection literature gives no clear answer; that is why the result is a suspicion, not a verdict.
When it sticks
| Problem | Likely cause & fix |
|---|---|
CUDA out of memory | Lower per_device_train_batch_size (e.g. 8) or shorten max_length. In Colab pick the GPU runtime. |
| Training extremely slow | It is running on the CPU. In Colab "change runtime type → GPU"; otherwise reduce data and epochs. |
| Model always says the same role | Pseudo-labels strongly imbalanced (Leech is rare). Balance the classes or use a class_weight loss. |
eval_strategy unknown | Older transformers version: there the parameter is called evaluation_strategy. |
| Sentence-BERT loads forever | The model is downloaded once; after that from the cache. Internet needed the first time. |
Food for thought
- This is the leap the whole book is about: from counting to understanding. A word counter sees surface, a language model sees meaning — and yet this one too stays fallible.
- The Leech detector is powerful and delicate at once. It delivers a suspicion, not a verdict — and a suspicion about a person demands special care. Whoever treats it like proof misuses the tool.
- Weak supervision shows a general truth of AI: a coarse, honest method can train a finer one — but it also passes on its blind spots. Whoever builds models must know what they believe of their teacher.
Extension
- Test against real labels. Label 50 messages by hand and compare: how much better is the fine-tuned BERT really over the plain word list?
- The Butterfly. Build a second similarity test that measures stylistic (not content) closeness and check whether it finds the "aesthetically writing" person the word list misses.
- Independent convergence. Construct a chat in which two people arrive at the same idea independently. How often does your Leech detector falsely fire — and how could you soften that?