Sửa lỗi dấu phẩy động Python

Bạn đã bao giờ gặp phải các vấn đề kỳ lạ khi làm việc với các số dấu phẩy động - chẳng hạn như khi thực hiện thao tác nào đó như sau?

a = 10b = 0.1c = 0.2a*[b+c] == a*b + a*c # This will print False

Đợi đã, cái gì?

Hãy xem một ví dụ khác

>>> .2 + .2 + .2 == .6False

Hoặc hãy xem vòng lặp này - nó sẽ chạy vô thời hạn

foo = 0while foo != 1.0:    foo = foo + 0.1print[foo]

Chuyện Gì Đang Xảy Ra?

TL; DR — Số dấu phẩy động thật kỳ lạ

Số dấu phẩy động hoạt động khá kỳ lạ trên các hệ thống nhị phân. Do đó, điều quan trọng là bạn nhận thức được hành vi này. Tôi sẽ giúp bạn tránh những sai lầm đơn giản có thể bỏ qua, chẳng hạn như một vòng lặp vô tận hoặc chỉ so sánh cơ bản

Điều trên có thể gây sốc cho nhiều độc giả. Trong bài đăng ngắn này, chúng ta sẽ tìm hiểu chuyện gì đang xảy ra

số nguyên

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np

Phần đầu tiên của câu chuyện là số nguyên. Lớn lên, chúng ta học cách sử dụng hệ thống số cơ sở 10 [hoặc hệ thống “từ chối”]. Denary dựa trên lũy thừa của 10 và chúng ta có thể sử dụng các chữ số 0 - 9 để giúp chúng ta biểu diễn các số. Ví dụ: số 104 có thể được hiểu là

  • 4 x 1 [$10^0$]
  • 0 x 10 [$10^1$]
  • 1 x 100 [$10^2$]

Hãy đặt nó ở định dạng bảng dễ đọc

Đơn vị$10^2$$10^1$$10^0$Giá trị100101Số lượng đơn vị104Tổng cộng10004

Ngược lại, máy tính sử dụng hệ thống cơ sở 2 [i. e. , “binary”] để lưu số. Hệ nhị phân dựa trên lũy thừa của 2 chứ không phải lũy thừa của 10 và chúng ta chỉ có hai số, 0 và 1, để làm việc [hãy nghĩ về chúng lần lượt là công tắc “tắt” và “bật”]. Ở dạng nhị phân, 104 trông giống như

>>> f"{104:b}"
'1101000'

Trong định dạng bảng của chúng tôi

Đơn vị$2^6$$2^5$$2^4$$2^3$$2^2$$2^1$$2^0$Giá trị6432168421Số lượng đơn vị1101000Tổng643208000

Chúng tôi gọi những giá trị 0/1 nhị phân đơn này, bit. Chúng ta có thể thấy rằng chúng ta cần 7 bit để biểu diễn số 104. Chúng tôi có thể xác nhận rằng bằng cách sử dụng phương thức số nguyên

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
3 trong Python

>>> x = 104
>>> x.bit_length[]
7

Bất kỳ số nguyên nào cũng có thể được biểu diễn dưới dạng nhị phân [nếu chúng ta có đủ bit]. Phạm vi của các số nguyên không dấu mà chúng ta có thể biểu diễn bằng

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
4 bit là. 0 đến $2^{N}-1$. Tuy nhiên, chúng tôi thường dành một bit để chỉ định dấu [- hoặc +] của một số, vì vậy phạm vi số nguyên có dấu mà bạn có thể biểu diễn bằng
>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
4 bit là. $-2^{N-1}$ đến $2^{N-1}-1$

Ví dụ: với 8 bit, chúng ta có thể biểu diễn các số nguyên không dấu từ 0 đến 255 hoặc các số nguyên có dấu -128 đến 127

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127

Chúng tôi có thể xác nhận tất cả điều này với chức năng NumPy hữu ích

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
6

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
>>> np.iinfo["uint8"]  # unsigned 8-bit integer
iinfo[min=0, max=255, dtype=uint8]
>>> np.iinfo["int16"]  # signed 16-bit integer
iinfo[min=-32768, max=32767, dtype=int16]
>>> np.iinfo["int64"]  # signed 64-bit integer
iinfo[min=-9223372036854775808, max=9223372036854775807, dtype=int64]

