Helpex - Trao đổi & giúp đỡ Đăng nhập

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Trong bài đăng này, chúng tôi sẽ phát triển một ứng dụng cho mục đích phát hiện email spam. Thuật toán sẽ được sử dụng là hồi quy logistic  với triển khai từ SPARK MLib . Không có kiến ​​thức sâu về lĩnh vực này là bắt buộc, vì các chủ đề được mô tả từ mức độ cao của một quan điểm càng tốt. Mã làm việc đầy đủ  sẽ được cung cấp cùng với một ứng dụng đang chạy để thử nghiệm thêm về lựa chọn email của bạn.

Hồi quy logistic

Hồi quy logistic là một thuật toán được sử dụng cho các vấn đề phân loại. Trong các vấn đề phân loại, chúng tôi được cung cấp rất nhiều dữ liệu được dán nhãn (spam và không phải spam) và khi một ví dụ mới sắp xuất hiện, chúng tôi muốn biết nó thuộc danh mục nào. Vì nó là một thuật toán học máy, hồi quy logistic được đào tạo với dữ liệu được dán nhãn và dựa trên đào tạo, nó đưa ra dự đoán về các ví dụ mới sắp tới.

Các ứng dụng

Nói chung, khi có sẵn nhiều dữ liệu và chúng tôi cần phát hiện ví dụ thuộc loại nào, hồi quy logistic có thể được sử dụng (ngay cả khi kết quả không luôn luôn thỏa đáng).

Chăm sóc sức khỏe

Hồi quy logistic có thể được sử dụng khi, ví dụ, phân tích hàng triệu tình trạng sức khỏe của bệnh nhân để dự đoán liệu bệnh nhân có bị nhồi máu cơ tim hay không. Logic tương tự có thể được áp dụng để dự đoán nếu một bệnh nhân sẽ bị ung thư đặc biệt, bị ảnh hưởng bởi trầm cảm, v.v. Trong ứng dụng này, chúng tôi có một lượng dữ liệu đáng kể, do đó hồi quy logistic thường đưa ra gợi ý tốt.

Phân loại hình ảnh

Dựa trên màu sắc mật độ hình ảnh, chúng ta có thể phân loại xem, giả sử, một hình ảnh chứa một con người hoặc chứa một chiếc xe hơi. Ngoài ra, vì đó là một vấn đề phân loại, chúng tôi cũng có thể sử dụng hồi quy logistic để phát hiện nếu một hình ảnh có các ký tự hoặc thậm chí để phát hiện chữ viết tay.

Phân loại thư rác và email

Một trong những ứng dụng phổ biến nhất của hồi quy logistic là phân loại thư rác. Trong ứng dụng này, thuật toán xác định xem email hoặc tin nhắn đến có phải là thư rác hay không. Khi một thuật toán không được cá nhân hóa được xây dựng, rất nhiều dữ liệu là cần thiết. Các bộ lọc được cá nhân hóa thường hoạt động tốt hơn vì trình phân loại thư rác phụ thuộc vào một số mức độ nhất định đối với sở thích và nền tảng của người đó.

Làm thế nào nó hoạt động

Chúng tôi có rất nhiều ví dụ được gắn nhãn và muốn đào tạo thuật toán của mình đủ thông minh để nói liệu các ví dụ mới là một phần của một hoặc loại khác. Để đơn giản hóa, trước tiên chúng ta sẽ chỉ tham khảo phân loại nhị phân (1 hoặc 0). Thuật toán cũng dễ dàng chia tỷ lệ thành đa phân loại.

Cái nhìn sâu sắc

