8  Fairness i maskinlæring

I dette kapittelet skal vi bruke følgende pakker:

library(tidyverse)     # datahåndtering og grafikk
library(randomForest)  # random forest-modeller
library(gbm)           # gradient boosting
library(caret)         # confusionMatrix()
library(DALEX)         # lage explainer-objekter for modeller
library(fairmodels)    # fairness-sjekk og visualisering

Nå som vi har sett på klassifikasjonstrær, bagging, random forest og boosting er det på tide å gjøre noen rettferdighetsbetraktninger. I maskinlæring er fairness et sentralt tema: modeller kan systematisk behandle ulike grupper ulikt, noe som kan ha alvorlige konsekvenser avhengig av hva modellen brukes til.

8.1 Hva slags rettferdighet?

I denne settingen kan rettferdighet komme i betraktning på flere måter, herunder følgende:

  • I hvilken grad maskiner vs mennesker tar avgjørelser, og herunder mulighet til å bli hørt og legge frem sin sak
  • I hvilken grad dataene algoritmen er trent opp på inneholder skjevheter i utgangspunktet som så reproduseres i videre implementering
  • I hvilken grad sluttresultatet har rimelig presisjon og akseptable feilrater, herunder vurdering av asymetriske feilrater
  • I hvilken grad forrige punkt er avpasset mot hvilke tiltak man så setter i verk
  • I hvilken grad feilrater og presisjon varierer systematisk med undergrupper i populasjonen

Det er nok av ting å ta tak i her, men vi skal her fokusere på det som kan tallfestes gitt den modellen man har. Men for all del: Er datakvaliteten dårlig er det begrenset hvor bra det kan bli uansett. Selv om kjente skjevheter i dataene i prinsippet kan motarbeides, er det vel i praksis slik at en skjevhet sjelden kommer alene.

8.2 COMPAS-datasettet

COMPAS er et risikoverktøy brukt av amerikansk politi i flere stater som benyttes på individnivå til å vurdere risikoen for at en person begår ny kriminalitet. Bruken av dette verktøyet har vært kontroversielt i flere år og kraftig kritisert. En viktig grunn er at prediksjonene slår forskjellig ut for ulike grupper og er slik sett “biased” mot bl.a. svarte borgere. Resultatet er at de blir mer utsatt for politiets oppmerksomhet enn andre.1 Et datasett er gjort tilgjengelig av Propublica her som vi skal bruke.

compas <- readRDS("data/compas.rds")
glimpse(compas)
Rows: 6,172
Columns: 7
$ Two_yr_Recidivism    <fct> 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1…
$ Number_of_Priors     <int> 0, 0, 4, 0, 14, 3, 0, 0, 3, 0, 0, 1, 7, 0, 3, 6, …
$ Age_Above_FourtyFive <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0…
$ Age_Below_TwentyFive <fct> 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0…
$ Misdemeanor          <fct> 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0…
$ Ethnicity            <fct> Other, African_American, African_American, Other,…
$ Sex                  <fct> Male, Male, Male, Male, Male, Male, Female, Male,…

8.3 Baseline-modell

Vi tilpasser en random forest-modell på datasettet:

set.seed(4356)
rf <- randomForest(Two_yr_Recidivism ~ .,
                   data = compas)

Vi lagrer prediksjonen i en kopi av datasettet, slik at vi kan sammenligne prediksjon og observert utfall:

compas_p <- compas %>%
  mutate(pred_rf = predict(rf))

En overordnet confusion matrix viser modellens ytelse for alle observasjoner samlet:

