Algorytm jaki Ci teraz pokażę, jest preludium do uczenia maszynowego - dziedziny sztucznej inteligencji dotyczącej analizy danych i wyciągania wniosków na ich podstawie 🎉. Poznaj "K najbliższych sąsiadów", zagadnienie identyfikacji nowych obiektów na podstawie sąsiedztwa z innymi obiektami (i nie tylko) 😱!

K NAJBLIŻSZYCH SĄSIADÓW POMOŻE CI PRZY KLASYFIKACJI ORAZ REGRESJI!

Zaczniemy poznawanie tego algorytmu od ukazania dwóch historyjek 😊.

GDZIE K NAJBLIŻSZYCH SĄSIADÓW MOŻE POMÓC?

Zaczynamy pierwszą opowieść 1️⃣! 

KLASYFIKACJA

Wyobraźmy sobie taką sytuację. Nasz znajomy informuje nas o pewnej osobie, która jest zainteresowana wstąpieniem do jednego z 3 kół 👇:

  1. muzyczne,
  2. rysunkowe,
  3. teatralne.

Problem w tym, że posiadamy o niej bardzo niewiele informacji 🤏. Nie znamy jej twarzy, nie wiemy jak się nazywa, nie wiemy co lubi robić w czasie wolnym. Możemy natomiast dowiedzieć się jaka jest jej płeć, w jakim jest wieku i jakie ma umiejętności 👍. Naszym zadaniem jest określić do którego koła ta osoba będzie pasowała najbardziej 🤔.

Problem: zidentyfikowanie nieznanego obiektu i przypisanie go do odpowiedniej grupy już znanych obiektów na podstawie podanych kryteriów

"K najbliższych sąsiadów" (ang. "K nearest neighbors") właśnie służy do rozstrzygania ostatecznej identyfikacji obiektu, o którym nie wiemy niczego lub mamy o nim szczątkowe informacje 🚀.  W uczeniu maszynowym takie informacje są określane jako "cechy" i na ich podstawie możemy podjąć próbę przypisania nowej osoby do którejś z grup społecznych (to jest tylko jedno z co najmniej kilkudziesięciu przykładowych zastosowań) 👨‍👩‍👦‍👦.

Nazywamy to "klasyfikacją" ℹ️.

K najbliższych sąsiadów (klasyfikacja)

Jedną z dwóch zdolności algorytmu K najbliższych sąsiadów, to klasyfikacja obiektu o nieznanych bądź tylko szczątkowych informacjach na jego temat, na podstawie liczby najbliższych obiektów sąsiednich, jakimi się otacza.

REGRESJA

A teraz druga historia 2️⃣. Otrzymałeś(-aś) pewną okrągłą sumkę dolarów amerykańskich USD od swojej babci 🥰. Aby jednak z nich skorzystać, musisz udać się kantoru i wymienić na złotówki (PLN) 🙂. Zauważasz, że kurs jest bardzo chwiejny na tyle, że jednego dnia możesz trochę zyskać, a drugiego wiele stracić 😨. Chcesz uniknąć ryzyka, więc szukasz sposobu na uzyskanie wiedzy jak ten kurs będzie wyglądał w niedalekiej przyszłości ❓. Naszym zadaniem jest opracować metodę na wiarygodną predykcję przyszłego kursu dolarowego, przynajmniej o 24 godziny do przodu 🔥.

Problem: predykcja przyszłości/odpowiedzi na podstawie podanych kryteriów

I tu niespodzianka 💥! "KNN" i w tym przypadku może Ci pomóc 💪! Jego "drugą stroną medalu", jest zdolność do wnioskowania na temat obecnie zgromadzonych danych 💡. Przykładowo, prognoza na temat kursu wymiany waluty USD może powstać w wyniku zaobserwowania powtarzających się zachowań na przestrzeni nawet kilku lat 🔥. Skoro w styczniu 2 lata temu, dolar lekko spadł (piszę hipotetycznie 😄) i w styczniu zeszłego roku było to samo, to możemy śmiało przypuszczać, że nadchodzący styczeń również będzie wykazywał tendencje spadkowe 📉. Uczenie maszynowe jest w stanie wnioskować w taki właśnie sposób 🤩!

