ทำไม np.dot ไม่แม่นยำ? (อาร์เรย์ n-dim)


15

สมมติว่าเราใช้np.dotสอง'float32'อาร์เรย์สองมิติ:

res = np.dot(a, b)   # see CASE 1
print(list(res[0]))  # list shows more digits
[-0.90448684, -1.1708503, 0.907136, 3.5594249, 1.1374011, -1.3826287]

เบอร์ ยกเว้นพวกเขาสามารถเปลี่ยน:


กรณีที่ 1 : ฝานa

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(6, 6).astype('float32')

for i in range(1, len(a)):
    print(list(np.dot(a[:i], b)[0])) # full shape: (i, 6)
[-0.9044868,  -1.1708502, 0.90713596, 3.5594249, 1.1374012, -1.3826287]
[-0.90448684, -1.1708503, 0.9071359,  3.5594249, 1.1374011, -1.3826288]
[-0.90448684, -1.1708503, 0.9071359,  3.5594249, 1.1374011, -1.3826288]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]
[-0.90448684, -1.1708503, 0.907136,   3.5594249, 1.1374011, -1.3826287]

ผลลัพธ์ต่างกันถึงแม้ว่าชิ้นงานที่พิมพ์ออกมานั้นจะมาจากตัวเลขเดียวกันแน่นอนคูณ


กรณีที่ 2 : แผ่aใช้รุ่น 1D ของb, แล้วฝานa:

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(1, 6).astype('float32')

for i in range(1, len(a)):
    a_flat = np.expand_dims(a[:i].flatten(), -1) # keep 2D
    print(list(np.dot(a_flat, b)[0])) # full shape: (i*6, 6)
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]
[-0.3393164, 0.9528787, 1.3627989, 1.5124314, 0.46389243, 1.437775]

กรณีที่ 3 : การควบคุมที่แข็งแกร่ง ตั้งค่าการไม่เกี่ยวข้องทั้งหมดให้เป็นศูนย์ : เพิ่มa[1:] = 0เป็นรหัส CASE 1 ผลลัพธ์: ข้อขัดแย้งยังคงอยู่


กรณีที่ 4 : ตรวจสอบดัชนีอื่นที่ไม่ใช่[0]; เช่นเดียวกับ[0]ผลลัพธ์จะเริ่มมีเสถียรภาพจำนวนการขยายอาร์เรย์คงที่จากจุดสร้างของพวกเขา เอาท์พุต

np.random.seed(1)
a = np.random.randn(9, 6).astype('float32')
b = np.random.randn(6, 6).astype('float32')

for j in range(len(a) - 2):
    for i in range(1, len(a)):
        res = np.dot(a[:i], b)
        try:    print(list(res[j]))
        except: pass
    print()

ดังนั้นสำหรับกรณี 2D * 2D ผลลัพธ์ที่แตกต่าง - แต่สอดคล้องกันสำหรับ 1D * 1D จากการอ่านบางส่วนของฉันสิ่งนี้ดูเหมือนจะเกิดขึ้นจาก 1D-1D โดยใช้การเพิ่มอย่างง่ายในขณะที่ 2D-2D ใช้ 'นักเล่น' การเพิ่มประสิทธิภาพที่เพิ่มขึ้นซึ่งอาจมีความแม่นยำน้อยลง อย่างไรก็ตามฉันไม่สามารถเข้าใจได้ว่าทำไมความแตกต่างหายไปในกรณีที่ 1 เมื่อaหั่นบาง ๆ ผ่านชุด 'ธรณีประตู'; ยิ่งใหญ่กว่าaและbภายหลังเกณฑ์นี้ดูเหมือนจะโกหก แต่มันก็มีอยู่เสมอ

ทั้งหมดกล่าวว่า: ทำไมจึงnp.dotไม่แน่นอน (และไม่สอดคล้องกัน) สำหรับอาร์เรย์ ND-ND Git ที่เกี่ยวข้อง


ข้อมูลเพิ่มเติม :

  • สภาพแวดล้อม : ระบบปฏิบัติการ Win-10, Python 3.7.4, Spyder 3.3.6 IDE, Anaconda 3.0 2019/10
  • CPU : i7-7700HQ 2.8 GHz
  • Numpy v1.16.5

ห้องสมุดผู้ร้ายที่เป็นไปได้ : Numpy MKL - รวมถึงห้องสมุด BLASS; ขอบคุณBi Ricoสำหรับการสังเกต


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

np.random.seed(1)
a = (0.01*np.random.randn(9, 9999)).astype('float32') # first multiply then type-cast
b = (0.01*np.random.randn(9999, 6)).astype('float32') # *0.01 to bound mults to < 1

for i in range(1, len(a)):
    print(list(np.dot(a[:i], b)[0]))

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

ด้านล่างนี้เป็น gif ของอาร์เรย์ที่เกิดจากการป้อนไปยังแบบจำลองที่มีพื้นฐานa[0]คือ w / len(a)==1vs. len(a)==32:


ผลลัพธ์แพลตฟอร์มอื่น ๆตามและด้วยการทดสอบของPaul :

ทำซ้ำกรณีที่ 1 (บางส่วน) :

  • Google Colab VM - Intel Xeon 2.3 G-Hz - Jupyter - Python 3.6.8
  • Win-10 Pro Docker Desktop - Intel i7-8700K - jupyter / scipy-notebook - Python 3.7.3
  • Ubuntu 18.04.2 LTS + Docker - AMD FX-8150 - jupyter / scipy-notebook - Python 3.7.3

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

กรณีที่ 1 ไม่ได้ทำซ้ำ :

  • Ubuntu 18.04.3 LTS - Intel i7-8700K - IPython 5.5.0 - Python 2.7.15+ และ 3.6.8 (ทดสอบ 2 ครั้ง)
  • Ubuntu 18.04.3 LTS - Intel i5-3320M - IPython 5.5.0 - Python 2.7.15+
  • Ubuntu 18.04.2 LTS - AMD FX-8150 - IPython 5.5.0 - Python 2.7.15rc1

หมายเหตุ :

  • เชื่อมโยง Colab โน๊ตบุ๊คและ jupyter สภาพแวดล้อมที่แสดงให้เห็นความแตกต่างห่างไกลน้อย (และเฉพาะสำหรับสองแถวแรก) มากกว่าเป็นที่สังเกตในระบบของฉัน นอกจากนี้กรณีที่ 2 ยังไม่เคยมีความไม่แน่นอน
  • ภายในตัวอย่างที่ จำกัด มากนี้สภาพแวดล้อม Jupyter ปัจจุบัน (เชื่อมต่อ) มีความไวมากกว่าสภาพแวดล้อม IPython
  • np.show_config()ยาวเกินไปที่จะโพสต์ แต่โดยสรุป: IPython envs นั้นใช้ BLAS / LAPACK Colab ใช้ OpenBLAS ใน IPython Linux envs ไลบรารี BLAS ถูกติดตั้งระบบ - ใน Jupyter และ Colab พวกเขามาจาก / opt / conda / lib

UPDATE : คำตอบที่ยอมรับนั้นถูกต้อง แต่กว้างและไม่สมบูรณ์ คำถามยังคงเปิดอยู่สำหรับทุกคนที่สามารถอธิบายพฤติกรรมในระดับรหัสได้นั่นคืออัลกอริธึมที่แน่นอนที่ใช้โดยnp.dotและอธิบายถึง 'ความไม่สอดคล้องที่สอดคล้องกัน' ที่สังเกตได้ในผลลัพธ์ข้างต้น (ดูความคิดเห็นด้วย) นี่คือการใช้งานโดยตรงนอกเหนือจากการถอดรหัสของฉัน: sdot.c - arraytypes.c.src


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
ซามูเอล Liew

อัลกอริธึมทั่วไปที่ndarraysมักจะไม่สนใจการสูญเสียความแม่นยำของตัวเลข เพราะเพื่อความเรียบง่ายพวกมันreduce-sumไปตามแต่ละแกนลำดับของการดำเนินการอาจไม่ใช่สิ่งที่ดีที่สุด ... โปรดทราบว่าหากคุณคำนึงถึงข้อผิดพลาดที่แม่นยำคุณอาจใช้เช่นกันfloat64
Vitor SRG

ฉันอาจไม่มีเวลาตรวจสอบในวันพรุ่งนี้ดังนั้นให้มอบรางวัลตอนนี้
พอล

@Paul มันจะได้รับรางวัลโดยอัตโนมัติสำหรับคำตอบที่ได้รับการโหวตสูงสุดอยู่ดี - แต่ก็ดีขอบคุณที่แจ้งให้ทราบ
OverLordGoldDragon

คำตอบ:


7

ดูเหมือนว่าความไม่แน่นอนของตัวเลขที่หลีกเลี่ยงไม่ได้ ตามที่อธิบายไว้ที่นี่ , NumPy ใช้สูงเพิ่มประสิทธิภาพอย่างรอบคอบปรับวิธีหน่ายการคูณเมทริกซ์ ซึ่งหมายความว่าอาจเป็นลำดับของการดำเนินการ (ผลรวมและผลิตภัณฑ์) ตามด้วยเมทริกซ์คูณ 2 การเปลี่ยนแปลงเมื่อขนาดของเมทริกซ์เปลี่ยน