Thật thú vị khi xem điều gì sẽ xảy ra nếu bạn cố gắng lưu trữ một số nguyên nằm ngoài phạm vi có thể biểu thị. Trong NumPy, bạn thực sự sẽ không gặp lỗi. Thay vào đó, NumPy sẽ thay đổi số của bạn nằm trong phạm vi có thể biểu thị, chỉ bằng cách lặp xung quanh phạm vi

>>> np.array[[-87, 31], "int8"]  # this is fine
array[[-87,  31], dtype=int8]
#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
0

Số phân số

Vì vậy, chúng tôi đã xem xét các số nguyên ở trên, nhưng còn các số phân số như 14 thì sao?. 75? . Ở định dạng bảng của chúng tôi, 14. 75 hình như

Đơn vị$10^1$$10^0$$10^{-1}$$10^{-2}$Value1010. 10. 01Số căn1475Tổng số1040. 70. 05

Điều đó có vẻ khá tự nhiên và ở dạng nhị phân, nó cũng giống như vậy. Chúng ta chỉ cần giới thiệu một “điểm nhị phân”. Mọi thứ ở bên trái của điểm nhị phân có số mũ dương và mọi thứ ở bên phải của điểm nhị phân có số mũ âm. Vì vậy, trong hệ nhị phân, 14. 75 sẽ trông như thế này.

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
7. Ở định dạng bảng

Đơn vị$2^3$$2^2$$2^1$$2^0$$2^{-1}$$2^{-2}$Value84210. 50. 25 Số lượng đơn vị 111011 Tổng cộng 84200. 50. 25

Số điểm cố định

Như chúng ta đã thấy trước đó, chúng ta thường có một số bit cố định để làm việc khi lưu trữ một số, e. g. , 8, 16, 32, 64 bit. Điều này là do, theo thuật ngữ thông thường, việc có một số bit cố định giúp máy tính của chúng ta phân bổ và sử dụng bộ nhớ dễ dàng hơn

Vì vậy, nếu chúng ta có một số bit cố định, giả sử là 8 bit, làm cách nào để quyết định vị trí đặt điểm nhị phân để giúp chúng ta lưu trữ các số phân số?

Đơn vị$2^3$$2^2$$2^1$$2^0$$2^{-1}$$2^{-2}$$2^{-3}$$2^{-4}$Value84210. 50. 250. 1250. 0625

Trong trường hợp này, các số lớn nhất và nhỏ nhất [gần 0 nhất] mà chúng ta có thể biểu diễn trong trường hợp không dấu là

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
1____3_______2

Nhưng nếu chúng ta muốn biểu diễn những số lớn hơn thế này thì sao?

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
3_______3_______4

Như bạn có thể thấy, chúng ta có sự đánh đổi giữa việc có thể biểu diễn số lớn và số nhỏ tùy thuộc vào vị trí chúng ta đặt điểm nhị phân. Nhưng có định dạng nào cho phép chúng tôi biểu diễn cả số lớn và số nhỏ không?

Số dấu phẩy động

Tại thời điểm này, bạn có thể đã đọc “dấu phẩy động” và có một chút “ah-ha. ” thời điểm bởi vì bạn thấy chúng ta đang đi đâu. Thay vì có một vị trí cố định cho điểm nhị phân của chúng tôi, chúng tôi có thể để nó “trôi nổi” xung quanh tùy thuộc vào số lượng chúng tôi muốn lưu trữ. Nhớ lại “ký hiệu khoa học” trong hệ thống từ chối, đối với số 1234 trông giống như $1. 234 \times 10^3$, hoặc đối với máy tính, chúng tôi thường sử dụng

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
8 để viết tắt

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
5

Tôi muốn bạn lưu ý rằng “số mũ” đang thực sự kiểm soát vị trí của dấu thập phân, Hãy xem xét các số sau

  • $1. 234 \times 10^0 = 1. 234$
  • $1. 234 \lần 10^1 = 12. 34$
  • $1. 234 \lần 10^2 = 123. 4$
  • $1. 234 \lần 10^3 = 1234. $

