Att förutsäga molekylära egenskaper, såsom en substans förmåga att blockera hERG-kanalen, är en viktig uppgift inom läkemedelsutveckling och keminformatik. Denna uppgift kräver ofta övervakad inlärning, där ett system tränas för att förutsäga ett specifikt resultat baserat på uppsatta egenskaper. I detta avsnitt går vi igenom hur man samlar in, utforskar och bearbetar data för att skapa en modell som kan förutsäga om en substans blockerar hERG-kanalen eller inte.

För att arbeta med denna typ av data, är en bra början att identifiera lämpliga dataset. Det finns ett antal offentliga databaser som kan ge värdefull information för molekylär prediktion, som PubChem och ChEMBL. Dessa resurser är särskilt användbara för att hitta existerande data relaterad till molekylära egenskaper och deras biologiska aktivitet. Förutom dessa databaser finns det också vetenskapliga publikationer som tillhandahåller data från experiment som kan vara användbara för att skapa prediktiva modeller. Om du inte är insatt i ett specifikt delområde kan det vara nyttigt att använda aggregerade dataset från plattformar som Papers With Code, som öppet delar dataset inom olika maskininlärningsområden, inklusive läkemedelsutveckling.

En annan resurs som specifikt samlar data om läkemedelsutveckling är Therapeutics Data Commons (TDC). Genom att använda dessa resurser kan vi få tillgång till dataset som till exempel innehåller information om hERG-blockerare, vilket är avgörande för att förutsäga en molekyls förmåga att påverka den aktuella jonkanalen.

Lastning och utforskning av hERG-blockerare

Låt oss titta närmare på ett dataset med hERG-blockerare, som kan laddas ner från TDC. Datasetet består av 587 föreningar, där varje rad representerar en molekyl. Kolumnerna i datasetet inkluderar bland annat SMILES (den textbaserade representationen av molekylstrukturen), molekylens namn, pIC50-värdet (en indikator på substansens aktivitet), samt en klassificering som anger om molekylen blockerar hERG-kanalen eller inte.

För att ladda och bearbeta datasetet kan vi använda ett Python-skript som hämtar filen från en webbadress och laddar in den i en Pandas DataFrame. När vi har datasetet i minnet kan vi börja utforska det. Det första steget är att undersöka varje kolumn och dess innehåll. För detta dataset består kolumnerna av namn, SMILES, pIC50, klass, och två uppsättningar som beskriver hur data är uppdelade för träning och testning.

Data Exploration

När vi har laddat in data är det viktigt att förstå dess struktur och kvalitet. I datasetet är pIC50-värdet ett centralt attribut för att beskriva aktivitet, där ett högre pIC50 innebär en större potent effekt. pIC50-värdet är härlett från IC50, vilket är koncentrationen av läkemedlet vid vilken det inhiberar en biologisk process med 50%. För att underlätta jämförelsen av IC50-värden konverteras dessa till pIC50 genom att ta det negativa logaritmet av IC50 i molar (M).

En utmaning med datasetet är att pIC50 inte är tillgängligt för alla molekyler, och vi kan inte använda detta som en funktion för att träna en modell. Istället förlitar vi oss på klasskolumnen, som anger om en molekyl blockerar hERG-kanalen (1) eller inte (0). Denna kolumn är vår målvariabel för att skapa en prediktiv modell.

För att säkerställa att data är användbart för maskininlärning är det viktigt att vi inte bara litar på värdena som de presenteras. Vi behöver göra en grundlig dataexploration för att identifiera eventuella inkonsekvenser eller felaktigheter, såsom problem med enhetshantering eller saknade värden.

Visualisering och dataanalys

För att få en bättre förståelse för datasetet kan vi visualisera distributionen av pIC50-värdena. Det kan hjälpa oss att upptäcka om det finns några stora avvikelser eller potentiella datakorrigeringsfel. Om exempelvis distributionen verkar följa ett ovanligt mönster, såsom en bimodal fördelning, kan det indikera att felaktiga enheter har använts för att registrera IC50-värdena. Ett sådant scenario kan tyda på att annoteraren av misstag använt nM i stället för µM.

Därför är det viktigt att regelbundet genomföra en grundlig undersökning av datasetet, inklusive att kontrollera statistiska fördelningar, identifiera avvikelser och verifiera data mot kända referenser. Detta kallas ofta Exploratory Data Analysis (EDA) och är en avgörande del i datakurering.

