Erstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (2023)

Die Kreuzvalidierung ist eine weit verbreitete Technik, um die Verallgemeinerung eines maschinellen Lernmodells zu bewerten. Nicht hierSTATISTIKENWir diskutieren oft Messmetriken und wie wir sie effizient in unseren Data-Science-Workflow integrieren können. In diesem Blog-Beitrag stelle ich Ihnen die Grundlagen der Kreuzvalidierung vor, gebe Ihnen Tipps zur Optimierung ihrer Parameter und zeige Ihnen einen effizienten Weg, sie von Grund auf neu zu erstellen.

Grundlagen der Modellbewertung und Kreuzvalidierung

Die Kreuzvalidierung ist eine Technik zur Bewertung von Modellen für maschinelles Lernen. Die zentrale Erkenntnis hinter dieser Modellbewertung ist herauszufinden, ob das trainierte Modell verallgemeinerbar ist, d. das heißt, ob die Vorhersagekraft, die wir im Training beobachten, auch in unsichtbaren Daten zu erwarten ist. Wir könnten das Modell direkt mit den Daten füttern, für die es entworfen wurde, dh H., die es vorhersagen sollte. Aber selbst dann gibt es für uns keine Möglichkeit zu wissen oderüberprüfenwenn die Vorhersagen stimmen.

Natürlich möchten wir eine Art Benchmark für die Verallgemeinerung unseres Modells haben, bevor wir es in Produktion nehmen. Die Idee ist also, die vorhandenen Trainingsdaten in einen eigentlichen Trainingsdatensatz und einen Validierungstestdatensatz aufzuteilen, der nicht für das Training verwendet wird und als "unsichtbare" Daten dient. Da dieser Testdatensatz Teil der ursprünglichen Trainingsdaten ist, haben wir einen vollständigen Satz „korrekter“ Ergebnisse zu verifizieren. Eine geeignete Fehlermetrik, wie z. B. der mittlere quadratische Fehler (RMSE) oder der mittlere absolute prozentuale Fehler (MAPE), kann verwendet werden, um die Modellleistung zu bewerten. Bei der Auswahl der geeigneten Bewertungsmetrik ist jedoch Vorsicht geboten, da es Fallstricke gibt (wie inseinbeschrieben im Blogbeitrag meines Kollegen Jan).

Für viele maschinelle Lernalgorithmen können Hyperparameter angegeben werden, z. B. die Anzahl der Bäume in einem zufälligen Wald. Die Kreuzvalidierung kann auch verwendet werden, um die Hyperparameter eines Modells anzupassen, indem der Generalisierungsfehler verschiedener Modellspezifikationen verglichen wird.

Allgemeine Ansätze zur Modellbewertung

Es gibt Dutzende von Modellbewertungstechniken, jede mit einem Kompromiss zwischen Varianz, Bias und Rechenzeit. Bei der Bewertung eines Modells ist es wichtig, diese Kompromisse im Auge zu behalten, da die Wahl der geeigneten Technik stark vom vorliegenden Problem und den beobachteten Daten abhängt. Ich werde dieses Thema behandeln, nachdem ich zwei der gebräuchlichsten Modellevaluierungstechniken vorgestellt habe: Split-Test-Training und k-fache Kreuzvalidierung.

Die erste Methode teilt die Trainingsdaten nach dem Zufallsprinzip in eine Trainingseinheit und eine Testeinheit auf (Abbildung 1), wobei normalerweise ein großer Teil der Daten als Trainingsdatensatz beibehalten wird. Die Verhältnisse von 70/30 oder 80/20 werden in der Literatur am häufigsten verwendet, und das genaue Verhältnis hängt von der Größe der Daten ab.

Der Nachteil dieses Ansatzes besteht darin, dass diese einzelne zufällige Aufteilung dazu führen kann, dass die Daten in zwei sehr unausgewogene Teile aufgeteilt werden, was zu verzerrten Schätzungen des Generalisierungsfehlers führt. Dies ist besonders kritisch, wenn Sie über begrenzte Daten verfügen, da einige Merkmale oder Muster vollständig im Testteil landen können. In diesem Fall hat das Modell keine Chance, sie zu lernen, und seine Leistung wird möglicherweise unterschätzt.

Erstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (1)