Takie zdolności algorytmu dotyczą nie tylko spraw finansowych 💵. Tak samo można analizować Twoje ulubione filmy według ruchu w jakimś popularnym serwisie, które potem pojawiają Ci się na stronie głównej często opatrzone hasłem "Proponowane" bądź "Sugerowane" 😊. Spróbuj dać wiarę, że właśnie w ten sposób Netflix wie, jakie filmy Ci zarekomendować jako następne do obejrzenia 😱!

"Regresja" to jest przewidywanie niedalekiej przyszłości wywnioskowanej przez dotychczas zgromadzone dane ℹ️.

K najbliższych sąsiadów (regresja)

Druga zdolność algorytmu K najbliższych sąsiadów to regresja, czyli przewidywanie niedalekiej przyszłości czegoś poprzez zaobserwowanie powtarzających się zachowań.

ZASTOSOWANIA K NAJBLIŻSZYCH SĄSIADÓW

Reasumując, "K najbliższych sąsiadów" nadaje się do 2 rzeczy 👇:

  1. klasyfikacji (przyporządkowanie nieznanego obiektu do jednej z grup znanych obiektów, na podstawie zestawu cech),
  2. regresji (przewidywanie czyjegoś ruchu w niedalekiej przyszłości).

W obu przypadkach aby algorytm mógł pomóc, musi mieć zestaw danych wejściowych czyli pisząc poprawnie, zestaw cech ✔️. I tu dopiero zaczyna się zabawa 🌀!

TO, CO DOSTARCZYSZ, TAKI WYNIK OTRZYMASZ

Aby nie otrzymać na wyjściu żadnych głupot, "KNN" musi mieć precyzyjnie przemyślany i jednocześnie dosyć "gęsty" zestaw informacji 🌐. Im więcej dobrze dobranych informacji, tym bardziej precyzyjny wynik 📈. Zarówno dla klasyfikacji, jak i regresji, najważniejszy jest dobór odpowiednich cech ⚠️.

Weźmy ponownie człowieka bez wiedzy o jego tożsamości ❓. Co może być najważniejsze w klasyfikacji człowieka 🤔? To zależy od zastosowania 😄! W przypadku grupy fanów jakiejś serii filmów, to kluczowe okażą się jego zainteresowania kinematograficzne 🎥. Kiedy mamy na myśli udział w grze zespołowej, możemy brać pod uwagę jego wzrost 📶. A co do wysokości emerytury, lata przepracowane w charakterze doświadczenia komercyjnego 🔢. I tak dalej 😊.

DZIAŁANIE K NAJBLIŻSZYCH SĄSIADÓW DLA KLASYFIKACJI

Czas na ukazanie konkretnego przykładu 🚀! Będzie on dotyczył klasyfikacji, czyli pierwszego zastosowania "K najbliższych sąsiadów" 🧨! Naszymi obiektami będą...owoce 😊!

Wyobraźmy sobie, że mamy 2 rodzaje owoców: pomarańcze 🍊 i cytryny 🍋. Nagle wśród nich, dostrzegamy nieznany nam owoc, który wyglądem przypomina pomarańczę i cytrynę, lecz nie jesteśmy w stanie stwierdzić jednoznacznie jaki to jest owoc ❓. Zatem, musimy go uklasyfikować na podstawie cechy 💥! Aby dać bardzo prosty przykład, naszą cechą będzie odległość nieznanego owocu od pozostałych N owoców 😲!

