คำอธิบายที่เข้าใจง่ายเกี่ยวกับเทคนิคการเพิ่มความคาดหวังคืออะไร? [ปิด]


109

Expectation Maximization (EM) เป็นวิธีการที่น่าจะเป็นประเภทหนึ่งในการจำแนกข้อมูล กรุณาแก้ไขฉันถ้าฉันผิดถ้าไม่ใช่ลักษณนาม

คำอธิบายที่เข้าใจง่ายเกี่ยวกับเทคนิค EM นี้คืออะไร? ที่expectationนี่คืออะไรและเป็นmaximizedอย่างไร


12
อัลกอริทึมการเพิ่มความคาดหวังสูงสุดคืออะไร? , Nature Biotechnology 26 , 897–899 (2008) มีภาพที่ดีที่แสดงให้เห็นว่าอัลกอริทึมทำงานอย่างไร
chl

@chl ในส่วนbของภาพที่สวยงามพวกเขาได้รับค่าของการแจกแจงความน่าจะเป็นบน Z (เช่น 0.45xA, 0.55xB เป็นต้น) ได้อย่างไร?
Noob Saibot

3
คุณสามารถดูคำถามนี้math.stackexchange.com/questions/25111/…
v4r

3
อัปเดตลิงก์ไปยังรูปภาพที่ @chl กล่าวถึง
n1k31t4

คำตอบ:


121

หมายเหตุ: รหัสหลังคำตอบนี้สามารถพบได้ที่นี่


สมมติว่าเรามีข้อมูลที่สุ่มตัวอย่างจากสองกลุ่มที่แตกต่างกันสีแดงและสีน้ำเงิน:

ใส่คำอธิบายภาพที่นี่

ที่นี่เราสามารถดูว่าจุดข้อมูลใดเป็นของกลุ่มสีแดงหรือสีน้ำเงิน ทำให้ง่ายต่อการค้นหาพารามิเตอร์ที่แสดงลักษณะของแต่ละกลุ่ม ตัวอย่างเช่นค่าเฉลี่ยของกลุ่มสีแดงอยู่ที่ประมาณ 3 ค่าเฉลี่ยของกลุ่มสีน้ำเงินอยู่ที่ประมาณ 7 (และเราสามารถหาค่าเฉลี่ยที่แน่นอนได้หากต้องการ)

นี่คือการพูดโดยทั่วไปที่รู้จักกันในการประมาณค่าความน่าจะเป็นสูงสุด จากข้อมูลบางส่วนเราจะคำนวณค่าของพารามิเตอร์ (หรือพารามิเตอร์) ที่อธิบายข้อมูลนั้นได้ดีที่สุด

ตอนนี้ลองนึกดูว่าเราไม่สามารถดูว่าค่าใดถูกสุ่มตัวอย่างมาจากกลุ่มใด ทุกอย่างดูเป็นสีม่วงสำหรับเรา:

ใส่คำอธิบายภาพที่นี่

ที่นี่เรามีความรู้ว่ามีค่าสองกลุ่ม แต่เราไม่รู้ว่าค่าใดเป็นของกลุ่มใด

เรายังสามารถประมาณค่าเฉลี่ยของกลุ่มสีแดงและกลุ่มสีน้ำเงินที่เหมาะสมกับข้อมูลนี้มากที่สุดได้หรือไม่

ใช่เราทำได้บ่อยครั้ง! Expectation Maximizationทำให้เรามีวิธีทำ แนวคิดทั่วไปที่อยู่เบื้องหลังอัลกอริทึมคือ:

  1. เริ่มต้นด้วยการประมาณค่าเริ่มต้นของพารามิเตอร์แต่ละตัว
  2. คำนวณความเป็นไปได้ที่พารามิเตอร์แต่ละตัวสร้างจุดข้อมูล
  3. คำนวณน้ำหนักสำหรับจุดข้อมูลแต่ละจุดเพื่อระบุว่าเป็นสีแดงหรือสีน้ำเงินมากกว่าโดยพิจารณาจากความเป็นไปได้ที่จะเกิดจากพารามิเตอร์ รวมน้ำหนักกับข้อมูล ( ความคาดหวัง )
  4. คำนวณประมาณการที่ดีกว่าสำหรับพารามิเตอร์โดยใช้ข้อมูลน้ำหนักปรับ ( สูงสุด )
  5. ทำซ้ำขั้นตอนที่ 2 ถึง 4 จนกระทั่งค่าประมาณพารามิเตอร์มาบรรจบกัน (กระบวนการหยุดสร้างค่าประมาณอื่น)

ขั้นตอนเหล่านี้ต้องการคำอธิบายเพิ่มเติมดังนั้นฉันจะอธิบายปัญหาที่อธิบายไว้ข้างต้น

ตัวอย่าง: การประมาณค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐาน

ฉันจะใช้ Python ในตัวอย่างนี้ แต่โค้ดควรเข้าใจง่ายพอสมควรหากคุณไม่คุ้นเคยกับภาษานี้

สมมติว่าเรามีสองกลุ่มคือสีแดงและสีน้ำเงินโดยมีการกระจายค่าดังภาพด้านบน โดยเฉพาะแต่ละกลุ่มมีค่าที่ดึงมาจากการแจกแจงปกติโดยมีพารามิเตอร์ต่อไปนี้:

import numpy as np
from scipy import stats

np.random.seed(110) # for reproducible results

# set parameters
red_mean = 3
red_std = 0.8

blue_mean = 7
blue_std = 2

# draw 20 samples from normal distributions with red/blue parameters
red = np.random.normal(red_mean, red_std, size=20)
blue = np.random.normal(blue_mean, blue_std, size=20)

both_colours = np.sort(np.concatenate((red, blue))) # for later use...

นี่คือภาพของกลุ่มสีแดงและสีน้ำเงินเหล่านี้อีกครั้ง (เพื่อไม่ให้คุณต้องเลื่อนขึ้น):

ใส่คำอธิบายภาพที่นี่

เมื่อเราเห็นสีของแต่ละจุด (เช่นว่าอยู่ในกลุ่มใด) การประมาณค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานของแต่ละกลุ่มนั้นทำได้ง่ายมาก เราแค่ส่งค่าสีแดงและสีน้ำเงินไปยังฟังก์ชัน builtin ใน NumPy ตัวอย่างเช่น:

>>> np.mean(red)
2.802
>>> np.std(red)
0.871
>>> np.mean(blue)
6.932
>>> np.std(blue)
2.195

แต่ถ้าเรามองไม่เห็นสีของแต้มล่ะ? นั่นคือแทนที่จะเป็นสีแดงหรือสีน้ำเงินทุกจุดจะถูกเปลี่ยนเป็นสีม่วง

ในการพยายามกู้คืนค่าเฉลี่ยและค่าเบี่ยงเบนมาตรฐานสำหรับกลุ่มสีแดงและสีน้ำเงินเราสามารถใช้การขยายความคาดหวัง

ขั้นตอนแรกของเรา ( ขั้นตอนที่ 1ด้านบน) คือการเดาค่าพารามิเตอร์สำหรับค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานของแต่ละกลุ่ม เราไม่จำเป็นต้องเดาอย่างชาญฉลาด เราสามารถเลือกหมายเลขที่เราชอบ:

# estimates for the mean
red_mean_guess = 1.1
blue_mean_guess = 9

# estimates for the standard deviation
red_std_guess = 2
blue_std_guess = 1.7

ค่าประมาณพารามิเตอร์เหล่านี้สร้างเส้นโค้งระฆังที่มีลักษณะดังนี้:

ใส่คำอธิบายภาพที่นี่

สิ่งเหล่านี้เป็นการประมาณการที่ไม่ดี ทั้งสองวิธี (เส้นประแนวตั้ง) ดูห่างไกลจาก "ตรงกลาง" ทุกประเภทสำหรับกลุ่มของจุดที่เหมาะสมตัวอย่างเช่น เราต้องการปรับปรุงค่าประมาณเหล่านี้

ขั้นตอนต่อไป ( ขั้นตอนที่ 2 ) คือการคำนวณความเป็นไปได้ของจุดข้อมูลแต่ละจุดที่ปรากฏภายใต้การคาดเดาพารามิเตอร์ปัจจุบัน:

likelihood_of_red = stats.norm(red_mean_guess, red_std_guess).pdf(both_colours)
likelihood_of_blue = stats.norm(blue_mean_guess, blue_std_guess).pdf(both_colours)

ที่นี่เราได้ใส่จุดข้อมูลแต่ละจุดลงในฟังก์ชันความหนาแน่นของความน่าจะเป็นสำหรับการแจกแจงปกติโดยใช้การคาดเดาปัจจุบันของเราที่ค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานสำหรับสีแดงและสีน้ำเงิน นี้จะบอกเรายกตัวอย่างเช่นว่ามีการคาดเดาของเราในปัจจุบันจุดข้อมูลที่ 1.761 เป็นมากมีแนวโน้มที่จะสีแดง (0.189) มากกว่าสีฟ้า (0.00003)

สำหรับจุดข้อมูลแต่ละจุดเราสามารถเปลี่ยนค่าความเป็นไปได้ทั้งสองนี้ให้เป็นน้ำหนัก ( ขั้นตอนที่ 3 ) เพื่อให้รวมเป็น 1 ได้ดังนี้:

likelihood_total = likelihood_of_red + likelihood_of_blue

red_weight = likelihood_of_red / likelihood_total
blue_weight = likelihood_of_blue / likelihood_total

ด้วยค่าประมาณปัจจุบันและน้ำหนักที่คำนวณใหม่ของเราตอนนี้เราสามารถคำนวณค่าประมาณใหม่สำหรับค่าเฉลี่ยและส่วนเบี่ยงเบนมาตรฐานของกลุ่มสีแดงและสีน้ำเงินได้แล้ว ( ขั้นตอนที่ 4 )

เราคำนวณค่าเฉลี่ยและค่าเบี่ยงเบนมาตรฐานสองครั้งโดยใช้จุดข้อมูลทั้งหมดแต่มีน้ำหนักต่างกัน: หนึ่งครั้งสำหรับน้ำหนักสีแดงและอีกครั้งสำหรับน้ำหนักสีน้ำเงิน

สัญชาตญาณที่สำคัญคือยิ่งน้ำหนักของสีบนจุดข้อมูลมากเท่าไหร่จุดข้อมูลก็จะมีผลต่อค่าประมาณถัดไปสำหรับพารามิเตอร์ของสีนั้น สิ่งนี้มีผลในการ "ดึง" พารามิเตอร์ไปในทิศทางที่ถูกต้อง

def estimate_mean(data, weight):
    """
    For each data point, multiply the point by the probability it
    was drawn from the colour's distribution (its "weight").

    Divide by the total weight: essentially, we're finding where 
    the weight is centred among our data points.
    """
    return np.sum(data * weight) / np.sum(weight)

def estimate_std(data, weight, mean):
    """
    For each data point, multiply the point's squared difference
    from a mean value by the probability it was drawn from
    that distribution (its "weight").

    Divide by the total weight: essentially, we're finding where 
    the weight is centred among the values for the difference of
    each data point from the mean.

    This is the estimate of the variance, take the positive square
    root to find the standard deviation.
    """
    variance = np.sum(weight * (data - mean)**2) / np.sum(weight)
    return np.sqrt(variance)

# new estimates for standard deviation
blue_std_guess = estimate_std(both_colours, blue_weight, blue_mean_guess)
red_std_guess = estimate_std(both_colours, red_weight, red_mean_guess)

# new estimates for mean
red_mean_guess = estimate_mean(both_colours, red_weight)
blue_mean_guess = estimate_mean(both_colours, blue_weight)

เรามีค่าประมาณใหม่สำหรับพารามิเตอร์ หากต้องการปรับปรุงอีกครั้งเราสามารถข้ามกลับไปที่ขั้นตอนที่ 2 และทำซ้ำขั้นตอนนี้ เราทำเช่นนี้จนกว่าค่าประมาณจะมาบรรจบกันหรือหลังจากดำเนินการซ้ำไปแล้วจำนวนหนึ่ง ( ขั้นตอนที่ 5 )

สำหรับข้อมูลของเราการทำซ้ำห้าครั้งแรกของกระบวนการนี้มีลักษณะเช่นนี้ (การทำซ้ำล่าสุดมีลักษณะที่ชัดเจนกว่า):

ใส่คำอธิบายภาพที่นี่

เราเห็นว่าค่าเฉลี่ยได้มาบรรจบกับค่าบางค่าแล้วและรูปร่างของเส้นโค้ง (ควบคุมโดยค่าเบี่ยงเบนมาตรฐาน) ก็มีเสถียรภาพมากขึ้นเช่นกัน

