Phân loại hình ảnh

Xem trên TensorFlow.org Chạy trong Google Colab Xem nguồn trên GitHub Tải xuống sổ ghi chép

Hướng dẫn này chỉ ra cách phân loại hình ảnh của hoa. Nó tạo bộ phân loại hình ảnh bằng mô hình tf.keras.Sequential và tải dữ liệu bằng tf.keras.utils.image_dataset_from_directory . Bạn sẽ có được kinh nghiệm thực tế với các khái niệm sau:

  • Tải tập dữ liệu ra đĩa một cách hiệu quả.
  • Xác định trang bị quá mức và áp dụng các kỹ thuật để giảm thiểu nó, bao gồm cả việc tăng dữ liệu và bỏ qua.

Hướng dẫn này tuân theo quy trình công việc học máy cơ bản:

  1. Kiểm tra và hiểu dữ liệu
  2. Xây dựng đường dẫn đầu vào
  3. Xây dựng mô hình
  4. Đào tạo mô hình
  5. Kiểm tra mô hình
  6. Cải thiện mô hình và lặp lại quy trình

Nhập TensorFlow và các thư viện khác

import matplotlib.pyplot as plt
import numpy as np
import os
import PIL
import tensorflow as tf

from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

Tải xuống và khám phá tập dữ liệu

Hướng dẫn này sử dụng tập dữ liệu khoảng 3.700 bức ảnh về hoa. Tập dữ liệu chứa năm thư mục con, mỗi thư mục con:

flower_photo/
  daisy/
  dandelion/
  roses/
  sunflowers/
  tulips/
import pathlib
dataset_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/flower_photos.tgz"
data_dir = tf.keras.utils.get_file('flower_photos', origin=dataset_url, untar=True)
data_dir = pathlib.Path(data_dir)

Sau khi tải xuống, bây giờ bạn sẽ có sẵn một bản sao của tập dữ liệu. Có tổng cộng 3.670 hình ảnh:

image_count = len(list(data_dir.glob('*/*.jpg')))
print(image_count)
3670

Đây là một số loại hoa hồng:

roses = list(data_dir.glob('roses/*'))
PIL.Image.open(str(roses[0]))

png

PIL.Image.open(str(roses[1]))

png

Và một số hoa tulip:

tulips = list(data_dir.glob('tulips/*'))
PIL.Image.open(str(tulips[0]))

png

PIL.Image.open(str(tulips[1]))

png

Tải dữ liệu bằng tiện ích Keras

Hãy tải những hình ảnh này ra đĩa bằng tiện ích tf.keras.utils.image_dataset_from_directory hữu ích. Điều này sẽ đưa bạn từ một thư mục hình ảnh trên đĩa sang tf.data.Dataset chỉ trong một vài dòng mã. Nếu muốn, bạn cũng có thể viết mã tải dữ liệu của riêng mình từ đầu bằng cách truy cập hướng dẫn Tải và xử lý trước hình ảnh .

Tạo tập dữ liệu

Xác định một số tham số cho bộ tải:

batch_size = 32
img_height = 180
img_width = 180

Bạn nên sử dụng phân tách xác thực khi phát triển mô hình của mình. Hãy sử dụng 80% hình ảnh để đào tạo và 20% để xác thực.

train_ds = tf.keras.utils.image_dataset_from_directory(
  data_dir,
  validation_split=0.2,
  subset="training",
  seed=123,
  image_size=(img_height, img_width),
  batch_size=batch_size)
Found 3670 files belonging to 5 classes.
Using 2936 files for training.
val_ds = tf.keras.utils.image_dataset_from_directory(
  data_dir,
  validation_split=0.2,
  subset="validation",
  seed=123,
  image_size=(img_height, img_width),
  batch_size=batch_size)
Found 3670 files belonging to 5 classes.
Using 734 files for validation.

Bạn có thể tìm tên lớp trong thuộc tính class_names trên các tập dữ liệu này. Chúng tương ứng với tên thư mục theo thứ tự bảng chữ cái.

class_names = train_ds.class_names
print(class_names)
['daisy', 'dandelion', 'roses', 'sunflowers', 'tulips']

Trực quan hóa dữ liệu

Dưới đây là chín hình ảnh đầu tiên từ tập dữ liệu đào tạo:

import matplotlib.pyplot as plt

plt.figure(figsize=(10, 10))
for images, labels in train_ds.take(1):
  for i in range(9):
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(images[i].numpy().astype("uint8"))
    plt.title(class_names[labels[i]])
    plt.axis("off")

png