confusionMatrix(compas_p$pred_rf,
                compas_p$Two_yr_Recidivism, positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction    0    1
         0 2462 1132
         1  901 1677
                                          
               Accuracy : 0.6706          
                 95% CI : (0.6587, 0.6823)
    No Information Rate : 0.5449          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.3313          
                                          
 Mcnemar's Test P-Value : 3.378e-07       
                                          
            Sensitivity : 0.5970          
            Specificity : 0.7321          
         Pos Pred Value : 0.6505          
         Neg Pred Value : 0.6850          
             Prevalence : 0.4551          
         Detection Rate : 0.2717          
   Detection Prevalence : 0.4177          
      Balanced Accuracy : 0.6645          
                                          
       'Positive' Class : 1               
                                          

For å forstå bias kan vi dele opp confusion matrix etter kjønn. Her er modellens ytelse for menn:

compas_m <- compas_p %>% filter(Sex == "Male")
confusionMatrix(compas_m$pred_rf,
                compas_m$Two_yr_Recidivism, positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction    0    1
         0 1807  897
         1  794 1499
                                          
               Accuracy : 0.6616          
                 95% CI : (0.6483, 0.6747)
    No Information Rate : 0.5205          
    P-Value [Acc > NIR] : < 2e-16         
                                          
                  Kappa : 0.3209          
                                          
 Mcnemar's Test P-Value : 0.01312         
                                          
            Sensitivity : 0.6256          
            Specificity : 0.6947          
         Pos Pred Value : 0.6537          
         Neg Pred Value : 0.6683          
             Prevalence : 0.4795          
         Detection Rate : 0.3000          
   Detection Prevalence : 0.4589          
      Balanced Accuracy : 0.6602          
                                          
       'Positive' Class : 1               
                                          

Og for kvinner:

compas_f <- compas_p %>% filter(Sex == "Female")
confusionMatrix(compas_f$pred_rf,
                compas_f$Two_yr_Recidivism, positive = "1")
Confusion Matrix and Statistics

          Reference
Prediction   0   1
         0 655 235
         1 107 178
                                         
               Accuracy : 0.7089         
                 95% CI : (0.682, 0.7348)
    No Information Rate : 0.6485         
    P-Value [Acc > NIR] : 6.251e-06      
                                         
                  Kappa : 0.3128         
                                         
 Mcnemar's Test P-Value : 6.539e-12      
                                         
            Sensitivity : 0.4310         
            Specificity : 0.8596         
         Pos Pred Value : 0.6246         
         Neg Pred Value : 0.7360         
             Prevalence : 0.3515         
         Detection Rate : 0.1515         
   Detection Prevalence : 0.2426         
      Balanced Accuracy : 0.6453         
                                         
       'Positive' Class : 1              
                                         

Modellen er mer presis for kvinner enn for menn — dette er et eksempel på bias i modellen. Forholdet mellom accuracy for menn og kvinner gir et enkelt mål på skjevheten. Men å sammenligne confusion matriser manuelt for mange grupper og mange mål er arbeidskrevende. Det er her fairness-verktøy kommer inn.

8.4 Mål på fairness med fairmodels

Pakken fairmodels gjør det enkelt å måle fairness på tvers av grupper for mange mål på én gang. For å bruke den trengs et explainer-objekt fra DALEX-pakken. Et explainer-objekt er en standardisert innpakning rundt en modell, slik at fairmodels kan bruke ulike typer modeller (random forest, boosting, etc.) med det samme grensesnittet.

Vi lager explainer-objektet for baseline random forest-modellen. Vi angir modellen, prediktordata (uten utfallsvariabelen), og den numeriske versjonen av utfallsvariabelen:

y_num    <- as.numeric(as.character(compas$Two_yr_Recidivism))
compas_X <- compas %>% select(-Two_yr_Recidivism)

explainer_rf <- DALEX::explain(
  model = rf,
  data  = compas_X,
  y     = y_num,
  label = "Random Forest"
)
Preparation of a new explainer is initiated
  -> model label       :  Random Forest 
  -> data              :  6172  rows  6  cols 
  -> target variable   :  6172  values 
  -> predict function  :  yhat.randomForest  will be used (  default  )
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package randomForest , ver. 4.7.1.2 , task classification (  default  ) 
  -> predicted values  :  numerical, min =  0 , mean =  0.4037122 , max =  1  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -1 , mean =  0.05140765 , max =  1  
  A new explainer has been created!  

Deretter bruker vi fairness_check() med Sex som beskyttet variabel og "Male" som referansegruppe:

fcheck <- fairness_check(
  explainer_rf,
  protected  = compas$Sex,
  privileged = "Male"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 1 in total (  compatible  )
-> Metric calculation       : 13/13 metrics calculated for all models
 Fairness object created succesfully  
plot(fcheck)

Plottet viser parity loss for en rekke ulike fairness-mål. Den grønne sonen angir et akseptabelt område basert på 4/5-regelen: forholdet mellom gruppen og referansegruppen bør ligge mellom 0.8 og 1.25. Søyler utenfor den grønne sonen betyr at modellen er statistisk sett unfair på det aktuelle målet.

Tre mål er særlig sentrale:

Mål i fairmodels Hva måles Konsept
PPV Andelen sanne positive av alle predikerte positive Predictive rate parity
TPR Andelen sanne positive av alle faktisk positive (sensitivitet) Equalized odds
FPR Andelen falske positive av alle faktisk negative False positive rate parity

Hvilket mål som er viktigst avhenger av hva modellen skal brukes til og hva konsekvensene av feil er. Et eksempel: Hvis modellen brukes til å vurdere prøveløslatelse, vil en høy FPR for én gruppe bety at den gruppen urettmessig beholder i fengsel mer enn andre grupper.

Vi kan også sjekke fairness for etnisitet ved å bytte ut den beskyttede variabelen:

fcheck_etni <- fairness_check(
  explainer_rf,
  protected  = compas$Ethnicity,
  privileged = "Caucasian"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 1 in total (  compatible  )
-> Metric calculation       : 11/13 metrics calculated for all models ( 2 NA created )
 Fairness object created succesfully  
plot(fcheck_etni)
Warning in plot.fairness_object(fcheck_etni): Omiting NA for models: Random Forest
Information about passed metrics may be inaccurate due to NA present, it is advisable to check metric_scores plot.
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_bar()`).

8.5 Forbedre fairness: Stratifisering

Vi har sett at sampsize i random forest styrer fordelingen av treningsdata per utfallskategori. For å motvirke bias mellom undergrupper kan vi bruke stratifisert sampling: ved å trekke fra grupper i styrte proporsjoner kan vi justere modellens feilrater per gruppe.

sampsize i random forest styrer ikke bare fordelingen av utfall, men kan kombineres med strata for å stratifisere etter en egendefinert variabel. Stratifiseringsvektoren kombinerer kjønn og utfall til fire kategorier:

strat <- compas %>%
  mutate(strat = paste0(Sex, Two_yr_Recidivism)) %>%
  pull(strat)

table(strat)
strat
Female0 Female1   Male0   Male1 
    762     413    2601    2396 

Tallene i sampsize oppgis i samme rekkefølge som tabellen: kvinner uten tilbakefall, kvinner med tilbakefall, menn uten tilbakefall, menn med tilbakefall. Her vektes kvinner med tilbakefall opp relativt, for å øke modellens sensitivitet for denne gruppen:

set.seed(45)
rf_strat <- randomForest(Two_yr_Recidivism ~ .,
                         data     = compas,
                         strata   = strat,
                         sampsize = c(215, 290, 1500, 1500),
                         ntree    = 800)

Vi lager en explainer for den stratifiserte modellen og sammenligner med baseline ved å sende begge explainere til fairness_check():

explainer_rf_strat <- DALEX::explain(
  model = rf_strat,
  data  = compas_X,
  y     = y_num,
  label = "RF stratifisert"
)
Preparation of a new explainer is initiated
  -> model label       :  RF stratifisert 
  -> data              :  6172  rows  6  cols 
  -> target variable   :  6172  values 
  -> predict function  :  yhat.randomForest  will be used (  default  )
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package randomForest , ver. 4.7.1.2 , task classification (  default  ) 
  -> predicted values  :  numerical, min =  0 , mean =  0.4916229 , max =  1  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -1 , mean =  -0.03650296 , max =  1  
  A new explainer has been created!  
fcheck2 <- fairness_check(
  explainer_rf,
  explainer_rf_strat,
  protected  = compas$Sex,
  privileged = "Male"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 2 in total (  compatible  )
-> Metric calculation       : 13/13 metrics calculated for all models
 Fairness object created succesfully  
plot(fcheck2)

Dette er en av de store fordelene med fairmodels: vi kan sammenligne flere modeller direkte i ett plot. Ble den stratifiserte modellen mer rettferdig enn baseline-modellen?

Merk at det ikke er noe åpenbart svar på hvilke verdier man skal velge for sampsize. Utgangspunktet er å sørge for at det trekkes omtrent like mange observasjoner fra grupper man ønsker å balansere. Det krever litt utprøving.

8.5.1 Hva med andre subgrupper?

Vi justerte for kjønn — men slo det positivt ut for etnisitet også?

fcheck2_etni <- fairness_check(
  explainer_rf,
  explainer_rf_strat,
  protected  = compas$Ethnicity,
  privileged = "Caucasian"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 2 in total (  compatible  )
-> Metric calculation       : 8/13 metrics calculated for all models ( 5 NA created )
 Fairness object created succesfully  
plot(fcheck2_etni)
Warning in plot.fairness_object(fcheck2_etni): Omiting NA for models: Random Forest
Information about passed metrics may be inaccurate due to NA present, it is advisable to check metric_scores plot.
Warning: Removed 1 row containing missing values or values outside the scale range
(`geom_bar()`).

Sannsynligvis ikke. Vi justerte for kjønn, ikke for etnisitet. En mulighet er å stratifisere på begge variable samtidig. Vi starter med å lage en kombinert stratifiseringsvektor:

strat2 <- compas %>%
  mutate(caucasian = ifelse(Ethnicity == "Caucasian", "Caucasian", "Other"),
         strat = paste0(Sex, caucasian, Two_yr_Recidivism)) %>%
  pull(strat)

table(strat2)
strat2
FemaleCaucasian0 FemaleCaucasian1     FemaleOther0     FemaleOther1 
             312              170              450              243 
  MaleCaucasian0   MaleCaucasian1       MaleOther0       MaleOther1 
             969              652             1632             1744 

Problemet her er at noen grupper er svært små. Å stratifisere på disse gir bare støy. Vi begrenser oss til å skille mellom “Caucasian” og “Other” fremfor alle etnisitetskategorier:

set.seed(45)
rf_strat2 <- randomForest(Two_yr_Recidivism ~ .,
                          data     = compas,
                          strata   = strat2,
                          sampsize = c(120, 120,
                                       200, 200,
                                       400, 400,
                                       900, 900),
                          ntree    = 800)
explainer_rf_strat2 <- DALEX::explain(
  model = rf_strat2,
  data  = compas_X,
  y     = y_num,
  label = "RF strat. kjønn+etni"
)
Preparation of a new explainer is initiated
  -> model label       :  RF strat. kjønn+etni 
  -> data              :  6172  rows  6  cols 
  -> target variable   :  6172  values 
  -> predict function  :  yhat.randomForest  will be used (  default  )
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package randomForest , ver. 4.7.1.2 , task classification (  default  ) 
  -> predicted values  :  numerical, min =  0 , mean =  0.4721089 , max =  1  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -0.99875 , mean =  -0.01698902 , max =  1  
  A new explainer has been created!  
fcheck3 <- fairness_check(
  explainer_rf,
  explainer_rf_strat,
  explainer_rf_strat2,
  protected  = compas$Sex,
  privileged = "Male"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 3 in total (  compatible  )
-> Metric calculation       : 13/13 metrics calculated for all models
 Fairness object created succesfully  
plot(fcheck3)

Sammenlign de tre modellene. Man kan justere for noe, men ikke alt samtidig. Med mer data kan man justere for mer, men man er alltid begrenset av datamaterialet.

8.6 Justere rettferdighet med vekting

En annen strategi er å vekte observasjoner i treningsfasen. Dette fungerer godt i boosting, der vektene direkte påvirker hva algoritmen fokuserer på. Vektingen er en måte å fortelle modellen at noen typer feil er viktigere enn andre.

gbm krever at utfallsvariabelen er numerisk (0/1) med distribution = "bernoulli". Vi lager en kopi av datasettet med konvertert utfall:

compas_gbm <- compas %>%
  mutate(Two_yr_Recidivism = as.numeric(as.character(Two_yr_Recidivism)))

Deretter lager vi en vektvektor der kvinner og observasjoner med tilbakefall vektes opp. Tanken er at modellen skal ta disse tilfellene mer alvorlig under trening:

wts <- compas_gbm %>%
  mutate(wts = case_when(
    Sex == "Male"   & Two_yr_Recidivism == 0 ~ 1,
    Sex == "Male"   & Two_yr_Recidivism == 1 ~ 2,
    Sex == "Female" & Two_yr_Recidivism == 0 ~ 2,
    Sex == "Female" & Two_yr_Recidivism == 1 ~ 4)) %>%
  pull(wts)

case_when() setter én vektverdi per observasjon basert på kombinasjonen av kjønn og utfall, og pull() trekker ut vektene som en vektor. Kvinner med tilbakefall vektes fire ganger så mye som menn uten tilbakefall.

set.seed(542)
gradboost <- gbm(formula = Two_yr_Recidivism ~ .,
                 data         = compas_gbm,
                 weights      = wts,
                 distribution = "bernoulli",
                 n.trees      = 4000,
                 interaction.depth  = 3,
                 n.minobsinnode = 1,
                 shrinkage    = 0.001,
                 bag.fraction = 0.5)

For å bruke gbm med DALEX::explain() må vi angi en tilpasset prediksjonsfunksjon, fordi gbm trenger n.trees eksplisitt:

predict_gbm <- function(model, newdata) {
  predict(model, newdata = newdata,
          n.trees = model$n.trees, type = "response")
}

compas_gbm_X <- compas_gbm %>% select(-Two_yr_Recidivism)

explainer_gbm <- DALEX::explain(
  model            = gradboost,
  data             = compas_gbm_X,
  y                = compas_gbm$Two_yr_Recidivism,
  predict_function = predict_gbm,
  label            = "GBM med vekter"
)
Preparation of a new explainer is initiated
  -> model label       :  GBM med vekter 
  -> data              :  6172  rows  6  cols 
  -> target variable   :  6172  values 
  -> predict function  :  predict_gbm 
  -> predicted values  :  No value for predict function target column. (  default  )
  -> model_info        :  package gbm , ver. 2.2.2 , task classification (  default  ) 
  -> predicted values  :  numerical, min =  0.2275137 , mean =  0.599534 , max =  0.8853893  
  -> residual function :  difference between y and yhat (  default  )
  -> residuals         :  numerical, min =  -0.8737405 , mean =  -0.1444141 , max =  0.7417262  
  A new explainer has been created!  

Nå kan vi sammenligne alle tre modellene i ett fairness-kall:

fcheck_all <- fairness_check(
  explainer_rf,
  explainer_rf_strat,
  explainer_gbm,
  protected  = compas$Sex,
  privileged = "Male"
)
Creating fairness classification object
-> Privileged subgroup      : character ( Ok  )
-> Protected variable       : factor ( Ok  ) 
-> Cutoff values for explainers : 0.5 ( for all subgroups ) 
-> Fairness objects     : 0 objects 
-> Checking explainers      : 3 in total (  compatible  )
-> Metric calculation       : 13/13 metrics calculated for all models
 Fairness object created succesfully  
plot(fcheck_all)

For en kompakt oversikt over alle modeller og alle mål kan vi bruke fairness_heatmap():

fairness_heatmap(fcheck_all)
heatmap data top rows: 
  parity_loss_metric           model score
1                TPR   Random Forest  0.33
2                TPR RF stratifisert  0.04
3                TPR  GBM med vekter  0.30
4                TNR   Random Forest  0.21
5                TNR RF stratifisert  0.05

matrix model not scaled :
                       TPR        TNR         PPV        NPV        FNR
Random Forest   0.32585661 0.21473973 0.001931638 0.09351694 0.41239929
RF stratifisert 0.04097714 0.05222386 0.183665525 0.12662643 0.08413193
GBM med vekter  0.29995238 0.48139816 0.101917421 0.02585717 0.90244226
                      FPR         FDR        FOR        TS       STP        ACC
Random Forest   0.8504339 0.003894726 0.23922716 0.2571368 0.6344638 0.07622932
RF stratifisert 0.1125916 0.275123636 0.37186916 0.1704876 0.1678504 0.01579906
GBM med vekter  0.6048768 0.123561862 0.08350239 0.2719322 0.5085737 0.05840212
                       F1 NEW_METRIC
Random Forest   0.1796063  0.7382559
RF stratifisert 0.1165885  0.1251091
GBM med vekter  0.1866799  1.2023946

Sammenlign modellene. Ble den vektede boosting-modellen mer rettferdig enn random forest? Husk at det alltid er en trade-off: økt rettferdighet for én gruppe eller på ett mål kan gå på bekostning av total presisjon eller rettferdighet på et annet mål.

8.7 Avsluttende betraktninger

Hvilket fairness-mål som er viktigst er det ikke noe klart svar på. Det kommer an på hva man har tenkt å bruke prediksjonen til, hvilke konsekvenser tiltaket har, og hvilke konsekvenser det har å ikke gjøre noe. Disse konsekvensene kan vurderes forskjellig for ulike personer, grupper og situasjoner.

Problemet er at det ikke går an å justere i det uendelige for alle variable. I tillegg kan det uansett være andre egenskaper man ikke har data for som vil vise seg å gi urettferdige utslag. Man kan justere for noe, men ikke alt. Med nok data kan man justere for mer — men fremdeles ikke alt. I tillegg er justeringer som regel ikke gratis: økt rettferdighet for én gruppe kan innebære lavere presisjon totalt sett, eller redusert rettferdighet for en annen gruppe.

Så da er vi tilbake til det litt ubehagelige gjennomgangstemaet om prioriteringer og valg: Vil du ha en rettferdig modell eller en presis modell? Hvilke grupper bør den være rettferdig for — og hvilke grupper er det ikke så farlig om den er urettferdig for? Dette kan lett bli umulige valg.

Bør det nevnes at noen ganger ville man gjort noe uansett — altså gjort de samme tiltakene basert på skjønn eller andre typer vurderinger. Det vil også ha feilrater og forskjeller mellom undergrupper, selv om man ikke har et oppsett som gir testing-data å estimere dette på. Det betyr at du ikke bare må ta stilling til om algoritmen er “fair” eller ikke, men også om den er mer eller mindre “fair” enn den alternative fremgangsmåten.


  1. Det er verd å minne på at politi i USA i stor grad er mer hardhendte enn norsk politi. Konsekvensene er altså litt mer alvorlig enn at unødig mange føler seg unødig mistenkte.↩︎