Klasyfikacja na podstawie odległości, polegać będzie na obliczeniu długości odcinka od miejsca nieznanego obiektu do każdego ze "znanych" owoców obu rodzajów i uwzględnieniu tylko 3 najkrótszych odległości ℹ️. Następnie, odczytujemy rodzaje tych 3 owoców i patrzymy czego jest więcej 👀. Jeżeli dla przykładu, obiekt do uklasyfikowania znajduje się blisko 2 cytryn i jednej pomarańczy, to uznajemy, że ten obiekt jest cytryną i na tym kończymy klasyfikację ✅.

Pamiętaj ⚠️! To jest bardzo prosty przykład ukazujący działanie "K najbliższych sąsiadów"! Aby taka klasyfikacja była naprawdę użyteczna, musielibyśmy uwzględnić dużo więcej współczynników jakie mają owoce, takich jak 👇:

  • kolor,
  • kształt,
  • wielkość.

Także przestrzegam, aby nie sugerować się tym przykładem (i kodem źródłowym zamieszczonym niżej), że to jest definitywna postać "KNN" 🙂!

Jak myślisz, jaka jest złożoność czasowa ⌚? Mamy N obiektów, które musimy zbadać celem klasyfikacji 1️⃣. Algorytm korzysta również z zestawu cech (tutaj ukazałem jedną cechę, natomiast zazwyczaj podaje się ich mnóstwo, aby wynik pochodził od dostatecznej "gęstości" współczynników ℹ️), a każdy taki sam zestaw posiada każdy z obiektów do zbadania 2️⃣. Zatem, notacja dużego O prezentuje taką oto złożoność:

O(n*d) : n = liczba obiektów, d = liczba cech

Iloczyn liczby znanych obiektów i liczby cech u każdego z nich 🔥! Mamy złożoność pseudowielomianową 😊!

KOD ŹRÓDŁOWY

Na postawienie kropki nad i, zapoznaj się z kodem źródłowym w języku Java ukazującym bardzo uproszczone działanie algorytmu "K najbliższych sąsiadów" dla klasyfikacji obiektu (na podstawie wyżej opisanego działania) 😄!

KLASA "MAIN"
public class Main {
	public static void main(String[] args) {
		new Launcher();
	}
}

W klasie uruchomieniowej "Main", mamy jak zwykle uruchomienie całego algorytmu jako tworzenie instancji klasy, której tłumaczenie (jak zwykle) zostawiam na sam koniec 😉.

REKORD "POINT"
public record Point(int x, int y) {
	@Override
	public String toString() {
		return "(" + x + ", " + y + ")";
	}

	public double getDistanceTo(Point point) {
		var differenceX = this.x - point.x;
		var differenceY = this.y - point.y;
		var squaredX = differenceX*differenceX;
		var squaredY = differenceY*differenceY;
		var distance = Math.sqrt(squaredX + squaredY);

		return Math.round(distance);
	}
}

Powyżej mamy rekord (klasę danych w języku Java zawierającą niemodyfikowalne dane składowe) dla punktu w dwuwymiarowym układzie współrzędnych 🔘. Jedyne 2 metody, jakie się znajdują, to 👇:

  1. przesłonięcie "toString" celem zdefiniowania własnej treści podczas powoływania się na referencję w wywołaniach "println",
  2. obliczenie i zwrócenie na wyjściu odległości pomiędzy dwoma obiektami 🌐.

Nie trzeba więcej tłumaczyć 😉. A odchodząc od tematu, "var" w języku Java pozwala Tobie ominąć wpisywania typu danych zmiennej znajdującej się wewnątrz instancji klasy 📦. Przydatna rzecz ❤️!

KLASA WYLICZENIOWA "ITEMTYPE"
public enum ItemType {
	Unknown("Nieznany"),
	Orange("Pomarańcza"),
	Lemon("Cytryna");

	private final String typeName;

	@Override
	public String toString() {
		return typeName;
	}

	ItemType(String typeName) {
		this.typeName = typeName;
	}
}

Aby określać typy obiektów, najlepiej skorzystać z typu wyliczeniowego ("enum") ✔️. Java ma ten duży plus, że "enum" może być całą klasą (więcej informacji o takiej konstrukcji, w załączonym artykule 👈) ❤️!