Bạn sẽ đào tạo một mô hình bằng cách sử dụng các tập dữ liệu này bằng cách chuyển chúng tới Model.fit trong giây lát. Nếu muốn, bạn cũng có thể lặp lại theo cách thủ công qua tập dữ liệu và truy xuất hàng loạt hình ảnh:

for image_batch, labels_batch in train_ds:
  print(image_batch.shape)
  print(labels_batch.shape)
  break
(32, 180, 180, 3)
(32,)

image_batch là một tenxơ của hình dạng (32, 180, 180, 3) . Đây là một lô gồm 32 hình ảnh có hình dạng 180x180x3 (kích thước cuối cùng đề cập đến các kênh màu RGB). label_batch là một tenxơ của hình (32,) , đây là những nhãn tương ứng với 32 hình ảnh.

Bạn có thể gọi .numpy() trên image_batch và các tensors labels_batch để chuyển đổi chúng thành numpy.ndarray .

Định cấu hình tập dữ liệu cho hiệu suất

Hãy đảm bảo sử dụng tìm nạp trước có bộ đệm để bạn có thể mang lại dữ liệu từ đĩa mà không bị chặn I / O. Đây là hai phương pháp quan trọng bạn nên sử dụng khi tải dữ liệu:

  • Dataset.cache giữ các hình ảnh trong bộ nhớ sau khi chúng được tải ra khỏi đĩa trong kỷ nguyên đầu tiên. Điều này sẽ đảm bảo tập dữ liệu không trở thành nút cổ chai trong khi đào tạo mô hình của bạn. Nếu tập dữ liệu của bạn quá lớn để vừa với bộ nhớ, bạn cũng có thể sử dụng phương pháp này để tạo bộ đệm ẩn trên đĩa hoạt động hiệu quả.
  • Dataset.prefetch chồng lên quá trình tiền xử lý dữ liệu và thực thi mô hình trong khi đào tạo.

Bạn đọc quan tâm có thể tìm hiểu thêm về cả hai phương pháp cũng như cách lưu dữ liệu vào bộ nhớ cache vào đĩa trong phần Tìm nạp trước của Hiệu suất tốt hơn với hướng dẫn API tf.data .

AUTOTUNE = tf.data.AUTOTUNE

train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)

Chuẩn hóa dữ liệu

Giá trị kênh RGB nằm trong phạm vi [0, 255] . Điều này không lý tưởng cho mạng nơ-ron; nói chung, bạn nên tìm cách giảm giá trị đầu vào của mình.

Tại đây, bạn sẽ chuẩn hóa các giá trị nằm trong phạm vi [0, 1] bằng cách sử dụng tf.keras.layers.Rescaling .

normalization_layer = layers.Rescaling(1./255)

Có hai cách để sử dụng lớp này. Bạn có thể áp dụng nó vào tập dữ liệu bằng cách gọi Dataset.map :

normalized_ds = train_ds.map(lambda x, y: (normalization_layer(x), y))
image_batch, labels_batch = next(iter(normalized_ds))
first_image = image_batch[0]
# Notice the pixel values are now in `[0,1]`.
print(np.min(first_image), np.max(first_image))
0.0 1.0

Hoặc, bạn có thể bao gồm lớp bên trong định nghĩa mô hình của mình, điều này có thể đơn giản hóa việc triển khai. Hãy sử dụng cách tiếp cận thứ hai ở đây.

Tạo mô hình

Mô hình Tuần tự bao gồm ba khối tích chập ( tf.keras.layers.Conv2D ) với một lớp tổng hợp tối đa ( tf.keras.layers.MaxPooling2D ) trong mỗi khối. Có một lớp được kết nối đầy đủ ( tf.keras.layers.Dense ) với 128 đơn vị trên cùng được kích hoạt bởi chức năng kích hoạt ReLU ( 'relu' ). Mô hình này chưa được điều chỉnh để có độ chính xác cao — mục tiêu của hướng dẫn này là thể hiện một cách tiếp cận tiêu chuẩn.

num_classes = len(class_names)

model = Sequential([
  layers.Rescaling(1./255, input_shape=(img_height, img_width, 3)),
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(num_classes)
])

Biên dịch mô hình

Đối với hướng dẫn này, hãy chọn trình tối ưu hóa tf.keras.optimizers.Adam và hàm mất mát tf.keras.losses.SparseCategoricalCrossentropy . Để xem độ chính xác của quá trình đào tạo và xác thực cho từng kỷ nguyên đào tạo, hãy chuyển đối metrics vào Model.compile .

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])