Eine robustere Alternative ist die sogenannte k-fache Kreuzvalidierung (Abbildung 2). Hier werden die Würfel gemischt und dann zufälligErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (2)in Teilmengen aufgeteilt. Der Hauptvorteil gegenüber der Aufteilung von Testzügen besteht darinalleÖErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (3)Partitionen wird iterativ als Testdatensatz (d. h. Validierungssatz) mit dem Rest verwendetErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (4)-Die Teile in dieser Iteration dienen als Trainingsdaten. Dieser ProzessErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (5)Wiederholungszeiten für jede Beobachtung, die in die Trainings- und Testdaten aufgenommen werden sollen. Die entsprechende Fehlermetrik wird einfach als Durchschnitt aller berechnetErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (6)Die Iterationen werden berechnet und liefern den Kreuzvalidierungsfehler.

es ist eher wie einVerlängerungdes Bereichs Zugtest als völlig neue Methode: nämlich das ZugtestverfahrenErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (7)wiederholte Male. JaErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (8)Zum Beispiel so niedrig wieErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (9)ausgewählt ist, haben Sie nur zwei Spiele. Aber auch diese Vorgehensweise ist dem Test Train Splitting immer noch überlegen, dabeideTeile können iterativ für das Training ausgewählt werden, was dem Modell die Möglichkeit dazu gibtAllenLerndaten und nicht nur eine zufällige Teilmenge davon. Daher führt dieser Ansatz im Allgemeinen zu stärkeren Leistungsschätzungen.

Erstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (10)

Ein Vergleich der beiden obigen Abbildungen zeigt, dass eine 80/20-Aufteilung zwischen Training und Prüfung bestehteine Iteration5 mal (bzwErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (11)) entspricht einer Kreuzvalidierung, bei der 4/5 der Daten für das Training und 1/5 für die Validierung aufbewahrt werden. Der Hauptunterschied besteht darin, dass bei der k-fachen Kreuzvalidierung die Validierung in jedem derErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (12)Iterationen scrollen. Beachten Sie, dass die k-fache Kreuzvalidierung robuster ist als nur die Aufteilung der TrainingsversucheErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (13)Falten: Bei der k-fachen Kreuzvalidierung wird die Aufteilungeinmalgetan und dann durch die Partitionen iteriert, während das Training und Testen die Daten iterativ aufteilteErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (14)-Zeiten werden neu verteilt, was dazu führen kann, dass einige Daten während des Trainings ausgelassen werden.

Wiederholter CV und LOOCV

Es gibt viele Varianten der k-fachen Kreuzvalidierung. Sie können beispielsweise auch eine „wiederholte Kreuzvalidierung“ durchführen. Die Idee dahinter ist, sobald die Daten da sindErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (15)Die Partitionen wurden geteilt, diese Teilung wird auf das gesamte Verfahren angewendet. Auf diese Weise besteht kein Risiko, dass wir einige zufällige Teile löschen. Wiederholen Sie bei wiederholtem CV diesen Vorgang des Mischens und Randomisierens der DatenErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (16)Subsets eine bestimmte Anzahl von Malen. Sie können dann die resultierenden Kreuzvalidierungsfehler aus jedem Schritt mitteln, um eine Schätzung der Gesamtleistung zu erhalten.

Ein weiterer Sonderfall der k-fachen Kreuzvalidierung ist die "Leave One Out Cross-Validation" (LOOCV), bei derErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (17)setzen. Dies bedeutet, dass in jeder Iteration eine verwendet wird.gerechtBetrachten Sie Ihre Daten als Teil der Validierung und des RestsErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (18)Beobachtungen als Trainingsset. Obwohl es wie eine sehr robuste Version der Kreuzvalidierung aussieht, wird von dieser Methode im Allgemeinen aus zwei Gründen abgeraten:

  • Erstens sie normalerweisesehr rechenintensiv. Für die meisten Datensätze, die beim angewandten maschinellen Lernen verwendet werden, ist es weder wünschenswert noch machbar, das Modell zu verwenden.Erstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (19)Zeiten zu trainieren (obwohl es für sehr kleine Datensätze nützlich sein kann).
  • Zweitens, selbst wenn man die Rechenleistung (und Zeit) hätte, um diesen Prozess durchzuführen, ist ein weiteres Argument der Kritiker von LOOCV aus statistischer Sicht, dass der resultierende Kreuzvalidierungsfehler eine große Varianz aufweisen kann. Denn das „Validierungsset“ besteht aus einer einzigen Beobachtung, und je nach Verteilung Ihrer Daten (und möglicher Ausreißer) kann diese stark variieren.