Hãy xem bằng cách thay đổi giá trị của số mũ, chúng ta có thể kiểm soát vị trí của dấu phẩy động và biểu thị một dải giá trị như thế nào? . Đây là định dạng dấu phẩy động

$$1. M \times 2^E$$

  • $M$ = “lớp phủ”
  • $E$ = “số mũ”

Lưu ý rằng $. $ là một “điểm nhị phân”, các chữ số bên trái là 2 với số mũ +ve, các chữ số bên phải là 2 với số mũ âm, như trước đây

Xét con số 10. $10 = 1. 25 \lần 8 = 1. 25 \lần 2^3$. Vậy $M=. 25$ và $E=3$. Nhưng chúng tôi muốn nhị phân, không phải thập phân, vì vậy $M=01$ và $E=11$ [Tôi sẽ để bạn giải quyết vấn đề này]. Do đó, 10 trong dấu phẩy động là. $1. 01 \times 2^{11}$

Đây là nơi điều kỳ diệu xảy ra, giống như số mũ của 10 trong ký hiệu khoa học từ chối xác định vị trí của dấu thập phân, do đó, số mũ của số dấu phẩy động xác định vị trí của dấu nhị phân. Với $1. 01 \times 2^{11}$, số mũ là

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
9 ở dạng nhị phân và là
>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
0 ở dạng thập phân - vì vậy hãy di chuyển dấu nhị phân sang bên phải ba vị trí. $1. 01 \times 2^{11} = 1010. $
>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
1 ở dạng nhị phân là gì?

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
6

Tất nhiên là 10. Điều đó thật tuyệt - giờ đây chúng ta có định dạng “dấu phẩy động” này sử dụng số mũ để giúp chúng ta biểu diễn cả số phân số lớn và nhỏ [không giống như điểm cố định mà chúng ta sẽ phải chọn cái này hay cái khác. ] Tôi đã viết một hàm

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
2 để hiển thị bất kỳ số nào ở định dạng dấu phẩy động [đừng lo lắng về mã quá nhiều]

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
7

Hãy thử một số con số

#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
8
#  We'll use these imports later on
>>> import math
>>> import struct
>>> import numpy as np
9
>>> f"{104:b}"
'1101000'
0
>>> f"{104:b}"
'1101000'
1

Một câu hỏi bạn có thể có về những điều trên. Tôi nên sử dụng bao nhiêu bit cho phần định trị và bao nhiêu bit cho số mũ? . Tiêu chuẩn IEEE cho số học dấu phẩy động [IEEE 754], đây là thứ mà hầu hết các máy tính/phần mềm sử dụng. Bạn sẽ chủ yếu sử dụng các kiểu dữ liệu

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
3 và
>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
4

  • Float 64 [còn gọi là “độ chính xác kép”]
    • 53 bit cho lớp phủ
    • 11 bit cho số mũ
  • Float 32 [còn được gọi là “độ chính xác đơn”]
    • 24 bit cho lớp phủ
    • 8 bit cho số mũ

Lỗi làm tròn

Nhiều số phân số không thể được biểu diễn chính xác bằng định dạng dấu phẩy động. Rốt cuộc, chúng tôi đang sử dụng một số bit hữu hạn để thử và biểu diễn và vô số số. Điều này chắc chắn sẽ dẫn đến lỗi làm tròn số - máy tính của bạn sẽ luôn làm tròn số đến số có thể biểu thị gần nhất

ví dụ 0. 1 không thể được biểu diễn chính xác ở dạng nhị phân [vui lòng thử và tạo 0. 1 sử dụng định dạng dấu phẩy động]. Python thường che giấu sự thật này để thuận tiện cho chúng ta, nhưng đây là “số 0 thực. 1"

>>> f"{104:b}"
'1101000'
2

Vậy 0. 1 thực sự được biểu diễn dưới dạng một số hơi lớn hơn 0. 1. Tôi đã viết một hàm để tìm hiểu xem một số được lưu trữ chính xác hay không chính xác và để hiển thị lỗi làm tròn

>>> f"{104:b}"
'1101000'
3_______4_______4
>>> f"{104:b}"
'1101000'
5