Det är också värt att notera att även om pIC50-värden är användbara för att förstå molekylens aktivitet, kanske de inte alltid är den bästa för att träna en modell. De kan vara svåra att få tag på, kostsamma att mäta och osäkra i sin kvalitet. Därför är det ofta bättre att fokusera på andra molekylära egenskaper som kan vara mer tillförlitliga och lättare att extrahera från offentliga databaser.

Det är även nödvändigt att tänka på att även de mest pålitliga dataset kan innehålla fel. Därför är det viktigt att ha en strategi för datakuration som inkluderar både automatisk och manuell granskning. En bra datakuration kan hjälpa till att identifiera problematiska datapunkter, såsom felaktiga eller saknade värden, vilket gör det möjligt att justera datasetet för att förbättra modellens noggrannhet.

Hur man optimerar hyperparametrar och skapar effektiva pipeliner i maskininlärning

Inom maskininlärning är hyperparameteroptimering en avgörande process för att förbättra modellens prestanda. Hyperparametrar definierar olika aspekter av träningsalgoritmen, som kan påverka hur väl en modell generaliserar till nya data. Denna process innebär att man söker efter de optimala värdena på dessa hyperparametrar, vilket kan inkludera komponenter i själva modellen, inlärningsalgoritmen och till och med parametrar för standardisering och funktionsextraktion.

Ett klassiskt exempel på hyperparametrar är radien och bitlängden för Morgan-fingeravtryck i kemometriska modeller. Dessa parametrar kan variera från exempelvis en radie på 2, 4 eller 6, eller bitlängder på 512, 1024 och 2048. Dessutom finns det val av regulariseringstekniker, som Ridge-regression (L2-penalitet), Lasso-regression (L1-penalitet) och Elastic Net, som är en kombination av de två tidigare nämnda teknikerna. Varje hyperparameter kan ha flera möjliga värden, vilket kan leda till en explosion av möjliga kombinationer att testa.

För att effektivt hantera denna omfattande sökning används tekniker som Grid Search eller Randomized Search tillsammans med pipeliner i Scikit-Learn. Genom att använda pipeliner kan vi strukturera hela arbetsflödet för databehandling och modellträning på ett effektivt sätt. Pipeliner är uppbyggda av två huvudkomponenter: transformatorer och en estimator.

Transformatorerna ansvarar för förbehandling av data, vilket kan innefatta funktionell skalning, imputation av saknade värden, extraktion av funktioner och kodning av kategoriska variabler. Varje transformator appliceras på data, och den omvandlade datan skickas sedan vidare till nästa steg. Estimatorn, som vanligen är en maskininlärningsmodell (t.ex. en klassificerare eller regressionsmodell), tränas på den förbehandlade datan. En av de största fördelarna med att använda pipeliner är att det minskar risken för data-leakage – ett vanligt problem där information från testuppsättningen kan påverka träningen av modellen och ge en optimistisk bedömning av prestandan.

Ett klassiskt problem är att man vid standardisering använder den genomsnittliga värdet från hela datasetet, inklusive testdata. Detta innebär att information från testuppsättningen "läcker" in i träningsfasen, vilket kan snedvrida resultaten och leda till överdrivna prestandaindikatorer. För att undvika detta problem garanterar pipeliner att alla transformationer utförs på träningsdata först, och samma transformationer appliceras sedan på testuppsättningen.

För att optimera hyperparametrarna i en pipeline används GridSearchCV, som söker igenom alla möjliga kombinationer av hyperparametrar definierade i en parametergrid. När sökningen är klar, kommer den bästa modellen och de bästa parametrarna att väljas ut baserat på prestandan på en tvärvalidering.

Scikit-Learn erbjuder dessutom en cache-mekanism för att optimera prestandan vid upprepade beräkningar. Eftersom vissa delar av pipelinen, som omvandling av SMILES-strängar till molekylära objekt, kan vara tidskrävande men inte påverkas av modellens hyperparametrar, kan vi använda denna cache för att minska redundanta beräkningar.