Im Allgemeinen ist anzumerken, dass die LOOCV-Leistung sowohl in der akademischen Literatur als auch in der breiteren Gemeinschaft des maschinellen Lernens ein etwas kontroverses Thema ist. Daher empfehle ich, diese Diskussion zu lesen, wenn Sie die Verwendung von LOOCV in Betracht ziehen, um die Verallgemeinerung eines Modells abzuschätzen (siehe z. B.Hier) und ähnliche Beiträge auf StackExchange). Wie so oft könnte die Antwort lauten: „Es kommt darauf an.“ Bedenken Sie auf jeden Fall den Rechenaufwand von LOOCV, der kaum ausgeschlossen werden kann (es sei denn, Sie haben einen kleinen Datensatz).

Der Wert vonErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (20)und der Kompromiss zwischen Verzerrung und Varianz

SeErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (21)ist (unbedingt) nicht die beste Wahl, wie finden Sie also einen geeigneten Wert fürErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (22)? Es stellt sich heraus, dass die Antwort auf diese Frage berüchtigt istBias-Varianz-KompensationEs ist aus. Denn ist was?

Der Wert fürErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (23)bestimmt, in wie viele Partitionen die Daten aufgeteilt werden, und damit die Größe (d. h. die Anzahl der Beobachtungen, die in jedem Teil enthalten sind). Wir wollenErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (24)Wählen Sie einen aus, damit ein ausreichend großer Teil unserer Daten im Trainingssatz verbleibt. Schließlich wollen wir nicht zu viele Beobachtungen liefern, die zum Trainieren unseres Modells verwendet werden können. Je größer der Wert vonErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (25)Das heißt, bei jeder Iteration werden dem Trainingssatz mehr Beobachtungen hinzugefügt.

Angenommen, wir haben 1200 Beobachtungen in unserem Datensatz, dann wäre unser Trainingssatz drinErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (26)außenErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (27)Es gibt Beobachtungen, aber mitErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (28)würde 1050 Beobachtungen beinhalten. Je mehr Beobachtungen für das Training verwendet werden, desto näher ist die tatsächliche Leistung des Modells (als ob es auf dem gesamten Datensatz trainiert worden wäre) und desto weniger Verzerrung (oder Bias) wird die Fehlerschätzung im Vergleich zu einem kleineren Teil aufweisen das Modell die Modelldaten. Aber mit der SteigerungErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (29)die Größe der haltenden Partition nimmt ab und die Fehlerschätzung wird bei jeder Iteration empfindlicher für diese wenigen Datenpunkte, was die Gesamtvarianz erhöhen kann. Im Grunde geht es um die Wahl zwischen den „Extremen“ der Zugtestsparte einerseits und LOOCV andererseits. Die folgende Abbildung veranschaulicht schematisch (!) die Bias-Varianz-Performance und den Rechenaufwand der verschiedenen Kreuzvalidierungsmethoden.

Erstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (30)

Als allgemeine Regel gilt, dass die Verzerrung mit höheren Werten von k abnimmt und die Varianz zunimmt. Herkömmlicherweise werden Werte wieErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (31)ÖErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (32)als guter Kompromiss und sind daher in den meisten Fällen von maschinellem Lernen fast zum Standard geworden.

„Diese Werte haben empirisch gezeigt, dass sie Schätzungen der Testfehlerrate liefern, die nicht unter übermäßig hoher Verzerrung oder sehr hoher Varianz leiden.“

James und AI. 2013: 184

Wenn Sie sich nicht besonders für den Cross-Validation-Prozess interessieren, sondern ihn einfach in Ihren Data-Science-Workflow integrieren möchten (ich empfehle ihn sehr!), sollten Sie einen dieser Werte für verwendenErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (33)auswählen und es dabei belassen.

Implementierung der Kreuzvalidierung in

Apropos Integration der Kreuzvalidierung in Ihren täglichen Arbeitsablauf, welche Möglichkeiten haben Sie? Glücklicherweise ist die Kreuzvalidierung ein Standardwerkzeug in beliebten Paketen für maschinelles Lernen wie B.Zirkumflex-Akzentin R. Hier können Sie die Methode mit der Funktion verwendentrenControlto set Im folgenden Skript trainieren wir einen 10-fach kreuzvalidierten Random Forest auf demIrisAufzeichnen.

biblioteca(caret)set.seed(12345)inTrain <- createDataPartition(y = iris[["Species"]], p = .7, list = FALSE)iris.train <- iris[inTrain, ]iris.test <- iris[- inTrain, ]fit.control <- caret::trainControl(method = "cv", number = 10)rf.fit <- caret::train(Species ~ ., data = iris.train, method = "rf ", trControl = ajuste.control)