Vì vậy, làm thế nào xấu là những lỗi làm tròn? . Trước khi thực hiện, hãy tưởng tượng chúng ta lại ở trong hệ thập phân. Đối với một số có số lượng chữ số có nghĩa cố định, khoảng cách giữa số đó và số lớn nhất tiếp theo có cùng định dạng có thể được xác định là chữ số có nghĩa nhỏ nhất nhân với số mũ

Số Tiếp theo Khoảng cách lớn nhất8. 982e08. 983e00. 001e0 = 0. 0010. 001e10. 002e10. 001e1 = 0. 013. 423e23. 424e20. 001e2 = 0. 1

Điều tương tự cũng xảy ra với các số dấu phẩy động nhị phân của chúng tôi. Khoảng cách có thể được xác định là phần nhỏ nhất của phần định trị nhân với số mũ của một số. Phần nhỏ nhất của lớp phủ là gì? . 1 bit được dành riêng cho dấu của số, vì vậy chúng ta còn lại 52. Do đó chữ số có nghĩa nhỏ nhất là $2^{-52}$

Để rõ ràng ở đây, đây là định dạng dấu phẩy động được viết dưới dạng lũy ​​thừa 2 cho tất cả 52 bit định trị

$$2^0{}. {}2^{-1}2^{-2}2^{-3}…{}2^{-50}2^{-51}\boldsymbol{2^{-52}} \times 2 ^ {

Xem $2^{-52}$ là giá trị nhỏ nhất chúng ta có thể thay đổi như thế nào?

>>> f"{104:b}"
'1101000'
6

Như bạn có thể thấy, số 1 có số mũ là 0. Hãy nhớ chữ số có nghĩa nhỏ nhất nhân với số mũ của chúng tôi sẽ cho chúng tôi khoảng cách giữa số này và số tiếp theo mà chúng tôi có thể biểu thị bằng định dạng của mình. Vì vậy, đối với số 1, khoảng cách là $2^{-52} * 2^0$

>>> f"{104:b}"
'1101000'
7

NumPy có một chức năng hữu ích mà chúng ta có thể sử dụng để tính khoảng cách.

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
6 trả về giá trị dấu phẩy động có thể biểu diễn tiếp theo sau
>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
7 đối với
>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
8. Số có thể biểu thị tiếp theo sau 1 phải là 1 cộng với khoảng cách ở trên, hãy kiểm tra

>>> f"{104:b}"
'1101000'
8
>>> f"{104:b}"
'1101000'
9
>>> x = 104
>>> x.bit_length[]
7
0

Nếu bạn thực hiện một phép tính đặt bạn ở đâu đó giữa khoảng trống của hai số, máy tính sẽ tự động làm tròn đến số gần nhất. Tôi thích coi đây là một dãy núi trong máy tính. Các thung lũng là những con số đại diện. Nếu một phép tính đặt chúng ta ở sườn núi, chúng ta sẽ lăn xuống núi đến thung lũng gần nhất. Ví dụ: khoảng cách cho số 1 là $2^{-52}$, vì vậy nếu chúng tôi không thêm ít nhất một nửa số đó, chúng tôi sẽ không đạt được số đại diện tiếp theo

>>> x = 104
>>> x.bit_length[]
7
1

>>> x = 104
>>> x.bit_length[]
7
2

Chúng tôi đang làm việc với những con số khá nhỏ ở đây nên bạn có thể không quá sốc. Nhưng hãy nhớ rằng, số mũ của chúng ta càng lớn thì sai số làm tròn của chúng ta càng lớn.

>>> x = 104
>>> x.bit_length[]
7
3

Số mũ là 83 nên khoảng cách phải là

>>> x = 104
>>> x.bit_length[]
7
4

Thế là hơn 2 tỷ. 1 tỷ ít hơn một nửa khoảng cách đó, vì vậy nếu chúng ta thêm 1 tỷ vào

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
9, chúng ta sẽ không vượt qua đỉnh núi mà sẽ trượt trở lại xuống cùng một thung lũng

>>> x = 104
>>> x.bit_length[]
7
5

2 tỷ là hơn một nửa khoảng cách đó, vì vậy nếu chúng ta thêm nó vào

>>> np.iinfo["int8"]  # signed 8-bit integer
iinfo[min=-128, max=127, dtype=int8]
9, chúng ta sẽ trượt vào thung lũng tiếp theo [số đại diện]

>>> x = 104
>>> x.bit_length[]
7
6

Một điều khác bạn nên biết là, giống như với các kiểu dữ liệu số nguyên, có số lớn nhất và số nhỏ nhất mà bạn có thể biểu diễn bằng dấu phẩy động. Bạn có thể xác định điều này bằng tay [bằng cách làm cho phần định trị và số mũ lớn/nhỏ nhất có thể], nhưng chúng tôi sẽ sử dụng NumPy

>>> x = 104
>>> x.bit_length[]
7
7
>>> x = 104
>>> x.bit_length[]
7
8

Nếu phép tính của bạn cho kết quả là một số cao hơn

>>> np.iinfo["uint8"]  # unsigned 8-bit integer
iinfo[min=0, max=255, dtype=uint8]
1, bạn sẽ nhận được "lỗi tràn". Trong NumPy, số của bạn được chuyển thành
>>> np.iinfo["uint8"]  # unsigned 8-bit integer
iinfo[min=0, max=255, dtype=uint8]
2

>>> x = 104
>>> x.bit_length[]
7
9

Giá trị gần nhất với 0 mà bạn có thể đại diện là

>>> bits = 8
>>> print[f"unsigned min: 0"]
>>> print[f"unsigned max: {2 ** bits - 1}"]
>>> print[f"signed min: {-2 ** [bits - 1]}"]
>>> print[f"signed max: {2 ** [bits - 1] - 1}"]
unsigned min: 0
unsigned max: 255 
signed min: -128
signed max: 127
0

Nếu phép tính của bạn cho kết quả là một số thấp hơn

>>> np.iinfo["uint8"]  # unsigned 8-bit integer
iinfo[min=0, max=255, dtype=uint8]
3 [số gần nhất với 0], bạn sẽ nhận được "lỗi tràn". Trong NumPy, số của bạn được chuyển thành 0

Điều này thực sự không hoàn toàn đúng trong Python, vì tồn tại “các số bất thường” cho phép làm việc với các số nhỏ hơn một chút so với

>>> np.iinfo["uint8"]  # unsigned 8-bit integer
iinfo[min=0, max=255, dtype=uint8]
3. Tôi sẽ để bạn tự nghiên cứu vì bài đăng này hơi dài, nhưng giá trị nhỏ nhất thực tế mà bạn có thể đại diện là

Cái gì đang nổi

Đó là sự cố xảy ra khi biểu diễn bên trong các số dấu phẩy động, sử dụng một số chữ số nhị phân cố định để biểu thị một số thập phân . Rất khó để biểu diễn một số thập phân dưới dạng nhị phân nên trong nhiều trường hợp dẫn đến sai số làm tròn nhỏ.

Tại sao Python float không chính xác?

Các phép tính dấu phẩy động không chính xác vì chủ yếu là các số hữu tỉ là xấp xỉ không thể biểu diễn hữu hạn trong cơ số 2 và nói chung chúng là xấp xỉ .

Tại sao 0. 1 0. 2 không phải là 0. 3 con trăn?

Tương tự, giá trị nhị phân của 0. 2 được lưu dưới dạng 0. 001100110. Bây giờ, khi bạn thêm 0. 1 + 0. 2 bằng Python [hoặc bằng một số ngôn ngữ lập trình khác], Python chuyển đổi 0. 1 và 0. 2 về dạng nhị phân. Sau đó, nó thực hiện phép cộng. Kết quả sẽ không bao giờ bằng 0. 3 chính xác .

Kiểu float trong Python 3 có thể biểu thị số thập phân 0 không. 1 mà không có lỗi?

Một số giá trị không thể được biểu diễn chính xác trong kiểu dữ liệu float. Chẳng hạn, lưu trữ 0. 1 trong biến float [là giá trị dấu phẩy động nhị phân], chúng ta chỉ nhận được giá trị gần đúng . Tương tự, giá trị 1/3 không thể được biểu diễn chính xác theo kiểu dấu chấm động thập phân.

Chủ Đề