Zdefiniowałem 3 typy 👇:

  1. nieznany,
  2. pomarańcza,
  3. cytryna.

czyli zgodnie z powyższym (owocnym 🍎) przykładem 😁. Każdy z typów przyjmuje w konstruktorze parametr jako łańcuch znaków, dzięki czemu możemy nadawać nazwę jaką chcemy, by się ukazała dla danego typu w konsoli (podczas wywoływania "println") 🤩! W tym celu musimy tak samo przesłonić metodę "toString" i wskazać zwracanie podanego łańcucha znaków 🫵.

KLASA "ITEM"
public class Item {
	private final Point position;

	private ItemType itemType;

	@Override
	public String toString() {
		return "Typ obiektu: " + getItemType() + ", pozycja: " + getPosition();
	}

	public Item(Point position) {
		this(position, ItemType.Unknown);
	}

	public Item(Point position, ItemType itemType) {
		this.position = position;

		setItemType(itemType);
	}

	public void setItemType(ItemType itemType) {
		this.itemType = itemType;
	}

	public double getDistanceTo(Item item) {
		return getPosition().getDistanceTo(item.getPosition());
	}

	public Point getPosition() {
		return position;
	}

	public ItemType getItemType() {
		return itemType;
	}
}

Teraz klasa do tworzenia obiektów 🔧! Troszkę szersza od poprzednich, natomiast nie ma w niej niczego skomplikowanego 🙂. Nie nazwałem klasy jako "Object", ponieważ to jest już zarezerwowane w języku Java jako "klasa wszystkich klas" - odsyłam do załączonego materiału po więcej 😄!

Obiekt posiada 2 składowe 👇:

  1. pozycję w układzie współrzędnych,
  2. rodzaj obiektu jako typ wyliczeniowy.

Pozycja może być oznaczona modyfikatorem "final", ponieważ raz przypisanej pozycji na świecie nie będziemy zmieniać przez cały cykl życia programu ℹ️. Rodzaj obiektu już będziemy zmieniać, aby nieznany obiekt przyporządkować do jednej z grup owoców 👍.

Metody są następujące 👇:

  1. przesłonięcie "toString" do definiowania niestandardowego komunikatu,
  2. konstruktor przyjmujący jedynie parametr pozycji,
  3. konstruktor przyjmujący parametr pozycji i rodzaju obiektu,
  4. ustawianie rodzaju obiektu (to się przyda do klasyfikacji ℹ️),
  5. pobieranie odległości pomiędzy tym obiektem, a drugim wskazanym w parametrze (skraca ciąg przedostawania się do metody wyznaczającej odległość do punktu),
  6. pobieranie pozycji obiektu,
  7. pobieranie rodzaju obiektu.

Przeciążenie konstruktorów nie jest bez powodu. Podczas tworzenia obiektu, będziemy mieli możliwość pominięcia wskazania jakiego jest typu, dzięki czemu od razu zostanie mu nadany typ "nieznany". Konstruktor przyjmujący 2 parametry, to jest ten "domyślny". Drugi z nich, to wariant dla obiektu nieznanego pochodzenia, o którym wiemy tylko tyle gdzie się znajduje ℹ️.

KLASA "ITEMS"
import java. util.ArrayList;

public class Items extends ArrayList<Item> {
	public Items() {
		add(new Item(new Point(2, 2), ItemType.Orange));
		add(new Item(new Point(6, 2), ItemType.Orange));
		add(new Item(new Point(4, 5), ItemType.Orange));
		add(new Item(new Point(12, 12), ItemType.Lemon));
		add(new Item(new Point(10, 10)));
	}
}

Teraz lista naszych obiektów. Tutaj tworzymy sobie klasę pochodną, która dziedziczy po "ArrayList". Znajduje się tu tylko konstruktor, a w nim dodawanie przykładowych obiektów, więc można by bez problemu pominąć tę klasę, natomiast w ten sposób mamy wydzielenie od siebie instrukcji, przez co lepiej się nimi zarządza ✅.