Thông thường, chúng tôi có dữ liệu đa chiều  hoặc dữ liệu với nhiều tính năng . Mỗi tính năng này bằng cách nào đó đóng góp vào quyết định cuối cùng về loại ví dụ mới thuộc về loại nào. Ví dụ, trong một vấn đề phân loại ung thư, chúng ta có thể có các tính năng như tuổi tác, hút thuốc hay không, cân nặng, chiều cao, gen gia đình, và vân vân Mỗi tính năng này đóng góp theo cách quyết định loại cuối cùng. Các tính năng không đóng góp như nhau nhưng có tác động khác nhau trong việc xác định trạng thái cuối cùng. Ví dụ, trọng lượng có tác động thấp hơn bộ gen gia đình trong dự đoán ung thư. Trong hồi quy logistic, đó chính xác là những gì chúng tôi đang cố gắng tìm hiểu: trọng số / tác động của các tính năng của dữ liệu của chúng tôi. Khi chúng tôi có nhiều ví dụ về dữ liệu, chúng tôi có thể xác định trọng số của từng tính năng và khi các ví dụ mới xuất hiện, chúng tôi sử dụng các trọng số để xem ví dụ được phân loại như thế nào. Trong ví dụ dự đoán ung thư, chúng ta có thể viết như sau:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Chính thức hơn:

n = số ví dụ

k = số tính năng

θ j  = trọng lượng cho tính năng j

X j i  = ví dụ thứ i X với tính năng j

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Đại diện người mẫu

Để sắp xếp dữ liệu của chúng tôi thành các danh mục, chúng tôi cần một hàm (giả thuyết), dựa trên các ví dụ, giá trị và tính năng, có thể đưa dữ liệu vào một trong hai loại. Hàm chúng ta sử dụng cho hàm này được gọi là hàm Sigmoid và về mặt đồ họa, nó trông giống như dưới đây:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Như chúng ta có thể thấy, khi các giá trị trên trục X là dương, các giá trị hàm Sigmoid có xu hướng đi về 1 và khi các giá trị trên trục X là âm, nó có xu hướng về 0. Về cơ bản, chúng ta có một mô hình để biểu diễn hai loại và theo toán học, hàm này trông như dưới đây:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Z là chức năng được giải thích ở trên trong "Thông tin chi tiết."

Để nhận các giá trị rời rạc (1 hoặc 0), chúng ta có thể nói rằng khi giá trị hàm (trục Y) lớn hơn 0,5, chúng ta phân loại nó thành 1 và khi giá trị hàm (trục Y) nhỏ hơn 0,5, chúng ta phân loại nó thành 0, như được mô tả dưới đây:

  • Y> 0,5 = 1 (thư rác / ung thư)

  • Y <0,5 = 0 (không phải thư rác / không phải ung thư)

  • Z> 0 = 1 (thư rác / ung thư)

  • Z <0 = 0 (không phải thư rác / không phải ung thư)

Chức năng ước lượng

Chúng tôi không muốn tìm bất kỳ trọng lượng nào mà thay vào đó là các trọng số tốt nhất chúng tôi có thể có với dữ liệu thực tế. Để tìm các trọng số tốt nhất, chúng ta cần một hàm khác tính toán mức độ tốt của một giải pháp cho các trọng số cụ thể mà chúng tôi đã tìm thấy. Với chức năng này, chúng ta có thể so sánh các giải pháp khác nhau với các trọng lượng khác nhau và tìm ra giải pháp tốt nhất. Hàm này được gọi là hàm chi phí. Nó so sánh giá trị hàm giả thuyết (Sigmoid) với giá trị dữ liệu thực. Vì dữ liệu chúng tôi sử dụng cho đào tạo được gắn nhãn (thư rác hoặc không phải thư rác), chúng tôi so sánh dự đoán giả thuyết (Sigmoid) với giá trị thực tế mà chúng tôi biết chắc chắn. Chúng tôi muốn sự khác biệt giữa giả thuyết và giá trị thực càng nhỏ càng tốt; lý tưởng nhất, chúng tôi muốn hàm chi phí bằng không. Chính thức hơn, hàm chi phí được định nghĩa là:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

... Trong đó y i là giá trị / danh mục thực, như thư rác / không phải thư rác hoặc 1/0 và h (x) là giả thuyết. 

Về cơ bản, phương trình này tính toán mức độ tốt (trung bình) dự đoán của chúng tôi được so sánh với dữ liệu được dán nhãn thực ( y ). Vì chúng ta có hai trường hợp (1 và 0), nên chúng ta có hai Hs (giả thuyết): h1  và h0 . Chúng tôi áp dụng  nhật ký  cho giả thuyết để hàm này lồi và an toàn hơn khi tìm mức tối thiểu toàn cầu.