หากเราดำเนินการซ้ำ 20 ครั้งเราจะได้สิ่งต่อไปนี้:

ใส่คำอธิบายภาพที่นี่

กระบวนการ EM ได้แปลงเป็นค่าต่อไปนี้ซึ่งกลายเป็นค่าที่ใกล้เคียงกับค่าจริงมาก (ซึ่งเราสามารถมองเห็นสี - ไม่มีตัวแปรที่ซ่อนอยู่):

          | EM guess | Actual |  Delta
----------+----------+--------+-------
Red mean  |    2.910 |  2.802 |  0.108
Red std   |    0.854 |  0.871 | -0.017
Blue mean |    6.838 |  6.932 | -0.094
Blue std  |    2.227 |  2.195 |  0.032

ในโค้ดด้านบนคุณอาจสังเกตเห็นว่าค่าประมาณใหม่สำหรับค่าเบี่ยงเบนมาตรฐานคำนวณโดยใช้ค่าประมาณการวนซ้ำก่อนหน้านี้สำหรับค่าเฉลี่ย ท้ายที่สุดแล้วไม่สำคัญว่าเราจะคำนวณค่าใหม่สำหรับค่าเฉลี่ยก่อนเนื่องจากเราเพียงแค่ค้นหาความแปรปรวน (ถ่วงน้ำหนัก) ของค่ารอบ ๆ จุดศูนย์กลาง เราจะยังคงเห็นค่าประมาณสำหรับพารามิเตอร์ที่มาบรรจบกัน


จะเป็นอย่างไรหากเราไม่รู้จำนวนการแจกแจงปกติที่มาจาก? ที่นี่คุณได้ยกตัวอย่างของการแจกแจง k = 2 เราสามารถประมาณ k และชุดพารามิเตอร์ k ได้หรือไม่
stackit

1
@stackit: ฉันไม่แน่ใจว่ามีวิธีทั่วไปที่ตรงไปตรงมาในการคำนวณค่า k ที่เป็นไปได้มากที่สุดซึ่งเป็นส่วนหนึ่งของกระบวนการ EM ในกรณีนี้ ประเด็นหลักคือเราจะต้องเริ่ม EM ด้วยค่าประมาณสำหรับแต่ละพารามิเตอร์ที่เราต้องการค้นหาและนั่นหมายถึงว่าเราจำเป็นต้องรู้ / ประมาณค่า k ก่อนที่เราจะเริ่ม อย่างไรก็ตามเป็นไปได้ที่จะประมาณสัดส่วนของคะแนนที่เป็นของกลุ่มผ่าน EM ที่นี่ บางทีถ้าเราประเมินค่า k สูงเกินไปสัดส่วนของทั้งหมดยกเว้นสองกลุ่มก็จะลดลงจนใกล้ศูนย์ ฉันยังไม่ได้ทดลองกับสิ่งนี้ดังนั้นฉันไม่รู้ว่ามันจะได้ผลดีแค่ไหนในทางปฏิบัติ
Alex Riley

1
@AlexRiley คุณสามารถพูดเพิ่มเติมเกี่ยวกับสูตรสำหรับคำนวณค่าเฉลี่ยและค่าเบี่ยงเบนมาตรฐานใหม่ได้หรือไม่?
มะนาว

2
@AlexRiley ขอบคุณสำหรับคำอธิบาย เหตุใดค่าประมาณส่วนเบี่ยงเบนมาตรฐานใหม่จึงคำนวณโดยใช้การเดาค่าเฉลี่ยแบบเก่า จะเป็นอย่างไรหากพบการประมาณค่าเฉลี่ยใหม่ก่อน
GoodDeeds

1
@Lemon GoodDeeds Kaushal - ขอโทษสำหรับการตอบคำถามของคุณล่าช้า ฉันได้พยายามแก้ไขคำตอบเพื่อแก้ไขประเด็นที่คุณได้เพิ่มขึ้น ฉันยังทำให้รหัสทั้งหมดที่ใช้ในคำตอบนี้สามารถเข้าถึงได้ในสมุดบันทึกที่นี่ (ซึ่งรวมถึงคำอธิบายรายละเอียดเพิ่มเติมของบางประเด็นที่ฉันได้สัมผัสด้วย)
Alex Riley

36

EM เป็นอัลกอริทึมสำหรับเพิ่มฟังก์ชันความเป็นไปได้สูงสุดเมื่อตัวแปรบางตัวในโมเดลของคุณไม่ถูกสังเกต (เช่นเมื่อคุณมีตัวแปรแฝง)

คุณอาจถามพอสมควรว่าถ้าเราแค่พยายามเพิ่มฟังก์ชันให้สูงสุดทำไมเราไม่ใช้เครื่องจักรที่มีอยู่เพื่อเพิ่มฟังก์ชันให้ได้มากที่สุด ถ้าคุณพยายามเพิ่มสิ่งนี้ให้มากที่สุดโดยการหาอนุพันธ์และตั้งค่าเป็นศูนย์คุณจะพบว่าในหลาย ๆ กรณีเงื่อนไขลำดับที่หนึ่งไม่มีทางแก้ไข มีปัญหาไก่และไข่ในการแก้ปัญหาสำหรับพารามิเตอร์แบบจำลองของคุณคุณจำเป็นต้องทราบการกระจายของข้อมูลที่ไม่ได้รับการตรวจจับของคุณ แต่การกระจายข้อมูลที่ไม่ได้รับการจองของคุณเป็นฟังก์ชันของพารามิเตอร์โมเดลของคุณ

EM พยายามหลีกเลี่ยงปัญหานี้โดยการคาดเดาการแจกแจงของข้อมูลที่ไม่ถูกสังเกตซ้ำ ๆ จากนั้นจึงประมาณค่าพารามิเตอร์ของโมเดลโดยการเพิ่มสิ่งที่มีขอบเขตต่ำกว่าให้กับฟังก์ชันความเป็นไปได้ที่แท้จริงและทำซ้ำจนกว่าการบรรจบกัน:

อัลกอริทึม EM

เริ่มต้นด้วยการเดาค่าพารามิเตอร์โมเดลของคุณ