Ostatni obiekt w kolejności, jest jako ten "obcy" i który nasz algorytm spróbuje zidentyfikować patrząc na sąsiadujące obiekty 👽. Dlatego on wyjątkowo nie ma przypisanego typu 🙂.

KLASA "RESULTSPRINTER"
import java. io.PrintStream;
import java. util.List;
import java. util.function.Function;

public class ResultsPrinter {
	private final PrintStream printStream = System.out;

	public void printAboutItem(Item item, Function<Item, String> function, String message) {
		printLine(message + function.apply(item));
	}

	public <T> void printAboutList(List<T> list, String message) {
		printLine(message + list);
	}

	private void printLine(String message) {
		printStream.println(message);
	}
}

Ta klasa jest absolutnie poboczna i nie ma związku z jakąkolwiek częścią działania "K najbliższych sąsiadów" ℹ️. Służy tylko do wypisywania komunikatów podczas działania.

Jedyna finalna dana składowa jest tylko do zwiększenia komfortu w pisaniu "println" 🙂. A metody dotyczą wypisywania w konsoli szczegółów na temat przedmiotu oraz listy 🌟.

Twoją uwagę może przykuć rzadziej spotykany element - "Function" 😳! "Function" w języku Java ma dokładnie takie samo zastosowanie, co niejakie "Func" w języku C# - to jest możliwość wstawienia instrukcji w miejsce parametru, które musi na wyjściu zwrócić wartość odpowiedniego typu (to jest zawsze ostatni parametr wewnątrz nawiasów kątowych) 😱.

Druga metoda, która odpowiada za wypisywanie listy, korzysta z typu generycznego, tak aby nie ograniczać się wyłącznie do listy przedmiotów 😉.

KLASA "LAUNCHER"
import java. util.Map;
import java. util.List;
import java. util.ArrayList;
import java. util.Comparator;
import java. util.stream.Collectors;
import java. util.function.Function;

public class Launcher {
	private final Items items = new Items();
	private final ArrayList<Item> leftUnknownItems = new ArrayList<>(items.stream().filter(item -> item.getItemType() == ItemType.Unknown).toList());
	private final ResultsPrinter resultsPrinter = new ResultsPrinter();

	private static final int NUMBER_OF_NEIGHBOURS = 3;

	public Launcher() {
		printAboutItems();

		while (!leftUnknownItems.isEmpty()) {
			classifyNextItem(leftUnknownItems.removeFirst());
		}

		printAboutItems();
	}

	private void printAboutItems() {
		resultsPrinter.printAboutList(items, "Lista wszystkich obiektów: ");
	}

	private void classifyNextItem(Item item) {
		var nearestNeighbours = getKNearestNeighboursOf(item, NUMBER_OF_NEIGHBOURS);
		var itemTypeByHighestNumberOfItems = getItemTypeByHighestNumberOfItems(nearestNeighbours);

		setItemTypeToItem(item, itemTypeByHighestNumberOfItems);
	}

	private List<Item> getKNearestNeighboursOf(Item unknownItem, int numberOfNeighbours) {
		var neighbouringItems = new ArrayList<>(items.stream().filter(item -> item.getItemType() != ItemType.Unknown).toList());

		neighbouringItems.sort(Comparator.comparingDouble(item -> item.getDistanceTo(unknownItem)));
		neighbouringItems.subList(numberOfNeighbours, neighbouringItems.size()).clear();
		resultsPrinter.printAboutList(neighbouringItems, "Najbliższe obiekty sąsiednie: ");

		return neighbouringItems;
	}

