Hidden Signals · Companion site ← All activities DE·EN
Chapter 13 · Plants that Read Us · Activity 13.2

Plant Emotions with AI

From the signal comes an image, and a pre-trained network reads it: can you tell from the plant signal whether the person beside it was happy? Pure inference — you train no network of your own.

Duration double lesson Difficulty high Group alone or in pairs Fully digital S

In a nutshell

What: You take the raw voltage signal of a houseplant and, measured at the same time by a smartwatch, the mood of the person beside it. For each mood reading you cut out the plant signal of the last few minutes, make — as everywhere in this book — a spectrogram of it, and let a ready pre-trained network (ResNet) read the image. A simple nearest-neighbour method guesses the mood from that.

The core idea: pure inference. The deep network has long since learned from millions of images — you do not train it, you use it as a ready-made "eye". And this is the scepticism chapter: you check soberly how much — and how little — really sits in the signal.

You need: the dataset from figshare (31527778) with plant_raw.csv and predictions.csv, plus Python with pandas, scipy, torch/torchvision and scikit-learn.

What it's about

If a plant reacts to light — does it also react to us? In a self-study over eleven days a person sat at their desk, half a metre from a Tradescantia. A simple sensor picked up the plant's electrical signal; at the same time a Happimeter smartwatch on the wrist continuously estimated the person's mood — happy, stressed, low. So two traces lie side by side: here the plant signal, there the person's mood.

The question is delicate and therefore exciting: does the plant signal really hold something about the person? Instead of believing it, you turn it into an image and let an AI look — and then check honestly whether its guess beats pure chance.

Before you start — the data

The dataset lies on figshare under the DOI 10.6084/m9.figshare.31527778. Two files: plant_raw.csv (columns timestamp, voltage — about 312,000 measurement points over eleven days, roughly one per second) and predictions.csv (the Happimeter readings with timestamp and happy, stress, depression, each on a scale from 0 to 2). The two are joined via the timestamp.

A little background

Again an image from the signal. The procedure is familiar: a signal over time becomes a spectrogram — an image of its frequencies — and then the same image-recognising machinery reads it that read cat sound, voice and heartbeat. New is only this: we do not train the network ourselves but take a ready one that has already learned from millions of images, and use its "view" as features. That is the core of transfer and inference.

Why the plant is slow. The plant signal changes over seconds to minutes, not milliseconds. So per mood reading we look at a window of a few minutes before it — long enough for the slow rhythms of the plant to show in the spectrogram.

Cut windows and build spectrograms

For each Happimeter reading we take the plant signal of the last three minutes, put it on an even 1 Hz grid and make a spectrogram. The full code is on GitHub.

import numpy as np, pandas as pd
from scipy import signal, interpolate

raw   = pd.read_csv("plant_raw.csv",   parse_dates=["timestamp"]).sort_values("timestamp")
reads = pd.read_csv("predictions.csv", parse_dates=["timestamp"])   # Happimeter readings

EPOCH = pd.Timestamp("1970-01-01")                                  # fixed zero point
t    = ((raw["timestamp"] - EPOCH) / pd.Timedelta(seconds=1)).to_numpy()   # seconds
volt = raw["voltage"].to_numpy()

WINDOW = 180                             # seconds of plant signal before each reading
GRID   = np.arange(0, WINDOW, 1.0)       # unify to 1 Hz

def spectrogram(end_s):
    m = (t >= end_s - WINDOW) & (t < end_s)
    if m.sum() < 120:                    # too few points -> skip
        return None
    w = interpolate.interp1d(t[m] - (end_s - WINDOW), volt[m],
                             bounds_error=False, fill_value="extrapolate")(GRID)
    w = (w - w.mean()) / (w.std() + 1e-9)          # normalise
    _, _, S = signal.spectrogram(w, fs=1.0, nperseg=32, noverlap=16)
    return np.log1p(S)                             # image of the slow rhythms