E-step: สำหรับจุดข้อมูลแต่ละจุดที่มีค่าที่ขาดหายไปให้ใช้สมการแบบจำลองของคุณเพื่อแก้ปัญหาสำหรับการกระจายข้อมูลที่ขาดหายไปตามการคาดเดาปัจจุบันของคุณเกี่ยวกับพารามิเตอร์แบบจำลองและให้ข้อมูลที่สังเกตได้ (โปรดทราบว่าคุณกำลังแก้ปัญหาสำหรับการกระจายสำหรับแต่ละจุดที่ขาดหายไป มูลค่าไม่ใช่สำหรับมูลค่าที่คาดหวัง) ตอนนี้เรามีการแจกแจงสำหรับแต่ละค่าที่ขาดหายไปแล้วเราสามารถคำนวณความคาดหวังของฟังก์ชันความเป็นไปได้ที่เกี่ยวข้องกับตัวแปรที่ไม่ได้สังเกต หากการคาดเดาของเราสำหรับพารามิเตอร์โมเดลถูกต้องความเป็นไปได้ที่คาดไว้นี้จะเป็นความเป็นไปได้ที่แท้จริงของข้อมูลที่เราสังเกตได้ หากพารามิเตอร์ไม่ถูกต้องก็จะเป็นขอบเขตล่าง

M-step: ตอนนี้เรามีฟังก์ชันความเป็นไปได้ที่คาดหวังโดยไม่มีตัวแปรที่ไม่ถูกสังเกตอยู่แล้วให้เพิ่มฟังก์ชันให้สูงสุดตามที่คุณทำในกรณีที่สังเกตได้ทั้งหมดเพื่อรับค่าประมาณใหม่ของพารามิเตอร์แบบจำลองของคุณ

ทำซ้ำจนกว่าจะบรรจบกัน


5
ฉันไม่เข้าใจ E-step ของคุณ ส่วนหนึ่งของปัญหาคือในขณะที่ฉันกำลังเรียนรู้สิ่งนี้ฉันไม่พบคนที่ใช้คำศัพท์เดียวกัน สมการแบบจำลองหมายความว่าอย่างไร? ฉันไม่รู้ว่าคุณหมายถึงอะไรจากการแก้การแจกแจงความน่าจะเป็น?
user678392

27

นี่คือสูตรตรงไปตรงมาเพื่อทำความเข้าใจอัลกอริทึมการเพิ่มความคาดหวัง:

1-อ่านเอกสารการสอน EMโดย Do และ Batzoglou

2-คุณอาจจะมีเครื่องหมายคำถามในหัวของคุณมีลักษณะที่คำอธิบายเกี่ยวกับคณิตศาสตร์นี้สแต็แลกเปลี่ยนหน้า

3-ดูรหัสนี้ที่ฉันเขียนใน Python ซึ่งอธิบายตัวอย่างในเอกสารการสอน EM ของข้อ 1:

คำเตือน:โค้ดอาจยุ่ง / ไม่เหมาะสมเนื่องจากฉันไม่ใช่นักพัฒนา Python แต่มันไม่ได้ผล

import numpy as np
import math

#### E-M Coin Toss Example as given in the EM tutorial paper by Do and Batzoglou* #### 

def get_mn_log_likelihood(obs,probs):
    """ Return the (log)likelihood of obs, given the probs"""
    # Multinomial Distribution Log PMF
    # ln (pdf)      =             multinomial coeff            *   product of probabilities
    # ln[f(x|n, p)] = [ln(n!) - (ln(x1!)+ln(x2!)+...+ln(xk!))] + [x1*ln(p1)+x2*ln(p2)+...+xk*ln(pk)]     

    multinomial_coeff_denom= 0
    prod_probs = 0
    for x in range(0,len(obs)): # loop through state counts in each observation
        multinomial_coeff_denom = multinomial_coeff_denom + math.log(math.factorial(obs[x]))
        prod_probs = prod_probs + obs[x]*math.log(probs[x])

    multinomial_coeff = math.log(math.factorial(sum(obs))) -  multinomial_coeff_denom
    likelihood = multinomial_coeff + prod_probs
    return likelihood

# 1st:  Coin B, {HTTTHHTHTH}, 5H,5T
# 2nd:  Coin A, {HHHHTHHHHH}, 9H,1T
# 3rd:  Coin A, {HTHHHHHTHH}, 8H,2T
# 4th:  Coin B, {HTHTTTHHTT}, 4H,6T
# 5th:  Coin A, {THHHTHHHTH}, 7H,3T
# so, from MLE: pA(heads) = 0.80 and pB(heads)=0.45

# represent the experiments
head_counts = np.array([5,9,8,4,7])
tail_counts = 10-head_counts
experiments = zip(head_counts,tail_counts)

# initialise the pA(heads) and pB(heads)
pA_heads = np.zeros(100); pA_heads[0] = 0.60
pB_heads = np.zeros(100); pB_heads[0] = 0.50

# E-M begins!
delta = 0.001  
j = 0 # iteration counter
improvement = float('inf')
while (improvement>delta):
    expectation_A = np.zeros((5,2), dtype=float) 
    expectation_B = np.zeros((5,2), dtype=float)
    for i in range(0,len(experiments)):
        e = experiments[i] # i'th experiment
        ll_A = get_mn_log_likelihood(e,np.array([pA_heads[j],1-pA_heads[j]])) # loglikelihood of e given coin A
        ll_B = get_mn_log_likelihood(e,np.array([pB_heads[j],1-pB_heads[j]])) # loglikelihood of e given coin B

        weightA = math.exp(ll_A) / ( math.exp(ll_A) + math.exp(ll_B) ) # corresponding weight of A proportional to likelihood of A 
        weightB = math.exp(ll_B) / ( math.exp(ll_A) + math.exp(ll_B) ) # corresponding weight of B proportional to likelihood of B                            

        expectation_A[i] = np.dot(weightA, e) 
        expectation_B[i] = np.dot(weightB, e)

    pA_heads[j+1] = sum(expectation_A)[0] / sum(sum(expectation_A)); 
    pB_heads[j+1] = sum(expectation_B)[0] / sum(sum(expectation_B)); 

    improvement = max( abs(np.array([pA_heads[j+1],pB_heads[j+1]]) - np.array([pA_heads[j],pB_heads[j]]) ))
    j = j+1

ฉันพบว่าโปรแกรมของคุณจะส่งผลทั้ง A และ B ถึง 0.66 ฉันก็ใช้มันโดยใช้สกาล่าเช่นกันพบว่าผลลัพธ์คือ 0.66 คุณช่วยตรวจสอบได้ไหม
zjffdu

