Syvällisempi katsaus graafisten hermoverkkojen maailmaan
Graafiverkkohermoverkot (GNN:t) ovat muodostuneet tehokkaaksi syväoppimisen arkkitehtuuriksi monimutkaisen, toisiinsa liittyvän datan ymmärtämisessä. Toisin kuin perinteiset hermoverkot, jotka toimivat kiinteänkokoisilla syötteillä, kuten kuvilla tai sekvensseillä, GNN:t on suunniteltu ymmärtämään suhteita, jotka kuvaavat, miten eri yksiköt yhdistyvät, vaikuttavat toisiinsa ja vuorovaikuttavat verkossa. Tämä tekee niistä erityisen arvokkaita aloilla, joissa rakenne on tärkeää, sosiaalisista verkostoista ja molekyylikemiasta suositusjärjestelmiin ja petosten tunnistamiseen.
Tässä artikkelissa pureudumme GNN:ien keskeisiin ideoihin, tutkimme niiden kehitystä ja korostamme todellisen maailman haasteita, joita ne auttavat ratkaisemaan. Opit myös, miten GNN:it toteutetaan käytännössä, käytännön esimerkin avulla, joka on rakennettu PyTorch-kirjaston avulla.
Tärkeimmät asiat
- Graafineuroverkot (GNN:t) toimivat erinomaisesti yhteyksissä olevien tietojen suhteiden mallintamisessa, mikä tekee niistä ihanteellisia tehtäviin, joissa rakenne ja riippuvuudet ovat tärkeitä.
- GNN:t yleistävät perinteisen syväoppimisen graafimuotoihin, mahdollistaen oppimisen solmuista, kaarista ja niiden yhteyksistä.
- Suositut GNN-arkkitehtuurit, kuten Graph Convolutional Networks (GCN), Graph Attention Networks (GAT) ja GraphSAGE, tarjoavat erilaisia tapoja tiedon kokoamiseen ja välittämiseen solmujen välillä.
- GNN:ien käytännön sovellukset kattavat sosiaalisen median verkkoanalyysin, molekyylien ominaisuuksien ennustamisen, suositusjärjestelmät, petosten havaitsemisen ja tietämyskaaviot.
- GNN:ien toteuttaminen on helppoa nykyaikaisten kehysten avulla, kuten PyTorch Geometric, joka yksinkertaistaa tietojen käsittelyä, viestinvälitystä ja mallin rakentamista.
- Käytännön toteutus havainnollistaa GNN-työnkulun, mukaan lukien graafidatan lataamisen, mallin määrittämisen, koulutuksen ja tulosten arvioinnin.
- GNN:t kehittyvät edelleen nopeasti, ja edistysaskeleet parantavat niiden skaalautuvuutta, tehokkuutta ja suorituskykyä yhä suurempien ja monimutkaisempien verkkojen käsittelyssä.
Mitä ovat graafiset neuroverkot?
Graafiverkkoneuroverkot, eli GNN:t lyhyesti, ovat suhteellisen uusi tyyppi neuroverkkoa, joka pystyy käsittelemään graafimaisessa muodossa olevaa dataa. Graafit koostuvat pohjimmiltaan joukosta objekteja, jotka esitetään solmuina, sekä näiden objektien välisistä suhteista, jotka esitetään solmuja yhdistävinä kaarina. GNN:t pystyvät käsittelemään sekä suunnattuja graafeja, joissa kaarilla on suunta, että suunnattomia graafeja, joissa kaarilla ei ole tiettyä suuntaa. Nämä graafit voivat myös vaihdella merkittävästi sekä kooltaan että muodoltaan. GNN:n arkkitehtuuri koostuu useista kerroksista, joista kukin ottaa tietoa edellisestä kerroksesta. Syötämme GNN:lle graafin, joka esitetään solmujen ja kaarien joukkona sekä niiden liitetyillä ominaisuuksilla. Se, mitä saamme ulos, on joukko solmuin upotuksia (node embeddings) jokaiselle solmulle syötteessä olevassa verkossa. Nämä upotukset edustavat ominaisuuksia, joita verkko on oppinut kullekin solmulle. Sen sijaan, että toimisimme vain vektoreiden, matriisien tai tensoreiden kanssa kuten tavallinen neuroverkko, GNN:t voivat työskennellä tietojen kanssa, jotka on jäsennelty kokonaisiksi verkoiksi. Tämä tekee niistä todella joustavia työskentelemään verkostoituneen datan kanssa, kuten sosiaaliverkkojen, molekyylirakenteiden tai liikennejärjestelmien kanssa. Matematiikka, johon liittyy, on monimutkaista, mutta perusidea on, että ne käyvät läpi verkkoa ja välittävät viestejä solmujen välillä oppiakseen hyödyllisiä esityksiä.
Miten graafiset neuroverkot toimivat?
Graafiset neuroverkot, tai lyhyesti GNN:t, keskittyvät oppimaan kaavioiden solmujen välisiä malleja. Pääidea on, että jokainen solmu välittää viestejä naapurisolmuilleen jakaen tietoa itsestään.
- GNN:ssä kukin solmu vaihtaa tietoa niiden solmujen kanssa, joihin se on yhteydessä, mikä auttaa mallia ymmärtämään vähitellen koko graafin rakenteen.
- Jokainen solmu luo "viestin" käyttäen omia ominaisuuksiaan sekä naapurisolmuistaan peräisin olevia ominaisuuksia.
- Samaan aikaan naapurisolmut luovat omat viestinsä ja lähettävät ne takaisin.
- Kun solmu vastaanottaa nämä viestit, se päivittää sisäisen tilansa yhdistämällä kaiken saapuvan tiedon.
- Tämä toistuva viestinvälitys mahdollistaa tiedon virtaamisen verkon läpi, jolloin solmut voivat oppia verkon osista, jotka ovat niiden välittömien naapurien ulkopuolella.
- Pinomalla useita tämän prosessin kerroksia päällekkäin, GNN:t voivat oppia monimutkaisempia ja syvempiä suhteita.
- Jokaisen lisätyn kerroksen myötä malli rakentaa rikkaampia ja merkityksellisempiä ominaisuusesityksiä verkosta.
Graafinen neuroverkko PyTorchiin implementoiminen
Cora-datasarja
Cora-datasarja on suosittu vertailuaineisto, jota tutkijat käyttävät graafiesitysten oppimisen parissa. Tämä datasarja sisältää joukon tieteellisiä julkaisuja, jotka on jaettu seitsemään kategoriaan, kuten ”Case Based”, ”Genetic Algorithms”, ”Neural Networks”, ”Probabilistic Methods”, ”Reinforcement Learning” ja ”Rule Learning”. Cora-datasarja on ollut käytössä jo jonkin aikaa ja on edelleen monien projektien keskeinen resurssi tällä alalla. Se tarjoaa keinon arvioida, kuinka tehokkaasti mallisi voi analysoida sekä asiakirjojen tekstisisältöä että niiden välisiä viittausverkostoja. Monet merkittävät graafisten hermoverkkojen tutkimusartikkelit ovat hyödyntäneet Coraa arvioidakseen suorituskykyä näissä kahdessa tehtävässä. Se on rakennettu graafiksi, jossa julkaisut ovat solmuja ja niiden väliset viittaukset ovat solmuja yhdistäviä kaaria. Jokainen dokumentti on liitetty ominaisuusvektoriin, joka edustaa sen sisältöä. Haasteena on kehittää malli, joka kykenee tarkastelemaan viittausgraafia, sisältövektoreita ja niiden välisiä suhteita ennustaakseen, mihin seitsemästä luokasta kukin julkaisu kuuluu.
Datan esikäsittely
Asennamme PyTorch Geometric -kirjaston komennolla:
pip install torch_geometricVoimme sitten käyttää PyTorch Geometric -kirjastoa datan lataamiseen ja esikäsittelyyn.
from torch_geometric.datasets import Planetoid
import torch_geometric.transforms as T
dataset = Planetoid(root='data/Cora', name='Cora', transform=T.NormalizeFeatures())Planetoid-luokka lataa Cora-datasettin ja normalisoi ominaisuusvektorit. Voimme saada esikäsitellyn datan käyttämällä datasettia, joka antaa meille Data-olion näillä ominaisuuksilla:
- x: solmujen ominaisuuksien matriisi muodossa ‘(num_nodes, num_features)’
- edge_index: särmän yhteysmatriisi muodoltaan ‘(2, num_edges)’
- y: vektori solmutunnisteista muodossa ‘(num_nodes)’
- train_mask, val_mask, test_mask: totuusarvot maskit, jotka osoittavat, mitkä solmut ovat harjoittelua, validointia ja testausta varten.
Mallin arkkitehtuuri
Kun rakennetaan graafista neuroverkkoa, oikeanmallisen arkkitehtuurin valitseminen on erittäin tärkeää. Käymme läpi perusimplementaation käyttäen PyTorchiin kuuluvaa torch_geometric-kirjastoa. Hyödynnämme graafista konvoluutioverkkoa, joka toimii hyvänä lähtökohtana erilaisille graafioppimistehtäville.
import torch
import torch.nn.functional as F
from torch_geometric.nn import GCNConv
class GNN(torch.nn.Module):
def __init__(self, in_channels, hidden_channels, out_channels):
super(GNN, self).__init__()
# Define the first graph convolutional layer
self.conv1 = GCNConv(in_channels, hidden_channels)
# Define the second graph convolutional layer
self.conv2 = GCNConv(hidden_channels, out_channels)
# Define the linear layer
self.linear = torch.nn.Linear(out_channels, out_channels)
def forward(self, x, edge_index):
# Apply the first graph convolutional layer
x = self.conv1(x, edge_index)
# Apply the ReLU activation function
x = F.relu(x)
# Apply the second graph convolutional layer
x = self.conv2(x, edge_index)
# Apply the ReLU activation function
x = F.relu(x)
# Apply the linear layer
x = self.linear(x)
# Apply the log softmax activation function
return F.log_softmax(x, dim=1)- Yllä olevassa koodissa tuotimme torchin ja torch.nn.functionalin saadaksemme pääsyn joihinkin hyödyllisiin neuroverkkokomponentteihin ja -funktioihin. Sen jälkeen määrittelimme GNN-luokan, joka periytyy torchin ja nn.Modulen kautta.
- Init-metodissa määrittelimme kaksi konvoluutiokerrosta käyttämällä GCNConv-moduulia PyTorch Geometric -kirjastosta. Tämä mahdollistaa graafikonvoluutioiden helpon toteutuksen. Olemme myös lisänneet yksinkertaisen lineaarisen kerroksen.
- Etupääsy syöttää ensin syötteen kahden konvoluutiokerroksen läpi, käyttäen joka kerta ReLU-aktivointia. Sitten se menee lineaarisen kerroksen läpi ja lopuksi log-softmaxin läpi, kutistaen tulokset.
Muutamalla koodirivillä voimme rakentaa mukavan pienen graafisen neuroverkon!
Ilmiselvästi tämä on yksinkertainen esimerkki, mutta voimme nähdä, kuinka PyTorch ja PyTorch Geometric antavat meidän nopeasti prototyypittää ja testata graafisten neuroverkkoarkkitehtuurien kehitystä. GCNConv-kerrokset tekevät graafisen rakenteen sisällyttämisestä malleihimme erittäin helppoa.
Koulutus
Koulutusta varten käytämme ristiinentropiahäviötä ja Adam-optimointia. Voimme jakaa datan koulutus-, validointi- ja testijoukkoihin käyttämällä näitä maskiominaisuuksia Data-objektissa.
# Set the device to CUDA if available, otherwise use CPU
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# Define the GNN model with the specified input, hidden, and output dimensions, and move it to the device
model = GNN(dataset.num_features, 16, dataset.num_classes).to(device)
# Define the Adam optimizer with the specified learning rate and weight decay
optimizer = torch.optim.Adam(model.parameters(), lr=0.01, weight_decay=5e-4)
# Define the training function
def train():
# Set the model to training mode
model.train()
# Zero the gradients of the optimizer
optimizer.zero_grad()
# Perform a forward pass of the model on the training nodes
out = model(dataset.x.to(device), dataset.edge_index.to(device))
# Compute the negative log-likelihood loss on the training nodes
loss = F.nll_loss(out[dataset.train_mask], dataset.y[dataset.train_mask])
# Compute the gradients of the loss with respect to the model parameters
loss.backward()
# Update the model parameters using the optimizer
optimizer.step()
# Return the loss as a scalar value
return loss.item()
# Define the testing function
@torch.no_grad()
def test():
# Set the model to evaluation mode
model.eval()
# Perform a forward pass of the model on all nodes
out = model(dataset.x.to(device), dataset.edge_index.to(device))
# Compute the predicted labels by taking the argmax of the output scores
pred = out.argmax(dim=1)
# Compute the training, validation, and testing accuracies
train_acc = pred[dataset.train_mask].eq(dataset.y[dataset.train_mask]).sum().item() / dataset.train_mask.sum().item()
val_acc = pred[dataset.val_mask].eq(dataset.y[dataset.val_mask]).sum().item() / dataset.val_mask.sum().item()
test_acc = pred[dataset.test_mask].eq(dataset.y[dataset.test_mask]).sum().item() / dataset.test_mask.sum().item()
# Return the accuracies as a tuple
return train_acc, val_acc, test_acc
# Train the model for 500 epochs
for epoch in range(1, 500):
# Perform a single training iteration and get the loss
loss = train()
# Evaluate the model on the training, validation, and testing sets and get the accuracies
train_acc, val_acc, test_acc = test()
# Print the epoch number, loss, and accuracies
print(f'Epoch: {epoch:03d}, Loss: {loss:.4f}, Train Acc: {train_acc:.4f}, Val Acc: {val_acc:.4f}, Test Acc: {test_acc:.4f}')
Trainausfunktio suorittaa yhden koulutuskierroksen ja palauttaa häviön. Testausfunktio tarkistaa, miten malli suoriutuu koulutus-, validointi- ja testijoukoilla, ja palauttaa tarkkuuden. Koulutimme mallia 500 epookin ajan ja tulostimme koulutus- ja testitarkkuudet jokaisen epookin jälkeen.
Laske GNN-mallin tarkkuus
Alla oleva koodi määrittelee funktion, joka laskee mallin tarkkuuden koko aineistossa. compute_accuracy()-funktio asettaa mallin arviointitilaan, suorittaa eteenpäin suuntautuvan laskennan ja ennustaa labelit jokaiselle solmulle. Se vertaa näitä ennustettuja labeleita todellisiin labeleihin ja laskee oikeiden ennusteiden määrän. Sitten se jakaa oikeiden ennusteiden määrän aineiston solmujen kokonaismäärällä saadakseen tarkkuusprosentin.
@torch.no_grad()
def compute_accuracy():
model.eval()
out = model(dataset.x.to(device), dataset.edge_index.to(device))
pred = out.argmax(dim=1)
correct = pred.eq(dataset.y.to(device)).sum().item()
total = dataset.y.shape[0]
accuracy = correct / total
return accuracy
accuracy = compute_accuracy()
print(f"Accuracy: {accuracy:.4f}")Tässä tapauksessa mallin tarkkuus Cora-datasarjassa oli 0,8006. Tämä tarkoittaa, että noin 80 % ajasta malli pystyi ennustamaan luokkatunnisteen oikein. Se on melko hyvä, mutta ei täydellinen. Tarkkuus antaa meille nopean, yleiskuvan siitä, kuinka hyvin malli suoriutuu kokonaisuutena. Mutta todellisen ymmärryksen saamiseksi siitä, missä se menestyy ja missä se kamppailee, on tutkittava syvemmin.
Syvällisemmän ymmärryksen saamiseksi mallin tehokkuudesta on suositeltavaa harkita myös muita arviointimittareita, kuten tarkkuutta, palautusastetta, F1-pistemääriä ja sekoitusmatriisia. Nämä mittarit tarjoavat näkemyksiä mallin suorituskyvystä eri osa-alueilla, kuten positiivisten ja negatiivisten tapausten oikeaoppiseen tunnistamiseen sekä epätasapainoisten tietojoukkojen käsittelyyn. Joten vaikka 80 % tarkkuus onkin vankka, haluaisimme enemmän kontekstia ennen kuin julistamme tämän mallin täydelliseksi menestykseksi. Pelkkä tarkkuusmittari ei anna kokonaiskuvaa siitä, mitä tapahtuu taustalla. Mutta se on hyvä lähtökohta suorituskyvyn arvioimiseksi.
Arviointi
Voimme arvioida, miten GNN toimii, käyttämällä esimerkiksi tarkkuutta, precisionia, recallia ja F1-pistettä. Voimme myös visualisoida mallin oppimia solmusijoituksia t-SNE:n avulla. Se ottaa korkeanulotteiset sijoitukset ja projisoi ne 2D-muotoon, mikä mahdollistaa niiden visualisoinnin.
# Import the necessary libraries
from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
# Set the model to evaluation mode
model.eval()
# Perform a forward pass of the model on the dataset
out = model(dataset.x.to(device), dataset.edge_index.to(device))
# Apply t-SNE to the output feature matrix to obtain a 2D embedding
emb = TSNE(n_components=2).fit_transform(out.cpu().detach().numpy())
# Create a figure with a specified size
plt.figure(figsize=(10,10))
# Create a scatter plot of the embeddings, color-coded by the true labels
plt.scatter(emb[:,0], emb[:,1], c=dataset.y, cmap='jet')
# Display the plot
plt.show()Huomautus: Lukija voi suorittaa yllä olevan koodin. Se näyttää hajontakuvion, jota voidaan tulkita. Koodi käyttää t-SNE:tä näyttääkseni opitut solmun upotukset 2D-hajontakuviossa, mikä on fiksu tapa visualisoida korkean ulottuvuuden dataa. Käydään läpi, mitä tapahtuu:
- Jokainen piste kuvaajassa edustaa solmua tietojoukossa. X- ja Y-akselit ovat kaksi ulottuvuutta, joihin t-SNE puristi upotukset. Kunkin pisteen väri edustaa kyseisen solmun todellista luokkaa tietojoukossa.
- Solmujen, joilla on samankaltaiset upotukset, tulisi myös saada samankaltaiset tunnisteet, joten ne ryhmittyvät yhteen kuvaajaan. Toisaalta solmut, joilla on hyvin erilaiset upotukset, saattavat todennäköisesti saada erilaiset tunnisteet, joten ne ovat kauempana toisistaan.
- Kaiken kaikkiaan kaavio tarjoaa selkeän kuvan solmujen välisistä suhteista niiden opittujen upotusten perusteella. Voit nähdä muodostuvan ryhmiä, joilla täytyy olla jokin taustalla oleva samankaltaisuus. Se on kätevä tapa kurkistaa mallin sisälle ja ymmärtää, miten se järjestää käsitteitä.
Mahdolliset haasteet ja huomioon otettavat seikat
- 2708 solmulla ja 5429 kaarella Cora-datasettiä pidetään melko pienenä. Tämä saattaa heikentää GNN:n tehokkuutta, mikä edellyttää kehittyneempien menetelmien, kuten datan laajentamisen ja siirto-opetuksen, käyttöönottoa.
- Cora-datasarja koostuu yhdestä solmutyypistä ja yhdestä reunatyypistä, mikä tekee siitä homogeenisen verkon. Tämä saattaa rajoittaa GNN:n soveltuvuutta, kun sitä käytetään monimutkaisempiin verkkoihin, jotka sisältävät erilaisia solmu- ja reunatyyppejä.
- Sopivien hyperparametrien arvojen, kuten piilotettujen kerrosten lukumäärän, piilotettujen yksiköiden määrän ja oppimisnopeuden, valinta voi merkittävästi vaikuttaa GNN:n suorituskykyyn ja vaatii huolellista virittämistä.
UKK
- 1. Mikä on graafinen hermoverkko (GNN)?
GNN on neuroverkko, joka on suunniteltu toimimaan graafi-rakenteisen datan kanssa. Se oppii solmu-, kaari- tai graafitasoisia esityksiä keräämällä tietoa naapurisolmuista.
- 2. Miten GNN eroaa perinteisestä neuroverkosta?
Perinteiset verkot käsittelevät ruudukkomaisia tietoja (kuvia, sekvenssejä), kun taas GNN:t voivat mallintaa epäsäännöllisiä, toisiinsa kytkeytyneitä rakenteita, kuten sosiaalisia verkostoja tai molekyyligraafeja.
- 3. Minkä tyyppisiin ongelmiin GNN:iä käytetään?
Niitä käytetään yleisesti solmu-luokitteluun, linkkien ennustamiseen, graafiluokitteluun, suosituksiin, petosten havaitsemiseen ja molekyylien ominaisuuksien ennustamiseen.
- 4. Mitä viestinvälitys tarkoittaa GNN:issä?
Viestinvälitys on ydintoiminto, jossa solmut vaihtavat tietoa naapureidensa kanssa, päivittävät upotuksia ja oppivat kontekstuaalisia suhteita.
- 5. Skaalaavatko GNN:t hyvin erittäin suurissa verkoissa?
Laajentaminen voi olla haastavaa muistin ja naapuruston kasvun vuoksi. Tekniikat kuten näytteenotto (GraphSAGE), mini-erät ja hajautettu koulutus auttavat.
- 6. Mitkä ohjelmointikirjastot ovat parhaita GNN:ien toteuttamiseen?
Suosittuja kehysjärjestelmiä ovat PyTorch Geometric (PyG) ja Deep Graph Library (DGL), jotka molemmat tarjoavat valmiita kerroksia ja työkaluja GNN-mallien rakentamiseen.
- 7. Ovatko GNN:t sopivia reaaliaikaisiin sovelluksiin?
Kyllä, mallin monimutkaisuudesta riippuen. Kevyet arkkitehtuurit ja näytteisiin perustuvat menetelmät auttavat saavuttamaan lähes reaaliaikaisen suorituskyvyn.
- 8. Pystyvätkö GNN:t käsittelemään dynaamisia tai kehittyviä verkkoja?
Kyllä, dynaamiset GNN-variantit voivat käsitellä ajan myötä muuttuvia verkkoja, mikä on hyödyllistä liikenteen ennustamiseen, ajallisiin suosituksiin ja poikkeavuuksien tunnistamiseen.
- 9. Millainen datan esikäsittely on tarpeen GNN:ille?
Yleensä sinun täytyy valmistella naapuruustiedot, solmu-/sivutuotetiedot ja varmistaa, että kaavio on oikein muotoiltu valitsemallesi kirjastolle.
- 10. Ovatko GNN:t tulkittavissa?
Tulkinnoitavuus paranee työkalujen, kuten attention-mekanismien ja GNNExplainerin, avulla, jotka korostavat vaikutusvaltaisia solmuja ja kaaria.
Yhteenveto
Tässä artikkelissa kävimme läpi graafisia neuroverkkoja (GNN) koskevat keskeiset käsitteet ja tutkimme, kuinka niitä voidaan soveltaa eri aloilla. GNN-mallit soveltuvat erityisen hyvin graafimuotoisen datan käsittelyyn, mikä mahdollistaa mallien pohtia monimutkaisia suhteita, joita löytyy sosiaalisista verkostoista, molekyyligraafeista, liikennejärjestelmistä ja muista sovelluksista.
Näiden ideoiden käytännön havainnollistamiseksi käytimme suositua Cora-datasarjaa, joka on graafioppimisen vertailuaineisto. Tässä datasarjassa kukin julkaisu edustetaan solmuna, ja niiden väliset viittaukset muodostavat reunat. Tavoitteemme oli käyttää sekä kunkin paperin tekstuaalisia piirteitä että viittauslinkkejä sen luokan ennustamiseen.
Valmistelemme tietojoukon käyttämällä PyTorch Geometric -kirjastoa, normalisoimalla ominaisuusvektorit ja jakamalla tiedot harjoittelu-, validointi- ja testijoukkoihin. Sen jälkeen rakensimme yksinkertaisen GNN:n käyttäen graafisia konvoluutiokerroksia, jota seurasi lineaarinen luokitin, ja koulutimme sitä ristiinentropiahäviöllä Adam-optimointia käyttäen. Lopuksi arvioimme mallin suorituskyvyn mittaamalla sen tarkkuuden.
Vaikka on olemassa monia lisätekniikoita ja parannuksia, joita voisi tutkia, tämä projekti tarjoaa vankan johdannon siihen, miten GNN:t toimivat ja kuinka tehokkaita ne voivat olla monimutkaisesta relaatioaineistosta oppimisessa.
Resurssit
- Neuroverkkojen rakentaminen alusta alkaen: Osa 1
- Mitä on syväoppiminen? Aloittelijan opas neuroverkkoihin
- Syväoppimisen arkkitehtuurit selitetty: ResNet, InceptionV3, SqueezeNet
- PyTorch 101: Opettele syväoppimista PyTorchilla
Kiitos, että opiskelet Linux-Console.net-yhteisön kanssa.