	private ItemType getItemTypeByHighestNumberOfItems(List<Item> items) {
		var itemTypeStream = items.stream().map(Item::getItemType);
		var setOfItemsCountByItemType = itemTypeStream.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).entrySet();
		var optionalItemType = setOfItemsCountByItemType.stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey);

		return optionalItemType.orElse(ItemType.Unknown);
	}

	private void setItemTypeToItem(Item unknownItem, ItemType itemType) {
		unknownItem.setItemType(itemType);
		resultsPrinter.printAboutItem(unknownItem, item -> item.getItemType().toString(), "Nadano obiektowi typ! Jest to: ");
	}
}

Tak oto przechodzimy do "serca" aplikacji ❤️‍🔥! Algorytm "KNN", to ostatnia klasa potrzebująca wyjaśnień 📢.

U góry, 3 składowe 👇:

  1. obiekt z listą wszystkich przedmiotów,
  2. obiekt listy wszystkich przedmiotów nieuklasyfikowanych (w konstruktorze jest zawarte wyrażenie lambda, które zaraz wyjaśnię ⚠️),
  3. obiekt do wypisywania komunikatów.

Wszystkie obiekty są oznaczone jako finalne, ponieważ nie zamierzamy przypisywać do nich nowych referencji 😊.

Niżej jest jeszcze dodatkowo stała statyczna do określania ilu najbliższych sąsiadów bierzemy pod uwagę ℹ️. A teraz wyjaśnię to:

private final ArrayList<Item> leftUnknownItems = new ArrayList<>(items.stream().filter(item -> item.getItemType() == ItemType.Unknown).toList());

Jak pewnie wiesz, "ArrayList" pozwala na dodawanie elementów już od razu, w miejscu wywoływania konstruktora. Jeżeli chcemy wprowadzić wiele elementów pod rząd, to musi to być lista.

Strumień w języku Java to sekwencja elementów, na której możemy przeprowadzać operacje w stylu programowania funkcyjnego (rolę odgrywają dane bez modyfikowania czegokolwiek po drodze ℹ️). Instrukcja u góry, oznacza: "skonwertuj listę obiektów na strumień, wyklucz te obiekty, które mają przypisany rodzaj inny niż nieznany i skonwertuj na listę".

Efekt? Do listy trafią wszystkie elementy nieuklasyfikowane 🧨. I to wszystko w jednym wierszu 💥!

Teraz sam konstruktor. Wewnątrz niego mamy wywołania metody do wypisywania listy w komunikacie, natomiast pomiędzy nimi znajduje się pętla "while" 🔴. W tym miejscu następuje klasyfikowanie obiektów jeden po drugim 📌. Identyfikujemy tak długo, aż wyczerpiemy wszystkie obiekty, które czekają na przypisanie 🔁.

Gdy okaże się, że wciąż jest coś, co wymaga klasyfikacji, wchodzimy do kolejnej osobnej metody ▶️. Metoda "removeFirst" wykonuje 2 w jednym: usuwa pierwszy w kolejności element z listy i go zwraca. To dzięki temu, nie jest potrzebne dzielenie na pobieranie elementu i usuwanie 😊.

W pierwszym kroku klasyfikacji obiektu, jest pobranie N najbliższych sąsiadów 📝. Jest za to odpowiedzialna osobna metoda, którą wytłumaczę za chwilę. Drugim krokiem jest pobranie rodzaju obiektu, który posiada największa liczba najbliższych sąsiadów 🎉. Na to również jest przydzielona osobna metoda 🙂. Na samym końcu jest przypisanie rodzaju obiektu naszemu nieznanemu obiektowi ✅. Tu się zamyka klasyfikacja pojedynczego obiektu 🔒!

Teraz opiszę poszczególne kroki, czyli każdą z metod 3️⃣.

WYZNACZENIE NAJBLIŻSZYCH SĄSIADÓW

Ustalenie które obiekty sąsiednie "stoją" najbliżej tego, któremu chcemy przypisać rodzaj, polega na 3 etapach 👇:

  1. pobranie z listy obiektów, których rodzaj jest inny niż "nieznany",
  2. posortowanie listy według odległości od obiektu, któremu chcemy przypisać rodzaj (od najkrótszego, do najdłuższego),
  3. wybranie spośród wszystkich obiektów posortowanych według odległości, pierwszych N jako "stojących" najbliżej.