För att skapa en pipeline i Scikit-Learn kan vi definiera anpassade transformatorer. Ett exempel på detta är en transformer som omvandlar SMILES-strängar till RDKit Mol-objekt, för att sedan standardisera och omvandla dessa objekt till fingeravtryck, vilket därefter används i en klassificeringsmodell. Här definieras en pipeline med två anpassade transformatorer: SmilesToMols, som hanterar SMILES-konvertering och standardisering, och FingerprintFeaturizer, som skapar fingeravtryck från de standardiserade molekylära objekten. Estimatorn, i det här fallet en SGDClassifier, används sedan för att klassificera de genererade funktionerna.

När pipelinen är skapad kan vi använda GridSearchCV för att testa olika kombinationer av hyperparametrar, till exempel olika radier för Morgan-fingeravtryck eller olika typer av regularisering i SGDClassifier. GridSearchCV kommer att utvärdera varje kombination av parametrar och returnera den bästa modellen baserat på prestanda, mätt genom en tvärvalidering.

En viktig aspekt att beakta under denna process är vikten av korrekt implementering av anpassade transformatorer. För detta krävs att vi ärver från de basklassene BaseEstimator och TransformerMixin, och att vi implementerar metoderna fit() och transform() som säkerställer att data kan bearbetas på ett korrekt sätt i pipelinen.

Det är också värt att notera att hyperparameteroptimering och modellvalidering är iterativa processer. I de flesta fall är det inte tillräckligt att bara köra en grundläggande sökning. Att förstå och hantera resultat från dessa sökningar är lika viktigt, eftersom vi inte bara vill hitta en uppsättning hyperparametrar, utan också förstå hur varje parametrar påverkar modellens prestation. En noggrant utförd grid search ger oss en tydlig översikt över olika konfigurationer, vilket gör att vi kan fatta informerade beslut om vilken modell och vilka parametrar som bäst passar våra data.

Hur Scaffold Splitting och Distributioner påverkar Modeller inom Läkemedelsforskning

Vid användning av kemiska datasätt i maskininlärning är det avgörande att välja rätt datadelning för att träna och testa modeller på ett sätt som speglar verkliga tillämpningar. En av de mest använda teknikerna inom denna process är scaffold splitting, vilket innebär att data delas baserat på molekylens kärnstruktur, eller scaffold. Scaffold splitting bevarar den kemiska mångfalden genom att säkerställa att träning och testning sker på molekyler som är strukturellt olika, vilket gör det möjligt att skapa mer robusta modeller som kan generalisera bättre. Denna metod ger en mer utmanande uppgift än en slumpmässig uppdelning av data, eftersom modeller tränas på molekyler som är mindre lika de som testas, vilket speglar den verkliga variationen som kan uppstå i nya, otestade data.

För att göra detta effektivt är det också viktigt att förstå distributionen av de målvariabler (target variables) som används i modellerna. I fallet med AqSolDB, där den målvariabeln är den vattenlöslighet (solubility) som uttrycks i logaritmisk form (logS), märker vi att fördelningen inte är normalfördelad utan täcker ett stort intervall från -13 till 2,5 logS. Ett större logS indikerar bättre vattenlöslighet. Solubilitet är en kontinuerlig variabel, och det är vanligt att se ett stort spann mellan mycket svårlösliga ämnen och de med god löslighet, vilket kan påverka modellens förmåga att prediktera under praktiska förhållanden.

Ett intressant fenomen som uppstår vid visualisering av solubilitetens fördelning är närvaron av extrema värden. Exempelvis kan molekyler med logS-värden under -8 vara mycket svårlösliga och svåra att hantera inom läkemedelsutveckling, medan molekyler med logS över -2 kan vara överdrivet lösliga, vilket gör att de inte representerar typiska kandidater för läkemedelsforskning. Att upptäcka dessa uteliggare (outliers) är avgörande, eftersom deras närvaro kan negativt påverka modellens prestanda. Det finns flera metoder för att identifiera dessa uteliggare, såsom interkvartilavstånd (IQR), där vi definierar data som uteliggare om de ligger utanför intervallet Q1 - 1,5 * IQR och Q3 + 1,5 * IQR.

En annan viktig aspekt är att vissa extrema värden kan vara legitima, beroende på sammanhanget. Till exempel, även om en molekyl är mycket svårlöslig, kan den fortfarande vara relevant för vissa läkemedelsmål. Därför är det viktigt att noggrant granska dessa extrema värden för att avgöra om de bör tas med eller tas bort. Vid inspektion av molekyler i AqSolDB kan man exempelvis se att vissa av de minst lösliga molekylerna innehåller metalljoner, vilket gör dem relevanta för andra forskningsområden men inte nödvändigtvis för läkemedelsutveckling.