Tóm tắt mô hình

Xem tất cả các lớp của mạng bằng phương thức Model.summary của mô hình:

model.summary()
Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 rescaling_1 (Rescaling)     (None, 180, 180, 3)       0         
                                                                 
 conv2d (Conv2D)             (None, 180, 180, 16)      448       
                                                                 
 max_pooling2d (MaxPooling2D  (None, 90, 90, 16)       0         
 )                                                               
                                                                 
 conv2d_1 (Conv2D)           (None, 90, 90, 32)        4640      
                                                                 
 max_pooling2d_1 (MaxPooling  (None, 45, 45, 32)       0         
 2D)                                                             
                                                                 
 conv2d_2 (Conv2D)           (None, 45, 45, 64)        18496     
                                                                 
 max_pooling2d_2 (MaxPooling  (None, 22, 22, 64)       0         
 2D)                                                             
                                                                 
 flatten (Flatten)           (None, 30976)             0         
                                                                 
 dense (Dense)               (None, 128)               3965056   
                                                                 
 dense_1 (Dense)             (None, 5)                 645       
                                                                 
=================================================================
Total params: 3,989,285
Trainable params: 3,989,285
Non-trainable params: 0
_________________________________________________________________

Đào tạo mô hình

epochs=10
history = model.fit(
  train_ds,
  validation_data=val_ds,
  epochs=epochs
)
Epoch 1/10
92/92 [==============================] - 3s 16ms/step - loss: 1.2769 - accuracy: 0.4489 - val_loss: 1.0457 - val_accuracy: 0.5804
Epoch 2/10
92/92 [==============================] - 1s 11ms/step - loss: 0.9386 - accuracy: 0.6328 - val_loss: 0.9665 - val_accuracy: 0.6158
Epoch 3/10
92/92 [==============================] - 1s 11ms/step - loss: 0.7390 - accuracy: 0.7200 - val_loss: 0.8768 - val_accuracy: 0.6540
Epoch 4/10
92/92 [==============================] - 1s 11ms/step - loss: 0.5649 - accuracy: 0.7963 - val_loss: 0.9258 - val_accuracy: 0.6540
Epoch 5/10
92/92 [==============================] - 1s 11ms/step - loss: 0.3662 - accuracy: 0.8733 - val_loss: 1.1734 - val_accuracy: 0.6267
Epoch 6/10
92/92 [==============================] - 1s 11ms/step - loss: 0.2169 - accuracy: 0.9343 - val_loss: 1.3728 - val_accuracy: 0.6499
Epoch 7/10
92/92 [==============================] - 1s 11ms/step - loss: 0.1191 - accuracy: 0.9629 - val_loss: 1.3791 - val_accuracy: 0.6471
Epoch 8/10
92/92 [==============================] - 1s 11ms/step - loss: 0.0497 - accuracy: 0.9871 - val_loss: 1.8002 - val_accuracy: 0.6390
Epoch 9/10
92/92 [==============================] - 1s 11ms/step - loss: 0.0372 - accuracy: 0.9922 - val_loss: 1.8545 - val_accuracy: 0.6390
Epoch 10/10
92/92 [==============================] - 1s 11ms/step - loss: 0.0715 - accuracy: 0.9813 - val_loss: 2.0656 - val_accuracy: 0.6049

Hình dung kết quả đào tạo

Tạo các biểu đồ về tổn thất và độ chính xác trên các tập huấn luyện và xác nhận:

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(epochs)

plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

png

Các biểu đồ cho thấy rằng độ chính xác của quá trình huấn luyện và độ chính xác của việc xác nhận bị ảnh hưởng bởi biên lớn và mô hình chỉ đạt được độ chính xác khoảng 60% trên bộ xác thực.

Hãy kiểm tra xem có gì sai và cố gắng tăng hiệu suất tổng thể của mô hình.

Overfitting

Trong các biểu đồ ở trên, độ chính xác của quá trình huấn luyện đang tăng tuyến tính theo thời gian, trong khi độ chính xác của quá trình xác nhận sẽ giảm khoảng 60% trong quá trình huấn luyện. Ngoài ra, sự khác biệt về độ chính xác giữa đào tạo và độ chính xác xác thực là đáng chú ý — một dấu hiệu của việc trang bị quá nhiều.

Khi có một số lượng nhỏ các ví dụ đào tạo, mô hình đôi khi học hỏi từ các tiếng ồn hoặc các chi tiết không mong muốn từ các ví dụ đào tạo — đến mức nó tác động tiêu cực đến hiệu suất của mô hình đối với các ví dụ mới. Hiện tượng này được gọi là overfitting. Có nghĩa là mô hình sẽ gặp khó khăn khi tổng quát hóa trên một tập dữ liệu mới.