เมื่อใช้สเปรดชีตฉันจะพบผลลัพธ์ 0.66 ของคุณหากการคาดเดาเริ่มต้นเท่ากัน มิฉะนั้นฉันสามารถสร้างผลลัพธ์ของบทช่วยสอนขึ้นมาใหม่ได้
soakingley

@zjffdu EM ทำงานซ้ำกี่ครั้งก่อนส่งคืนคุณ 0.66 หากคุณเริ่มต้นด้วยค่าที่เท่ากันมันอาจติดขัดที่ค่าสูงสุดในเครื่องและคุณจะเห็นว่าจำนวนการทำซ้ำนั้นต่ำมาก (เนื่องจากไม่มีการปรับปรุง)
Zhubarb

คุณสามารถดูสไลด์นี้โดยบันทึกหลักสูตรของ
Minh Phan

17

ในทางเทคนิคคำว่า "EM" เป็นเพียงเล็กน้อยที่ระบุ แต่ฉันถือว่าคุณอ้างถึงเทคนิคการวิเคราะห์คลัสเตอร์ Gaussian Mixture Modeling ซึ่งเป็นตัวอย่างของหลักการ EM ทั่วไป

ที่จริงแล้วการวิเคราะห์กลุ่ม EM ไม่ได้แยกประเภท ฉันรู้ว่าบางคนคิดว่าการจัดกลุ่มเป็น "การจัดประเภทที่ไม่มีผู้ดูแล" แต่จริงๆแล้วการวิเคราะห์คลัสเตอร์เป็นสิ่งที่แตกต่างกันมาก

ความแตกต่างที่สำคัญและใหญ่คนเข้าใจผิดจำแนกมักจะมีกับการวิเคราะห์กลุ่มคือ: ใน analaysis คลัสเตอร์ไม่มี "วิธีการแก้ปัญหาที่ถูกต้อง" เป็นวิธีการค้นพบความรู้จริง ๆ แล้วมีไว้เพื่อค้นหาสิ่งใหม่ ๆ ! ทำให้การประเมินยุ่งยากมาก มักจะได้รับการประเมินโดยใช้การจำแนกประเภทที่รู้จักกันเป็นข้อมูลอ้างอิง แต่ก็ไม่เหมาะสมเสมอไป: การจัดประเภทที่คุณมีอาจหรือไม่สะท้อนถึงสิ่งที่อยู่ในข้อมูล

ขอยกตัวอย่างคุณมีกลุ่มข้อมูลลูกค้าจำนวนมากรวมถึงข้อมูลเพศ วิธีที่แยกชุดข้อมูลนี้ออกเป็น "ชาย" และ "หญิง" นั้นเหมาะสมที่สุดเมื่อคุณเปรียบเทียบกับคลาสที่มีอยู่ วิธีคิดแบบ "การคาดคะเน" นี่เป็นสิ่งที่ดีสำหรับผู้ใช้ใหม่ตอนนี้คุณสามารถคาดเดาเพศของพวกเขาได้ ในวิธีคิดแบบ "การค้นพบความรู้" สิ่งนี้ไม่ดีจริงๆเพราะคุณต้องการค้นพบโครงสร้างใหม่ในข้อมูล วิธีการที่จะแยกข้อมูลออกเป็นผู้สูงอายุและเด็ก แต่จะให้คะแนนแย่ลงเท่าที่จะทำได้เมื่อเทียบกับชั้นเรียนชาย / หญิง อย่างไรก็ตามนั่นจะเป็นผลการจัดกลุ่มที่ดีเยี่ยม (หากไม่ระบุอายุ)

ตอนนี้กลับไปที่ EM โดยพื้นฐานแล้วจะถือว่าข้อมูลของคุณประกอบด้วยการแจกแจงปกติหลายตัวแปร (โปรดทราบว่านี่เป็นข้อสันนิษฐานที่ชัดเจนมากโดยเฉพาะอย่างยิ่งเมื่อคุณกำหนดจำนวนคลัสเตอร์!) จากนั้นก็พยายามที่จะหารูปแบบที่เหมาะสมในท้องถิ่นสำหรับการนี้โดยalternatingly การปรับปรุงรูปแบบและการกำหนดวัตถุที่รูปแบบ

เพื่อให้ได้ผลลัพธ์ที่ดีที่สุดในบริบทการจัดหมวดหมู่ให้เลือกจำนวนคลัสเตอร์ที่ใหญ่กว่าจำนวนคลาสหรือแม้กระทั่งใช้การจัดกลุ่มกับคลาสเดี่ยวเท่านั้น (เพื่อดูว่ามีโครงสร้างบางอย่างภายในคลาสหรือไม่!)

สมมติว่าคุณต้องการฝึกลักษณนามเพื่อแยก "รถยนต์" "จักรยาน" และ "รถบรรทุก" มีประโยชน์เพียงเล็กน้อยในการสมมติว่าข้อมูลประกอบด้วยการแจกแจงปกติ 3 แบบ อย่างไรก็ตามคุณอาจสันนิษฐานได้ว่ามีรถยนต์มากกว่าหนึ่งประเภท (และรถบรรทุกและจักรยาน) ดังนั้นแทนที่จะฝึกลักษณนามสำหรับทั้งสามคลาสนี้ให้คุณจัดกลุ่มรถยนต์รถบรรทุกและจักรยานเป็นกลุ่มละ 10 คัน (หรืออาจจะ 10 คันรถบรรทุก 3 คันและจักรยาน 3 คันอะไรก็ได้) จากนั้นฝึกลักษณนามเพื่อแยกประเภททั้ง 30 คลาสแล้วจากนั้น รวมผลการเรียนกลับไปที่คลาสเดิม นอกจากนี้คุณยังอาจพบว่ามีคลัสเตอร์หนึ่งที่จัดประเภทยากเป็นพิเศษเช่น Trikes พวกเขาค่อนข้างเป็นรถยนต์และค่อนข้างเป็นจักรยาน หรือรถบรรทุกส่งของซึ่งเหมือนกับรถขนาดใหญ่มากกว่ารถบรรทุก


EM ไม่ระบุอย่างไร
sam boosalis

