เอาล่ะ! ในที่สุดฉันก็สามารถทำงานได้อย่างสม่ำเสมอ! ปัญหานี้ดึงฉันเข้ามาหลายวัน ... สนุกมาก! ขออภัยสำหรับความยาวของคำตอบนี้ แต่ฉันต้องอธิบายบางสิ่งอย่างละเอียด ... (แม้ว่าฉันจะสร้างสถิติสำหรับคำตอบที่ไม่ใช่สแปม stackoverflow ที่ยาวที่สุดเท่าที่เคยมีมา!)
ตามบันทึกข้างฉันใช้ชุดข้อมูลเต็มรูปแบบที่ Ivo จัดให้มีการเชื่อมโยงไปของเขาในคำถามเดิม เป็นชุดของไฟล์ rar (หนึ่งตัวต่อสุนัข) ซึ่งแต่ละไฟล์มีการทดสอบที่แตกต่างกันหลายรายการที่จัดเก็บเป็นอาร์เรย์ ascii แทนที่จะพยายามคัดลอกและวางตัวอย่างโค้ดแบบสแตนด์อะโลนในคำถามนี้นี่คือที่เก็บ bitbucket mercurial ที่มีโค้ดแบบสแตนด์อะโลนเต็มรูปแบบ คุณสามารถโคลนด้วยไฟล์
hg clone https://joferkington@bitbucket.org/joferkington/paw-analysis
ภาพรวม
โดยพื้นฐานแล้วมีสองวิธีในการแก้ไขปัญหาดังที่คุณระบุไว้ในคำถามของคุณ ฉันจะใช้ทั้งสองอย่างในรูปแบบที่ต่างกัน
- ใช้คำสั่ง (ชั่วคราวและเชิงพื้นที่) ของผลกระทบของอุ้งเท้าเพื่อกำหนดว่าอุ้งเท้าใด
- พยายามระบุ "ตีนผี" ตามรูปร่างของมัน
โดยทั่วไปวิธีแรกใช้ได้กับอุ้งเท้าของสุนัขตามรูปแบบคล้ายสี่เหลี่ยมคางหมูที่แสดงในคำถามของ Ivo ด้านบน แต่จะล้มเหลวเมื่อใดก็ตามที่อุ้งเท้าไม่เป็นไปตามรูปแบบนั้น มันค่อนข้างง่ายที่จะตรวจจับทางโปรแกรมเมื่อไม่ได้ผล
ดังนั้นเราสามารถใช้การวัดที่ได้ผลในการสร้างชุดข้อมูลการฝึกอบรม (จาก ~ 2,000 ผลกระทบของอุ้งเท้าจากสุนัขที่แตกต่างกัน ~ 30 ตัว) เพื่อรับรู้ว่าอุ้งเท้าคืออะไรและปัญหาจะลดลงเป็นการจัดประเภทภายใต้การดูแล (มีริ้วรอยเพิ่มเติมบางส่วน .. การจดจำรูปภาพยากกว่าปัญหาการจัดหมวดหมู่ที่มีการควบคุมดูแล "ปกติ" เล็กน้อย)
การวิเคราะห์รูปแบบ
เพื่ออธิบายรายละเอียดเกี่ยวกับวิธีการแรกเมื่อสุนัขเดิน (ไม่วิ่ง!) ตามปกติ (ซึ่งสุนัขเหล่านี้บางตัวอาจไม่ใช่) เราคาดว่าอุ้งเท้าจะกระทบตามลำดับ: ด้านหน้าซ้าย, หลังขวา, ด้านหน้าขวา, หลังซ้าย , ด้านหน้าซ้าย ฯลฯ รูปแบบอาจเริ่มต้นด้วยอุ้งเท้าด้านหน้าซ้ายหรือด้านหน้าขวา
หากเป็นเช่นนี้เสมอเราสามารถจัดเรียงผลกระทบตามเวลาสัมผัสครั้งแรกและใช้โมดูโล 4 เพื่อจัดกลุ่มตามอุ้งเท้า
อย่างไรก็ตามแม้ว่าทุกอย่างจะ "ปกติ" แต่ก็ไม่ได้ผล นี่เป็นเพราะรูปแบบคล้ายสี่เหลี่ยมคางหมู อุ้งเท้าหลังวางอยู่ด้านหลังอุ้งเท้าหน้าก่อนหน้า
ดังนั้นผลกระทบของอุ้งเท้าหลังหลังจากการกระแทกของอุ้งเท้าหน้าครั้งแรกมักจะหลุดออกจากแผ่นเซ็นเซอร์และจะไม่ได้รับการบันทึก ในทำนองเดียวกันการกระแทกอุ้งเท้าครั้งสุดท้ายมักไม่ใช่อุ้งเท้าถัดไปในลำดับเนื่องจากการกระแทกของอุ้งเท้าก่อนที่จะเกิดขึ้นจากแผ่นเซ็นเซอร์และไม่ได้รับการบันทึก
อย่างไรก็ตามเราสามารถใช้รูปร่างของรูปแบบการกระแทกอุ้งเท้าเพื่อพิจารณาว่าสิ่งนี้เกิดขึ้นเมื่อใดและเราเริ่มต้นด้วยอุ้งเท้าหน้าซ้ายหรือขวา (ฉันกำลังมองข้ามปัญหาเกี่ยวกับผลกระทบสุดท้ายที่นี่จริงๆแล้วมันไม่ยากเกินไปที่จะเพิ่มเข้าไป)
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
แม้ว่าทั้งหมดนี้มักจะทำงานไม่ถูกต้อง สุนัขหลายตัวในชุดข้อมูลทั้งหมดดูเหมือนจะวิ่งอยู่และการกระแทกของอุ้งเท้าจะไม่เป็นไปตามลำดับเวลาเช่นเดียวกับเมื่อสุนัขกำลังเดิน (หรือบางทีสุนัขก็มีปัญหาที่สะโพกอย่างรุนแรง ... )
โชคดีที่เรายังตรวจพบทางโปรแกรมได้ว่าผลกระทบที่อุ้งเท้าเป็นไปตามรูปแบบเชิงพื้นที่ที่เราคาดไว้หรือไม่:
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
ดังนั้นแม้ว่าการจำแนกเชิงพื้นที่อย่างง่ายจะไม่ได้ผลตลอดเวลา แต่เราสามารถระบุได้ว่าเมื่อใดจะได้ผลด้วยความมั่นใจอย่างสมเหตุสมผล
ชุดข้อมูลการฝึกอบรม
จากการจำแนกตามรูปแบบที่มันทำงานได้อย่างถูกต้องเราสามารถสร้างชุดข้อมูลการฝึกขนาดใหญ่ของอุ้งเท้าที่จำแนกได้อย่างถูกต้อง (~ 2400 ผลอุ้งเท้าจากสุนัข 32 ตัวที่แตกต่างกัน!)
ตอนนี้เราสามารถเริ่มดูว่า "ค่าเฉลี่ย" ด้านหน้าซ้าย ฯลฯ อุ้งเท้ามีลักษณะอย่างไร
ในการทำเช่นนี้เราต้องใช้ "เมตริกอุ้งเท้า" ที่มีขนาดเท่ากันสำหรับสุนัขทุกตัว (ในชุดข้อมูลแบบเต็มมีทั้งสุนัขตัวใหญ่และตัวเล็กมาก!) ลายอุ้งเท้าจากเอลคาวนด์ไอริชจะทั้งกว้างและ "หนัก" กว่าการพิมพ์อุ้งเท้าจากพุดเดิ้ลทอย เราจำเป็นต้องปรับขนาดการพิมพ์แต่ละอุ้งเท้าเพื่อให้ a) มีจำนวนพิกเซลเท่ากันและ b) ค่าความดันเป็นมาตรฐาน ในการทำเช่นนี้ฉันได้สุ่มตัวอย่างการพิมพ์อุ้งเท้าแต่ละชิ้นลงในตาราง 20x20 และปรับขนาดค่าความดันตามค่าสูงสุดค่าต่ำสุดและค่าความดันเฉลี่ยสำหรับผลกระทบของอุ้งเท้า
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
หลังจากทั้งหมดนี้ในที่สุดเราก็สามารถดูได้ว่าด้านหน้าซ้ายเฉลี่ยหลังขวา ฯลฯ อุ้งเท้าเป็นอย่างไร โปรดทราบว่านี่เป็นค่าเฉลี่ยของสุนัขมากกว่า 30 ตัวที่มีขนาดแตกต่างกันมากและดูเหมือนว่าเราจะได้ผลลัพธ์ที่สม่ำเสมอ!
อย่างไรก็ตามก่อนที่เราจะทำการวิเคราะห์สิ่งเหล่านี้เราจำเป็นต้องลบค่าเฉลี่ย (อุ้งเท้าเฉลี่ยสำหรับขาทั้งหมดของสุนัขทุกตัว)
ตอนนี้เราสามารถวิเคราะห์ความแตกต่างจากค่าเฉลี่ยซึ่งง่ายต่อการจดจำ:
การจดจำอุ้งเท้าแบบรูปภาพ
โอเค ... ในที่สุดเราก็มีชุดรูปแบบที่เราสามารถเริ่มลองจับคู่อุ้งเท้าได้ อุ้งเท้าแต่ละตัวสามารถถือเป็นเวกเตอร์ 400 มิติ (ส่งคืนโดยไฟล์paw_image
ฟังก์ชัน) ซึ่งสามารถเปรียบเทียบกับเวกเตอร์ 400 มิติทั้งสี่นี้ได้
น่าเสียดายที่หากเราใช้อัลกอริธึมการจัดหมวดหมู่ที่มีการควบคุมดูแล "ปกติ" (เช่นค้นหาว่ารูปแบบใดใน 4 รูปแบบที่ใกล้เคียงกับการพิมพ์อุ้งเท้าโดยใช้ระยะทางที่เรียบง่าย) มันจะไม่ทำงานอย่างสม่ำเสมอ ในความเป็นจริงมันไม่ได้ดีไปกว่าโอกาสสุ่มในชุดข้อมูลการฝึกอบรม
นี่เป็นปัญหาทั่วไปในการจดจำภาพ เนื่องจากข้อมูลที่ป้อนเข้ามีมิติข้อมูลสูงและลักษณะของภาพที่ค่อนข้าง "เลือน" (เช่นพิกเซลที่อยู่ติดกันมีความแปรปรวนร่วมสูง) เพียงแค่ดูที่ความแตกต่างของภาพจากภาพแม่แบบไม่ได้ให้การวัดที่ดีนัก ความคล้ายคลึงกันของรูปร่าง
Eigenpaws
ในการหลีกเลี่ยงปัญหานี้เราจำเป็นต้องสร้างชุด "eigenpaws" (เช่นเดียวกับ "eigenfaces" ในการจดจำใบหน้า) และอธิบายการพิมพ์อุ้งเท้าแต่ละชิ้นเป็นการรวมกันของ eigenpaws เหล่านี้ สิ่งนี้เหมือนกับการวิเคราะห์องค์ประกอบหลักและโดยพื้นฐานแล้วเป็นวิธีลดขนาดของข้อมูลของเราดังนั้นระยะทางจึงเป็นตัวชี้วัดที่ดี
เนื่องจากเรามีภาพการฝึกอบรมมากกว่าขนาด (2400 vs 400) จึงไม่จำเป็นต้องทำพีชคณิตเชิงเส้นแบบ "แฟนซี" เพื่อความเร็ว เราสามารถทำงานโดยตรงกับเมทริกซ์ความแปรปรวนร่วมของชุดข้อมูลการฝึกอบรม:
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
สิ่งเหล่านี้basis_vecs
คือ "eigenpaws"
ในการใช้สิ่งเหล่านี้เราเพียงแค่ดอท (เช่นการคูณเมทริกซ์) แต่ละรูปอุ้งเท้า (เป็นเวกเตอร์ 400 มิติแทนที่จะเป็นภาพ 20x20) ด้วยเวกเตอร์พื้นฐาน สิ่งนี้ทำให้เรามีเวกเตอร์ 50 มิติ (หนึ่งองค์ประกอบต่อเวกเตอร์พื้นฐาน) ที่เราสามารถใช้เพื่อจำแนกภาพได้ แทนที่จะเปรียบเทียบภาพขนาด 20x20 กับภาพ 20x20 ของอุ้งเท้า "แม่แบบ" แต่ละภาพเราจะเปรียบเทียบภาพขนาด 50 มิติกับอุ้งเท้าแม่แบบแปลงร่าง 50 มิติแต่ละภาพ สิ่งนี้มีความอ่อนไหวน้อยกว่ามากสำหรับรูปแบบเล็ก ๆ น้อย ๆ ในการวางตำแหน่งของนิ้วเท้าแต่ละข้าง ฯลฯ และโดยทั่วไปจะลดขนาดของปัญหาให้เหลือเพียงมิติที่เกี่ยวข้อง
การจำแนกอุ้งเท้าตาม Eigenpaw
ตอนนี้เราสามารถใช้ระยะห่างระหว่างเวกเตอร์ 50 มิติกับเวกเตอร์ "แม่แบบ" สำหรับแต่ละขาเพื่อจำแนกว่าอุ้งเท้าคืออะไร:
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()]
นี่คือผลลัพธ์บางส่วน:
ปัญหาที่เหลืออยู่
ยังคงมีปัญหาบางอย่างโดยเฉพาะอย่างยิ่งกับสุนัขที่มีขนาดเล็กเกินไปที่จะสร้างรอยตีนที่ชัดเจน ... (ใช้ได้ดีที่สุดกับสุนัขตัวใหญ่เนื่องจากนิ้วเท้าแยกออกจากความละเอียดของเซ็นเซอร์ได้ชัดเจนกว่า) นอกจากนี้ยังไม่รู้จักรอยตีนผีบางส่วนด้วย ระบบในขณะที่สามารถอยู่กับระบบที่ใช้รูปแบบสี่เหลี่ยมคางหมู
อย่างไรก็ตามเนื่องจากการวิเคราะห์ eigenpaw ใช้เมตริกระยะทางโดยเนื้อแท้เราจึงสามารถจำแนกอุ้งเท้าได้ทั้งสองวิธีและถอยกลับไปใช้ระบบที่ใช้รูปแบบสี่เหลี่ยมคางหมูเมื่อระยะทางที่เล็กที่สุดของการวิเคราะห์ eigenpaw จาก "codebook" เกินเกณฑ์บางอย่าง ฉันยังไม่ได้ใช้สิ่งนี้
ฟิ้ว ... นานแล้ว! หมวกของฉันไม่เหมาะกับ Ivo เพราะมีคำถามสนุก ๆ !