Compare commits
No commits in common. "master" and "master" have entirely different histories.
3
.gitignore
vendored
@ -2,5 +2,4 @@
|
||||
.idea
|
||||
bin
|
||||
venv
|
||||
.venv
|
||||
__pycache__
|
||||
.venv
|
@ -1,282 +0,0 @@
|
||||
# Podprojekt indywidualny - raport
|
||||
|
||||
|
||||
**Temat:** Sugerowanie potraw
|
||||
**Metoda uczenia:** Decision Tree(CART)
|
||||
**Autor:** Marcin Jerzy Dobrowolski
|
||||
**Źródła:** https://github.com/random-forests/tutorials/blob/master/decision_tree.ipynb
|
||||
|
||||
**Link do repozytorium projektu:** https://git.wmi.amu.edu.pl/s444427/Sztuczna_Inteligencja_2020
|
||||
|
||||
## Wstęp
|
||||
|
||||
Projekt rozwiązuje problem zasugerowania dania przez kelnera na podstawie danych reprezentujących preferencje gościa. W tym celu wykorzystałem metodę uczenia drzew decyzyjnych CART, którą zaprogramowałem w czystym języku python. Struktura podprojektu:
|
||||
|
||||
* [suggestionDecisionTree.py](src/SubprojectMarcinDobrowolski/suggestionDecisionTree.py) - klasa drzew decyzyjnego
|
||||
* [question.py](src/SubprojectMarcinDobrowolski/question.py) - klasa pytania
|
||||
* [leaf.py](src/SubprojectMarcinDobrowolski/leaf.py) - klasa liścia
|
||||
* [decisionNode.py](src/SubprojectMarcinDobrowolski/decisionNode.py) - klasa wierzchołka pytającego
|
||||
* [utility.py](src/SubprojectMarcinDobrowolski/utility.py) - klasa użytkowa
|
||||
* [trainingData.csv](src/SubprojectMarcinDobrowolski/Data/trainingData.csv) - zbiór uczący
|
||||
* [testData.csv](src/SubprojectMarcinDobrowolski/Data/testData.csv) - klasa użytkowa
|
||||
|
||||
Struktura danych reprezentujących preferencje gościa:
|
||||
|
||||
type, size, money, appetite, name
|
||||
['meat', 'small', 31, 55, 'schabowy']
|
||||
|
||||
* type - typ potrawy: wegańska lub z mięsem
|
||||
* appetite - apetyt gościa, świadczy o tym jak duża potrawa spełni jego oczekiwania,
|
||||
np. duże danie będzie inne dla osób o dużym i małym apetycie
|
||||
* size - rozmiar dania
|
||||
* money - zawartość portfela gościa, czyli na jak dużo może sobie pozwolić
|
||||
* name - etykieta potrawy, dopasowana w taki sposób by spełnić powyższe wymagania
|
||||
|
||||
|
||||
## Opis modułów
|
||||
|
||||
W pliku *utility* znajdują się podręczne funkcje użytkowe wykorzystywane przez pozostałe moduły programu.
|
||||
|
||||
def uniqueValues(rows, column):
|
||||
return set([row[column] for row in rows])
|
||||
|
||||
Znajduje unikalne wartości wyznaczonej kolumny z podanego zbioru danych.
|
||||
|
||||
def classCount(rows):
|
||||
counts = {}
|
||||
for row in rows:
|
||||
label = row[-1]
|
||||
if label not in counts:
|
||||
counts[label] = 0
|
||||
counts[label] += 1
|
||||
return counts
|
||||
|
||||
Podlicza ilość elementów każdej etykiety z danego zbioru danych.
|
||||
|
||||
def is_numeric(value):
|
||||
return isinstance(value, int) or isinstance(value, float)
|
||||
|
||||
Sprawdza czy podana wartość jest liczbą.
|
||||
|
||||
def partition(rows, question):
|
||||
trueRows, falseRows = [], []
|
||||
for row in rows:
|
||||
if question.match(row):
|
||||
trueRows.append(row)
|
||||
else:
|
||||
falseRows.append(row)
|
||||
return trueRows, falseRows
|
||||
|
||||
Dzieli dany zbiór ze względu na zadane pytanie.
|
||||
|
||||
def generateTestData(path, quantity):
|
||||
with open('path', 'w') as csvFile:
|
||||
csvWriter = csv.writer(csvFile, delimiter=',')
|
||||
...
|
||||
|
||||
Generuje zbiór danych testowych o zadanej wielkości i zapisuje go w pliku o podanej ścieżce. Ze względu na długość kodu nie zamieściłem go w całości. Dane są generowane przy użyciu liczb pseudo losowych w taki sposób by były zgodne z prawami rządzącymi tym zbiorem danych.
|
||||
|
||||
def generateTestExample():
|
||||
example = []
|
||||
category = random.randrange(0, 1)
|
||||
size = random.randrange(0, 2)
|
||||
if category == 0:
|
||||
example.append('meat')
|
||||
...
|
||||
|
||||
Działa na takiej samej zasadzie jak jej poprzedniczka. Z tym wyjątkiem, że generuje pojedynczy przykład.
|
||||
|
||||
Klasa *Question* reprezentuje pytanie, które służy do podziału danych w wierzchołkach decyzyjnych drzewa. Jej atrybuty oznajczają kolejno: etykiete kolumny zbioru danych, indeks wspomnianej kolumny oraz wartość, której dotyczyy pytanie.
|
||||
|
||||
class Question:
|
||||
def __init__(self, columnLabel, column, value):
|
||||
self.columnLabel = columnLabel
|
||||
self.column = column
|
||||
self.value = value
|
||||
|
||||
def match(self, example):
|
||||
val = example[self.column]
|
||||
if is_numeric(val):
|
||||
return val <= self.value
|
||||
else:
|
||||
return val == self.value
|
||||
|
||||
Metoda match decyduje jak na pytanie odpowiada zadany przykład.
|
||||
|
||||
Jądro algorytmu znajduje się w klasie *suggestionTree* w pilku *suggestionDecisionTree*. Metoda *readTrainingData* służy do odczytania zbioru uczącego z pliku *trainingData.csv* i zwrócenia przechowywanych w nim danych wraz z ich etykietami.
|
||||
|
||||
|
||||
def readTrainingData(path):
|
||||
with open(path) as csv_file:
|
||||
csvReader = csv.reader(csv_file, delimiter=',')
|
||||
lineCount = 0
|
||||
trainingData = []
|
||||
labels = []
|
||||
|
||||
for row in csvReader:
|
||||
example = []
|
||||
for column in row:
|
||||
if lineCount == 0:
|
||||
labels.append(column)
|
||||
else:
|
||||
if column.isdigit():
|
||||
example.append(int(column))
|
||||
else:
|
||||
example.append(column)
|
||||
if lineCount > 0:
|
||||
trainingData.append(example)
|
||||
lineCount += 1
|
||||
|
||||
print('Processed lines: ', lineCount)
|
||||
return trainingData, labels
|
||||
|
||||
Jest wykorzystana w inicjalizacji zmiennej globalnej *trainigData* oraz *labels*, z których korzystają niektóre z metod tej klasy.
|
||||
|
||||
```
|
||||
trainingData, labels = SuggestionTree.readTrainingData(
|
||||
'src/SubprojectMarcinDobrowolski/Data/trainingData.csv')
|
||||
```
|
||||
|
||||
Metoda *gini* oblicza tzw. *gini impurity*. Jest to miara, która mówi jak duże jest prawdopodobieństwo by losowo wybrany element ze zbioru został błędnie oznaczony.
|
||||
|
||||
def gini(rows):
|
||||
counts = classCount(rows)
|
||||
impurity = 1
|
||||
for lbl in counts:
|
||||
prob_of_lbl = counts[lbl] / float(len(rows))
|
||||
impurity -= prob_of_lbl**2
|
||||
return impurity
|
||||
|
||||
def infoGain(left, right, currentUncertainty):
|
||||
p = float(len(left)) / (len(left) + len(right))
|
||||
return currentUncertainty - p * SuggestionTree.gini(left) - (1 - p) * SuggestionTree.gini(right)
|
||||
|
||||
Natomiast metoda *infoGain* oblicza w jakim stopniu zmniejsza się wskaźnik *gini impurity* ze względu na wybór lewego i prawego podziału zbioru.
|
||||
|
||||
Metoda *findBestSplit* znajduje pytanie, które w danej chwili spowoduje jak największy przyrost informacji.
|
||||
|
||||
def findBestSplit(rows):
|
||||
bestGain = 0
|
||||
bestQuestion = None
|
||||
currentUncertainty = SuggestionTree.gini(rows)
|
||||
nFeatures = len(labels) - 1
|
||||
|
||||
for column in range(nFeatures):
|
||||
|
||||
values = set([row[column] for row in rows])
|
||||
|
||||
for value in values:
|
||||
|
||||
question = Question(labels[column], column, value)
|
||||
|
||||
trueRows, falseRows = partition(rows, question)
|
||||
|
||||
if len(trueRows) == 0 or len(falseRows) == 0:
|
||||
continue
|
||||
|
||||
gain = SuggestionTree.infoGain(
|
||||
trueRows, falseRows, currentUncertainty)
|
||||
|
||||
if gain > bestGain:
|
||||
bestGain, bestQuestion = gain, question
|
||||
|
||||
return bestGain, bestQuestion
|
||||
|
||||
Klasa *Leaf* jest reprezentacją liścia drzewa czyli najbardziej zewnętrznego wierzchołka. W atrybucie *predictions* przechowuje oszacowanie etykiet/y, które może przyjąć przykład, który doń trafi. Metoda *printLeaf* służy do eleganckiego wyświetlania oszacowania.
|
||||
|
||||
class Leaf:
|
||||
def __init__(self, rows):
|
||||
self.predictions = classCount(rows)
|
||||
|
||||
def printLeaf(self):
|
||||
total = sum(self.predictions.values()) * 1.0
|
||||
probs = {}
|
||||
for label in self.predictions.keys():
|
||||
probs[label] = str(
|
||||
int(self.predictions[label] / total * 100)) + '%'
|
||||
return probs
|
||||
|
||||
Klasa *DecisionNode* reprezentuje wierzchołek drzewa, w którym następuje zadanie pytania oraz podział danych. Atrybuty *trueBranch* oraz *falseBranch* reprezentują odpowiednio lewego i prawego potomka.
|
||||
|
||||
class DecisionNode:
|
||||
def __init__(self,
|
||||
question,
|
||||
trueBranch,
|
||||
falseBranch):
|
||||
self.question = question
|
||||
self.trueBranch = trueBranch
|
||||
self.falseBranch = falseBranch
|
||||
|
||||
Metoda *buildTree* odpowiada za rekurencyjne skonstruowanie drzewa na bazie przekazanego zbioru danych. Zwraca korzeń drzewa.
|
||||
|
||||
def buildTree(rows):
|
||||
gain, question = SuggestionTree.findBestSplit(rows)
|
||||
|
||||
if gain == 0:
|
||||
return Leaf(rows)
|
||||
|
||||
trueRows, falseRows = partition(rows, question)
|
||||
|
||||
trueBranch = SuggestionTree.buildTree(trueRows)
|
||||
falseBranch = SuggestionTree.buildTree(falseRows)
|
||||
|
||||
return DecisionNode(question, trueBranch, falseBranch)
|
||||
|
||||
Metoda *printTree* wyświetla strukturę drzewa w postaci tekstu w wierszu poleceń.
|
||||
|
||||
def printTree(node, spacing=' '):
|
||||
|
||||
if isinstance(node, Leaf):
|
||||
print(spacing + 'Predict', node.predictions)
|
||||
return
|
||||
|
||||
print(spacing + str(node.question))
|
||||
|
||||
print(spacing + '--> True:')
|
||||
SuggestionTree.printTree(node.trueBranch, spacing + ' ')
|
||||
|
||||
print(spacing + '--> False:')
|
||||
SuggestionTree.printTree(node.falseBranch, spacing + ' ')
|
||||
|
||||
def classify(row, node):
|
||||
|
||||
if isinstance(node, Leaf):
|
||||
return node
|
||||
|
||||
if node.question.match(row):
|
||||
return SuggestionTree.classify(row, node.trueBranch)
|
||||
else:
|
||||
return SuggestionTree.classify(row, node.falseBranch)
|
||||
|
||||
Metoda *classify* przyporządkowuje danemu przykładowi odpowiadającą mu etykiete w drzewie.
|
||||
|
||||
## Działanie
|
||||
|
||||
Drzewo jest budowane na podstawie zbioru uczącego. Algorytm podejmuje decyzje o tym jak podzielić zbiór na podstawie przyrostu informacji wynikającego z potencjalnego podziału. Przerywa działanie gdy przyrost wynosi *0*, co oznacza, że dotarł do liścia drzewa i podział nie jest możliwy. Kończąc swoje działanie zwraca korzeń klasy *DecisionNode*, który zawiera w atrybutach potomków co umożliwia rekurencyjne schodzenie po drzewie.
|
||||
|
||||
W projekcie można je wykorzystać poprzez wciśnięcie, któregoś z klawiszy:
|
||||
|
||||
* 0 - sprawdzenie poprawności algorytmu na losowo wygenerowanym zbiorze testowym. =
|
||||
* 1 - sprawdzenie poprawności algorytmu na losowo wygenerowanym przykładzie.
|
||||
* 2 - wyświetlenie struktury drzewa
|
||||
|
||||
Rezultaty zostają wypisane w konsoli.
|
||||
|
||||
```
|
||||
module: pygames
|
||||
Python: 3.7.7
|
||||
```
|
||||
|
||||
macOS / Linux
|
||||
|
||||
```
|
||||
pygames
|
||||
python main.py
|
||||
```
|
||||
|
||||
Windows
|
||||
|
||||
```
|
||||
pygames
|
||||
python main.py
|
||||
```
|
@ -1,159 +0,0 @@
|
||||
# Sztuczna Inteligencja 2020 - Raport z podprojektu
|
||||
|
||||
|
||||
**Autor:** Maksymilian Kierski
|
||||
|
||||
**Raportowany okres:** 15.05.2020-26.05.2020
|
||||
|
||||
**Wybrana metoda uczenia:** Splotowe sieci neuronowe (CNN)
|
||||
|
||||
## Cel podprojektu
|
||||
Celem podprojektu jest umożliwienie kelnerowi stwierdzenia czy na talerzu znajdującym się na stole jest jeszcze jedzenie, czy już go nie ma. Do tego celu zastosowałem splotowe sieci neuronowe (CNN), oraz biblioteki:
|
||||
|
||||
* numpy, cv2 - tworzenie danych wejściowych
|
||||
* tensorflow keras - tworzenie modelu
|
||||
* tensorboard - analiza modelów
|
||||
|
||||
|
||||
## Uczenie modelu
|
||||
|
||||
### Dane wejściowe
|
||||
Dane wejściowe składają się z dwóch rodzajów zdjęć talerzy, full - pełnych, oraz empty - pustych.
|
||||
Na początku, aby nasz model mógł się nauczać potrzebujemy nasze dane wejściowe odpowiednio przetworzyć.
|
||||
|
||||
```
|
||||
for category in CATEGORIES:
|
||||
path = os.path.join(DATADIR, category)
|
||||
class_num = CATEGORIES.index(category)
|
||||
for img in os.listdir(path):
|
||||
try:
|
||||
img_array = cv2.imread(os.path.join(path, img),
|
||||
cv2.IMREAD_GRAYSCALE)
|
||||
new_array = cv2.resize(img_array, (IMG_SIZE, IMG_SIZE))
|
||||
training_data.append([new_array, class_num])
|
||||
except Exception as e:
|
||||
pass
|
||||
random.shuffle(training_data)
|
||||
```
|
||||
Tutaj nasze dane wejściowe są odpowiednio przetwarzane. Na początku zdjęcie jest sczytywane, oraz przetwarzane aby każdy pixel był w skali szarości 0 - 255 (ponieważ kolor w tym zadaniu według mnie, nie odgrywa ważnej roli). Następnie skalowany jest do mniejszych rozmiarów i w końcu jako macierz zadeklarowanych wymiarów trafia do tablicy ze swoją etykietą. Na końcu cała tablica jest przetasowywana, aby umożliwić modelowi lepszą naukę.
|
||||
|
||||
Teraz dane zostają podzielone na zestaw cech i zestaw etykiet, oraz zostają zapisane do plików za pomocą **pickle**.
|
||||
```
|
||||
for features, label in training_data:
|
||||
X.append(features)
|
||||
y.append(label)
|
||||
|
||||
pickle_in = open(relative_path + 'SavedData/X.pickle', 'rb')
|
||||
X = pickle.load(pickle_in)
|
||||
|
||||
pickle_in = open(relative_path + 'SavedData/y.pickle', 'rb')
|
||||
y = pickle.load(pickle_in)
|
||||
```
|
||||
|
||||
### Tworzenie modelu i proces jego nauki
|
||||
#### Wczytywanie danych potrzebnych do nauki modelu
|
||||
Na początku sczytywane są odpowiednio przygotowane dane (funkcja load_dataset)
|
||||
```
|
||||
pickle_in = open(relative_path + 'SavedData/X.pickle', 'rb')
|
||||
X = pickle.load(pickle_in)
|
||||
|
||||
pickle_in = open(relative_path + 'SavedData/y.pickle', 'rb')
|
||||
y = pickle.load(pickle_in)
|
||||
|
||||
return X, y
|
||||
```
|
||||
Wczytywanie danych i normalizacja X, ponieważ używam kolorów w skali szarości 0 - 255, to do normalizacji (skala 0-1) wystarczy przemnożyć każde pole macierzy przez 255. Inicjalizacja tensorboardu, który przyda się nam do analizy stworzonych przez nas modeli.
|
||||
```
|
||||
X = load_dataset()[0]
|
||||
y = load_dataset()[1]
|
||||
|
||||
X = np.array(X).reshape(-1, IMG_SIZE, IMG_SIZE, 1)
|
||||
y = np.array(y)
|
||||
|
||||
X = X / 255.0
|
||||
|
||||
tenserboard = TensorBoard(log_dir='{}/logs/{}'.format(relative_path, NAME))
|
||||
```
|
||||
|
||||
#### Tworzenie sekwencyjnego modelu splotowych sieci neuronowych
|
||||
Tutaj inicjalizuje model jako sekwencyjny, czyli w którym każda warstwa wykonywana jest po kolei.
|
||||
```
|
||||
model = Sequential()
|
||||
```
|
||||
Jako iż tworzę splotową sieć neuronową w której podstawową strukturą jest
|
||||
```mermaid
|
||||
graph LR
|
||||
A((splot)) --> B(suma)
|
||||
B --> C((splot))
|
||||
C --> D(suma)
|
||||
D --> E[w pełni połączona warstwa]
|
||||
E --> F[wynik]
|
||||
```
|
||||
to jako pierwsze tworzę warstwę splotu i sumy.
|
||||
```
|
||||
model.add(Conv2D(64, (3, 3),input_shape=X.shape[1:]))
|
||||
model.add(Activation('relu'))
|
||||
model.add(MaxPooling2D(pool_size=(2, 2)))
|
||||
```
|
||||
Warstwa splotu opiera się na 64 filtrach, które sprawdzają pola o powierzchni 9 pixeli.
|
||||
Jak działa splot? Splot to czynność polegająca na pobieraniu oryginalnych danych i tworzeniu z nich mapy cech z pól o zadeklarowanych wymiarach. Jest ich tak dużo jak pozwala na to nam całość naszych danych.
|
||||
Po czym wykonywana jest funkcja aktywacyjna ReLu.
|
||||
A na końcu na naszej wykonujemy pooling z atrybutem **max**, czyli z wielkości 2 na 2 z naszej warstwy splotu wybieramy największą wartość.
|
||||
|
||||
W tym pod projekcie najbardziej efektywne okazało się zastosowanie dwóch takich warstw.
|
||||
```
|
||||
layer size | conv layer | Dense layer |
|
||||
64 | 1 | 0 | loss: 0.0443 - accuracy: 0.9942 - val_loss: 0.3614 - val_accuracy: 0.7692
|
||||
64 | 2 | 0 | loss: 0.0931 - accuracy: 0.9625 - val_loss: 0.4772 - val_accuracy: 0.8462
|
||||
64 | 3 | 0 | loss: 0.2491 - accuracy: 0.9020 - val_loss: 0.3762 - val_accuracy: 0.7949
|
||||
64 | 1 | 1 | loss: 0.0531 - accuracy: 0.9971 - val_loss: 0.4176 - val_accuracy: 0.8205
|
||||
64 | 2 | 1 | loss: 0.0644 - accuracy: 0.9798 - val_loss: 0.5606 - val_accuracy: 0.8462
|
||||
64 | 3 | 1 | loss: 0.1126 - accuracy: 0.9625 - val_loss: 0.5916 - val_accuracy: 0.8205
|
||||
```
|
||||
![accuracy](src/SubprojectMaksymilianKierski/Data/LogsIMG/accuracy.png)
|
||||
![loss](src/SubprojectMaksymilianKierski/Data/LogsIMG/loss.png)
|
||||
|
||||
W drugim przypadku nie musimy już zmieniać naszych danych ponieważ pochodzą one z poprzedniej warstwy.
|
||||
```
|
||||
model.add(Conv2D(64, (3, 3),input_shape=X.shape[1:]))
|
||||
model.add(Activation('relu'))
|
||||
model.add(MaxPooling2D(pool_size=(2, 2)))
|
||||
|
||||
model.add(Conv2D(64, (3, 3)))
|
||||
model.add(Activation('relu'))
|
||||
model.add(MaxPooling2D(pool_size=(2, 2)))
|
||||
```
|
||||
Na koniec spłaszczam naszą trójwymiarową tablice, na macierz stosując funkcje Flatten(), oraz używam warstwę Dense z tylko jednym neuronem, który będzie naszym wynikiem. Funkcja aktywacji jest to oczywiście funkcja sigmoid ponieważ chcemy otrzymać wynik (0-1).
|
||||
|
||||
```
|
||||
model.add(Flatten())
|
||||
model.add(Dense(1))
|
||||
model.add(Activation('sigmoid'))
|
||||
```
|
||||
Następnie zachodzi kompilacja modelu. Używam funkcję straty **binary_crosentropy** , ponieważ mierzymy się z problemem klasyfikacji binarnej. Problem klasyfikacji binarnej jest wtedy, gdy do rozpatrzenia mamy tylko dwa przypadki, w tym zadaniu talerz z jedzeniem(0) oraz bez jedzenia(1). Optymalizator, który użyłem jest to **adam**, a za pomocą **metrics** aktywuje monitorowanie dokładności.
|
||||
Kilka słów o optymalizatorze **adam** - _Adam_, jest obecnie zalecany przy większości zadań optymalizacyjnych związanych z uczeniem, ponieważ łączy on zalety Adadelty i RMSprop, a zatem lepiej radzi sobie z większością problemów.
|
||||
|
||||
|
||||
Na sam koniec wywołuję funkcję uczenia, oraz zapisuję model do wykorzystania go w projekcie głównym.
|
||||
```
|
||||
model.fit(X, y, batch_size=32, epochs=10, validation_split=0.1, callbacks=[tenserboard])
|
||||
|
||||
model.save(relative_path + 'SavedModels/{}.model'.format(NAME))
|
||||
```
|
||||
## Integracja z projektem
|
||||
|
||||
Podprojekt wywołujemy naciskając **m** na klawiaturze, kelner wtedy wybiera losowo stolik i do niego idzie zaimplementowanym wcześniej algorytmem A*. Po dotarciu do wybranego miejsca, możemy wywołać funkcje sprawdzającą talerz **use_model_to_predict('img')**
|
||||
```
|
||||
def prepare(filepath):
|
||||
img_array = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE)
|
||||
new_array = cv2.resize(img_array, (IMG_SIZE, IMG_SIZE))
|
||||
plt.imshow(img_array, cmap=plt.cm.binary)
|
||||
plt.show()
|
||||
return new_array.reshape(-1, IMG_SIZE, IMG_SIZE, 1)
|
||||
|
||||
model = tf.keras.models.load_model(relative_path + 'SavedModels/plate-64x2-cnn.model')
|
||||
prediction = model.predict([prepare(relative_path + 'TestData/' + name + '.jpg')])
|
||||
return int(prediction[0][0])
|
||||
```
|
||||
Funkcja ta konwertuję zdjęcie zadeklarowane do wylosowanego stolika oraz odpowiednio je konwertuję. Następnie ładuję zadeklarowany przez nas model, który zwraca nam odpowiednią liczbę, która w int() daje 0 lub 1. Odpowiednio 0 to talerz pełny a 1 to pusty. Dzięki czemu funkcją **text_speech()** możemy wyświetlić odpowiednią informacje na ekranie.
|
||||
|
@ -1,56 +0,0 @@
|
||||
# Kolejkowanie zadań
|
||||
|
||||
**Metoda uczenia:** Drzewa decyzyjne
|
||||
**Autor:** Dominik Zawadzki
|
||||
|
||||
Funkcja kolejkowania zadań jak i drzewo decyzyjne znajdują się w pliku:
|
||||
```sh
|
||||
decisionTree.py
|
||||
```
|
||||
|
||||
## Funkcje drzewa decyzyjnego:
|
||||
```sh
|
||||
BuildDf(self)
|
||||
```
|
||||
Buduje Data Frame złożony z trzech atrybutów:
|
||||
1. actionName - nazwa zadania
|
||||
2. distance - dystans pomiędzy kelnerem a stolikiem
|
||||
3. priority - priorytet danej akcji
|
||||
|
||||
```sh
|
||||
FindPriorityEntropy(self,df)
|
||||
```
|
||||
Oblicza entropie dla priorytetu za pomocą wzoru:
|
||||
* ent = pi * log2pi
|
||||
gdzie i oznacza wartośc priorytetu i = {0, 1, 2, 3, 4}
|
||||
|
||||
```sh
|
||||
FindAttributesEntropy(self, df, attribute)
|
||||
```
|
||||
Zwraca entropie danego w parametrze atrybutu, gdzie jako prawdopodobieństwo liczona jest ilość wystąpień tego atrybutu w kolejnych priorytetach.
|
||||
|
||||
```sh
|
||||
FindWinner(self, df):
|
||||
```
|
||||
Zwraca atrubut o najwyższym info gain wyliczanym na podstawie wzoru:
|
||||
* ent(pr) - ent(atr)
|
||||
gdzie ent(pr) - entropia po priorytecie
|
||||
a ent(atr) - entropia atrybutu po uwzględnieniu priorytetu
|
||||
|
||||
```sh
|
||||
BuildTree(self, df, tree=None)
|
||||
```
|
||||
Rekurencyjna funkcja budująca drzewo decyzyjne. Na początek wyznaczamy atrybut o najwyższym info gain, a następnie budowane jest z niego poddrzewo, jeśli dane poddrzewo jest "puste", dodajemy je do drzewa i kończymy, bo drzewo zostało zbudowane, jesli nie to funkcja wywołuje samą siebie biorąc za parametr poddrzewo.
|
||||
|
||||
## Funkcje kolejkowania zadań:
|
||||
```sh
|
||||
TasksList(self, name, coordinate):
|
||||
```
|
||||
Funkcja oblicza dystans dzielący kelnera od stolika i razem z nazwą zadania dodawane jest do listy zadań.
|
||||
|
||||
```sh
|
||||
Queue(self, tasksList):
|
||||
```
|
||||
W tej funkcji wykorzystywane jest drzewo decyzyjne.
|
||||
Dla każdego zadanie z listy zadań pętla idzie po odpowiednich wierzchołkach i odnajduje priorytet, który z poprzednimi parametrami dodawany jest do listy stanowiącej kolejkę zadań.
|
||||
Na koniec funkcji kolejka jest sortowana po priorytecie.
|
255
main.py
@ -1,94 +1,22 @@
|
||||
import secrets
|
||||
import sys
|
||||
|
||||
from random import randrange
|
||||
from src.decisionTree import *
|
||||
|
||||
from src.plate import *
|
||||
|
||||
from src.SubprojectMaksymilianKierski.PlateRecognition import use_model_to_predict, text_speech
|
||||
|
||||
# Marcin Dobrowolski
|
||||
from src.SubprojectMarcinDobrowolski.suggestionDecisionTree import *
|
||||
from src.SubprojectMarcinDobrowolski.utility import generateTestData, generateTestExample
|
||||
from src.guest import *
|
||||
from src.graphics import *
|
||||
from src.waiter import *
|
||||
|
||||
if __name__ == "__main__":
|
||||
# SETUP
|
||||
pygame.init()
|
||||
clock = pygame.time.Clock()
|
||||
fps = 40
|
||||
graphics = Graphics()
|
||||
waiter = Waiter(graphics)
|
||||
tree = DecisionTree()
|
||||
|
||||
# Init functions
|
||||
# init functions
|
||||
graphics.drawBackground(waiter.matrix)
|
||||
graphics.update(waiter)
|
||||
|
||||
# AStar
|
||||
goal = None
|
||||
path = ''
|
||||
|
||||
# Dominik
|
||||
queue = []
|
||||
fromBar = 0
|
||||
barNode = (3, 12)
|
||||
node = ()
|
||||
goalList = [0, 0]
|
||||
|
||||
# Marcin Dobrowolski
|
||||
suggestionTreeRoot = SuggestionTree.buildTree(trainingData)
|
||||
newGuests = []
|
||||
actions = []
|
||||
|
||||
# Maksymilian
|
||||
go = 0
|
||||
randomPlate = None
|
||||
plate = 0
|
||||
changePlateVar = 0
|
||||
waitPos = [[1, 2], [1, 5], [1, 8], [5, 4], [5, 8],
|
||||
[8, 2], [8, 5], [8, 8], [12, 3], [12, 7]]
|
||||
|
||||
tabPos = [[1, 2], [1, 5], [1, 8], [5, 4], [5, 8],
|
||||
[8, 2], [8, 5], [8, 8], [12, 3], [12, 7]]
|
||||
|
||||
pltPos = [[2, 3], [2, 6], [2, 9], [4, 4], [4, 8],
|
||||
[9, 3], [9, 6], [9, 9], [11, 4], [11, 8]]
|
||||
|
||||
plateArr = []
|
||||
|
||||
|
||||
def changePlate():
|
||||
if plateArr and len(plateArr) > 0:
|
||||
if randrange(30) == 17:
|
||||
plate = random.choice(plateArr)
|
||||
if not plate.empty:
|
||||
randomPlate = randrange(4) + 11
|
||||
plate.changePlate('plate-empty.png', 'test-{}'.format(randomPlate))
|
||||
|
||||
|
||||
def addGuest():
|
||||
if randrange(10) == 5 and path == '':
|
||||
newGuests.append(Guest(graphics))
|
||||
|
||||
guest = newGuests[len(newGuests) - 1]
|
||||
goal = [0, 0]
|
||||
goal[0] = guest.cord[0]
|
||||
goal[1] = guest.cord[1]
|
||||
|
||||
tree.TasksList('order', goal, [waiter.X, waiter.Y])
|
||||
|
||||
tree.print()
|
||||
print(actions)
|
||||
return tree.ReturnQueueList()
|
||||
|
||||
graphics.update(waiter.X, waiter.Y)
|
||||
|
||||
while True:
|
||||
changePlate()
|
||||
# queue = addGuest()
|
||||
|
||||
for event in pygame.event.get():
|
||||
# rabbit.check(waiter.matrix, waiter.X, waiter.Y)
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
@ -100,173 +28,8 @@ if __name__ == "__main__":
|
||||
sys.exit()
|
||||
break
|
||||
|
||||
if event.key == pygame.K_s:
|
||||
waitPosition = [waiter.X, waiter.Y]
|
||||
tree.TasksList('check', [2, 3], waitPosition)
|
||||
tree.TasksList('eat', [9, 6], waitPosition)
|
||||
tree.TasksList('order', [4, 8], waitPosition)
|
||||
tree.TasksList('goToBar', [11, 4], waitPosition)
|
||||
tree.TasksList('check', [2, 9], waitPosition)
|
||||
tree.TasksList('eat', [1, 1], waitPosition)
|
||||
queue = tree.ReturnQueueList()
|
||||
tree.print()
|
||||
|
||||
# Marcin Dobrowolski
|
||||
if event.key == pygame.K_0:
|
||||
newGuests.append(Guest(graphics))
|
||||
|
||||
# Marcin Dobrowolski
|
||||
if event.key == pygame.K_1:
|
||||
guest = newGuests[0]
|
||||
goal = [0, 0]
|
||||
|
||||
if guest.cord[0] == 2 or guest.cord[1] == 9:
|
||||
goal[0] = guest.cord[0] - 1
|
||||
goal[1] = guest.cord[1]
|
||||
else:
|
||||
goal[0] = guest.cord[0] + 1
|
||||
goal[1] = guest.cord[1]
|
||||
|
||||
actions.append(('takeOrder', goal))
|
||||
print(actions)
|
||||
|
||||
# AStar
|
||||
if event.key == pygame.K_r:
|
||||
temp = False
|
||||
while not temp:
|
||||
x = secrets.randbelow(graphics.width)
|
||||
y = secrets.randbelow(graphics.height)
|
||||
print(x, y)
|
||||
|
||||
if waiter.matrix.matrix[x][y].walk_through == 1:
|
||||
temp = True
|
||||
|
||||
goal = (x, y)
|
||||
path = waiter.findPath(goal)
|
||||
path = waiter.translatePath(path)
|
||||
|
||||
if event.key == pygame.K_a:
|
||||
if path == '':
|
||||
newGuests.append(Guest(graphics))
|
||||
|
||||
guest = newGuests[len(newGuests) - 1]
|
||||
goalList[0] = guest.cord[0]
|
||||
goalList[1] = guest.cord[1]
|
||||
|
||||
tree.TasksList('order', goalList, [waiter.X, waiter.Y])
|
||||
tree.TasksList('goToBar', goalList, [waiter.X, waiter.Y])
|
||||
queue = tree.ReturnQueueList()
|
||||
|
||||
if plateArr and event.key == pygame.K_m:
|
||||
randomPlate = random.choice(plateArr)
|
||||
if (not [waiter.X, waiter.Y] == randomPlate.table) or go == 0:
|
||||
print(waiter.X, waiter.Y)
|
||||
print(randomPlate.table)
|
||||
print(go)
|
||||
print([waiter.X, waiter.Y] in randomPlate.table)
|
||||
if [waiter.X, waiter.Y] in waitPos:
|
||||
model = 'waiter_' + waiter.direction
|
||||
for x in range(-1, 2):
|
||||
waiterX = waiter.X + (x * 0.1)
|
||||
graphics.clear(waiterX, waiter.Y - 1)
|
||||
print(randomPlate)
|
||||
changePlateVar = 0
|
||||
# if this plate is exists
|
||||
goal = (randomPlate.table[0], randomPlate.table[1])
|
||||
path = waiter.findPath(goal)
|
||||
path = waiter.translatePath(path)
|
||||
print('sec-1')
|
||||
|
||||
go = 1
|
||||
plate = 0
|
||||
else:
|
||||
if randomPlate.checked:
|
||||
predict = 'CHECKED'
|
||||
else:
|
||||
predict = use_model_to_predict(randomPlate.pictureAI)
|
||||
|
||||
if predict == 1:
|
||||
predict = 'EMPTY'
|
||||
randomPlate.clearTable()
|
||||
plateArr.remove(randomPlate)
|
||||
tree.TasksList('goToBar', barNode, [waiter.X, waiter.Y])
|
||||
queue = tree.ReturnQueueList()
|
||||
changePlateVar = 1
|
||||
else:
|
||||
predict = 'FOOD'
|
||||
print('sec-3')
|
||||
text_speech('arialnarrow.ttf', 25, predict, (255, 255, 255), (0, 128, 0),
|
||||
(waiter.X * 50 + 25), (waiter.Y * 50 - 25), False, False, screen=graphics.screen)
|
||||
pygame.display.flip()
|
||||
go = 0
|
||||
plate = 0
|
||||
|
||||
if plate == 1 and (goalList == [waiter.X - 1, waiter.Y] or goalList == [waiter.X + 1, waiter.Y]):
|
||||
randTable = goalList
|
||||
randIndex = pltPos.index(goalList)
|
||||
tableCord = tabPos[randIndex]
|
||||
plateCord = pltPos[randIndex]
|
||||
print('rand{}'.format(randIndex))
|
||||
pictureAI = 'test-{}'.format(randIndex)
|
||||
plateArr.append(Plate(graphics, plateCord, tableCord, 'plate-full.png', pictureAI))
|
||||
tabPos.pop(randIndex)
|
||||
pltPos.pop(randIndex)
|
||||
print(plateArr, pltPos)
|
||||
|
||||
goal = (plateArr[len(plateArr) - 1].table[0], plateArr[len(plateArr) - 1].table[1])
|
||||
randGo = len(plateArr) - 1
|
||||
path = waiter.findPath(goal)
|
||||
path = waiter.translatePath(path)
|
||||
print('sec-2')
|
||||
go = 1
|
||||
plate = 0
|
||||
|
||||
|
||||
# Dominik
|
||||
if queue and path == '' and fromBar == 0:
|
||||
print(queue)
|
||||
task = queue.pop(0)
|
||||
print('tasks.{}'.format(task))
|
||||
tree.RemoveTask()
|
||||
node = (task[2][0], task[2][1])
|
||||
if task[0] == "goToBar":
|
||||
path = waiter.findPath(barNode)
|
||||
fromBar = 1
|
||||
else:
|
||||
changePlateVar = 0
|
||||
path = waiter.findPath(node)
|
||||
path = waiter.translatePath(path)
|
||||
|
||||
if path == '' and fromBar == 1 and changePlateVar == 0:
|
||||
path = waiter.findPath(node)
|
||||
path = waiter.translatePath(path)
|
||||
fromBar = 0
|
||||
plate = 1
|
||||
if path == '' and fromBar == 1:
|
||||
path = waiter.findPath(node)
|
||||
path = waiter.translatePath(path)
|
||||
fromBar = 0
|
||||
print(plate)
|
||||
# AStar
|
||||
if path == '' and actions:
|
||||
print('Goal: {}'.format(actions[0][1]))
|
||||
path = waiter.findPath(actions[0][1])
|
||||
print('Path: {}'.format(path))
|
||||
path = waiter.translatePath(path)
|
||||
print('Translated path: {}'.format(path))
|
||||
|
||||
if path != '':
|
||||
nextStep = path[0]
|
||||
path = path[1:]
|
||||
waiter.travel(nextStep, graphics)
|
||||
if path == '':
|
||||
print('')
|
||||
# action = actions.pop(0)
|
||||
# if action[0] == 'takeOrder':
|
||||
# guest = newGuests.pop(0)
|
||||
# waiter.takeOrder(suggestionTreeRoot, guest)
|
||||
# graphics.clearGuest(guest)
|
||||
# tables.append(guest.cord)
|
||||
|
||||
graphics.clear(waiter.X, waiter.Y)
|
||||
waiter.update(event, graphics)
|
||||
graphics.update(waiter.X, waiter.Y)
|
||||
pygame.display.flip()
|
||||
clock.tick(graphics.fps)
|
||||
clock.tick(fps)
|
||||
|
101
path
@ -1,101 +0,0 @@
|
||||
type,size,money,appetite
|
||||
meat,regular,29,34
|
||||
meat,little,38,47
|
||||
meat,regular,33,34
|
||||
meat,regular,54,29
|
||||
meat,regular,47,44
|
||||
meat,regular,27,15
|
||||
meat,regular,32,13
|
||||
meat,little,53,39
|
||||
meat,little,40,44
|
||||
meat,regular,25,39
|
||||
meat,regular,50,52
|
||||
meat,regular,29,20
|
||||
meat,little,58,31
|
||||
meat,regular,36,45
|
||||
meat,regular,59,20
|
||||
meat,regular,54,48
|
||||
meat,regular,25,32
|
||||
meat,regular,36,23
|
||||
meat,regular,57,29
|
||||
meat,little,13,54
|
||||
meat,little,43,52
|
||||
meat,little,58,24
|
||||
meat,little,30,35
|
||||
meat,little,24,33
|
||||
meat,regular,24,30
|
||||
meat,regular,41,48
|
||||
meat,regular,35,37
|
||||
meat,little,36,11
|
||||
meat,regular,22,24
|
||||
meat,regular,58,14
|
||||
meat,little,34,29
|
||||
meat,little,30,38
|
||||
meat,regular,54,45
|
||||
meat,regular,31,43
|
||||
meat,little,58,46
|
||||
meat,little,48,38
|
||||
meat,regular,44,44
|
||||
meat,regular,51,49
|
||||
meat,little,53,34
|
||||
meat,regular,51,48
|
||||
meat,regular,46,35
|
||||
meat,little,32,22
|
||||
meat,little,14,37
|
||||
meat,little,14,37
|
||||
meat,little,46,13
|
||||
meat,regular,40,16
|
||||
meat,regular,40,49
|
||||
meat,little,47,40
|
||||
meat,regular,23,33
|
||||
meat,regular,27,29
|
||||
meat,regular,49,32
|
||||
meat,regular,36,15
|
||||
meat,regular,59,25
|
||||
meat,little,17,43
|
||||
meat,regular,39,49
|
||||
meat,little,35,33
|
||||
meat,little,10,32
|
||||
meat,little,11,49
|
||||
meat,regular,53,15
|
||||
meat,regular,29,13
|
||||
meat,little,12,14
|
||||
meat,little,27,31
|
||||
meat,regular,31,21
|
||||
meat,regular,58,13
|
||||
meat,little,46,19
|
||||
meat,little,11,27
|
||||
meat,regular,44,28
|
||||
meat,regular,40,14
|
||||
meat,little,57,24
|
||||
meat,regular,38,52
|
||||
meat,little,37,31
|
||||
meat,regular,34,15
|
||||
meat,little,41,25
|
||||
meat,regular,30,22
|
||||
meat,little,58,15
|
||||
meat,regular,22,12
|
||||
meat,little,46,44
|
||||
meat,regular,47,11
|
||||
meat,little,52,30
|
||||
meat,little,49,24
|
||||
meat,little,28,30
|
||||
meat,regular,27,19
|
||||
meat,regular,51,43
|
||||
meat,little,59,10
|
||||
meat,regular,38,44
|
||||
meat,regular,30,37
|
||||
meat,little,44,24
|
||||
meat,regular,34,45
|
||||
meat,regular,50,49
|
||||
meat,regular,36,50
|
||||
meat,little,13,22
|
||||
meat,little,28,25
|
||||
meat,regular,29,14
|
||||
meat,regular,56,17
|
||||
meat,regular,57,45
|
||||
meat,little,28,22
|
||||
meat,regular,41,53
|
||||
meat,little,10,19
|
||||
meat,little,32,53
|
||||
meat,little,29,22
|
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 3.4 KiB |
BIN
resources/images/waiter.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
@ -1,6 +0,0 @@
|
||||
0 | price: 0 x: 0 y: 0
|
||||
1 | price: 100 x: 0 y: 0
|
||||
0 | price: 0 x: 1 y: 1
|
||||
1 | price: 100 x: 1 y: 1
|
||||
0 | price: 0 x: 2 y: 1
|
||||
1 | price: 100 x: 2 y: 1
|
@ -1,13 +1,13 @@
|
||||
______________
|
||||
______________
|
||||
__PW_____PW___
|
||||
__SWP____SWP__
|
||||
__OWS____OWS__
|
||||
_USWP___USWP__
|
||||
__OWS____OWSI_
|
||||
__PWO____PWO__
|
||||
__SW_____SW___
|
||||
_USW____USW___
|
||||
__OWP____OWP__
|
||||
__PWS____PWS__
|
||||
__SWO____SWO__
|
||||
__PWS____PWSI_
|
||||
_USWO___USWO__
|
||||
__OW_____OW___
|
||||
______________
|
||||
______________
|
||||
|
@ -1,77 +0,0 @@
|
||||
# Raport 2
|
||||
|
||||
Algorytm A* znajduje się w pliku:
|
||||
```sh
|
||||
waiter.py
|
||||
```
|
||||
jako funkcja
|
||||
```sh
|
||||
findPath(self, goal)
|
||||
```
|
||||
Rozpoczyna się stworzeniem pustych list i określeniem wierzchołka startowego i końcowego.
|
||||
Dla startowego wierzchołka określany jest rodzic: *None*
|
||||
|
||||
## Pętla główna algorytmu
|
||||
Główna pętla będzie trwać do momentu gdy *openList* będzie pusta.
|
||||
Na początek *openList* jest sortowana, następnie usuwany jest z niej pierwszy wierzchołek i dodawany jest do *closedList*.
|
||||
|
||||
Następnym krokiem jest dodanie do tabeli *children* wierzchołki sąsiednie - sprawdzając czy takie istnieją tj. czy ich pozycja nie wychodzi poza zakres.
|
||||
|
||||
### Wyszukiwanie następnika
|
||||
Wyszukiwanie następnika polega na znalezieniu następnego wierzchołka do jakiego powinien pójść agent. Zawarte jest ono w pętli:
|
||||
```sh
|
||||
for child in children:
|
||||
```
|
||||
Na początku sprawdzamy czy danego "dziecka" nie ma w *closedList* tzn. wykluczamy możliwość cofnięcia się agenta po wierzchołkach, które już przeszliśmy.
|
||||
Potem aktualizowane są parametry wierzchołka:
|
||||
- ustalamy rodzica jako *currentNode*
|
||||
- modyfikujemy *startCost* dodając jedynkę do *startCost* obecnego wierzchołka
|
||||
- ustalamy heurystykę jako suma długości przyprostokątnych
|
||||
- obliczamy *totalCost* dodając do siebie *startCost* i *heuristic*
|
||||
|
||||
Na koniec sprawdzamy jeszcze czy danego dziecka nie ma już w *openList*, a jeśli nie, to dodajemy go do *openList*.
|
||||
|
||||
### Zwracanie ścieżki
|
||||
Zwracanie ścieżki odbywa się gdy *currentNode* jest taki sam jak końcowy wierzchołek.
|
||||
Tworzymy wtedy ścieżkę cofając się od końcowego wierzchołka do wierzchołka, którego rodzic jest równy: *None*
|
||||
|
||||
# Translacja ścieżki
|
||||
Znajduje się w pliku
|
||||
```sh
|
||||
waiter.py
|
||||
```
|
||||
jako funkcja
|
||||
```sh
|
||||
translatePath(self, path):
|
||||
```
|
||||
Przyjmuje jako argument ściężkę zwracaną przez algorytm A*.
|
||||
Funckja ta sprawdza każdy wierzchołek ścieżki i porównując z obecną pozycją kelnera i kierunkiem w jaki jest odwrócony "tłumaczy" go na odpowiedni ciąg znaków, gdzie:
|
||||
- F - oznacza ruch do przodu
|
||||
- R - obrót w prawo
|
||||
- L - obrót w lewo
|
||||
|
||||
Następnie zwraca ciąg *output*
|
||||
|
||||
[//]: # (These are reference links used in the body of this note and get stripped out when the markdown processor does its job. There is no need to format nicely because it shouldn't be seen. Thanks SO - http://stackoverflow.com/questions/4823468/store-comments-in-markdown-syntax)
|
||||
|
||||
|
||||
[dill]: <https://github.com/joemccann/dillinger>
|
||||
[git-repo-url]: <https://github.com/joemccann/dillinger.git>
|
||||
[john gruber]: <http://daringfireball.net>
|
||||
[df1]: <http://daringfireball.net/projects/markdown/>
|
||||
[markdown-it]: <https://github.com/markdown-it/markdown-it>
|
||||
[Ace Editor]: <http://ace.ajax.org>
|
||||
[node.js]: <http://nodejs.org>
|
||||
[Twitter Bootstrap]: <http://twitter.github.com/bootstrap/>
|
||||
[jQuery]: <http://jquery.com>
|
||||
[@tjholowaychuk]: <http://twitter.com/tjholowaychuk>
|
||||
[express]: <http://expressjs.com>
|
||||
[AngularJS]: <http://angularjs.org>
|
||||
[Gulp]: <http://gulpjs.com>
|
||||
|
||||
[PlDb]: <https://github.com/joemccann/dillinger/tree/master/plugins/dropbox/README.md>
|
||||
[PlGh]: <https://github.com/joemccann/dillinger/tree/master/plugins/github/README.md>
|
||||
[PlGd]: <https://github.com/joemccann/dillinger/tree/master/plugins/googledrive/README.md>
|
||||
[PlOd]: <https://github.com/joemccann/dillinger/tree/master/plugins/onedrive/README.md>
|
||||
[PlMe]: <https://github.com/joemccann/dillinger/tree/master/plugins/medium/README.md>
|
||||
[PlGa]: <https://github.com/RahulHP/dillinger/blob/master/plugins/googleanalytics/README.md>
|
Before Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 307 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 93 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 8.5 KiB |
Before Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 353 KiB |
Before Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 238 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 9.4 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 160 KiB |
Before Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 187 KiB |