Hidden Signals · Companion site ← All activities DE·EN
Chapter 12 · The Language of Animals · Activity 12.2

Dog and Horse

One photo is enough: you let a model guess an animal's mood — and set your own judgement against it. The most interesting part is where the two disagree.

Duration 90 min Difficulty medium Group alone or in pairs Fully digital S

In a nutshell

What: You take (or collect) photos of an animal in different situations, assign each image a feeling yourself first — and then let a pre-trained image model guess the same. Afterwards you compare: where do you agree, where not?

The core idea: No training of your own, pure inference. You use a large, ready-made model (CLIP) like a tool and examine it critically. The value is not in the code but in the comparison — and in the question of who is actually right when you disagree.

You need: a few animal photos (your own dog, your own cat, a horse from the stable — or free images), Python with transformers, torch and Pillow. For the extension additionally the Kaggle dataset Pet's Facial Expression.

What it's about

A dog cannot tell you it is afraid. But its body says it loud and clear: the tucked tail, the crouched posture, the ears laid back. Exactly this we exploited with my team and determined a dog's mood from a single photo — with a hit rate of 60 to 70 per cent. That sounds modest, but, as studies show, it beats most humans. For horses we read mainly ears, eyes and mouth; there too the model lands at about two out of three.

These research models are trained at great effort. For you we take a different, equally honest route: a ready-made model that connects images and language, CLIP. You hold it a photo and a few descriptions — "an anxious dog", "a relaxed dog" — and it says which description fits best. You train nothing; you only ask and look critically at the answer.

Before you start

Only photograph animals you are allowed to, and do not pressure them for a picture — producing a stressed animal for a photo about stress would be absurd. Best capture moments that happen anyway: at play, dozing, on alert. If you have no animal to hand, take free images from the web.

A little background

A shared alphabet of feelings. With what do we even label an animal's feelings? The book draws on an order by the neuropsychologist Jaak Panksepp, who identified in all mammals a few basic feelings — seeking, play, care, fear, rage, grief. The beauty: it is the same alphabet for dog, horse and at heart for us too. For horses "pain" is added as an especially important eighth feeling.

Where the AI looks. The research models use exactly the places where we read too: in the horse the ears, eyes and mouth; in the dog the whole posture including the tail. When a model was once let to sort the animal images with no labels at all, it even formed groups that fit none of our boxes — perhaps a hint at finer animal feelings for which we simply lack the words.

Photograph and judge for yourself

  1. Install the packages. pip install transformers torch pillow.
  2. Collect photos. Five to eight images of the same animal in clearly different situations: attentive, relaxed, playing, perhaps anxious. Frontal, good light, face and posture visible.
  3. You first. Before the program, note your own judgement for each image from the alphabet of feelings. This matters — otherwise you unconsciously follow the machine.
  4. Then the model. Let CLIP guess and lay the two judgements side by side.

Asking the model

CLIP understands English best, so the descriptions are English — the display stays in your language. The full code is on GitHub.

from transformers import CLIPModel, CLIPProcessor
from PIL import Image
import torch

model     = CLIPModel.from_pretrained("openai/clip-vit-base-patch32")
processor = CLIPProcessor.from_pretrained("openai/clip-vit-base-patch32")

# Panksepp alphabet: label -> English description for CLIP
feelings = {
    "Joy / play":  "a happy, playful dog",
    "Affection":   "a relaxed, affectionate dog",
    "Seeking":     "an alert, curious dog exploring",
    "Fear":        "a frightened, anxious dog",
    "Rage":        "an angry, aggressive dog",
    "Rest":        "a calm, resting dog",
}
labels  = list(feelings)
prompts = list(feelings.values())

def guess(image_file):
    image = Image.open(image_file).convert("RGB")
    inputs = processor(text=prompts, images=image, return_tensors="pt", padding=True)
    with torch.no_grad():
        logits = model(**inputs).logits_per_image[0]
    probs = logits.softmax(dim=0)               # descriptions -> probabilities
    best = int(probs.argmax())
    return labels[best], float(probs[best])

for file in ["dog1.jpg", "dog2.jpg", "dog3.jpg"]:
    label, conf = guess(file)
    print(f"{file}:  model guesses {label}  ({conf:.0%} sure)")