W pkt. 1 1️⃣, używamy tego samego strumienia, który wyjaśniłem na samym początku, tylko teraz uwzględniamy jedynie elementy posiadające rodzaj inny niż "nieznany":

var neighbouringItems = new ArrayList<>(items.stream().filter(item -> item.getItemType() != ItemType.Unknown).toList());

czyli innymi słowy, na wyjściu otrzymamy wszystkie obiekty o już przypisanym typie ✅. To będzie pula obiektów dla przyporządkowywania obiektu obcego, więc siłą rzeczy nie możemy mieć żadnych "obcych osobników" 😆.

W pkt. 2 2️⃣, sortujemy te obiekty według odległości, od najkrótszej do najdłuższej:

neighbouringItems.sort(Comparator.comparingDouble(item -> item.getDistanceTo(unknownItem)));

Tutaj, korzystam ze statycznej metody "comparingDouble" dostępnej od klasy "Comparator" (dział w języku Java związany z sortowaniem kolekcji z użyciem interfejsu 💡). To wystarczy, aby otrzymać listę obiektów posortowanych według odległości, co umożliwi łatwe uwzględnienie wyłącznie pierwszych kilku obiektów 👍.

W pkt. 3 3️⃣, korzystamy z kolejnego "haczyka" ⚓ jakiego można użyć w Javie:

neighbouringItems.subList(numberOfNeighbours, neighbouringItems.size()).clear();

Najpierw tworzymy "podlistę" obiektów od N-tego indeksu, do końca listy, a następnie...usuwamy je metodą "clear" 💥! Służy to do ograniczenia liczebności do podanej liczby jako sąsiadów, gdyż w metodzie inicjującej K najbliższych sąsiadów, polegamy na ograniczonej liczbie danych ⚠️.

Podanie zakresu "obcięcia" od liczby sąsiadów branych pod uwagę do rozmiaru listy jak najbardziej ma sens ✔️. W Javie, indeksy w tablicach i listach liczymy od zera 0️⃣. Podanie w tym przypadku liczby 3, spowoduje rozpoczęcie "wycinania" od czwartego elementu, czyli pierwszego elementu spoza zakresu obiektów jaki nas interesuje 🔥.

Podstawianie za koniec rozmiaru listy też jest poprawne - "subList" nie uwzględnia indeksu górnej granicy (jest tzw. "exclusive" ℹ️), co oznacza, że tak naprawdę usunie elementy do (N - 1) 😉.

Po tym wszystkim, wypisujemy komunikat o uzyskanych w ten sposób 3 najbliższych sąsiadach i taki wynik jest zwracany na wyjściu ↩️.

WYZNACZENIE NAJCZĘŚCIEJ POWTARZAJĄCEGO SIĘ RODZAJU OBIEKTU

Przechodzimy do kolejnej metody ➡️. Mamy podobny podział na mniejsze kroki, aby wszystko było czytelne i zrozumiałe 🧠. Tutaj również mamy 3 etapy 👇:

  1. uzyskanie strumienia rodzajów obiektów (typu wyliczeniowego),
  2. wyznaczenie zbioru par klucz-wartość, gdzie kluczem jest rodzaj obiektu, a wartością jest liczba wystąpień danego rodzaju,
  3. wyznaczenie największej liczby wystąpień danego rodzaju obiektu i pobranie go w formie typu wyliczeniowego.

Zaczynam tłumaczyć każdy z punktów 🚀! W pkt. 1, ponownie konwertujemy kolekcję na strumień celem skorzystania z metody "map", która tworzy nowy strumień według wskazanej składowej ℹ️. Tutaj pobieramy rodzaj obiektu od każdego z nich:

var itemTypeStream = items.stream().map(Item::getItemType);