Wir definieren unsere gewünschte Kreuzvalidierungsmethode in der FunktiontrenControl, speichert die Ausgabe im ObjektAnpassung.Steuerungund übergeben Sie dann dieses Objekt an das ArgumenttrControldie FunktionSchütteln. Sie können die anderen in diesem Beitrag vorgestellten Methoden auf ähnliche Weise definieren:

# Leave-One-Out Cross-Validation:fit.control <- caret::trainControl(method = "LOOCV", number = 10)# Repeated CV (Anzahl der Wiederholungen nicht vergessen!) fit.control <- caret: :trainControl(method = "repeated cv", number = 10, repeats = 5)

Altmodisch: DieErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (34)-fold Kreuzvalidierung manuell implementieren

Data-Science-Projekte können jedoch schnell so komplex werden, dass Out-of-the-Box-Features in Machine-Learning-Paketen nicht mehr angemessen sind. In solchen Fällen müssen Sie den Algorithmus, einschließlich Kreuzvalidierungstechniken, manuell implementieren, zugeschnitten auf die spezifischen Anforderungen des Projekts. Ich möchte Ihnen ein behelfsmäßiges Skript zeigen, mit dem Sie manuell eine einfache k-fache Kreuzvalidierung in R implementieren können (wir gehen hier Schritt für Schritt durch das Skript; Sie finden den gesamten Code unterGitHub).

Simulieren Sie Daten, definieren Sie Fehlermetriken undErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (35)gründen

# devtools::install_github("andrebleier/Xy")library(tidyverse)library(Xy)sim <- Xy(n = 1000, numvars = c(2,2), catvars = 0, cor = c(-0.5, 0.9 ), Noisevars = 0)sim_data <- sim[["data"]]RMSE <- function(f, o){ sqrt(mean((f - o)^2))}k <- 5

Wir beginnen damit, die benötigten Pakete und einige Simulationsdaten von 1000 Beobachtungen mit dem Paket zu laden.xy()zu simulieren, was mein Kollege André entwickelt hat (siehe seinen Blogeintrag aufRegressionsdaten mit Xy simulieren). Da wir eine Art Fehlermaß benötigen, um die Leistung des Modells zu bewerten, definieren wir unsere RMSE-Funktion, die ziemlich einfach ist: Der RMSE ist diemittlerer quadratischer Fehler, der der mittlere quadratische Fehler ist, wobei der Fehler die Differenz zwischen den angepassten (f) und beobachteten (o) Werten ist; Sie können die Funktion von links nach rechts lesen. Schließlich geben wir unserekdie im Beispiel auf 5 gesetzt und als einfache Ganzzahl gespeichert wird.

die Daten aufteilen

set.seed(12345)sim_data <- mutate(sim_data, my.folds = sample(1:k, size = nrow(sim_data), replace = TRUE))

Wir teilen unsere Daten dann auf inErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (36)folds Dazu fügen wir den Daten eine neue Spalte hinzu,meine Falten: Wir extrahieren eine Probe (mit Ersatz) von 1 bis zum Wert vonErstellen Sie eine Kreuzvalidierung, um die Modellleistung von Grund auf neu zu bewerten (37), in unserem Fall von 1 bis 5, und fügen Sie zufällig eine dieser fünf Zahlen zu jeder Zeile (Beobachtung) in den Daten hinzu. Für 1000 Beobachtungen sollte jede Nummer ungefähr 200 Mal vergeben werden.

Modelltraining und -validierung

cv.fun <- function(this.fold, data){ train <- filter(data, my.folds != this.fold) validar <- filter(data, my.folds == this.fold) model <- lm (y ~ NLIN_1 + NLIN_2 + LIN_1 + LIN_2, dados = treinar) pred <- predizer(modelo, newdata = validar) %>% as.vector() this.rmse <- RMSE(f = pred, o = validar$y ) return(this.rmse)}

Als nächstes definieren wir cv.fun, das Herzstück unseres Cross-Validation-Prozesses. Diese Funktion akzeptiert zwei Argumente:diese.dobramiDaten. Über die Bedeutung vondiese.dobraIch komme gleich darauf zurück, setzen wir es jetzt einfach auf 1. Innerhalb der Funktion teilen wir die Daten in eine Trainingspartition und eine Validierungspartition auf und sortieren nach Werten vonmeine Faltenmidiese.dobraUnterteilen: Jede Beobachtung mit einer zufällig zugewiesenenmeine Falten-Wertanders als 1(also ca. 4/5 der Daten) geht ins Training. Alle Beobachtungen mitmeine Falten-Wertgleich 1(die verbleibenden 1/5) bilden den Validierungssatz. Zur Veranschaulichung passen wir ein einfaches lineares Modell mit dem simulierten Ergebnis und vier Prädiktoren an. Bitte beachten Sie, dass wir dieses Modell nur in der anbietenSchütteln-Daten anwenden! Wir verwenden dieses Modell dann, um unsere Buchungsdaten vorherzusagen. Da wir für diese Teilmenge der ursprünglichen Trainingsdaten echte Ergebnisse haben (das ist der Punkt!), können wir unseren RMSE berechnen und zurückgeben.

