5 Ocak 2019 Cumartesi

Kayar Noktalı Sayılar ve Kesme Hataları


Merhaba. Bu konuda uzun zamandır yazmak istiyordum ama zaman bulamıyordum. Konu, geniş olduğundan kafamda toparlamak ve yazmak uzun zaman aldı. Aynı nedenden yazıyı iki parça olarak böldüm. Bu yazılarda tek değerlikli (single precision) sayılar üzerinden bilgisayarda ondalıklı (floating point) sayıların nasıl tutulduğunu ve bunun ne tür hatalara yol açtığını anlatacağım. Öncesinde iki ilginç ekran görüntüsü ekledim. İlki Excel 2007'den ama daha güncel sürümlerde de benzer sonuçlar elde edilebilir:


A sütununda formüllerin sonuçları var. B sütununda, A sütunundaki formülleri metin olarak yazdım. Yani A1'de =1-0.2-0.2-0.2-0.2-0.2, A2'de =1-0.1-0.1...-0.1 (10 tane 0.1) var. A2 ve A3'teki sonuçlar ilginç. Sıfır olması gereken iki sonucun da sıfırdan saptığı görülüyor. Bu neden oldu? İkinci sorulması gereken aynı ilginçlik A4 veya A5'te neden tekrarlanmıyor? Aşağıda görüleceği gibi Octave/Matlab'le de aynı sonuçlar alınıyor.



Bu davranışla ilgili bir ipucu vereceğim. Octave'da varsayılan veri türü çift değerlikli (double) sayı. Tamsayı olduğu halde 1 bile çift değerlikli olarak işlem görüyor (yandaki ekran görüntüsüne bakınız). Sayıları 0.2 yerine single(0.2) verip (tek duyarlıklı sayıya dönüştürüp), bunları ardarda 1'den çıkardığımda hata artıyor. Örn. 0.2 için hata 10-17 mertebesinden 10-8 mertebesine yükseldi ama single(0.0625) için hata yine 0.

Sayıların Bilimsel Gösterimi
Hatanın nedenlerine inmeden önce okul yıllarına dönüp normalleştirilmiş sayı ve sayıların bilimsel gösterimini hatırlamak gerekiyor: Çok büyük veya çok ondalıklı sayılar yazılırken sayının anlamlı basamakları yazılıp yanına 10n gibi bir çarpan eklenir. Sıfıra yakın sayılar için n, negatif ve büyük sayılar için n pozitif olur.

Örn.
6 720 000 000 = 6.72 * 109
0.000 000 007 51 = 7.51 * 10-9

m, bir gerçel sayı ve n bir tamsayı olmak üzere, onluk tabandaki bilimsel gösterim, m * 10n olarak genellenir. m'e katsayı, n'e üs diyelim. m'i gerçel sayı seçtim ama bunun üzerine bazı kısıtlamalar daha koymak gerekir. di'ler rakamlar {0, 1, ..., 9} ve d0, sıfır olmamak üzere m aşağıdaki gibi yazılabilir:


d0'ın sıfır olduğunu düşünelim: Örn. 0.672 * 1010 = 6.72 * 109
d0'ın 10dan büyük bir değer olduğunu düşünelim: 67.2 * 108 = 6.72 * 109

Dolayısıyla her sayı yukarıdaki gibi tek bir şekilde yazılabilir. Yukarıda onluk tabanın altını çizdim çünkü herhangi bir sayı sistemine genellersem yukarıdaki ifade şöyle olur:


İkilik sayı sistemi için b = 2 seçilir ve kısıtlar gözönüne alınırsa ikilik sayı sisteminde d0'ın yalnızca 1 olabileceği görülür.

İkilik Sayı Sisteminde Ondalıkların Gösterimi
Bu konu lise matematiğinin dışında ama mantığı basit. Onluk sistemde basamak değerleri şöyle:
İkilik sayı sistemine genellendiğinde basamak değerleri 20, 21, 22... olarak gidiyorsa noktadan sonraki basamak değerleri de benzer biçimde 2-1, 2-2, 2-3... şeklinde devam eder.
Yukarıdaki sayı 23 + 21 + 20 + 2-2 + 2-3 = 11.375'dir. Kısacası ikilik sayı sisteminde de sayılar bilimsel gösterimle tekil olarak yazılabilir. Katsayının tamsayı kısmının her zaman 1 olacağını yazmıştım. Bu gösterimde tamsayılar aşağıdaki gibidir:

1 = 1 * 20
2 = 1 * 21
3 = 1.5 * 21
4 = 1 * 22
5 = 1.25 * 22
6 = 1.5 * 22
7 = 1.75 * 22
8 = 1 * 23
...

Ondalıklı sayıları bilgisayarda göstermek için, Hexpert adında bir Hex düzenleyici (editör) kullanacağım. Bu düzenleyiciyi bilgisayarla tanıştığım zamandan beri kullanıyorum. En sevdiğim düzenleyici ama artık geliştirilmiyor. İkinci sevdiğim düzenleyici HxD ve aktif olarak geliştiriliyor. Bu yazıyı hazırlarken baktığımda HxD'nin yeni sürümünün çıktığını farkettim. Son sürümde dosya düzenleme daha yetenekli duruma gelmiş ve bu düzenleyici de kullanılabilir. Ben ekran görüntülerini Hexpert'den aldım. Aşağıda küçük bir dosyanın ekran görüntüsü var. İçinde boşluklar olan bir metin dosyası oluşturup bunu açmak yeterli.

Yanda dosyanın byte'ları, alttaki kutularda imlecin olduğu yerdeki 8, 16 ve 32 bitlik değerler var. S'ler işaretli U'lar işaretsiz değerler. İşaretli sayılarda sayının ilk biti işaret (birse negatif, sıfırsa pozitif) biti. İşaretsiz sayılarda bu bit sayıya katılıyor. Alttaki kutular değerleri elle girmeye olanak veriyor. Örn. 30h ofsetindeki 8 bit (00h) ve 16 bit (00h 00h) değer sıfır. Gösterim Little Endian olduğundan 32 bitlik değerin byte'ları tersine okunduğunda (3fh 80h 00h 00h) 63 * 2563 + 128 * 2562 + 0 * 256 + 0 = 1 065 353 216 değerini alıyor. Asıl ilgilendiğim FP32 hücresi. Ondalıklı sayılar bilgisayarda her zaman işaretli olarak ele alındıklarından bunların işaretsizi yok. FP32, 32 bit floating point yani tek değerlikli anlamına geliyor. FP64 ise 64-bit floating point (çift değerlikli). Buna şimdilik değinmeyeceğim. Yalnızca 40h offsetinde FP64 1.0 değeri var.

Bu gösterim IEEE-754 standardıyla belirleniyor. 1985'teki standartta 'single' ve 'double' ifadeleri geçerken 2008 revizyonunda bunlar binary32 ve binary64 olarak anılıyor. Standarda göre 32 bitlik ondalıklı sayının ilk biti işaret, sonraki 8 bit [30-23] üs ve geri kalan 22 bit [22-0] katsayı. 1.0'ı buna göre ayrıştıralım:

3F 80 00 00 = 0|011 1111 1|000 0000 0000 0000 0000 0000