Hãy xem h1 , đó là giả thuyết liên quan đến hàm chi phí cho loại 1. 

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Chúng tôi đã áp dụng nhật ký cho giả thuyết của mình thay vì sử dụng trực tiếp vì chúng tôi muốn đạt được mối quan hệ sao cho khi giả thuyết gần với một, hàm chi phí sẽ về không. Hãy nhớ rằng chúng tôi muốn hàm chi phí của chúng tôi bằng 0 để không có sự khác biệt giữa dự đoán giả thuyết và dữ liệu được dán nhãn. Nếu giả thuyết sẽ dự đoán 0, hàm chi phí của chúng tôi sẽ tăng lên, vì vậy chúng tôi biết rằng đây không phải là một ví dụ thuộc loại 1; và nếu giả thuyết sẽ dự đoán 1, hàm chi phí sẽ về 0, báo hiệu rằng ví dụ này thuộc về loại 1.

Hãy xem h2 , đó là giả thuyết liên quan đến hàm chi phí cho loại 0.

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Trong trường hợp này, chúng tôi đã áp dụng nhật ký  một lần nữa, nhưng theo cách làm cho hàm chi phí trở về 0 khi giả thuyết cũng sẽ dự đoán bằng không. Nếu giả thuyết sẽ dự đoán 1, hàm chi phí của chúng tôi sẽ tăng lên, vì vậy chúng tôi biết rằng đây không phải là một ví dụ thuộc loại 0; và nếu giả thuyết sẽ dự đoán 0, hàm chi phí sẽ về 0, báo hiệu rằng ví dụ này thuộc về loại 0.

Bây giờ, chúng ta có hai hàm chi phí và chúng ta cần kết hợp chúng thành một. Sau này, phương trình trở nên hơi lộn xộn, nhưng về nguyên tắc, nó chỉ là sự hợp nhất của hai hàm chi phí mà chúng tôi đã giải thích ở trên:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Lưu ý rằng thuật ngữ đầu tiên là hàm chi phí cho h1 và thuật ngữ thứ hai là hàm chi phí cho h0. Vì vậy, nếu = 1, thì thuật ngữ thứ hai bị loại bỏ và nếu = 0, thì thuật ngữ đầu tiên bị loại bỏ.  

Giảm thiểu chức năng chi phí

Như chúng ta đã thấy ở trên, chúng ta muốn hàm chi phí của mình bằng 0 để dự đoán của chúng ta càng gần với giá trị thực (được dán nhãn). May mắn thay, đã có một thuật toán để giảm thiểu hàm chi phí: descent.Once dốc chúng ta có hàm chi phí (mà về cơ bản so sánh giả thuyết của chúng tôi để giá trị thực), chúng ta có thể đặt trọng lượng của chúng tôi ( θ ) để giảm hàm chi phí càng nhiều càng tốt Đầu tiên, chúng tôi nhận các giá trị ngẫu nhiên của θ chỉ để có một số giá trị. Sau đó, chúng tôi tính toán hàm chi phí. Tùy thuộc vào kết quả, chúng ta có thể giảm hoặc tăng  θ giá trị của mình để hàm chi phí được tối ưu hóa về 0. Chúng tôi lặp lại điều này cho đến khi hàm chi phí gần như bằng không (0,0001) hoặc không cải thiện nhiều từ lần lặp đến lần lặp.