För att ytterligare bearbeta dessa data kan vi tillämpa kemisk domänkunskap för att filtrera bort molekyler som innehåller ovanliga fragment eller element som inte förväntas vara relevanta för läkemedelsforskning. Vidare kan man också standardisera molekyler för att ta bort vissa fragment eller atomkombinationer som påverkar de experimentella resultaten. Det innebär att man kan ta bort molekyler som har flera fragment, vilket ofta är fallet när salter eller komplicerade strukturer finns närvarande.

När vi har hanterat de extrema värdena och eventuella uteliggare i datamängden, måste vi också vara medvetna om fenomenet distributionsskifte (distribution shift), vilket kan inträffa när träningsdata inte speglar testdata i tillräcklig utsträckning. Till exempel, om träningsdata endast består av molekyler med hög löslighet medan vi under validering testar molekyler med ett brett spektrum av löslighet, kan detta påverka modellens förmåga att generalisera. Det kan vara särskilt problematiskt om fördelningen av solubilitetsvärden skiljer sig avsevärt mellan tränings- och testuppsättningarna. Om modellen inte kan hantera denna skillnad kan prestandan försämras, och den kan ha svårt att förutsäga resultat på nya, otestade data.

När vi går vidare i vårt arbete med att extrahera och beräkna deskriptorer (descriptors) för molekyler, som är viktiga för att skapa en representativ modell, är det viktigt att använda rätt verktyg och funktioner från RDKit-biblioteket. Deskriptorer, såsom molekylvikt, antal vätebindningsdonatorer och polar yta (TPSA), används för att beskriva molekylers kemiska och fysiska egenskaper. Dessa beräknade deskriptorer hjälper till att skapa en bättre förståelse för hur molekyler interagerar med varandra och deras potentiella biologiska aktivitet. Genom att analysera dessa deskriptorer kan vi bättre förstå molekylernas struktur och hur dessa faktorer kan påverka deras funktion i läkemedelsutveckling.

Förutom de grundläggande deskriptorerna, som molekylvikt och antal vätebindningsdonatorer, kan vi också beräkna mer specifika deskriptorer som representerar strukturella egenskaper. Exempelvis kan andelen aromatiska atomer i förhållande till totala atomer i molekylen vara en viktig indikator på dess biologiska aktivitet. Denna typ av beräkning kan göras med hjälp av anpassade funktioner för att ta hänsyn till aromatiska strukturer i molekylen.

För att summera, måste vi vid arbete med kemiska datasätt vara medvetna om både datadelningstekniker, fördelningen av målvariabler, och de uteliggare som kan finnas i datan. Genom att noggrant filtrera och bearbeta data kan vi skapa bättre och mer robusta modeller som är väl anpassade för att förutsäga biologiska egenskaper i läkemedelsutveckling. Det är också viktigt att ha en djup förståelse för hur distributionsskiftet kan påverka modellens prestanda och att alltid använda rätt metoder och verktyg för att extrahera meningsfulla deskriptorer som kan användas för att förutsäga molekylers aktivitet.

Hur djupinlärning revolutionerar läkemedelsutveckling och proteinstrukturer

Den snabbt växande användningen av djupinlärning inom kemisk syntes och proteinforskning öppnar upp nya möjligheter för både läkemedelsutveckling och bioteknik. Genom att utnyttja maskininlärning och avancerade algoritmer, kan vi nu skapa nya kemikalier som potentiella terapier, syntetisera dem snabbt och kostnadseffektivt, samt förstå proteiners komplexa strukturer med en noggrannhet som tidigare var omöjlig att uppnå.

I den kemiska världen, där varje molekyl kan ge upphov till hundratals eller till och med tusentals möjliga syntetiska vägar, innebär det att det finns ett stort antal transformationsalternativ att överväga. När syntetiska kemister själva genomför denna sökning, elimineras många av dessa alternativ baserat på heuristiska principer, som utvecklats genom deras kunskap om organisk kemi, eller genom användning av regelbaserade verktyg för syntesplanering. Detta kan dock bli mindre pålitligt när man arbetar med okända kemikalier som består av understuderade substrukturer. Här erbjuder djupinlärning en möjlighet att effektivt utforska det kemiska rummet och kan avsevärt förenkla och påskynda retrosyntesprocessen.