Có nhiều cách để chống lại tình trạng ăn quá nhiều trong quá trình luyện tập. Trong hướng dẫn này, bạn sẽ sử dụng tăng dữ liệu và thêm Dropout vào mô hình của mình.

Tăng dữ liệu

Overfitting thường xảy ra khi có một số lượng nhỏ các ví dụ đào tạo. Tăng cường dữ liệu áp dụng phương pháp tạo dữ liệu đào tạo bổ sung từ các ví dụ hiện có của bạn bằng cách tăng cường chúng bằng cách sử dụng các phép biến đổi ngẫu nhiên mang lại hình ảnh trông đáng tin cậy. Điều này giúp mô hình tiếp xúc với nhiều khía cạnh của dữ liệu hơn và khái quát hóa tốt hơn.

Bạn sẽ thực hiện tăng dữ liệu bằng cách sử dụng các lớp tiền xử lý Keras sau: tf.keras.layers.RandomFlip , tf.keras.layers.RandomRotationtf.keras.layers.RandomZoom . Chúng có thể được đưa vào bên trong mô hình của bạn giống như các lớp khác và chạy trên GPU.

data_augmentation = keras.Sequential(
  [
    layers.RandomFlip("horizontal",
                      input_shape=(img_height,
                                  img_width,
                                  3)),
    layers.RandomRotation(0.1),
    layers.RandomZoom(0.1),
  ]
)

Hãy hình dung một vài ví dụ tăng cường trông như thế nào bằng cách áp dụng tăng dữ liệu cho cùng một hình ảnh nhiều lần:

plt.figure(figsize=(10, 10))
for images, _ in train_ds.take(1):
  for i in range(9):
    augmented_images = data_augmentation(images)
    ax = plt.subplot(3, 3, i + 1)
    plt.imshow(augmented_images[0].numpy().astype("uint8"))
    plt.axis("off")

png

Bạn sẽ sử dụng tăng dữ liệu để đào tạo một mô hình trong giây lát.

Rơi ra ngoài

Một kỹ thuật khác để giảm việc trang bị quá nhiều là đưa quy định về việc bỏ học cho mạng lưới.

Khi bạn áp dụng droppout cho một lớp, nó sẽ ngẫu nhiên rơi ra (bằng cách đặt kích hoạt thành 0) một số đơn vị đầu ra từ lớp trong quá trình đào tạo. Bỏ học lấy một số phân số làm giá trị đầu vào của nó, ở dạng chẳng hạn như 0,1, 0,2, 0,4, v.v. Điều này có nghĩa là bỏ 10%, 20% hoặc 40% đơn vị đầu ra ngẫu nhiên khỏi lớp được áp dụng.

Hãy tạo một mạng nơ-ron mới với tf.keras.layers.Dropout trước khi đào tạo nó bằng cách sử dụng các hình ảnh tăng cường:

model = Sequential([
  data_augmentation,
  layers.Rescaling(1./255),
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Dropout(0.2),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(num_classes)
])

Biên dịch và đào tạo mô hình

model.compile(optimizer='adam',
              loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
              metrics=['accuracy'])
model.summary()
Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 sequential_1 (Sequential)   (None, 180, 180, 3)       0         
                                                                 
 rescaling_2 (Rescaling)     (None, 180, 180, 3)       0         
                                                                 
 conv2d_3 (Conv2D)           (None, 180, 180, 16)      448       
                                                                 
 max_pooling2d_3 (MaxPooling  (None, 90, 90, 16)       0         
 2D)                                                             
                                                                 
 conv2d_4 (Conv2D)           (None, 90, 90, 32)        4640      
                                                                 
 max_pooling2d_4 (MaxPooling  (None, 45, 45, 32)       0         
 2D)                                                             
                                                                 
 conv2d_5 (Conv2D)           (None, 45, 45, 64)        18496     
                                                                 
 max_pooling2d_5 (MaxPooling  (None, 22, 22, 64)       0         
 2D)                                                             
                                                                 
 dropout (Dropout)           (None, 22, 22, 64)        0         
                                                                 
 flatten_1 (Flatten)         (None, 30976)             0         
                                                                 
 dense_2 (Dense)             (None, 128)               3965056   
                                                                 
 dense_3 (Dense)             (None, 5)                 645       
                                                                 