Gradient gốc là làm chính xác điều này trong nguyên tắc; nó chỉ là một  dẫn xuất của hàm chi phí để quyết định nên giảm hay tăng  θ giá trị. Nó cũng sử dụng một hệ số α để xác định có bao nhiêu thay đổi  θ giá trị. Thay đổi  θ giá trị quá nhiều (lớn α ) có thể làm cho gradient descent thất bại trong việc tối ưu hóa các chức năng chi phí bằng không, vì một sự gia tăng lớn có thể vượt qua những giá trị thực hoặc đi xa khỏi giá trị truy nã. Trong khi có một sự thay đổi nhỏ của  θ  (nhỏ α) có nghĩa là chúng tôi an toàn, thuật toán cần rất nhiều thời gian để đi đến giá trị tối thiểu của hàm chi phí (gần như bằng 0) vì chúng tôi đang tiến triển quá chậm về giá trị mong muốn hoặc giá trị thực (để giải thích trực quan hơn, xem  tại đây ). Chính thức hơn, chúng tôi có:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Thuật ngữ bên phải là đạo hàm của hàm chi phí (chỉ thay đổi theo bội số của X cho tính năng k ). Vì dữ liệu của chúng tôi là đa chiều (k tính năng) , chúng tôi thực hiện điều này cho từng trọng lượng tính năng ( θk ). 

Thực hiện thuật toán

Hãy xem việc chuẩn bị dữ liệu, chuyển đổi dữ liệu, thực hiện và kết quả.

Chuẩn bị dữ liệu

Trước khi thực hiện dữ liệu, chúng ta cần thực hiện một số dữ liệu để chuẩn bị trước để dọn sạch thông tin không hữu ích. Ý tưởng chính cho việc xử lý lại dữ liệu là từ bài tập Coursera này . Chúng tôi làm như sau:

  • -Vỏ thấp : Toàn bộ email được chuyển thành chữ thường nên vốn được bỏ qua (tức là, chỉ được xử lý giống như Nêu rõ).
  • Tước HTML : Tất cả các thẻ HTML được xóa khỏi email. Nhiều email thường đi kèm với định dạng HTML; chúng tôi xóa tất cả các thẻ HTML để chỉ còn lại nội dung.
  • Bình thường hóa các URL : Tất cả các URL được thay thế bằng văn bản Sê-ri XURLX.
  •  Bình thường hóa địa chỉ email : Tất cả các địa chỉ email được thay thế bằng văn bản XEMAILAILX.
  • Bình thường hóa số : Tất cả các số được thay thế bằng văn bản XN XNBERBERX.
  • Bình thường hóa đô la : Tất cả các ký hiệu đô la ($) được thay thế bằng văn bản XMONEYXX.
  • Từ gốc : Từ được giảm xuống dạng gốc của chúng. Ví dụ, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá, giảm giá và giảm giá , Tất cả các khu vực khác nhau
  • Loại bỏ các từ không: Không xóa các từ và dấu câu. Tất cả các khoảng trắng (ví dụ: tab, dòng mới, dấu cách) đều được cắt theo một ký tự khoảng trắng.

Việc thực thi mã sẽ như thế này:

private List<string> filesToWords(String fileName) throws Exception {
    URI uri = this.getClass().getResource("/" + fileName).toURI();
    Path start = getPath(uri);
    List<string> collect = Files.walk(start).parallel()
            .filter(Files::isRegularFile)
            .flatMap(file -> {
                try {

                    return Stream.of(new String(Files.readAllBytes(file)).toLowerCase());
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }).collect(Collectors.toList());


    return collect.stream().parallel().flatMap(e -> tokenizeIntoWords(prepareEmail(e)).stream()).collect(Collectors.toList());
}</string></string>
private String prepareEmail(String email) {
    int beginIndex = email.indexOf("\n\n");
    String withoutHeader = email;
    if (beginIndex > 0) {
        withoutHeader = email.substring(beginIndex, email.length());
    }
    String tagsRemoved = withoutHeader.replaceAll("<[^<>]+>", "");
    String numberedReplaced = tagsRemoved.replaceAll("[0-9]+", "XNUMBERX ");
    String urlReplaced = numberedReplaced.replaceAll("(http|https)://[^\\s]*", "XURLX ");
    String emailReplaced = urlReplaced.replaceAll("[^\\s]+@[^\\s]+", "XEMAILX ");
    String dollarReplaced = emailReplaced.replaceAll("[$]+", "XMONEYX ");
    return dollarReplaced;
}

private List<string> tokenizeIntoWords(String dollarReplaced) {
    String delim = "[' @$/#.-:&*+=[]?!(){},''\\\">_<;%'\t\n\r\f";
    StringTokenizer stringTokenizer = new StringTokenizer(dollarReplaced, delim);
    List<string> wordsList = new ArrayList<>();
    while (stringTokenizer.hasMoreElements()) {
        String word = (String) stringTokenizer.nextElement();
        String nonAlphaNumericRemoved = word.replaceAll("[^a-zA-Z0-9]", "");
        PorterStemmer stemmer = new PorterStemmer();
        stemmer.setCurrent(nonAlphaNumericRemoved);
        stemmer.stem();
        String stemmed = stemmer.getCurrent();
        wordsList.add(stemmed);
    }
    return wordsList;
}</string></string>

Chuyển đổi dữ liệu

Sau khi các email được chuẩn bị, chúng ta cần chuyển đổi dữ liệu thành một cấu trúc mà thuật toán hiểu được, như ma trận và các tính năng.

Bước đầu tiên là xây dựng một "từ vựng spam" bằng cách đọc tất cả các từ của email spam và đếm chúng. Ví dụ: chúng tôi đếm số lần "giao dịch", "XMONEYX," tài chính "," thắng "và" miễn phí "được sử dụng. Sau đó, hãy chọn 10 ( featureSize) từ thường xuyên nhất. có kích thước 10 ( featureSize) trong đó khóa là từ và giá trị là chỉ số từ 0 đến 9,999. Điều này sẽ phục vụ như một tài liệu tham khảo cho các từ spam có thể. Xem mã dưới đây:

public Map<string, integer=""> createVocabulary() throws Exception {
    String first = "allInOneSpamBase/spam";
    String second = "allInOneSpamBase/spam_2";
    List<string> collect1 = filesToWords(first);
    List<string> collect2 = filesToWords(second);

    ArrayList<string> all = new ArrayList<>(collect1);
    all.addAll(collect2);
    HashMap<string, integer=""> countWords = countWords(all);

    List<><string, integer="">> sortedVocabulary = countWords.entrySet().stream().parallel().sorted((o1, o2) -> o2.getValue().compareTo(o1.getValue())).collect(Collectors.toList());
    final int[] index = {0};
    return sortedVocabulary.stream().limit(featureSIze).collect(Collectors.toMap(e -> e.getKey(), e -> index[0]++));
}</string,></string,></string></string></string></string,>
HashMap<string, integer=""> countWords(List<string> all) {
    HashMap<string, integer=""> countWords = new HashMap<>();
    for (String s : all) {
        if (countWords.get(s) == null) {
            countWords.put(s, 1);
        } else {
            countWords.put(s, countWords.get(s) + 1);
        }
    }
    return countWords;
}</string,></string></string,>

Bước tiếp theo là đếm tần số từ cho những từ này trong cả email spam và không spam. Sau đó, chúng tôi tra cứu từng từ trong từ vựng spam để xem nó có ở đó không. Nếu đó là (có nghĩa là email có một từ spam có thể), chúng tôi đặt từ này trong cùng một chỉ mục có trong bản đồ từ vựng spam và là giá trị, chúng tôi đặt tần suất. Cuối cùng, chúng tôi xây dựng ma trận N x10.000 trong đó N là số lượng email được xem xét và 10.000 là vectơ chứa tần suất của các từ bản đồ từ vựng spam trong các email (nếu không tìm thấy từ spam trong email, chúng tôi sẽ đặt 0).

Ví dụ: giả sử chúng ta có từ vựng spam như dưới đây:

  • aa

  • làm sao

  • tỷ

  • bất cứ lúc nào

  • biết

  • số không

  • khóa kéo

Và cũng là một email như dưới đây trong hình thức chuẩn bị trước:

Dù sao cũng biết chi phí để lưu trữ một cổng thông tin web là bao nhiêu tùy thuộc vào mức độ khách truy cập mà bạn mong đợi có thể từ một số ít hơn một tháng đến một cuộc đảo chính của búp bê, bạn nên kiểm tra XURLX hoặc perhap amazon ecnumb nếu bạn chạy lớn tự hủy đăng ký khỏi danh sách thư gửi email đến XEMAILX

Sau khi chuyển đổi, chúng ta sẽ có:

0 2 0 1 1 1 0 0

Vậy ta có 0 aa, 2 how, 0 abil, 1 anyon, 1 know, 0 zero, 0  zip. Đây là ma trận a1x7 vì chúng tôi có một từ vựng và thư rác gồm 7 từ. Mã này trông như dưới đây:

private Vector transformToFeatureVector(Email email, Map<string, integer=""> vocabulary) {
    List<string> words = email.getWords();
    HashMap<string, integer=""> countWords = prepareData.countWords(words);
    double[] features = new double[featureSIze];//featureSIze==10.000
    for (Map.Entry<string, integer=""> word : countWords.entrySet()) {
        Integer index = vocabulary.get(word.getKey());//see if it is in //spam vocabulary 
        if (index != null) {
//put frequency the same index as the vocabulary
            features[index] = word.getValue();
        }
    }
    return Vectors.dense(features);
}</string,></string,></string></string,>

Thực thi và kết quả

Ứng dụng có thể được tải xuống  và thực thi mà không cần bất kỳ kiến ​​thức nào về Java, mặc dù Java phải được cài đặt trên máy tính của bạn. Hãy thử nghiệm thuật toán với email của riêng bạn.

Chúng ta có thể chạy ứng dụng từ  nguồn bằng cách thực hiện  RUN lớp. Hoặc, nếu bạn không muốn mở nó bằng IDE, hãy chạy  mvn clean install exec:java.

Sau đó, bạn sẽ thấy một cái gì đó như thế này:

Ứng dụng phân loại thư rác thư điện tử Java với Spark

Đầu tiên, đào tạo thuật toán bằng cách nhấp vào Train với LR SGD hoặc Train với LR LBFGS . Điều này có thể mất một đến hai phút. Sau khi kết thúc, một cửa sổ bật lên sẽ hiển thị độ chính xác đạt được. Đừng lo lắng về SGD so với LBFGS - chúng chỉ là những cách khác nhau để giảm thiểu chức năng chi phí và sẽ cho kết quả gần như giống nhau. Sau đó, sao chép và dán email bạn chọn vào vùng trắng và nhấn Test . Sau đó, một cửa sổ bật lên sẽ hiển thị dự đoán của thuật toán.

Độ chính xác đạt được trong quá trình thực hiện của tôi là khoảng 97%, sử dụng ngẫu nhiên 80% dữ liệu để đào tạo và 20% cho thử nghiệm. Không có kiểm tra xác thực chéo - chỉ có một tập huấn luyện và kiểm tra (cho độ chính xác) đã được sử dụng trong ví dụ này. Để tìm hiểu thêm về cách phân chia dữ liệu, xem  tại đây .

Mã để đào tạo thuật toán khá đơn giản:

public MulticlassMetrics execute() throws Exception {
    vocabulary = prepareData.createVocabulary();
    List<labeledpoint> labeledPoints = convertToLabelPoints();
    sparkContext = createSparkContext();
    JavaRDD<labeledpoint> labeledPointJavaRDD = sparkContext.parallelize(labeledPoints);
    JavaRDD<labeledpoint>[] splits = labeledPointJavaRDD.randomSplit(new double[]{0.8, 0.2}, 11L);
    JavaRDD<labeledpoint> training = splits[0].cache();
    JavaRDD<labeledpoint> test = splits[1];


    linearModel = model.run(training.rdd());//training with 80% data

//testing with 20% data
    JavaRDD<><object, object="">> predictionAndLabels = test.map(
            (Function<labeledpoint,></labeledpoint,><object, object="">>) p -> {
                Double prediction = linearModel.predict(p.features());
                return new Tuple2<>(prediction, p.label());
            }
    );

    return new MulticlassMetrics(predictionAndLabels.rdd());
}</object,></object,></labeledpoint></labeledpoint></labeledpoint></labeledpoint></labeledpoint>

Và đó là nó!

6 hữu ích 0 bình luận 5.9k xem chia sẻ

Có thể bạn quan tâm

loading