Retrosyntes handlar inte bara om att föreslå nya läkemedel, utan också om att möjliggöra kostnadseffektivare och snabbare syntes av redan existerande läkemedel. Detta har en nära koppling till processkemi, där kemister arbetar med att utveckla metoder för kommersiell tillverkning av läkemedel under ekonomiska och säkerhetsmässiga hänsyn. Genom att kombinera retrosyntes med djupinlärning kan vi optimera befintliga syntesvägar, vilket gör läkemedel mer tillgängliga och prisvärda.

Inom proteinforskning har AlphaFold2, utvecklat av DeepMind, visat sig vara en banbrytande tillämpning av djupinlärning för att bestämma proteiners tredimensionella struktur med imponerande noggrannhet. Proteiner är komplexa molekyler som består av kedjor av aminosyror. Aminosyrorna ordnas i en specifik sekvens, där varje aminosyra bidrar till den slutliga strukturen och funktionen av proteinet. Genom att förstå denna struktur kan vi identifiera funktionella problem som kan leda till sjukdomar hos människor. Trots att kunskap om proteinstruktur tidigare varit en dyr och tidskrävande process, har djupinlärning nu revolutionerat hur vi bestämmer dessa strukturer, vilket kompletterar experimentella metoder och ger detaljerad insikt om hur proteiner fungerar.

För att förstå hur dessa avancerade teknologier fungerar, är det viktigt att först definiera begreppen bakom maskininlärning (ML) och djupinlärning. Maskininlärning är en gren av artificiell intelligens som bygger på att modeller lär sig från stora mängder data snarare än att följas av programmerade regler. Detta innebär att en ML-modell lär sig att känna igen mönster i data genom att analysera exempel och kan tillämpa dessa mönster för att göra förutsägelser eller beslut om nya data.

Till exempel, om vi lär ett barn att känna igen hundar i bilder, skulle vi ge barnet en samling bilder på hundar, där barnet lär sig att identifiera gemensamma egenskaper som päls, fyra ben och svansar. För maskininlärning fungerar detta på samma sätt. Genom att träna en modell på stora mängder data, kan den lära sig att identifiera liknande mönster och använda dessa för att göra förutsägelser om nya, okända exempel. Ett viktigt begrepp här är generalisering – en modell som överanpassar sig till träningsdata kan ha svårt att göra korrekta förutsägelser för nya data. I läkemedelsutveckling, där vi ständigt letar efter nya molekyler med olika strukturer, är denna förmåga till generalisering avgörande för framgång.

Modeller baserade på djupinlärning är en specifik underkategori av maskininlärning, där konstgjorda neurala nätverk används för att extrahera mönster från data. Denna metod har visat sig särskilt effektiv för att arbeta med stora och komplexa datamängder, som de vi hittar inom läkemedelsforskning, där molekylstrukturer och proteinsekvenser kan vara mycket varierande.

Det är också värt att notera att, även om maskininlärning och djupinlärning erbjuder kraftfulla verktyg för att förutsäga och optimera läkemedelskandidater, är det avgörande att förstå deras begränsningar. Maskininlärning är beroende av kvaliteten på de data den tränas på, och om dessa data inte är representativa för verkliga tillämpningar kan resultaten bli missvisande. För att verkligen kunna utnyttja potentialen i dessa teknologier krävs en noggrann och detaljerad förståelse för de specifika data som används i läkemedelsutveckling och bioteknik.

Genom att integrera maskininlärning, djupinlärning och avancerade kemiska och biologiska data kan vi skapa en mer effektiv och snabb läkemedelsutveckling. Dessa teknologier har potentialen att förändra hur vi förstår och behandlar sjukdomar, genom att skapa nya molekyler, optimera befintliga läkemedel och avslöja komplexiteten i biologiska system som vi tidigare inte haft förmågan att förstå.

Hur man tränar och utvärderar en modell för ligandy-baserad virtuell screening med PyTorch