[ Hexpert'te Alt+B ile ikilik taban görünümüne geçilebilir. ]

Tamsayı kısmı her zaman 1 olduğundan bu saklanmaz, her hesaplamada kendiliğinden eklenir. Dolayısıyla burada katsayı 0 + 1 = 1. Üs 0111 1111b = 127 ve standart gereği üsten 127 çıkarılır. Yani gerçek üs 127 - 127 = 0. İşaret biti 0 olduğundan pozitif. Hesaplandığında 1 * 2(127-127) = 1 bulunur.

2 = 1 * 21 olarak yazılır. Katsayıdan bir çıkarılırsa 1 - 1 = 0 ve üsse 127 eklenirse 1 + 127 = 128. O halde 2 şöyle ifade edilir: 0|100 0000 0|000 0000 0000 0000 0000 0000 = (40 00 00 00)16. Tabii Little endian olduğundan byte'ların tersine yazılması gerekir.

Katsayı bitlerinin ondalıklı ikilik sayılar olduğuna dikkat edilmelidir. Örn. 200 = 128 + 64 + 8 = 27 + 26 + 23. Üs olarak en büyük üssün değeri seçilmeli. O halde 200 = 27 * (1 + 2-1 + 2-4) = 1.1001b * 27 = 1.5625 * 27. Katsayıdan 1'i atınca kalan .1001 ve üs 7 + 127 = 134 = 1000 0110b. Hepsini toparlayınca sayı: 0|100 0011 0|100 1000 0000 0000 0000 0000 = (43 48 00 00)16.

Ondalıklı sayıları ifade etmek için hiçbir engel kalmadı:

Örn. 0.25 = 1 * 2-2. Üs -2 + 127 = 125 olur : 0|011 1110 1|000 0000 0000 0000 0000 0000

Şimdi periyot seyreden sayıları hatırlayalım. Bölmeleri kesirler yerine ondalıkla ifade etmeye kalktığımızda bölünen bölene tam bölünmüyorsa bölümde ondalıklar birbirini tekrar eder. Örn: 7 / 90 = 0.0707... Ondalık sayının bilgisayarda gösteriminde sürekli ikiye bölme ve ikilik kesirlerle ifade etme var. Peki ondalıklı sayılarda böyle bir periyot durumu olur mu? 0.1'i ele alalım. Bunun karşılığı: 0|011 1101 1|100 1100 1100 1100 1100 1101. Üs 123 - 127 = -4. Katsayıları ayırıp toplarsak aşağıdaki gibi oluyor:


Eğer en düşük değerli 2-23 olmasaydı 0.599 999 904 632 568 359 15. Tam 0.6 (aslında 1.6) hiçbir zaman olmuyor. Sayıya 1 ekleyip 2-4 ile çarpınca elde edilen sayı 0.100 000 001 490 116 119 370 703 125 ama Hexpert, Excel, Matlab ve Octave bu sayının ilk 6-7 basamağını gösterdiklerinde 0.1 görüyoruz (elbette şimdilik Matlab'de sayıların double olmasını görmezden geldim). Bu küçük fark her -0.1 işleminde birikerek artıyor ve sonuç sıfıra yaklaşıkça önem kazanıyor.

Not: Bu durum ikilik sistemin eksikliği olarak görülmemeli. Bilgisayarlar 10'luk sistemi kullanabilseydi ama insanlık 12'lik sayı sistemini kullanıyor olsaydı aynı durum yine yaşanacaktı.

Yukarıdaki sayıda dokuzuncu ondalıktan sonra sıfırdan farklı değerler görüldü ama 10 kere çıkarma yapıldığında aslında yapılan işlem "1-1.000 000 014 901 161 193 707 031 25" olup hata birikerek sekizinci ondalığa taşar. Octave'da tek değerlikli sayılarla aynı işlem tekrarlandığında hatanın 10-8 mertebesinde olduğunu yazmıştım.

>> X=single(0.1)
X =  0.10000
>> 1-X-X-X-X-X-X-X-X-X-X
ans =  -7.4506e-008
>>

Bilgisayardaki katsayının son bitinin bir olması ve olmaması arasındaki fark, bilgisayarın iki ondalıklı sayı arasındaki anlayabileceği en küçük farktır. Eğer bu 1 ile kendisinden büyük ilk sayı arasındaysa bilimsel yazında makina epsilonu (machine epsilon) denir. Matlab'de eps işlevi bu değeri döndürür:

>> eps
ans =   2.2204e-016
>> eps('single')
ans =   1.1921e-007

Wikipedia'daki örneklerde aynı değerler karşılaştırılabilir:
Single-precision floating-point format
0|011 1111 1|000 0000 0000 0000 0000 00012 = 3f80 000116 = 1 + 2-23 ≈ 1.0000001192
(smallest number larger than one)

0|011 1111 1111 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 00012 ≙ 1 + 2-52 ≈ 1.0000000000000002, the smallest number > 1


Peki neden 0.0625'te hata olmadı? Çünkü 0.0625 aslında 0.00012 olduğundan işlemcide tam değeriyle ifade edilebiliyor. Haliyle hata olmuyor.

Yazının ikinci bölümünde ortaya çıkabilecek hataları detaylandırıp başka örnekler vereceğim.

Hiç yorum yok:

Yorum Gönder