W pkt. 2, interesuje nas wyznaczenie takiej "tabeli rekordów", czyli ile jest wystąpień każdego z rodzajów obiektów 🏆:

var setOfItemsCountByItemType = itemTypeStream.collect(Collectors.groupingBy(Function.identity(), Collectors.counting())).entrySet();

Po polsku 🇵🇱: "zbierz wszystkie elementy ze strumienia, podziel je według rodzaju obiektu, policz ile jest wystąpień dla każdego z nich i skonwertuj na zbiór par klucz-wartość, w których kluczem będzie rodzaj obiektu, a wartością, liczba wystąpień" 😅.

"Function.identity" oznacza w skrócie pobranie przypisanego w parametrze klucza i zwrócenie tego samego klucza na wyjściu, czyli:

x -> x

"Collectors.counting" zlicza nam liczbę wystąpień każdego z kluczy i zwraca ją 🔢. Jak na wejściu "zastanie" taki zbiór:

[Orange, Orange, Lemon]

to dla pomarańczy wyjdzie 2, a dla cytryny, 1 - proste 😄?

W pkt. 3, wybieramy spośród wszystkich rodzajów obiektów ten, który ma największą liczbę wystąpień i finalnie pobieramy jego typ (jako "enum"):

var optionalItemType = setOfItemsCountByItemType.stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey);

"Skonwertuj uzyskany zbiór na strumień, pobierz parę klucz-wartość z największą liczbą wystąpień i pobierz sam klucz". Tak uzyskujemy rodzaj obiektu "opakowany" w typ "Optional" 🌟. W języku Java, to typ danych stanowiący "otoczkę" wokół typu referencyjnego, który jak wiemy, może posiadać brak referencji (wartość "null"). "Optional" posiada zbiór różnych metod, którymi możemy uchronić jakieś instrukcje, które korzystają z danej referencji, bez zakładania w instrukcjach warunkowch, że "X jest różne od null" 🔒.

W końcu, na wyjściu zwracamy jeden z dwóch wyników (metoda "orElse"):

  1. jeżeli jest zdefiniowany rodzaj obiektu w typie "Optional", to zostanie on "wyjęty" i zwrócony,
  2. jeżeli (hipotetycznie) nie ma zdefiniowanego rodzaju obiektu, to ma zwrócić "nieznany" typ (to jest stworzone na okoliczność przypadków skrajnych).

Dodam od siebie, że naprawdę warto opanować programowanie funkcyjne w Javie, bo można w ten sposób sobie fajnie uprościć kod 😎.

PRZYPISANIE RODZAJU OBIEKTOWI

Ostatni krok jest najprzyjemniejszy 🥰. Tu tylko przypisujemy określony typ naszemu obiektowi i wypisujemy komunikat na ten temat ☑️.

Na tym etapie jest zakończenie pojedynczej klasyfikacji 🏁. Potem program wraca do pętli "while" i wykreśla jeden obiekt po drugim, aż wszystkim zostanie przypisany typ. Tak kończy się tłumaczenie całości kodu 🔚!

Na koniec, weź pod uwagę jedną rzecz ⚠️! Pokazałem bardzo prosty przykład dokonujący identyfikacji obiektu na podstawie tylko jednej cechy, jaką jest odległość euklidesowa 🔔. Żeby "K najbliższych sąsiadów" dawało jakiś użytek, to tych cech musi być co najmniej kilka 😱! Ponadto, odległość euklidesowa nie zawsze może przynieść oczekiwany rezultat. Może być tak, że obiekt jaki badamy spełnia wszystkie cechy do konkretnego przypisania, a jedyne co go wykreśla, to zbyt duża odległość i to spowoduje błąd ❌. Warto wtedy pochylić się nad podobieństwem cosinusowym, które określa stopień przynależności na podstawie cosinusa kąta, a nie długości odcinka ⭐.


To by było tyle na temat algorytmu K najbliższych sąsiadów i jego użycia dla klasyfikacji oraz regresji ✔️.

PODOBNE ARTYKUŁY