มีมากกว่าหนึ่งเวอร์ชัน ในทางเทคนิคคุณสามารถเรียก Lloyd style k-mean "EM" ได้เช่นกัน คุณต้องระบุว่าคุณใช้รุ่นอะไร
มี QUIT - Anony-Mousse

2

คำตอบอื่น ๆ ที่ดีฉันจะพยายามให้มุมมองอื่นและจัดการกับส่วนที่ใช้งานง่ายของคำถาม

อัลกอริทึม EM (Expectation-Maximization)เป็นตัวแปรของคลาสของอัลกอริทึมซ้ำโดยใช้ความเป็นคู่

ข้อความที่ตัดตอนมา (เน้นของฉัน):

ในทางคณิตศาสตร์ความเป็นคู่โดยทั่วไปจะพูดได้แปลแนวคิดทฤษฎีบทหรือโครงสร้างทางคณิตศาสตร์เป็นแนวคิดทฤษฎีบทหรือโครงสร้างอื่น ๆ ในรูปแบบหนึ่งต่อหนึ่งบ่อยครั้ง (แต่ไม่เสมอไป) โดยการดำเนินการอินโวลูชั่น: ถ้าคู่ของ A คือ B ดังนั้นคู่ของ B คือ A บางครั้งการรุกรานดังกล่าวมีจุดคงที่ดังนั้นคู่ของ A คือ A เอง

มักจะเป็นคู่ B ของวัตถุจะถูกที่เกี่ยวข้องกับในบางวิธีที่รักษาบางสมมาตรหรือความเข้ากันได้ ตัวอย่างเช่น AB = const

ตัวอย่างของอัลกอริทึมแบบวนซ้ำโดยใช้ความเป็นคู่ (ในความหมายก่อนหน้านี้) ได้แก่ :

  1. อัลกอริทึมแบบยุคลิดสำหรับตัวหารสามัญที่ยิ่งใหญ่ที่สุดและตัวแปรต่างๆ
  2. Gram – Schmidt Vector Basis อัลกอริทึมและตัวแปร
  3. ค่าเฉลี่ยเลขคณิต - อสมการค่าเฉลี่ยทางเรขาคณิตและตัวแปร
  4. อัลกอริทึม Expectation-Maximization และตัวแปรต่างๆ (ดูที่นี่สำหรับมุมมองข้อมูลเรขาคณิต )
  5. (.. อัลกอริทึมอื่น ๆ ที่คล้ายกัน .. )

ในทำนองเดียวกันอัลกอริทึม EM สามารถมองเห็นได้เป็นสองขั้นตอนการขยายใหญ่สุดคู่ :

.. [EM] ถูกมองว่าเป็นการเพิ่มฟังก์ชันร่วมของพารามิเตอร์และการแจกแจงมากกว่าตัวแปรที่ไม่มีการสังเกต .. E-step จะเพิ่มฟังก์ชันนี้ให้สูงสุดเมื่อเทียบกับการแจกแจงมากกว่าตัวแปรที่ไม่มีการสังเกต ขั้นตอน M เกี่ยวกับพารามิเตอร์ ..

ในอัลกอริธึมแบบวนซ้ำโดยใช้ความเป็นคู่มีข้อสันนิษฐานที่ชัดเจน (หรือโดยปริยาย) ของจุดบรรจบ (หรือคงที่) ของจุดบรรจบ (สำหรับ EM สิ่งนี้พิสูจน์ได้โดยใช้ความไม่เท่าเทียมกันของ Jensen)

ดังนั้นโครงร่างของอัลกอริทึมดังกล่าวคือ:

  1. E-like step:หาทางออกที่ดีที่สุดxเทียบกับy ที่ถูกตรึงไว้
  2. ขั้นตอนที่เหมือน M (คู่):ค้นหาคำตอบที่ดีที่สุดyเทียบกับx (ตามที่คำนวณในขั้นตอนก่อนหน้า) ซึ่งคงที่
  3. เกณฑ์การสิ้นสุด / ขั้นตอนการบรรจบ:ทำซ้ำขั้นตอนที่ 1, 2 ด้วยค่าที่อัปเดตเป็นx , yจนกว่าจะถึงการบรรจบกัน (หรือถึงจำนวนการวนซ้ำที่ระบุ)

โปรดทราบว่าเมื่ออัลกอริทึมดังกล่าวมาบรรจบกันเป็นค่าที่เหมาะสมที่สุด (global) จะพบการกำหนดค่าที่ดีที่สุดในทั้งสองประสาทสัมผัส (เช่นทั้งในโดเมนx / พารามิเตอร์และโดเมน / พารามิเตอร์y ) อย่างไรก็ตามอัลกอริทึมสามารถค้นหาเฉพาะที่ดีที่สุดในท้องถิ่นและไม่ใช่ค่าที่ดีที่สุดระดับโลก

ฉันจะบอกว่านี่เป็นคำอธิบายที่เข้าใจง่ายของโครงร่างของอัลกอริทึม

สำหรับข้อโต้แย้งทางสถิติและการใช้งานคำตอบอื่น ๆ ได้ให้คำอธิบายที่ดี (ตรวจสอบการอ้างอิงในคำตอบนี้ด้วย)


2

คำตอบที่ได้รับการยอมรับอ้างอิงถึงChuong EM Paperซึ่งเป็นงานที่ดีในการอธิบาย EM นอกจากนี้ยังมีวิดีโอ youtubeที่อธิบายรายละเอียดเพิ่มเติมในกระดาษ

สรุปนี่คือสถานการณ์:

1st:  {H,T,T,T,H,H,T,H,T,H} 5 Heads, 5 Tails; Did coin A or B generate me?
2nd:  {H,H,H,H,T,H,H,H,H,H} 9 Heads, 1 Tails
3rd:  {H,T,H,H,H,H,H,T,H,H} 8 Heads, 2 Tails
4th:  {H,T,H,T,T,T,H,H,T,T} 4 Heads, 6 Tails
5th:  {T,H,H,H,T,H,H,H,T,H} 7 Heads, 3 Tails

Two possible coins, A & B are used to generate these distributions.
A & B have an unknown parameter: their bias towards heads.

We don't know the biases, but we can simply start with a guess: A=60% heads, B=50% heads.

ในกรณีของคำถามของการทดลองครั้งแรกโดยสัญชาตญาณเราคิดว่า B สร้างมันขึ้นมาเนื่องจากสัดส่วนของหัวตรงกับอคติของ B เป็นอย่างดี ... แต่ค่านั้นเป็นเพียงการคาดเดาดังนั้นเราจึงไม่แน่ใจ