What you should see

On clear images (happy play, plain alertness) CLIP is often surprisingly good. On fine or ambiguous situations it deviates — and sometimes you are right, sometimes the model, and sometimes it is not even clear who is right. Exactly this disagreement is the real result of this activity.

For horses

Swap the animal name in the code and add the eighth feeling. For horses pain matters especially:

feelings = {
    "Attentive":  "an alert, attentive horse with ears forward",
    "Relaxed":    "a calm, relaxed horse",
    "Fear":       "a frightened horse with ears back and wide eyes",
    "Agitation":  "an agitated, stressed horse",
    "Pain":       "a horse in pain, with tense face and eyes",
}

Horses, by the way, read us too — often more finely than we read them. Keep that in mind when you think about the limits of the machine next.

Worksheet

Two judgements side by side

  1. Enter your judgement and the model's for each image. In how many of eight cases did you agree?
  2. Find an image where you did not agree. Who is right, in your view — and what do you base that on?
  3. A wagging tail counts as joy. Name a situation where a dog wags and is not happy. What does that mean for a model that only sees the photo?
  4. The training images of such models were labelled by humans. How can a human error thereby propagate into the machine?
  5. The model reads the expression. Put in one sentence what it thereby does not know.
Show solution

1. Individual. The aim is to count the agreement honestly — and to notice that "often agreed" is not "always agreed".

2. Individual. What matters is the reasoning: do you appeal to ears/tail/posture — or to context the photo does not show (what just happened)? Often the disagreement comes from you knowing the context and the model seeing only the image.

3. Dogs also wag when aroused, unsure or tense. A model that sees only a still image cannot tell these from joy — it lacks movement, context and history.

4. If the model learns from images humans labelled with their assumptions ("looks sad"), it takes over exactly those assumptions — including the tendency to ascribe human feelings to animals. The error then lies not in the code but in the labels.

5. It does not know how the animal feels — only how its expression looks in the image. Expression is a hint at the state, not proof, and certainly not a look into the experience.

When it sticks

ProblemLikely cause & fix
The first run takes longCLIP is downloaded once (several hundred MB). After that it is fast; internet needed the first time.
Image.open failsWrong path or not an image format. Use JPG/PNG, give the full path.
Always the same feelingDescriptions too similar or image ambiguous. Separate the prompts more clearly (mention posture/ears), choose clearer photos.
Very low confidences everywhereNormal — CLIP is no animal-emotion specialist. It is about the comparison, not high percentages.
torch will not installOlder Python version. Use current Python; if need be install the CPU variant of torch.

Food for thought

Extension — train your own (going further)

So far you only asked the model. Now you train one yourself — on real data, as in research. On Kaggle lies the dataset Pet's Facial Expression (kaggle.com/datasets/anshtanwar/pets-facial-expression-dataset) with faces of dogs, horses and cats, labelled happy, sad and angry. Instead of a heavy image network you use a light trick: CLIP gives each image a number vector — its "view" of the image — and on that, just like the cat in 12.1, a simple KNN learns.

import glob, os, numpy as np, torch
from PIL import Image
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split

def image_vector(file):
    image = Image.open(file).convert("RGB")
    inputs = processor(images=image, return_tensors="pt")
    with torch.no_grad():
        return model.get_image_features(**inputs)[0].numpy()   # CLIP's view of the image

# folder per feeling (happy/ sad/ angry/) - folder name = label
FOLDER = "pets-facial-expression"
X, y = [], []
for path in glob.glob(os.path.join(FOLDER, "**", "*.jpg"), recursive=True):
    feeling = os.path.basename(os.path.dirname(path))
    X.append(image_vector(path)); y.append(feeling)
X = np.array(X)
print("read:", len(X), "images")

Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25,
                                      random_state=0, stratify=y)
own = KNeighborsClassifier(n_neighbors=5).fit(Xtr, ytr)
print("hits on unseen images:", round(own.score(Xte, yte), 2))

Compare: does your self-trained KNN beat CLIP's blind guessing from the main part? And how does the picture change if you take only dogs or only horses?

← 12.1 The Language of the Cat 13.1 Day and Night →