W porządku! W końcu udało mi się uzyskać konsekwentną pracę! Ten problem wciągnął mnie na kilka dni ... Zabawne rzeczy! Przepraszam za długość tej odpowiedzi, ale muszę trochę rozwinąć niektóre rzeczy ... (Chociaż mogę ustanowić rekord dla najdłuższej odpowiedzi na temat przepełnienia stosu, która nie jest spamem!)
Na marginesie, używam pełnego zbioru danych, do którego Ivo podał link w swoim pierwotnym pytaniu . Jest to seria plików rar (po jednym na psa), z których każdy zawiera kilka różnych przebiegów eksperymentów przechowywanych jako tablice ascii. Zamiast próbować kopiować i wklejać przykłady samodzielnego kodu do tego pytania, oto repozytorium bitbucket mercurial z pełnym, samodzielnym kodem. Możesz go sklonować za pomocą
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
Przegląd
Jak zauważyłeś w swoim pytaniu, zasadniczo istnieją dwa sposoby podejścia do problemu. Właściwie zamierzam używać obu na różne sposoby.
- Użyj kolejności (czasowej i przestrzennej) uderzeń łap, aby określić, która łapa jest która.
- Spróbuj zidentyfikować „odcisk łapy” wyłącznie na podstawie jego kształtu.
Zasadniczo pierwsza metoda działa z łapami psa zgodnie z trapezoidalnym wzorem pokazanym w pytaniu Ivo powyżej, ale zawodzi, gdy łapy nie podążają za tym wzorem. Dość łatwo jest programowo wykryć, kiedy nie działa.
Dlatego możemy wykorzystać pomiary, w których działało, do zbudowania zestawu danych treningowych (z ~ 2000 uderzeń łap od ~ 30 różnych psów), aby rozpoznać, która łapa jest która, a problem sprowadza się do nadzorowanej klasyfikacji (z kilkoma dodatkowymi zmarszczkami. .. Rozpoznawanie obrazu jest nieco trudniejsze niż „normalny” nadzorowany problem klasyfikacji).
Analiza wzorców
Aby rozwinąć pierwszą metodę, kiedy pies normalnie chodzi (nie biegnie!) (Czego niektóre z tych psów mogą nie być), spodziewamy się uderzenia łap w kolejności: przednia lewa, tylna prawa, przednia prawa, tylna lewa , Przednia lewa itd. Wzór może zaczynać się od przedniej lewej lub przedniej prawej łapy.
Gdyby tak było zawsze, moglibyśmy po prostu posortować uderzenia według czasu początkowego kontaktu i użyć modulo 4, aby pogrupować je według łap.
Jednak nawet jeśli wszystko jest „normalne”, to nie działa. Wynika to z trapezowego kształtu wzoru. Tylna łapa przestrzennie opada za poprzednią przednią łapę.
Dlatego uderzenie tylnej łapy po początkowym uderzeniu przedniej łapy często spada z płytki czujnika i nie jest rejestrowane. Podobnie, ostatnie uderzenie łapy często nie jest kolejną łapą w sekwencji, ponieważ uderzenie łapy, zanim zdarzyło się to z płytki czujnika, nie zostało zarejestrowane.
Niemniej jednak, możemy użyć kształtu wzoru uderzenia łapy, aby określić, kiedy to się stało i czy zaczęliśmy od lewej czy prawej przedniej łapy. (Właściwie to ignoruję tutaj problemy z ostatnim wpływem. Jednak dodanie go nie jest trudne.)
def group_paws(data_slices, time):
# Sort slices by initial contact time
data_slices.sort(key=lambda s: s[-1].start)
# Get the centroid for each paw impact...
paw_coords = []
for x,y,z in data_slices:
paw_coords.append([(item.stop + item.start) / 2.0 for item in (x,y)])
paw_coords = np.array(paw_coords)
# Make a vector between each sucessive impact...
dx, dy = np.diff(paw_coords, axis=0).T
#-- Group paws -------------------------------------------
paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
paw_number = np.arange(len(paw_coords))
# Did we miss the hind paw impact after the first
# front paw impact? If so, first dx will be positive...
if dx[0] > 0:
paw_number[1:] += 1
# Are we starting with the left or right front paw...
# We assume we're starting with the left, and check dy[0].
# If dy[0] > 0 (i.e. the next paw impacts to the left), then
# it's actually the right front paw, instead of the left.
if dy[0] > 0: # Right front paw impact...
paw_number += 2
# Now we can determine the paw with a simple modulo 4..
paw_codes = paw_number % 4
paw_labels = [paw_code[code] for code in paw_codes]
return paw_labels
Mimo wszystko często nie działa poprawnie. Wydaje się, że wiele psów w pełnym zbiorze danych biegnie, a uderzenia łap nie są zgodne w tej samej kolejności czasowej, co podczas spaceru. (A może pies ma po prostu poważne problemy z biodrami ...)
Na szczęście nadal możemy programowo wykrywać, czy uderzenia łap są zgodne z naszym oczekiwanym wzorcem przestrzennym:
def paw_pattern_problems(paw_labels, dx, dy):
"""Check whether or not the label sequence "paw_labels" conforms to our
expected spatial pattern of paw impacts. "paw_labels" should be a sequence
of the strings: "LH", "RH", "LF", "RF" corresponding to the different paws"""
# Check for problems... (This could be written a _lot_ more cleanly...)
problems = False
last = paw_labels[0]
for paw, dy, dx in zip(paw_labels[1:], dy, dx):
# Going from a left paw to a right, dy should be negative
if last.startswith('L') and paw.startswith('R') and (dy > 0):
problems = True
break
# Going from a right paw to a left, dy should be positive
if last.startswith('R') and paw.startswith('L') and (dy < 0):
problems = True
break
# Going from a front paw to a hind paw, dx should be negative
if last.endswith('F') and paw.endswith('H') and (dx > 0):
problems = True
break
# Going from a hind paw to a front paw, dx should be positive
if last.endswith('H') and paw.endswith('F') and (dx < 0):
problems = True
break
last = paw
return problems
Dlatego nawet jeśli prosta klasyfikacja przestrzenna nie działa przez cały czas, możemy określić, kiedy działa z rozsądną pewnością.
Zestaw danych treningowych
Z klasyfikacji opartych na wzorcach, gdzie działało to poprawnie, możemy zbudować bardzo duży zestaw danych treningowych prawidłowo sklasyfikowanych łap (~ 2400 uderzeń łap z 32 różnych psów!).
Możemy teraz zacząć przyglądać się, jak wygląda „przeciętna” przednia lewa łapa.
Aby to zrobić, potrzebujemy pewnego rodzaju „metryki łapy”, która ma taką samą wymiarowość dla każdego psa. (W pełnym zestawie danych znajdują się zarówno bardzo duże, jak i bardzo małe psy!) Odcisk łapy irlandzkiego elkhounda będzie zarówno znacznie szerszy, jak i znacznie „cięższy” niż odcisk łapy pudla zabawkowego. Musimy przeskalować każdy odcisk łapy, aby a) miał taką samą liczbę pikseli oraz b) wartości nacisku były znormalizowane. Aby to zrobić, ponownie próbowałem każdy odcisk łapy na siatkę 20x20 i przeskalowałem wartości nacisku w oparciu o maksymalną, minimalną i średnią wartość nacisku dla uderzenia łapy.
def paw_image(paw):
from scipy.ndimage import map_coordinates
ny, nx = paw.shape
# Trim off any "blank" edges around the paw...
mask = paw > 0.01 * paw.max()
y, x = np.mgrid[:ny, :nx]
ymin, ymax = y[mask].min(), y[mask].max()
xmin, xmax = x[mask].min(), x[mask].max()
# Make a 20x20 grid to resample the paw pressure values onto
numx, numy = 20, 20
xi = np.linspace(xmin, xmax, numx)
yi = np.linspace(ymin, ymax, numy)
xi, yi = np.meshgrid(xi, yi)
# Resample the values onto the 20x20 grid
coords = np.vstack([yi.flatten(), xi.flatten()])
zi = map_coordinates(paw, coords)
zi = zi.reshape((numy, numx))
# Rescale the pressure values
zi -= zi.min()
zi /= zi.max()
zi -= zi.mean() #<- Helps distinguish front from hind paws...
return zi
Po tym wszystkim, możemy wreszcie przyjrzeć się, jak wygląda przeciętna lewa przednia, tylna prawa itd. Łapa. Zwróć uwagę, że jest to uśrednione dla> 30 psów o bardzo różnych rozmiarach i wydaje się, że uzyskujemy spójne wyniki!
Jednak zanim przeprowadzimy na nich jakąkolwiek analizę, musimy odjąć średnią (średnią łapę dla wszystkich nóg wszystkich psów).
Teraz możemy przeanalizować różnice od średniej, które są nieco łatwiejsze do rozpoznania:
Rozpoznawanie łapy na podstawie obrazu
Ok ... Wreszcie mamy zestaw wzorców, do których możemy zacząć dopasowywać łapy. Każda łapa może być traktowana jako 400-wymiarowy wektor (zwracany przez paw_image
funkcję), który można porównać do tych czterech 400-wymiarowych wektorów.
Niestety, jeśli użyjemy tylko „normalnego” nadzorowanego algorytmu klasyfikacji (tj. Stwierdzimy, który z 4 wzorców jest najbliższy danemu odciskowi łapy przy użyciu prostej odległości), nie działa on konsekwentnie. W rzeczywistości nie jest to dużo lepsze niż przypadkowa szansa na zestawie danych treningowych.
Jest to częsty problem w rozpoznawaniu obrazu. Ze względu na dużą wymiarowość danych wejściowych i nieco „rozmyty” charakter obrazów (tj. Sąsiednie piksele mają wysoką kowariancję), samo spojrzenie na różnicę obrazu z obrazu szablonu nie daje bardzo dobrego pomiaru podobieństwo ich kształtów.
Łapy własne
Aby obejść ten problem, musimy zbudować zestaw „własnych łap” (tak jak „własne twarze” w rozpoznawaniu twarzy) i opisać każdy odcisk łapy jako kombinację tych własnych łap. Jest to identyczne z analizą głównych komponentów i zasadniczo zapewnia sposób na zmniejszenie wymiarowości naszych danych, dzięki czemu odległość jest dobrą miarą kształtu.
Ponieważ mamy więcej obrazów szkoleniowych niż wymiarów (2400 vs 400), nie ma potrzeby robienia „wymyślnej” algebry liniowej dla szybkości. Możemy pracować bezpośrednio z macierzą kowariancji zbioru danych uczących:
def make_eigenpaws(paw_data):
"""Creates a set of eigenpaws based on paw_data.
paw_data is a numdata by numdimensions matrix of all of the observations."""
average_paw = paw_data.mean(axis=0)
paw_data -= average_paw
# Determine the eigenvectors of the covariance matrix of the data
cov = np.cov(paw_data.T)
eigvals, eigvecs = np.linalg.eig(cov)
# Sort the eigenvectors by ascending eigenvalue (largest is last)
eig_idx = np.argsort(eigvals)
sorted_eigvecs = eigvecs[:,eig_idx]
sorted_eigvals = eigvals[:,eig_idx]
# Now choose a cutoff number of eigenvectors to use
# (50 seems to work well, but it's arbirtrary...
num_basis_vecs = 50
basis_vecs = sorted_eigvecs[:,-num_basis_vecs:]
return basis_vecs
To basis_vecs
są „własne łapy”.
Aby ich użyć, po prostu kropkujemy (tj. Mnożenie macierzy) każdy obraz łapy (jako wektor o 400 wymiarach, a nie obraz 20x20) wektorami bazowymi. To daje nam 50-wymiarowy wektor (jeden element na wektor bazowy), którego możemy użyć do sklasyfikowania obrazu. Zamiast porównywać obraz 20x20 z obrazem 20x20 każdej łapy „szablonu”, porównujemy 50-wymiarowy, transformowany obraz z każdą 50-wymiarową transformowaną łapą szablonu. Jest to znacznie mniej wrażliwe na małe różnice w dokładnym ustawieniu każdego palca itp., I zasadniczo ogranicza wymiarowość problemu tylko do odpowiednich wymiarów.
Klasyfikacja łapy na podstawie własnej łapy
Teraz możemy po prostu użyć odległości między 50-wymiarowymi wektorami a wektorami „szablonowymi” dla każdej nogi, aby sklasyfikować, która łapa jest którą:
codebook = np.load('codebook.npy') # Template vectors for each paw
average_paw = np.load('average_paw.npy')
basis_stds = np.load('basis_stds.npy') # Needed to "whiten" the dataset...
basis_vecs = np.load('basis_vecs.npy')
paw_code = {0:'LF', 1:'RH', 2:'RF', 3:'LH'}
def classify(paw):
paw = paw.flatten()
paw -= average_paw
scores = paw.dot(basis_vecs) / basis_stds
diff = codebook - scores
diff *= diff
diff = np.sqrt(diff.sum(axis=1))
return paw_code[diff.argmin()]
Oto niektóre wyniki:
Pozostałe problemy
Nadal są pewne problemy, szczególnie w przypadku psów zbyt małych, aby zrobić wyraźny odcisk łapy ... (Działa najlepiej w przypadku dużych psów, ponieważ palce są wyraźniej oddzielone przy rozdzielczości czujnika). Ponadto częściowe odciski łap nie są rozpoznawane w tym przypadku systemu, podczas gdy mogą być z systemem opartym na wzorze trapezowym.
Jednakże, ponieważ analiza łapy własnej z natury wykorzystuje metrykę odległości, możemy sklasyfikować łapy w obie strony i wrócić do systemu opartego na wzorze trapezoidalnym, gdy najmniejsza odległość analizy łapy własnej od „książki kodów” przekracza pewien próg. Jednak jeszcze tego nie zaimplementowałem.
Uff ... To było długo! Mam głowę do Ivo za takie zabawne pytanie!