ด้วยเหตุนี้ฉันจึงชอบนึกถึงโซลูชัน EM ดังนี้:

  • การทดลองพลิกแต่ละครั้งจะได้รับการ 'โหวต' เหรียญที่ชอบมากที่สุด
    • ทั้งนี้ขึ้นอยู่กับว่าแต่ละเหรียญเหมาะกับการกระจายของเหรียญเพียงใด
    • หรือจากมุมมองของเหรียญมีความคาดหวังสูงที่จะได้เห็นการทดลองนี้เทียบกับเหรียญอื่น ๆ (ขึ้นอยู่กับความเป็นไปได้ในการบันทึก )
  • ขึ้นอยู่กับว่าการทดลองแต่ละครั้งชอบเหรียญแต่ละเหรียญมากแค่ไหนก็สามารถอัปเดตการเดาพารามิเตอร์ของเหรียญนั้นได้ (อคติ)
    • ยิ่งผู้ทดลองชอบเหรียญมากเท่าไหร่ก็ยิ่งได้รับการอัปเดตอคติของเหรียญให้สะท้อนถึงตัวมันเอง!
    • โดยพื้นฐานแล้วอคติของเหรียญจะได้รับการอัปเดตโดยการรวมการอัปเดตที่มีน้ำหนักเหล่านี้ในการทดลองทั้งหมดซึ่งเป็นกระบวนการที่เรียกว่า ( maximazation ) ซึ่งหมายถึงการพยายามคาดเดาที่ดีที่สุดสำหรับอคติของแต่ละเหรียญตามชุดการทดลอง

นี่อาจเป็นการทำให้เข้าใจง่ายเกินไป (หรือแม้กระทั่งผิดพื้นฐานในบางระดับ) แต่ฉันหวังว่านี่จะช่วยได้ในระดับที่เข้าใจง่าย!


1

EM ใช้เพื่อเพิ่มความเป็นไปได้สูงสุดของโมเดล Q ที่มีตัวแปรแฝง Z

เป็นการเพิ่มประสิทธิภาพแบบวนซ้ำ

theta <- initial guess for hidden parameters
while not converged:
    #e-step
    Q(theta'|theta) = E[log L(theta|Z)]
    #m-step
    theta <- argmax_theta' Q(theta'|theta)

e-step: การประมาณค่าปัจจุบันของ Z จะคำนวณฟังก์ชัน loglikelihood ที่คาดไว้

m-step: ค้นหาทีต้าที่ขยาย Q นี้ให้ใหญ่ที่สุด

ตัวอย่าง GMM:

e-step: ประมาณการการกำหนดฉลากสำหรับแต่ละจุดข้อมูลที่กำหนดค่าพารามิเตอร์ gmm ปัจจุบัน

m-step: ขยายทีต้าใหม่ให้ใหญ่ที่สุดโดยการกำหนดป้ายกำกับใหม่

K-mean เป็นอัลกอริทึม EM และมีการอธิบายภาพเคลื่อนไหวมากมายเกี่ยวกับ K-mean


1

การใช้บทความเดียวกันโดยทำและ Batzoglou อ้างในคำตอบ Zhubarb ผมนำมาใช้ EM สำหรับปัญหาในการที่Java ความคิดเห็นต่อคำตอบของเขาแสดงให้เห็นว่าอัลกอริทึมติดขัดที่จุดที่เหมาะสมในท้องถิ่นซึ่งเกิดขึ้นกับการใช้งานของฉันด้วยหากพารามิเตอร์ thetaA และ thetaB เหมือนกัน

ด้านล่างนี้เป็นเอาต์พุตมาตรฐานของโค้ดของฉันซึ่งแสดงการบรรจบกันของพารามิเตอร์

thetaA = 0.71301, thetaB = 0.58134
thetaA = 0.74529, thetaB = 0.56926
thetaA = 0.76810, thetaB = 0.54954
thetaA = 0.78316, thetaB = 0.53462
thetaA = 0.79106, thetaB = 0.52628
thetaA = 0.79453, thetaB = 0.52239
thetaA = 0.79593, thetaB = 0.52073
thetaA = 0.79647, thetaB = 0.52005
thetaA = 0.79667, thetaB = 0.51977
thetaA = 0.79674, thetaB = 0.51966
thetaA = 0.79677, thetaB = 0.51961
thetaA = 0.79678, thetaB = 0.51960
thetaA = 0.79679, thetaB = 0.51959
Final result:
thetaA = 0.79678, thetaB = 0.51960

ด้านล่างนี้คือการนำ EM ไปใช้งาน Java ของฉันเพื่อแก้ปัญหาใน (Do and Batzoglou, 2008) ส่วนหลักของการใช้งานคือลูปเพื่อเรียกใช้ EM จนกว่าพารามิเตอร์จะมาบรรจบกัน

private Parameters _parameters;

public Parameters run()
{
    while (true)
    {
        expectation();

        Parameters estimatedParameters = maximization();

        if (_parameters.converged(estimatedParameters)) {
            break;
        }

        _parameters = estimatedParameters;
    }

    return _parameters;
}

ด้านล่างนี้คือรหัสทั้งหมด

import java.util.*;

/*****************************************************************************
This class encapsulates the parameters of the problem. For this problem posed
in the article by (Do and Batzoglou, 2008), the parameters are thetaA and
thetaB, the probability of a coin coming up heads for the two coins A and B,
respectively.
*****************************************************************************/
class Parameters
{
    double _thetaA = 0.0; // Probability of heads for coin A.
    double _thetaB = 0.0; // Probability of heads for coin B.

    double _delta = 0.00001;

    public Parameters(double thetaA, double thetaB)
    {
        _thetaA = thetaA;
        _thetaB = thetaB;
    }

    /*************************************************************************
    Returns true if this parameter is close enough to another parameter
    (typically the estimated parameter coming from the maximization step).
    *************************************************************************/
    public boolean converged(Parameters other)
    {
        if (Math.abs(_thetaA - other._thetaA) < _delta &&
            Math.abs(_thetaB - other._thetaB) < _delta)
        {
            return true;
        }

        return false;
    }

    public double getThetaA()
    {
        return _thetaA;
    }

    public double getThetaB()
    {
        return _thetaB;
    }