=================================================================
Total params: 3,989,285
Trainable params: 3,989,285
Non-trainable params: 0
_________________________________________________________________
epochs = 15
history = model.fit(
  train_ds,
  validation_data=val_ds,
  epochs=epochs
)
Epoch 1/15
92/92 [==============================] - 2s 14ms/step - loss: 1.3840 - accuracy: 0.3999 - val_loss: 1.0967 - val_accuracy: 0.5518
Epoch 2/15
92/92 [==============================] - 1s 12ms/step - loss: 1.1152 - accuracy: 0.5395 - val_loss: 1.1123 - val_accuracy: 0.5545
Epoch 3/15
92/92 [==============================] - 1s 12ms/step - loss: 1.0049 - accuracy: 0.6052 - val_loss: 0.9544 - val_accuracy: 0.6253
Epoch 4/15
92/92 [==============================] - 1s 12ms/step - loss: 0.9452 - accuracy: 0.6257 - val_loss: 0.9681 - val_accuracy: 0.6213
Epoch 5/15
92/92 [==============================] - 1s 12ms/step - loss: 0.8804 - accuracy: 0.6591 - val_loss: 0.8450 - val_accuracy: 0.6798
Epoch 6/15
92/92 [==============================] - 1s 12ms/step - loss: 0.8001 - accuracy: 0.6945 - val_loss: 0.8715 - val_accuracy: 0.6594
Epoch 7/15
92/92 [==============================] - 1s 12ms/step - loss: 0.7736 - accuracy: 0.6965 - val_loss: 0.8059 - val_accuracy: 0.6935
Epoch 8/15
92/92 [==============================] - 1s 12ms/step - loss: 0.7477 - accuracy: 0.7078 - val_loss: 0.8292 - val_accuracy: 0.6812
Epoch 9/15
92/92 [==============================] - 1s 12ms/step - loss: 0.7053 - accuracy: 0.7251 - val_loss: 0.7743 - val_accuracy: 0.6989
Epoch 10/15
92/92 [==============================] - 1s 12ms/step - loss: 0.6884 - accuracy: 0.7340 - val_loss: 0.7867 - val_accuracy: 0.6907
Epoch 11/15
92/92 [==============================] - 1s 12ms/step - loss: 0.6536 - accuracy: 0.7469 - val_loss: 0.7732 - val_accuracy: 0.6785
Epoch 12/15
92/92 [==============================] - 1s 12ms/step - loss: 0.6456 - accuracy: 0.7500 - val_loss: 0.7801 - val_accuracy: 0.6907
Epoch 13/15
92/92 [==============================] - 1s 12ms/step - loss: 0.5941 - accuracy: 0.7735 - val_loss: 0.7185 - val_accuracy: 0.7330
Epoch 14/15
92/92 [==============================] - 1s 12ms/step - loss: 0.5824 - accuracy: 0.7735 - val_loss: 0.7282 - val_accuracy: 0.7357
Epoch 15/15
92/92 [==============================] - 1s 12ms/step - loss: 0.5771 - accuracy: 0.7851 - val_loss: 0.7308 - val_accuracy: 0.7343

Hình dung kết quả đào tạo

Sau khi áp dụng nâng cấp dữ liệu và tf.keras.layers.Dropout , ít bị overfitting hơn trước và độ chính xác của quá trình đào tạo và xác thực được liên kết chặt chẽ hơn:

acc = history.history['accuracy']
val_acc = history.history['val_accuracy']

loss = history.history['loss']
val_loss = history.history['val_loss']

epochs_range = range(epochs)

plt.figure(figsize=(8, 8))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')

plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

png

Dự đoán về dữ liệu mới

Cuối cùng, hãy sử dụng mô hình của chúng tôi để phân loại một hình ảnh không được bao gồm trong bộ đào tạo hoặc xác nhận.

sunflower_url = "https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg"
sunflower_path = tf.keras.utils.get_file('Red_sunflower', origin=sunflower_url)

img = tf.keras.utils.load_img(
    sunflower_path, target_size=(img_height, img_width)
)
img_array = tf.keras.utils.img_to_array(img)
img_array = tf.expand_dims(img_array, 0) # Create a batch

predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])

print(
    "This image most likely belongs to {} with a {:.2f} percent confidence."
    .format(class_names[np.argmax(score)], 100 * np.max(score))
)
Downloading data from https://storage.googleapis.com/download.tensorflow.org/example_images/592px-Red_sunflower.jpg
122880/117948 [===============================] - 0s 0us/step
131072/117948 [=================================] - 0s 0us/step
This image most likely belongs to sunflowers with a 89.13 percent confidence.