I den här processen handlar det om att bygga, träna och utvärdera en modell som kan användas för att förutsäga aktiviteten hos olika föreningar i en ligand-baserad virtuell screening. En central del av denna process är användningen av PyTorch och dess funktioner för att effektivisera hanteringen av data, träning och utvärdering av modellen.

Efter att ha definierat vår MoleculeDataset-klass, kan vi skapa PyTorch DataLoader-objekt för varje dataset. DataLoader är ett kraftfullt verktyg inom PyTorch som gör det möjligt att ladda data i batcher, blanda data och skapa iterators över datasetet genom att anropa len- och getitem-metoderna för att hämta batcher av data. När vi till exempel skapar en DataLoader för träningsdatan, specificerar vi en batchstorlek på 32, vilket innebär att data kommer att laddas i batcher om 32 prover. Bearbetning av data i batcher är minnes- och tidseffektiv, vilket gör att träningen går snabbare och kan hantera större dataset.

Genom att ange batch_size=32 säkerställer vi att DataLoadern bearbetar 32 prover åt gången under varje iteration av träningsdatan. Detta gör det möjligt att bearbeta alla exempel en gång under en epok. En epok avslutas när vi har bearbetat varje träningsprov exakt en gång. För att undvika problem med att sista batchen i en epok kan vara mindre än batchstorleken, kan vi sätta drop_last=True för att säkerställa att vi inte hamnar i en situation där batchstorleken inte är konsekvent.

En annan viktig aspekt är användningen av shuffling för träningsdata. Genom att ange shuffle=True ser vi till att DataLoadern blandar om datan efter varje epok. Detta förhindrar att modellen lär sig något baserat på ordningen i datasetet, vilket skulle kunna leda till överanpassning och påverka modellens generaliseringsförmåga negativt. För validerings- och testdata är det viktigt att behålla ordningen för att säkerställa en konsekvent utvärdering.

DataLoadern erbjuder också parallellisering för att optimera dataladdningen genom att utnyttja flera CPU-kärnor samtidigt. Genom att använda argumentet num_workers kan vi ytterligare förbättra dataladdningens hastighet och undvika att GPU:n sitter i vänteläge på grund av långsam dataladdning.

När vår data är korrekt förberedd måste vi definiera modellen. För att detta ska kunna göras effektivt måste vi också ange var modellen och dess tillhörande tensorer ska allokeras – antingen på CPU eller GPU. Detta görs genom att skapa ett torch.device-objekt, som styr om beräkningarna ska göras på en CPU eller på en GPU om CUDA är tillgänglig. Med hjälp av detta kan vi flytta alla modellens parametrar till den valda enheten, vilket gör att tensoroperationerna utförs på rätt plats.

När modellen har definierats och parametrarna har tilldelats rätt enhet, kan vi påbörja träningen. Träningen sker med en specifik loss-funktion (t.ex. MSELoss för regression) och en optimerare (t.ex. Adam), som uppdaterar modellens parametrar efter varje batch. En learning rate scheduler, som till exempel ReduceLROnPlateau, kan användas för att minska inlärningstakten när förbättringarna i modellens prestanda planar ut under träningen.

Vid träningens slut utvärderas modellen på ett testset för att säkerställa att den inte överanpassat till träningsdatan och att den ger bra förutsägelser för nya, osedda data. Vi använder här måtten för Mean Squared Error (MSE) och Mean Absolute Error (MAE), där ett lägre värde på dessa mått indikerar en bättre passform mellan de förutsagda och de verkliga pIC50-värdena. Det är också viktigt att visualisera förhållandet mellan de förutsagda och verkliga värdena för att få en uppfattning om modellens noggrannhet.

För att säkerställa att vi kan återanvända vår tränade modell sparar vi den på disk med funktionen torch.save(). Detta gör det möjligt att senare ladda modellen igen och använda den för inferens utan att behöva träna om den. För att ladda den sparade modellen använder vi load_state_dict() för att läsa in modellens tillstånd och eval() för att sätta modellen i inferensläge.

För att fullända vår ligand-baserade virtuella screening använder vi modellen för att screena en föreningsbibliotek, där vi kan prioritera de föreningar som har högst förutsagd aktivitet. Genom att använda denna metod kan vi effektivt minska den mängd av föreningar som behöver experimentell testning, genom att först screena en stor mängd föreningar med hjälp av vår tränade modell och sedan välja ut de mest lovande för vidare tester.

Endtext