    public String toString()
    {
        return String.format("thetaA = %.5f, thetaB = %.5f", _thetaA, _thetaB);
    }

}


/*****************************************************************************
This class encapsulates an observation, that is the number of heads
and tails in a trial. The observation can be either (1) one of the
experimental observations, or (2) an estimated observation resulting from
the expectation step.
*****************************************************************************/
class Observation
{
    double _numHeads = 0;
    double _numTails = 0;

    public Observation(String s)
    {
        for (int i = 0; i < s.length(); i++)
        {
            char c = s.charAt(i);

            if (c == 'H')
            {
                _numHeads++;
            }
            else if (c == 'T')
            {
                _numTails++;
            }
            else
            {
                throw new RuntimeException("Unknown character: " + c);
            }
        }
    }

    public Observation(double numHeads, double numTails)
    {
        _numHeads = numHeads;
        _numTails = numTails;
    }

    public double getNumHeads()
    {
        return _numHeads;
    }

    public double getNumTails()
    {
        return _numTails;
    }

    public String toString()
    {
        return String.format("heads: %.1f, tails: %.1f", _numHeads, _numTails);
    }

}

/*****************************************************************************
This class runs expectation-maximization for the problem posed by the article
from (Do and Batzoglou, 2008).
*****************************************************************************/
public class EM
{
    // Current estimated parameters.
    private Parameters _parameters;

    // Observations from the trials. These observations are set once.
    private final List<Observation> _observations;

    // Estimated observations per coin. These observations are the output
    // of the expectation step.
    private List<Observation> _expectedObservationsForCoinA;
    private List<Observation> _expectedObservationsForCoinB;

    private static java.io.PrintStream o = System.out;

    /*************************************************************************
    Principal constructor.
    @param observations The observations from the trial.
    @param parameters The initial guessed parameters.
    *************************************************************************/
    public EM(List<Observation> observations, Parameters parameters)
    {
        _observations = observations;
        _parameters = parameters;
    }

    /*************************************************************************
    Run EM until parameters converge.
    *************************************************************************/
    public Parameters run()
    {

        while (true)
        {
            expectation();

            Parameters estimatedParameters = maximization();

            o.printf("%s\n", estimatedParameters);

            if (_parameters.converged(estimatedParameters)) {
                break;
            }

            _parameters = estimatedParameters;
        }

        return _parameters;

    }

    /*************************************************************************
    Given the observations and current estimated parameters, compute new
    estimated completions (distribution over the classes) and observations.
    *************************************************************************/
    private void expectation()
    {

        _expectedObservationsForCoinA = new ArrayList<Observation>();
        _expectedObservationsForCoinB = new ArrayList<Observation>();

        for (Observation observation : _observations)
        {
            int numHeads = (int)observation.getNumHeads();
            int numTails = (int)observation.getNumTails();

            double probabilityOfObservationForCoinA=
                binomialProbability(10, numHeads, _parameters.getThetaA());

            double probabilityOfObservationForCoinB=
                binomialProbability(10, numHeads, _parameters.getThetaB());

            double normalizer = probabilityOfObservationForCoinA +
                                probabilityOfObservationForCoinB;

            // Compute the completions for coin A and B (i.e. the probability
            // distribution of the two classes, summed to 1.0).

            double completionCoinA = probabilityOfObservationForCoinA /
                                     normalizer;
            double completionCoinB = probabilityOfObservationForCoinB /
                                     normalizer;

            // Compute new expected observations for the two coins.

            Observation expectedObservationForCoinA =
                new Observation(numHeads * completionCoinA,
                                numTails * completionCoinA);

            Observation expectedObservationForCoinB =
                new Observation(numHeads * completionCoinB,
                                numTails * completionCoinB);

            _expectedObservationsForCoinA.add(expectedObservationForCoinA);
            _expectedObservationsForCoinB.add(expectedObservationForCoinB);
        }
    }

    /*************************************************************************
    Given new estimated observations, compute new estimated parameters.
    *************************************************************************/
    private Parameters maximization()
    {

        double sumCoinAHeads = 0.0;
        double sumCoinATails = 0.0;
        double sumCoinBHeads = 0.0;
        double sumCoinBTails = 0.0;

        for (Observation observation : _expectedObservationsForCoinA)
        {
            sumCoinAHeads += observation.getNumHeads();
            sumCoinATails += observation.getNumTails();
        }

        for (Observation observation : _expectedObservationsForCoinB)
        {
            sumCoinBHeads += observation.getNumHeads();
            sumCoinBTails += observation.getNumTails();
        }

        return new Parameters(sumCoinAHeads / (sumCoinAHeads + sumCoinATails),
                              sumCoinBHeads / (sumCoinBHeads + sumCoinBTails));

        //o.printf("parameters: %s\n", _parameters);

    }

    /*************************************************************************
    Since the coin-toss experiment posed in this article is a Bernoulli trial,
    use a binomial probability Pr(X=k; n,p) = (n choose k) * p^k * (1-p)^(n-k).
    *************************************************************************/
    private static double binomialProbability(int n, int k, double p)
    {
        double q = 1.0 - p;
        return nChooseK(n, k) * Math.pow(p, k) * Math.pow(q, n-k);
    }

    private static long nChooseK(int n, int k)
    {
        long numerator = 1;

        for (int i = 0; i < k; i++)
        {
            numerator = numerator * n;
            n--;
        }

        long denominator = factorial(k);

        return (long)(numerator / denominator);
    }

    private static long factorial(int n)
    {
        long result = 1;
        for (; n >0; n--)
        {
            result = result * n;
        }

        return result;
    }

    /*************************************************************************
    Entry point into the program.
    *************************************************************************/
    public static void main(String argv[])
    {
        // Create the observations and initial parameter guess
        // from the (Do and Batzoglou, 2008) article.

        List<Observation> observations = new ArrayList<Observation>();
        observations.add(new Observation("HTTTHHTHTH"));
        observations.add(new Observation("HHHHTHHHHH"));
        observations.add(new Observation("HTHHHHHTHH"));
        observations.add(new Observation("HTHTTTHHTT"));
        observations.add(new Observation("THHHTHHHTH"));

        Parameters initialParameters = new Parameters(0.6, 0.5);

        EM em = new EM(observations, initialParameters);

        Parameters finalParameters = em.run();

        o.printf("Final result:\n%s\n", finalParameters);
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.