การพยายามให้ชัดเจนยิ่งขึ้นเรารู้ว่าในทางคณิตศาสตร์แต่ละองค์ประกอบของเมทริกซ์ที่ได้นั้นสามารถคำนวณได้เป็นผลคูณดอทของเวกเตอร์สองตัว (ลำดับความยาวเท่ากันของตัวเลข) แต่นี่ไม่ใช่วิธีที่ NumPy คำนวณองค์ประกอบของเมทริกซ์ที่ได้ Infact มีอัลกอริธึมที่มีประสิทธิภาพมากกว่า แต่มีความซับซ้อนเช่นอัลกอริธึม Strassenที่ได้ผลลัพธ์เดียวกันโดยไม่ต้องคำนวณโดยตรงกับผลิตภัณฑ์จุดคอลัมน์แถว

เมื่อใช้อัลกอริธึมดังกล่าวแม้ว่าองค์ประกอบC ijของเมทริกซ์ผลลัพธ์C = ABจะถูกกำหนดทางคณิตศาสตร์เป็นผลิตภัณฑ์ดอทของแถวi-thของAกับคอลัมน์j-thของBหากคุณคูณเมทริกซ์A2 ที่มี เดียวกันที่ iแถวเป็นกับเมทริกซ์B2มีเหมือนกันที่ jคอลัมน์เป็นBองค์ประกอบC2 IJจะถูกคำนวณจริงต่อไปนี้ลำดับที่แตกต่างกันของการดำเนินงาน (ที่ขึ้นอยู่กับทั้งA2และB2 เมทริกซ์) อาจนำไปสู่ข้อผิดพลาดเชิงตัวเลขที่แตกต่างกัน

นั่นคือเหตุผลที่แม้ว่าทางคณิตศาสตร์C ij = C2 ij (เช่นในกรณีของคุณ 1) ลำดับการดำเนินการที่แตกต่างกันตามด้วยอัลกอริทึมในการคำนวณ (เนื่องจากการเปลี่ยนแปลงในขนาดเมทริกซ์) นำไปสู่ข้อผิดพลาดตัวเลขที่แตกต่างกัน ข้อผิดพลาดที่เป็นตัวเลขอธิบายถึงผลลัพธ์ที่แตกต่างกันเล็กน้อยขึ้นอยู่กับสภาพแวดล้อมและความจริงที่ว่าในบางกรณีสำหรับบางสภาพแวดล้อมข้อผิดพลาดที่เป็นตัวเลขอาจหายไป


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

นอกจากนี้ยังไม่ใช่ "หลีกเลี่ยงไม่ได้" ดังที่แสดงที่ด้านล่างของคำถาม - และขอบเขตของความไม่แน่นอนแตกต่างกันไปตามสภาพแวดล้อมซึ่งยังไม่สามารถอธิบายได้
OverLordGoldDragon

1
@OverLordGoldDragon: (1) ตัวอย่างเล็ก ๆ น้อย ๆ ด้วยการเพิ่ม: ใช้หมายเลข nนำตัวเลขkที่ต่ำกว่าความแม่นยำของkตัวเลขหลักสุดท้ายของ mantissa สำหรับชนพื้นเมืองของงูหลามn = 1.0และk = 1e-16ทำงาน ks = [k] * 100ตอนนี้ให้ ดูว่าsum([n] + ks) == nในขณะsum(ks + [n]) > nที่นั่นคือคำสั่งการรวมมีความสำคัญ (2) CPU ที่ทันสมัยมีหลายหน่วยที่จะดำเนินการกับการดำเนินการ floating-point (FP) ในแบบคู่ขนานและลำดับที่a + b + c + dคำนวณบน CPU ไม่ได้ถูกกำหนดแม้ว่าคำสั่งa + bมาก่อนc + dในรหัสเครื่อง
9000

1
@OverLordGoldDragon คุณควรระวังว่าตัวเลขส่วนใหญ่ที่คุณขอให้โปรแกรมของคุณจัดการกับไม่สามารถแสดงได้อย่างแน่นอนโดยจุดลอยตัว ลองformat(0.01, '.30f')ดู หากแม้แต่จำนวนอย่างง่ายอย่างเช่น0.01ไม่สามารถแสดงได้อย่างแม่นยำด้วยจุดลอยตัวของ NumPy ไม่จำเป็นต้องรู้รายละเอียดลึก ๆ ของอัลกอริทึมการคูณเมทริกซ์ NumPy เพื่อทำความเข้าใจจุดคำตอบของฉัน นั่นคือเมทริกซ์เริ่มต้นที่แตกต่างกันนำไปสู่ลำดับการดำเนินการที่แตกต่างกันดังนั้นผลลัพธ์ที่เท่าเทียมกันทางคณิตศาสตร์อาจแตกต่างกันในจำนวนเล็กน้อยเนื่องจากข้อผิดพลาดเชิงตัวเลข
mmj

2
@OverLordGoldDragon อีกครั้ง: มนต์ดำ มีกระดาษที่ต้องอ่านที่ CS MOOC สองสามแห่ง การเรียกคืนของฉันไม่ได้ดีขนาดนั้น แต่ฉันคิดว่ามันเป็นอย่างนั้น: itu.dk/~sestoft/bachelor/IEEE754_article.pdf
Paul
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.