specs, y = [], []
for _, r in reads.iterrows():
    S = spectrogram((r["timestamp"] - EPOCH) / pd.Timedelta(seconds=1))
    if S is not None:
        specs.append(S)
        y.append(int(r["happy"] >= 2))            # 1 = happy, 0 = less
y = np.array(y)
print(len(specs), "windows  |  happy balance:", np.bincount(y))

The pre-trained network reads — pure inference

Now the core: a ResNet, pre-trained on millions of images, turns each spectrogram into a feature vector — its "view" of the image. It learns nothing here; we only let a plain nearest-neighbour method guess the mood from these features.

import torch, torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
from PIL import Image
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score

weights = ResNet18_Weights.IMAGENET1K_V1         # loads the weights once (internet needed)
net = resnet18(weights=weights); net.fc = nn.Identity(); net.eval()
prepare = weights.transforms()

def features(S):                                 # spectrogram -> image -> ResNet view
    a = (S - S.min()) / (np.ptp(S) + 1e-9)
    img = Image.fromarray((a * 255).astype("uint8")).convert("RGB").resize((64, 64))
    with torch.no_grad():
        return net(prepare(img).unsqueeze(0))[0].numpy()

X = np.array([features(S) for S in specs])       # pure inference: the net is not trained

chance = max(np.bincount(y)) / len(y)
score  = cross_val_score(KNeighborsClassifier(5), X, y, cv=5).mean()
print("hits (5-fold):", round(score, 2), " |  chance line:", round(chance, 2))

What you should see

For "happy" the method lands a little above the chance line — a weak but real trace. If you try the same with stress instead of happy, you will usually find no signal above chance. That is exactly the honest, instructive double answer: yes, a breath of the person's mood sits in the plant signal — but only a breath, and not for every emotion. Whoever expected big percentages here has not taken the scepticism chapter seriously.

Worksheet

Real or imagined?

  1. How high was your hit rate for "happy", how high the chance line? How large is the gap really?
  2. Switch the target to stress (e.g. stress >= 1). Do you still find anything above chance? What follows from that for the claim "the plant reads feelings"?
  3. The network is never trained — yet useful features arise. Explain in one sentence how a network trained on photos can help with a plant spectrogram.
  4. The whole study rests on one person and one plant over a few days. Name two reasons why the result is exciting but not generalisable.
  5. Even if "happy" lies above chance: why does it not follow that the plant "knows" or "feels" how you are?
Show solution

1. Individual — typically a few percentage points above the chance line (which, at roughly equal class split, lies near 0.5). What matters is naming the gap honestly: "above chance" is the proof, not the absolute number.

2. Usually "stress" does not lie above chance. Conclusion: not every emotion leaves a readable trace; "the plant reads feelings" is too coarse. Honest is: a weak trace for some states, none for others.

3. The network has learned to recognise general image patterns — edges, areas, textures. A spectrogram is an image of such patterns, so the early, general layers of the network deliver useful features here too, without it ever having seen a plant signal.

4. One person/plant/span can have quirks that do not generalise; without many people and plants and without strict control it stays a single finding. Only replication makes it a robust statement.

5. What is measured is a correlation between mood and signal shape — no mechanism and no consciousness. The trace probably runs via the heartbeat (see 17.1) and scents, not via "understanding". Correlation is not understanding.

When it sticks

ProblemLikely cause & fix
ResNet loads forever / error on loadThe weights are downloaded once (internet needed). After that they sit in the cache. Behind a firewall: load the model beforehand on a machine with internet.
0 windowsTimestamps do not line up. Check that the time spans of plant_raw.csv and predictions.csv overlap; only overlapping readings yield a window.
Very unbalanced classesChoose a different threshold (e.g. happy >= 1) or use stratify so both classes are similar in size.
Hits exactly on the chance lineAn honest result, not an error — for this target (often "stress") there is simply nothing readable in the signal.
Very slowCompute the features once and keep them in X; a smaller image size (e.g. 64×64) is enough.

Food for thought

Extension

← 13.1 Day and Night 14.1 The Mood of the Swarm →