Päivitetty 19.10.2026
Koko ketju toimii ilman backendiä. Malli koulutetaan Windows-pöytäkoneen näytönohjaimella, viedään ONNX-tiedostoksi ja ajetaan käyttäjän selaimessa staattiselta GitHub Pages -sivulta. Sivu toimii myös offline.
https://luukalindgren.github.io/seeni/.flowchart LR
subgraph PC["Pöytäkone · WSL2 + RTX"]
A[GBIF-lataus<br/>CC0- ja CC-BY-kuvat] --> B[Koulutus<br/>EfficientNet-B0, PyTorch] --> C[Vienti<br/>ONNX + JSON]
end
subgraph GH["GitHub · repo seeni"]
D[Repo<br/>koodi, malli, species.json] --> E[Actions<br/>testit ja build] --> F[Pages<br/>staattinen sivu, HTTPS]
end
subgraph BR["Käyttäjän selain · toimii offline"]
G[PWA-välimuisti<br/>malli ja wasm laitteella] --> H[Tunnistus<br/>Web Worker, WebGPU/WASM] --> I[Varmistusohjaus<br/>ehdokkaat ja tarkistukset]
end
C -- git push --> D
F -- ladataan kerran --> G
Pöytäkoneelta GitHubiin siirtyy vain valmis malli ja sen JSON-tiedostot. Käyttäjän selain lataa ne kerran ja tunnistaa sen jälkeen ilman verkkoa.
Vaiheet 1–8 tehdään pöytäkoneella, vaiheet 9–11 enimmäkseen Macilla. Suurin työ on datassa (vaihe 4) ja lajitietokannassa (vaihe 9), ei koulutuksessa.
Tavoite: pöytäkoneen WSL2-Ubuntussa näkyy näytönohjain PyTorchille, ja pääset koneelle Macilta SSH:lla mistä tahansa.
nvidia-smi: pöytäkoneessa on RTX 3070, ja ajurin pitää tukea CUDA 13:a.wsl --install -d Ubuntu-24.04, sitten uudelleenkäynnistys.WSL-asetukset. Tiedostoon %UserProfile%\.wslconfig:
[wsl2]
memory=24GB
networkingMode=mirrored
Säädä memory noin 3/4:aan koneen muistista. Ubuntun puolelle /etc/wsl.conf, jotta Tailscale ja sshd voivat käynnistyä palveluina:
[boot]
systemd=true
uv ja Python 3.12. Kloonaa repo WSL:n omalle levylle (esim. ~/seeni), ja aja training/-kansiossa uv sync. PyTorch asentuu PyPI:stä, jonka Linux-paketit ovat CUDA-versioita. Nykyinen versio käyttää CUDA 13:a, joka vaatii Windowsiin NVIDIA-ajurin R580-sarjasta tai uudemman. Tarkista: uv run python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name())".~/seeni-data, ei /mnt/c:ssä. Windows-levyn kautta luku on moninkertaisesti hitaampaa ja hidastaa koulutusta.openssh-server, portiksi 2222, vain avaimella kirjautuminen (PasswordAuthentication no).~/.ssh/config: Host seeni-pc, HostName <Tailscale-nimi>, Port 2222.tmux-istunnossa, jolloin ne jatkuvat, vaikka SSH-yhteys katkeaisi. Asenna Claude Code WSL:ään, niin agentit ajavat komennot suoraan samassa ympäristössä.Yksi julkinen GitHub-repo seeni, jossa koulutus ja sivu ovat rinnakkain. Pages on ilmainen julkisille repoille.
seeni/
CLAUDE.md säännöt agenteille (esikäsittely, lajijärjestys, lisenssit)
docs/PLAN.md tämä ohje
training/ Python (uv): data, koulutus, arviointi, vienti
data-meta/ lajilista.csv, split.csv, attribuutiot (pieniä tiedostoja)
species/ species.json, käsin kuratoitu lajitieto
web/ Vite + TypeScript -sovellus
public/models/v1/ model.onnx, labels.json, preprocess.json, calibration.json
.github/workflows/pages.yml
~/seeni-data, ja .gitignore estää niiden lisäämisen. Poikkeuksia ovat julkaistava malli (web/public/models/), kuvat kansioissa web/, species/ ja docs/ (ikonit, tarkistusten kuvat, kuvakaappaukset), testien kultaiset testikuvat (tests/fixtures/, vaihe 8) ja attribuutiodata (data-meta/). Muiden kuin omien kuvien pitää olla CC0- tai CC-BY-kuvia, ja niiden attribuutiot kirjataan ATTRIBUTIONS.md:hen.v1, v2), jotta selaimen välimuisti päivittyy.labels.json-tiedostosta, ja vain CC0/CC-BY-kuvia saa käyttää.Aloita 100–200 luokalla. Ne muodostuvat Ruokaviraston kauppasienistä, niiden vaarallisista kaksoisolennoista ja yleisimmistä sienistä, joita metsässä tulee vastaan.
data-meta/lajilista.csv, jossa on yksi rivi taksonia kohden. Sarakkeet: class_id, tieteellinen nimi, suomenkielinen nimi, GBIF taxonKey, ryhmä (syötävä / esikäsiteltävä / myrkyllinen / tappava / ei ruokasieni), kaksoisolennot (luokkatunnisteina, puolipisteellä eroteltuina) ja in_model (true / false).taxonKey GBIF:n species match -rajapinnasta (api.gbif.org/v1/species/match?name=…) ja tarkista osumat käsin.Luokan tunniste. Jokaisella luokalla on pysyvä tekstitunniste class_id: pienet ASCII-kirjaimet ja väliviivat, yleensä tieteellinen nimi (cantharellus-cibarius, suvun tasolla cortinarius). Yhdistetyssä luokassa on useita rivejä samalla class_id:llä, koska sen lajeilla ei ole yhteistä taxonKeyta. Tunniste ei muutu sen jälkeen, kun sillä on kerran koulutettu malli, joten species.json toimii kaikkien malliversioiden kanssa. Jos luokan taksonit muuttuvat, luokka saa uuden tunnisteen.
muu (vaihe 7) ei ole lajilistalla, eikä sillä ole taxonKeyta. Koulutus lisää sen luokaksi.in_model on true. Luokka, joka jätetään pois mallista (esim. liian vähän kuvia, kohta 5), pysyy lajilistalla. Jokainen vaarallinen kaksoisolento on lajilistalla, vaikka siitä ei olisi kuvia. Jos se ei ole mallissa, sille tehdään species.json:iin varoitustietue (vaihe 9), jotta se näytetään aina.Yksi GBIF-lataus tuottaa havainnot, kuvalinkit ja jokaisen kuvan lisenssin. Kuvat suodatetaan lisenssin mukaan ennen lataamista.
POST api.gbif.org/v1/occurrence/download/request), ja ehdot ovat:
taxonKey lajilistastacountry FI, SE, NO, EE (lisää naapurimaita, jos jollakin lajilla on liian vähän kuvia)mediaType StillImage, basisOfRecord HUMAN_OBSERVATION, muoto DWCAmultimedia.txt sisältää jokaisen kuvan oman lisenssin, kuvaajan ja URL:n. Havainnon lisenssi ei kerro kuvan lisenssiä. Normalisoi lisenssi-URL:t ja pidä vain CC0 ja CC-BY.original → large, jolloin kuvat ovat noin 1024 pikseliä.~/seeni-data/images/<taxonKey>/<gbifID>_<n>.jpg. Tässä taxonKey on lajilistan rivin avain, ei havainnon oma taxonKey: havainto kuuluu riville, jonka taxonKey löytyy havainnon taksonipolusta (esim. speciesKey tai genusKey).manifest.parquet: polku, taxonKey, gbifID, lisenssi, kuvaaja, maa, päivämäärä ja URL. Sama tiedosto toimii myöhemmin attribuutiolistana.Jaa aineisto havainnon mukaan ja tee nopea baseline. Se kertoo minuuteissa, riittääkö data, ennen tuntien koulutusajoja.
gbifID:n mukaan 80 / 10 / 10 (koulutus / validointi / testi), kerrostettuna lajeittain ja kiinteällä siemenluvulla. Tallenna data-meta/split.csv. Saman sieniyksilön kuvat eivät saa päätyä eri joukkoihin, muuten tulokset näyttävät todellista paremmilta.Aloita EfficientNet-B0:lla 224 pikselin kuvakoossa. Se on pieni selaimelle ja nopea kouluttaa. Isompaa mallia (EfficientNet-B2, ConvNeXt-Nano, kuvakoko 288–320) kannattaa kokeilla vasta, kun koko ketju toimii päästä päähän.
labels.json:n pituus, muu mukaan lukien).channels_last, batch noin 64. Jos muisti loppuu, pienennä batchia.preprocess.json-tiedostoon.tmuxissa.Aikaa kuluu muutama tunti noin 100 000 kuvalla.
Tärkein mittari on se, kuinka usein myrkyllinen tai tappava sieni näyttäisi ruokasieneltä, ei kokonaistarkkuus.
in_model on false, mallin läpi ja laske niille sama vaarallinen sekaannus. Syötävät taksonit eivät siis vaikuta lukuun. Malli ei tunne näitä lajeja, joten tämä on riskialttein tapaus.Vienti tuottaa neljä tiedostoa, jotka ovat sopimus koulutuksen ja selaimen välillä. Jokainen tarkistetaan testillä ennen kuin se menee sivulle.
torch.onnx.export eval-tilassa, opset 17, syöte input (1×3×224×224), ulostulo logits, dynaaminen batch-ulottuvuus.onnxconverter-common-kirjastolla (keep_io_types=True), jolloin koko puolittuu. Tarkista uudelleen, että top-1 täsmää yli 99 %:ssa. int8-kvantisointia tuskin tarvitaan, ja se vaatisi kalibroinnin ja uuden arvioinnin.web/public/models/v1/:
model.onnxlabels.json: taulukko, jonka alkion paikka on mallin ulostulon indeksi ja alkio on { "id": "<class_id>", "taxonKeys": [...] }. Järjestys tulee suoraan koulutuksesta. muu-luokan taxonKeys on tyhjä.preprocess.json: resize_short 256, crop 224, mean, std, RGB, NCHWcalibration.json: lämpötila T ja tuntematon-kynnysweb/tests/fixtures/. Näillä vaiheen 10 yhdenmukaisuustesti varmistaa, että selain laskee saman tuloksen.Malli antaa ehdokkaat, ja käsin kuratoitu lajitieto ohjaa käyttäjän tarkistamaan juuri ne tuntomerkit, jotka erottavat ehdokkaat toisistaan.
species/species.json, yksi tietue luokkaa kohden, avaimena luokan tunniste (vaihe 3):
{
"id": "cantharellus-cibarius",
"taxonKeys": [0],
"tieteellinen": "Cantharellus cibarius",
"nimi": "kantarelli",
"ryhma": "syötävä",
"tuntomerkit": ["…"],
"kaksoisolennot": ["…"],
"tarkistukset": [
{ "id": "heltat", "kysymys": "Ovatko lakin alla aidot heltat vai haarovat poimut?", "vastaukset": { "poimut": ["cantharellus-cibarius"], "heltat": ["…"] } }
],
"lahteet": ["…"],
"tarkistanut": ""
}
kaksoisolennot ja vastaukset viittaavat luokkien tunnisteisiin. Varoitustietue on tietue, jonka id ei ole labels.json:ssa. Malli ei ennusta sitä, mutta se näytetään kaksoisolentona, ja tarkistukset voivat sulkea sen pois. muu-luokalla ei ole tietuetta.
CI-tarkistus species.json:lle:
id on lajilistan class_id, ja jokaisella mallin luokalla on tietue.ryhma, taxonKeys ja kaksoisolennot vastaavat lajilistaa (vaihe 3). Ne kannattaa generoida lajilistasta.kaksoisolennot, vastaukset) osuu olemassa olevaan tietueeseen.Ohjauksen logiikka:
muu ei ole koskaan ehdokas. Jos muu on kärjessä, tulos on “en tunnista varmasti”.Sisältö: pohjana Ruokaviraston kauppasienet ja Suomen sieniseuran aineistot. Kielimallilla voi tehdä luonnoksia, mutta jokainen tietue on tarkistettava asiantuntijalla (sieneuvoja tai sieniseura) ja merkittävä kenttään tarkistanut. Tarkistuksiin kannattaa liittää kuva siitä, mitä katsotaan, esim. jalan tyvi esiin kaivettuna.
Sovellus on Vite + TypeScript -sivu, joka ajaa mallin onnxruntime-webillä Web Workerissa. Julkaisu tapahtuu GitHub Actionsilla jokaisesta pushista main-haaraan. Rakenne ja design ovat vapaita; alla vain kohdat, joissa malli ja sivu kohtaavat.
Malli selaimessa
onnxruntime-web, ajotavat ['webgpu', 'wasm']. WebGPU on nopein, ja WASM toimii varalla, jos selain ei tue WebGPU:ta..wasm-tiedostot omaan public-kansioon ja aseta ort.env.wasm.wasmPaths niihin. Ilman tätä ne haetaan CDN:stä, eikä offline-tila toimi.OffscreenCanvasin kanssa, niin käyttöliittymä ei jäädy. Luo istunto kerran ja tee yksi lämmittelyajo.<input type="file" accept="image/*" capture="environment" multiple>. Lue se createImageBitmap(file, { imageOrientation: 'from-image' })-kutsulla, jotta puhelinkuvien EXIF-kierto tulee oikein.preprocess.json-tiedostosta: lyhyempi sivu 256 → keskirajaus 224 → RGB / 255 → (x − mean) / std → NCHW Float32Array.labels.json → species.json → varmistusohjaus.Yhdenmukaisuustesti
Playwright-testi ajaa vaiheen 8 kultaiset kuvat sovelluksen oikealla esikäsittelyllä Chromiumissa ja vertaa top-5:tä Pythonin tuloksiin. Testi ajetaan CI:ssä jokaisessa pushissa. Se on ainoa keino huomata hiljainen esikäsittelyvirhe.
PWA ja offline
vite-plugin-pwa: manifest (nimi, ikonit, display: standalone) ja Workboxin precache, joka sisältää myös mallin ja wasm-tiedostot.maximumFileSizeToCacheInBytes esim. 50 Mt:iin. Oletus on 2 MiB, jolloin malli jää hiljaa välimuistin ulkopuolelle.GitHub Pages
vite.config.ts: base: '/seeni/'..github/workflows/pages.yml, kun main-haaraan pushataan: checkout → setup-node → npm ci → testit → npm run build → actions/configure-pages → actions/upload-pages-artifact (polku web/dist) → actions/deploy-pages. Oikeuksiksi pages: write ja id-token: write.LICENSE), malli ja lajitiedot CC BY 4.0, kirjattu README:hen ja ATTRIBUTIONS.md:hen.web/public/CNAME (sisältö seeni.fi), aseta DNS:ään GitHub Pagesin A-tietueet ja www-CNAME, ota Pages-asetuksista Enforce HTTPS käyttöön ja vaihda base arvoon '/'.