Gehen Sie die Falten durch und berechnen Sie den CV-Fehler

cv.error <- sapply(seq_len(k), FUN = cv.fun, data = sim_data) %>% mean()cv.error

Schließlich packen wir den Funktionsaufruf einLebenslauf.Spaßin einemKette()-Loop: Hier passiert die ganze Magie: Wir iterieren über den Bereich vonk, wofürseq_len(k)in diesem Fall der Vektor[1] 1 2 3 4 5Lieferung. Wir geben jedes Element dieses Vektors ausLebenslauf.SpaßNEIN. NEIN*anwenden()Familie wird der Iterationsvektor immer als erstes Argument an die aufgerufene Funktion übergeben, also in unserem Fall jeweils jedes Element dieses Vektorsdiese.dobrawird geliefert Wir stellen auch unsere Simulationen vorja_datumals ArgumentDaten.

Fassen wir kurz zusammen, was das bedeutet: Bei der ersten Iteration ist this.fold gleich 1. Das bedeutet, dass unser Trainingsset aus allen Beobachtungen wo bestehtmeine Faltennicht 1 ist, und die Beobachtungen mit einem Wert von 1 bilden den Validierungssatz (wie im vorherigen Beispiel). In der nächsten Iteration der Schleife ist esdiese.dobragleich 2 ist. Somit bilden die Beobachtungen mit den Werten 1, 3, 4 und 5 den Trainingssatz und die Beobachtungen mit dem Wert 2 gehen in den Validierungssatz und so weiter. Gehen Sie alle Werte von durchkDurch Iteration erhalten wir das in Abbildung 2 gezeigte Diagonalmuster, bei dem jede Datenpartition einmal als Reservesatz verwendet wird.

Schließlich berechnen wir den Durchschnitt: Dies ist der Durchschnitt unsererkindividuelle RMSE-Werte und gibt unseren Kreuzvalidierungsfehler an. Und das war's: Wir haben gerade unsere eigene Kreuzvalidierungsfunktion definiert!

Dies ist nur eine Vorlage – Sie können beliebige Vorlagen und Fehlermetriken eingeben. Wenn Sie so weit gekommen sind, können Sie gerne versuchen, einen sich wiederholenden Lehrplan selbst oder einen mit unterschiedlichen Werten für zu implementierenkZeit verschwenden

Abschluss

Wie Sie sehen können, ist es nicht so schwierig, die Kreuzvalidierung selbst zu implementieren. Es bietet eine große Flexibilität, um spezifische Projektanforderungen zu berücksichtigen, wie z. B. benutzerdefinierte Fehlermetriken. Wenn Sie nicht so viel Flexibilität benötigen, ist die Implementierung der Kreuzvalidierung in beliebten Paketen für maschinelles Lernen ein Kinderspiel.

Ich hoffe, ich konnte einen ausreichenden Überblick über die Kreuzvalidierung geben und wie Sie sie in vordefinierten Funktionen und manuell implementieren können. Wenn Sie Fragen, Kommentare oder Ideen haben, können Sie mir gerne eine E-Mail senden.

Fuentes

James, Gareth, Daniela Witten, Trevor Hastie und Robert Tibshirani. 2013. Einführung in das statistische Lernen. New York: Springer.

Lukas Fick Lukas Fick

Top Articles
Latest Posts
Article information

Author: Annamae Dooley

Last Updated: 03/26/2023

Views: 5644

Rating: 4.4 / 5 (65 voted)

Reviews: 88% of readers found this page helpful

Author information

Name: Annamae Dooley

Birthday: 2001-07-26

Address: 9687 Tambra Meadow, Bradleyhaven, TN 53219

Phone: +9316045904039

Job: Future Coordinator

Hobby: Archery, Couponing, Poi, Kite flying, Knitting, Rappelling, Baseball

Introduction: My name is Annamae Dooley, I am a witty, quaint, lovely, clever, rich, sparkling, powerful person who loves writing and wants to share my knowledge and understanding with you.