Przypomnijmy, że wcześniej wprowadziliśmy pojęcie danych jako prostokątów. Dlatego w tym ćwiczeniu zadaniem konstruowania danych treningowych jest „prostokątizacja”. Musimy ukształtować zestaw danych e-mail, aby pasował do użytecznego zestawu funkcji. Funkcje, które wyodrębniamy z wiadomości e-mail, będą kolumnami naszych danych szkoleniowych, a każdy wiersz będzie unikatowymi wartościami z jednego e-maila wypełniającego prostokąt. Konceptualizacja danych w ten sposób jest bardzo przydatna, ponieważ musimy wziąć częściowo ustrukturyzowane dane tekstowe z wiadomości e-mail i przekształcić je w wysoce uporządkowany zestaw danych szkoleniowych, który możemy wykorzystać do uszeregowania przyszłych wiadomości e-mail.
parse.email <- funkcja (ścieżka) {
full.msg <- msg.full (ścieżka)
data <- get.date (full.msg)
od <- get.from (full.msg)
subj <- get.subject (full.msg)
msg <- get.msg (full.msg)
return (c (data, od, subj, msg, ścieżka))
}
Aby zbadać ten proces, będziemy pracować wstecz i rozpoczniemy od zbadania funkcji parse.email. Wywoła to szereg funkcji pomocniczych, które wyodrębnią odpowiednie dane z każdej wiadomości, a następnie uporządkują te elementy w jednym wektorze. Wektor utworzony za pomocą polecenia c (date, from, subj, msg, path) stanowi pojedynczy wiersz danych, który zapełni nasze dane treningowe. Proces przekształcania każdej wiadomości e-mail w te wektory wymaga jednak klasycznego hakowania tekstu. Uwzględniamy ciąg ścieżki jako ostatnią kolumnę, ponieważ będzie ona tworzyć łatwiejsze zamawianie danych podczas fazy testowania.
msg.full <- funkcja (ścieżka) {
con <- plik (ścieżka, open = “rt”, kodowanie = “latin1”)
msg <- readLines (con)
zamknij (con)
return (msg)
}
Po prostu otwieramy ścieżkę pliku połączenia i wczytujemy zawartość pliku do wektora znaków. Funkcja readLines będzie tworzyć wektor, którego elementami jest każda linia w pliku. Tutaj nie przetwarzamy danych na tym etapie, ponieważ musimy wyodrębnić różne elementy z wiadomości. Zamiast tego zwrócimy cały e-mail jako wektor znaków i napiszemy osobne funkcje do pracy nad tym wektorem, aby wyodrębnić niezbędne dane. Mając w ręku wektor wiadomości, musimy zacząć przedzierać się przez dane, aby wydobyć jak najwięcej użytecznych informacji z wiadomości e-mail – i zorganizować je w jednolity sposób – w celu zbudowania naszych danych szkoleniowych. Zaczniemy od stosunkowo łatwego zadania wyodrębnienia adresu nadawcy. Aby to zrobić – i wszystkie wyodrębnione dane w tej sekcji – musimy zidentyfikować wzorce tekstowe w wiadomościach e-mail, które identyfikują poszukiwane dane. Aby to zrobić, rzućmy okiem na kilka wiadomości e-mail.
Przykład 1. Przykłady e-maili „From” wzoru tekstu E-mail nr 1
………………………………………………..
X-Sender: fortean3@pop3.easynet.co.uk (Unverified)
Message-Id: <p05100300ba138e802c7d@[194.154.104.171]>
To: Yahoogroups Forteana <zzzzteana@yahoogroups.com>
From: Joe McNally <joe@flaneur.org.uk>
X-Yahoo-Profile: wolf_solent23
MIME-Version: 1.0
Mailing-List: list zzzzteana@yahoogroups.com; contact
forteana-owner@yahoogroups.com
………………………………………………..
Email #2
………………………………………………..
Return-Path: paul-bayes@svensson.org
Delivery-Date: Fri Sep 6 17:27:57 2002
From: paul-bayes@svensson.org (Paul Svensson)
Date: Fri, 6 Sep 2002 12:27:57 -0400 (EDT)
Subject: [Spambayes] Corpus Collection (Was: Re: Deployment)
In-Reply-To: <200209061431.g86EVM114413@pcp02138704pcs.reston01.va.comcast.net>
Message-ID: <Pine.LNX.4.44.0209061150430.6840-100000@familjen.svensson.org>
………………………………………………..
Po przejrzeniu kilku wiadomości e-mail możemy zaobserwować w tekście kluczowe wzorce identyfikujące adres e-mail nadawcy. Przykład 1 pokazuje dwa fragmenty wiadomości e-mail, które podkreślają te wzorce. Najpierw musimy zidentyfikować wiersz w każdej wiadomości zawierającej adres e-mail. Z przykładów możemy zobaczyć, że ten wiersz ma zawsze termin „From:”, który ponownie jest określony w protokole e-mail wspomnianym wcześniej. Tak więc wykorzystamy te informacje do przeszukania wektora znaków dla każdego e-maila w celu zidentyfikowania poprawny element. Jak widać z przykładu 1, w e-mailach występują różnice w sposobie pisania adresu e-mail. Wiersz ten zawsze zawiera nazwę nadawcy i adres e-mail nadawcy, ale czasami adres jest zamknięty w nawiasach kątowych (E-mail nr 1), podczas gdy w innych nie jest on ujęty w nawiasy (E-mail nr 2). Z tego powodu napiszemy funkcję get.from, która do wyodrębniania używa wyrażeń regularnych dane dla tej funkcji.
get.from <- function(msg.vec) {
from <- msg.vec[grepl(“From: “, msg.vec)]
from <- strsplit(from, ‘[“:<> ]’)[[1]]
from <- from[which(from !=”” & from !=” “)]
return(from[grepl(“@”, from)][1])
}
Jak już widzieliśmy, R ma wiele potężnych funkcji do pracy z wyrażeniami regularnymi. Funkcja grepl działa tak jak zwykła funkcja grep w celu dopasowania wzorców wyrażeń regularnych, ale „l” oznacza logikę. Zatem zamiast zwracać indeksy wektorowe, zwróci wektor o tej samej długości co msg.vec z wartościami logicznymi wskazującymi, gdzie wzorzec został dopasowany w wektorze znaków. Po pierwszym wierszu tej funkcji zmienna from jest wektorem znaków z jednym elementem: „From:”
Teraz, gdy mamy prawidłową linię, musimy wyodrębnić sam adres. W tym celu użyjemy funkcji strsplit, która podzieli element znakowy na listę według danego wzorca wyrażenia regularnego. Aby poprawnie wyodrębnić adresy, musimy uwzględnić różnice we wzorach tekstowych zaobserwowane w przykładzie 1. Aby to zrobić, tworzymy zestaw znaków dla naszego wzoru za pomocą nawiasów kwadratowych. Tutaj znakami, na które chcemy podzielić tekst, są dwukropki, nawiasy trójkątne i pusty znak. Ten wzorzec zawsze umieszcza adres jako pierwszy element na liście, więc możemy go pobrać z listy za pomocą. Jednak ze względu na zmienność wzoru doda również puste elementy do tego wektora. Aby zwrócić tylko sam adres e-mail, zignorujemy te puste elementy, a następnie wyszukamy pozostały element zawierający symbol „@” i zwrócimy go. Przetwarzaliśmy teraz jedną czwartą danych potrzebnych do wygenerowania naszych danych treningowych.
get.msg <- function(msg.vec) {
msg <- msg.vec[seq(which(msg.vec == “”)[1] + 1, length(msg.vec), 1)]
return(paste(msg, collapse=”\n”))
}
Wyodrębnienie dwóch kolejnych funkcji, tematu i treści wiadomości, jest stosunkowo proste. Wcześniej musieliśmy wyodrębnić treść wiadomości, aby określić ilościowo warunki w spamie i wiadomościach e-mail ham. Dlatego funkcja get.msg po prostu replikuje wzorzec użyty tutaj do wykonania tego samego zadania. Pamiętaj, że treść wiadomości zawsze pojawia się po pierwszym pustym wierszu w wiadomości e-mail. Po prostu szukamy pierwszego pustego elementu w msg.vec i zwracamy wszystkie elementy po tym. Aby uprościć proces eksploracji tekstu, zwijamy te wektory w wektor jednoznakowy z funkcją wklejania i zwracamy to.
get.subject <- function(msg.vec) {
subj <- msg.vec[grepl(“Subject: “, msg.vec)]
if(length(subj) > 0) {
return(strsplit(subj, “Subject: “)[[1]][2])
}
else {
return(“”)
}
}
Wyodrębnianie tematu wiadomości e-mail przypomina wyodrębnianie adresu nadawcy, ale w rzeczywistości jest nieco prostsze. Dzięki funkcji get.subject ponownie użyjemy funkcji grepl, aby wyszukać wzorzec „Temat:” w każdym e-mailu i znaleźć wiersz w wiadomości zawierającej ten temat. Jest jednak pewien haczyk: jak się okazuje, nie każda wiadomość w zbiorze danych faktycznie ma temat. W związku z tym stosowane przez nas dopasowanie wzorca wysadzi te przypadki na krawędziach. Aby tego uniknąć, po prostu przetestujemy, czy nasze wezwanie do grepl rzeczywiście coś zwróciło. Aby to sprawdzić, sprawdzamy, czy długość subj wynosi więcej niż zero. Jeśli tak, dzielimy linię na podstawie naszego wzoru i zwracamy drugi element. Jeśli nie, zwracamy pusty znak. Domyślnie w R, gdy dopasowane funkcje, takie jak grepl, nie dopasowują się, zwracane są wartości specjalne, takie jak liczba całkowita (0) lub znak (0). Wartości te mają zerową długość, więc ten rodzaj sprawdzania jest zawsze dobrym pomysłem podczas uruchamiania funkcji na wielu nieporządnych danych. Wyodrębniliśmy trzy czwarte naszych funkcji, ale to ostatni element – data i godzina otrzymania wiadomości – sprawi, że najbardziej ucierpiemy. Praca z tym polem będzie trudna z dwóch powodów. Po pierwsze, radzenie sobie z datami jest prawie zawsze bolesną perspektywą, ponieważ różne języki programowania często mają nieco inne sposoby myślenia o czasie, aw tym przypadku R nie różni się. W końcu będziemy chcieli przekonwertować ciągi dat na obiekty daty POSIX, aby uporządkować dane chronologicznie. Ale aby to zrobić, potrzebujemy wspólnej reprezentacji dat, która prowadzi bezpośrednio do drugiego powodu naszego cierpienia: w korpusie publicznym SpamAssassin występują znaczne różnice w sposobie reprezentowania dat i godzin otrzymywania wiadomości. Przykład 2 ilustruje kilka przykładów tej odmiany
Przykład 2. Przykłady daty i godziny wiadomości e-mail zmienności wzoru tekstu
Email #1
………………………………………………..
Date: Thu, 22 Aug 2002 18:26:25 +0700
Date: Wed, 21 Aug 2002 10:54:46 -0500
From: Chris Garrigues lt;cwg-dated-1030377287.06fa6d@DeepEddy.Comgt;
Message-ID: lt;1029945287.4797.TMDA@deepeddy.vircio.comgt;
………………………………………………..
Email #2
………………………………………………..
List-Unsubscribe: lt;https://example.sourceforge.net/lists/listinfo/sitescooper-talkgt;,
lt;mailto:sitescooper-talk-request@lists.sourceforge.net?subject=unsubscribegt;
List-Archive: lt;http://www.geocrawler.com/redir-sf.php3?list=sitescooper-talkgt;
X-Original-Date: 30 Aug 2002 08:50:38 -0500
Date: 30 Aug 2002 08:50:38 -0500
………………………………………………..
Email #3
………………………………………………..
Date: Wed, 04 Dec 2002 11:36:32 GMT
Subject: [zzzzteana] Re: Bomb Ikea
Reply-To: zzzzteana@yahoogroups.com
Content-Type: text/plain; charset=US-ASCII
………………………………………………..
Email #4
………………………………………………..
Path: not-for-mail
From: Michael Hudson lt;mwh@python.netgt;
Date: 04 Dec 2002 11:49:23 +0000
Message-Id: lt;2madyyyyqa0s.fsf@starship.python.netgt;
………………………………………………..
Jak widać, przy wydobywaniu informacji o dacie i godzinie z każdego e-maila musimy pamiętać o wielu rzeczach. Pierwszą rzeczą, na którą należy zwrócić uwagę w przykładach w przykładzie 4-2 jest to, że dane, które chcemy wyodrębnić, są zawsze identyfikowane przez „Data:”; istnieje jednak wiele pułapek w stosowaniu tego wzorca, o których musimy pamiętać. Jak pokazano w e-mailu nr 1 z przykładu 4-2, czasami będzie wiele linii pasujących do tego wzorca. Podobnie, e-mail nr 2 pokazuje, że niektóre wiersze mogą być częściowymi dopasowaniami i w obu przypadkach dane w tych wierszach mogą być sprzeczne – tak jak w e-mailu nr 1. Następnie możemy zaobserwować nawet w tych czterech przykładach, że daty i godziny nie są przechowywane w jednolity sposób we wszystkich wiadomościach e-mail. We wszystkich wiadomościach e-mail występują zewnętrzne przesunięcia GMT i inne rodzaje informacji na etykietach. Wreszcie format daty i godziny w e-mailu nr 4 różni się całkowicie od poprzednich dwóch. Wszystkie te informacje będą miały decydujące znaczenie dla przekształcenia danych w jednolitą i wykonalną formę. Na razie jednak musimy skupić się tylko na wyodrębnieniu informacji o dacie i godzinie bez dodatkowych informacji o przesunięciu, definiując funkcję get.date. Po uzyskaniu wszystkich ciągów daty / godziny będziemy musieli poradzić sobie z konwersją sprzecznych formatów daty / godziny na jednolity obiekt POSIX, ale nie będzie to obsługiwane przez funkcję get.date.
get.date <- function(msg.vec) {
date.grep <- grepl(“^Date: “, msg.vec)
date.grepl <- which(date.grep == TRUE)
date <- msg.vec[date.grep[1]]
date <- strsplit(date, “\\+|\\-|: “)[[1]][2]
date <- gsub(“^\\s+|\\s+$”, “”, date)
return(strtrim(date, 25))
}
Jak wspomnieliśmy, wiele e-maili ma wiele pełnych lub częściowych dopasowań do wzorca „Data:”. Zwróć jednak uwagę na wiadomości e-mail nr 1 i nr 2 w przykładzie 2, że tylko jeden wiersz wiadomości e-mail zawiera „Data:” na początku ciągu. W e-mailu nr 1 znajduje się kilka pustych znaków poprzedzających ten wzór, a w e-mailu nr 2 wzór jest częściowo dopasowany do „X-Original-Date:”. Możemy zmusić wyrażenie regularne do dopasowania tylko ciągów, które na początku łańcucha mają „Data:”, używając operatora daszka z „^ Data:”. Teraz grepl zwróci PRAWDA tylko wtedy, gdy ten wzorzec rozpoczyna element wektora wiadomości. Następnie chcemy zwrócić pierwszy element w msg.vec, który pasuje do tego wzorca. Być może uda nam się po prostu zwrócić element w msg.vec, który pasuje do naszego wzorca w grepl, ale co, jeśli wiadomość e-mail zawiera wiersz rozpoczynający się „Data:”? Jeśli taki przypadek krawędzi wystąpi, wiemy, że pierwszy element, który pasował do naszego wzorca, będzie pochodził z nagłówka wiadomości, ponieważ informacje nagłówka zawsze pojawiają się przed treścią wiadomości. Aby zapobiec temu problemowi, zawsze zwracamy pierwszy pasujący element. Teraz musimy przetworzyć ten wiersz tekstu, aby zwrócić ciąg znaków, który ostatecznie można przekonwertować na obiekt POSIX w R. Zauważyliśmy już, że istnieją obce informacje i że daty i godziny nie są przechowywane w jednolitym formacie . Aby wyodrębnić informacje o dacie i godzinie, podzielimy ciąg znaków na znaki, aby oznaczyć informacje obce. W tym przypadku będzie to dwukropek, znak plus lub znak minus. W większości przypadków pozostawi nam to tylko informacje o dacie i godzinie oraz niektóre końcowe puste znaki. Użycie funkcji gsub w następnym wierszu zastąpi wszelkie początkowe lub końcowe białe spacje w ciągu znaków. Wreszcie, aby poradzić sobie z rodzajem danych, które obserwujemy w e-mailu nr 3 w przykładzie 2, po prostu usuniemy dowolne znaki po limicie 25 znaków. Standardowy ciąg danych / czasu ma 25 znaków, więc wiemy, że cokolwiek z tego jest obcego.
easyham.docs <- dir(easyham.path)
easyham.docs <- easyham.docs[which(easyham.docs != “cmds”)]
easyham.parse <- lapply(easyham.docs, function(p) parse.email(paste(easyham.path,
p, sep=””)))
ehparse.matrix <- do.call(rbind, easyham.parse)
allparse.df <- data.frame(ehparse.matrix, stringsAsFactors=FALSE)
names(allparse.df) <- c(“Date”, “From.EMail”, “Subject”, “Message”, “Path”)
Gratulacje! Z powodzeniem cierpisz, przekształcając ten amorficzny zestaw e-maili w ustrukturyzowany prostokąt odpowiedni do szkolenia naszego algorytmu rankingu. Teraz wszystko, co musimy zrobić, to nacisnąć przełącznik. Podobnie jak wcześniej, utworzymy wektor ze wszystkimi plikami „easy ham”, usuniemy dodatkowy plik „cmds” z wektora, a następnie użyjemy funkcji lapply, aby zastosować funkcję parse.email do każdego plik e-mail. Ponieważ wskazujemy pliki w katalogu danych w poprzednich tekstach, musimy również upewnić się, że ścieżka względna do tych plików jest połączona za pomocą funkcji wklejania i naszej ścieżki easyham.path wewnątrz wywołania lapply. Następnie musimy przekonwertować listę wektorów zwracanych przez lapply na macierz – czyli nasz prostokąt danych. Tak jak poprzednio, do utworzenia obiektu ehparse.matrix użyjemy funkcji do.call z rbind. Następnie przekonwertujemy to na ramkę danych wektorów znaków, a następnie ustawimy nazwy kolumn na c („Data”, „Od.EMail”, „Temat”, „Wiadomość”, „Ścieżka”). Aby sprawdzić wyniki, użyj head (allparse.df) do sprawdzenia pierwszych kilku wierszy ramki danych. Aby zaoszczędzić miejsce, nie będziemy tego kopiować tutaj, ale zalecamy to. Jednak zanim przystąpimy do tworzenia schematu ważenia na podstawie tych danych, pozostaje jeszcze trochę porządku.
date.converter <- funkcja (daty, wzorzec1, wzorzec2) {
pattern1.convert <- strptime (daty, wzór1)
pattern2.convert <- strptime (daty, wzór2)
pattern1.convert [is.na (pattern1.convert)] <-
pattern2.convert [is.na (pattern1.convert)]
return (pattern1.convert)
}
wzorzec1 <- “% a,% d% b% Y% H:% M:% S”
wzorzec2 <- “% d% b% Y% H:% M:% S”
allparse.df $ Data <- data.converter (allparse.df $ Data, wzór1, wzór2)
Jak wspomnieliśmy, naszą pierwszą próbą wyodrębnienia dat była po prostu izolacja tekstu. Teraz musimy wziąć ten tekst i przekonwertować go na obiekty POSIX, które można porównać logicznie. Jest to konieczne, ponieważ musimy sortować wiadomości e-mail chronologicznie. Przypomnij sobie, że przejście przez całe ćwiczenie jest pojęciem czasu i tego, jak różnice czasowe między obserwowanymi cechami można wykorzystać do wnioskowania o znaczeniu. Przedstawienie postaci dat i godzin nie wystarczy. Jak widzieliśmy w przykładzie 2, istnieją dwie odmiany formatu daty. Z tych przykładów e-mail nr 3 ma ciąg daty / godziny w formacie „Środa, 04 grudnia 2002 11:36:32”, podczas gdy e-mail nr 4 ma format „04 grudnia 2002 11:49:23”. Aby przekonwertować te dwa ciągi na formaty POSIX, będziemy musieli użyć funkcji strptime, ale przekażemy jej dwa różne formaty daty / godziny, aby dokonać konwersji. Każdy element tych ciągów pasuje do określonego elementu formatu POSIX, więc będziemy musieli określić ciągi konwersji pasujące do tych wariantów.
R używa standardowych ciągów formatu daty / godziny POSIX do wykonania tych konwersji. Istnieje wiele opcji dla tych ciągów i zalecamy przejrzenie dokumentacji w funkcji strptime za pomocą polecenia? Strptime, aby zobaczyć wszystkie opcje. Będziemy tutaj używać tylko kilku wybranych, ale ich głębsze zrozumienie będzie bardzo przydatne w pracy z datami i godzinami w R. Musimy przekonwertować ciągi w kolumnie Data allparse.df na dwa różne formaty POSIX osobno , a następnie ponownie połącz je z powrotem w ramkę danych, aby zakończyć konwersję. Aby to osiągnąć, zdefiniujemy funkcję date.converter, aby pobrać dwa różne wzorce POSIX i wektor znaków ciągów daty. Gdy wzorzec przekazany do strptime nie pasuje do przekazanego mu ciągu, domyślnym zachowaniem jest zwrócenie NA. Możemy to wykorzystać do rekombinacji konwertowanych wektorów znaków, zastępując elementy NA z pierwszej konwersji na te z drugiego. Ponieważ wiemy, że w danych występują tylko dwa wzorce, wynikiem będzie pojedynczy wektor ze wszystkimi ciągami dat konwertowanymi na obiekty POSIX.
allparse.df $ Temat <- tolower (allparse.df $ Temat)
allparse.df $ From.EMail <- tolower (allparse.df $ From.EMail)
Priority.df <- allparse.df [with (allparse.df, order (Date)),]
Priority.train <- Priority.df [1: (okrągły (nrow (Prior.df) / 2)),]
Ostatnim etapem czyszczenia jest konwersja wektorów znaków w kolumnach Temat i Od wiadomości e-mail na wszystkie małe litery. Ponownie odbywa się to w celu zapewnienia, że wszystkie wprowadzane dane są tak jednolite, jak to możliwe, zanim przejdziemy do fazy szkolenia. Następnie sortujemy dane chronologicznie za pomocą kombinacji poleceń with i order w R. (R ma szczególnie nieintuicyjny sposób sortowania, ale ten skrót jest czymś, co często robisz, więc najlepiej się go zaznajomić.) zwraca wektor indeksów elementów w rosnącym porządku chronologicznym. Następnie, aby uporządkować ramkę danych według tych indeksów, musimy odwołać się do elementów allparse.df w tej kolejności i dodać ostatni przecinek przed zamknięciem nawiasu kwadratowego, aby wszystkie kolumny zostały posortowane w ten sposób. Wreszcie, pierwszą połowę chronologicznie posortowanej ramki danych przechowujemy jako priorytet ity.train. Dane w tej ramce danych zostaną wykorzystane do szkolenia naszego rankingu. Później wykorzystamy drugą połowę priorytetu.df do przetestowania rankingu. Po w pełni sformułowanych danych jesteśmy gotowi rozpocząć projektowanie naszego rankingu. Biorąc pod uwagę nasz zestaw funkcji, jednym ze sposobów jest zdefiniowanie wag dla każdej obserwowanej cechy w danych treningowych.