Note: see the link below for the English version of this article.

https://duongnt.com/adversarial-example

Adversarial example

Khi được train đúng cách trên một tập dữ liệu lớn, các model deep learning có thể đạt được độ chính xác đáng kinh ngạc. Tuy nhiên, nói cho cùng chúng vẫn chỉ là tập hợp của các công thức toán học. Ta có thể tái sử dụng quy trình train model để dễ dàng đánh lừa một model deep learning. Những ví dụ như thế được gọi là adversarial example, và hôm nay ta sẽ dùng Tensorflow để tạo một adversarial example.

Các bạn có thể tải toàn bộ code ví dụ trong bài từ link dưới đây.

https://gist.github.com/duongntbk/8cbb78b49264bcf0262d3856d04e6fec

Model ta muốn đánh lừa

Ta sẽ tìm cách đánh lừa model dự đoán giới tính trong bài viết trước. Các bạn có thể tải model đó từ đường link sau.

Đích của ta là bức ảnh của tôi ở dưới đây. Để ý là tôi đã chỉnh nó về 150×150 pixel cho phù hợp với kích thước đầu vào của model.

Original

Như đã nhắc đến trong bài trước, model của ta có độ chính xác khoảng 91%. Ta dùng đoạn code dưới để xác nhận là nó dự đoán đúng giới tính của bức ảnh gốc.

model_path = 'gender_prediction_best.keras'
model = load_model(model_path)

image_path = 'adversarial-example-origin.png'
image = load_img(image_path, target_size=(150, 150), interpolation='bilinear')
image = img_to_array(image, data_format='channels_last') / 255.0
image = image.reshape(1, 150, 150, 3)

pred = model.predict(image) # Trả về 0.000004351216

Vì model của ta gán nhãn 0 cho nam và nhãn 1 cho nữ nên giá trị trên nghĩa là model này cho rằng ảnh gốc là nam với độ chắc chắn là 99.9995648784%.

Tổng quan về fast sign gradient method (FSGM)

Cũng giống như khi train model, FSGM cũng sử dụng giải thuật stochastic gradient descent. Điểm khác biệt duy nhất là ở giá trị loss mà ta sẽ tối thiểu hóa.

  • Quá trình training thông thường: giá trị loss đo xem dự đoán của model gần với nhãn thực tế đến mức nào.
  • Đối với FSGM: giá trị loss đo xem dự đoán của model gần với một nhãn ta mong muốn đến mức nào (đó không nhất thiết phải là nhãn đúng).

Vì model của ta là classifier với 2 phân lớp, giá trị đầu ra của nó sẽ là một số thực nằm giữa 01. Giá trị đó càng gần 0 thì đầu vào càng nhiều khả năng thuộc vào lớp 0, và ngược lại. Vì thế, để tạo adversarial cho ảnh gốc, ta có thể định nghĩa giá trị loss là cách biệt giữa dự đoán của model với giá trị 1.

Tóm lại, các bước để thực hiện FSGM là như sau.

  • Chạy ảnh gốc qua model. kết quả trả về sẽ rất gần với 0.
  • Tính giá trị 1 - kết quả và dùng nó làm loss. Lúc đầu, loss sẽ rất gần với 1.
  • Tính gradient của loss trên giá trị của ảnh gốc.
  • Thay đổi giá trị ảnh gốc theo chiều ngược với gradient.
  • Lặp lại bước trên cho đến khi loss của ta là đủ nhỏ.

Áp dụng FGSM vào model và ảnh gốc của ta

Tính gradient của loss

Bước đầu tiên là chuyển ảnh gốc thành variable tensor. Ta phải thực hiện bước này để có thể cập nhật giá trị ảnh sau từng bước training, dựa theo gradient của loss.

tensor = tf.Variable(image)

Tensorflow giúp ta tính gradient một cách dễ dàng, ta chỉ cần sử dụng object GradientTape.

with tf.GradientTape() as tape:
    tape.watch(tensor)
    loss = 1 - model(tensor)

grad = tape.gradient(loss, tensor) # Tính gradient của loss trên tensor đầu vào

Đoạn code trên cho ta kết quả như dưới đây.

array([[[[-6.51887875e-12,  1.98002892e-10,  3.26648486e-10],..., dtype=float32)
dtype:dtype('float32')
max:2.1715496e-06
min:-3.3731865e-06
shape:(1, 150, 150, 3)
size:67500

Định nghĩa 1 bước trong quá trình training

Sau khi tính được gradient như trên, ta có thể viết code cho từng bước training như sau.

def training_step(tensor, learning_rate):
    with tf.GradientTape() as tape:
        tape.watch(tensor)
        loss = 1 - model(tensor)

    grad = tape.gradient(loss, tensor) # Tính gradient của loss trên tensor đầu vào
    tensor.assign_sub(grad * learning_rate)
    return loss

Ở đây có một vấn đề là ta nên dùng giá trị learning rate nào? Từ phần trước, ta thấy là gradient ban đầu rất nhỏ, giá trị của nó chỉ nằm trong khoảng [-3.3731865e-06, 2.1715496e-06]. Nếu ta sử dụng learning rate trong khoảng [1e-4, 1e-3] như đối với các bài toán thông thường thì quá trình training sẽ rất chậm. Vì giá trị trong tensor đầu vào nằm trong khoảng [0, 1], tôi nghĩ rằng sau mỗi bước training ta nên điều chỉnh giá trị ảnh tầm 1e-4. Vì thế, ta sẽ tạm đặt learning rate là 50.

Đoạn code để bắt đầu training là như sau.

learning_rate = 50
epsilon = 1e-4
for i in range(1000): # chạy 1000 bước
    loss = training_step(tensor, learning_rate)

    if loss < epsilon: # Dừng training nếu loss nhỏ hơn epsilon
        print(f'Stop training at step {i}/1000: loss={loss}.')
        break

    if i % 25 == 0: # In ra giá trị loss sau mỗi 25 bước
        print(f'Step {i}/1000: loss={loss}.')

Lần thử nghiệm đầu tiên

Sau 1000 bước, ta chạy ảnh sau chỉnh sửa qua model.

faked = tensor.numpy()
faked = np.clip(faked, 0, 1) # Normalize giá trị ảnh sau chỉnh sửa vào trong khoảng [0, 1]

pred = model.predict(faked) # Trả về 1

Model của ta tin chắc 100% rằng ảnh sau chỉnh sửa là nữ. Nhưng ta đã thực sự thành công hay chưa? Hãy xem ảnh kết quả trông như thế nào.

faked = faked.reshape(150, 150, 3)
plt.imshow(faked)
plt.show()

Not-so-good result

Bức ảnh này trông không được ổn lắm. Nó bị vỡ ở nhiều chỗ và màu cũng không được bình thường. Tôi tin là ta có thể làm tốt hơn thế.

Vấn đề nằm ở đâu?

Ta kiểm tra giá trị loss sau từng bước training.

Step 0/1000: loss=[[0.99999565]].
Step 25/1000: loss=[[0.9999919]].
Step 50/1000: loss=[[0.99994403]].
Stop training at step 57/1000: loss=[[0.]].

Ở bước 50, giá trị loss vẫn rất gần với 1, vậy mà đến bước 57 nó đã về 0. Từ sự thay đổi lớn này, ta có thể đoán được là vấn đề nằm ở learning rate. Ta sẽ thêm code để in ra giá trị lớn nhất và nhỏ nhất của gradient trong từng bước.

def training_step(tensor, learning_rate):
    with tf.GradientTape() as tape:
        tape.watch(tensor)
        loss = 1 - model(tensor)
    grad = tape.gradient(loss, tensor)
    grad_max = tf.math.reduce_max(grad).numpy() # Lấy giá trị lớn nhất trong gradient
    grad_min = tf.math.reduce_min(grad).numpy() # Lấy giá trị nhỏ nhất trong gradient

    tensor.assign_sub(grad * learning_rate)
    return loss, grad_max, grad_min

Ta hiển thị 2 giá trị trên cùng với loss ở tất cả các bước từ bước 50.

for i in range(1000):
    loss, grad_max, grad_min = training_step(tensor, learning_rate)
    if loss < 1e-4:
        break
    if i >= 50: # Hiển thị loss và giá trị lớn nhất/nhỏ nhất của gradient từ bước 50
        print(f'Step {i}/1000: loss={loss}. Maximum gradient: {grad_max}. Minimum gradient: {grad_min}')

Kết quả là như sau.

Step 50/1000: loss=[[0.99994403]]. Maximum gradient: 2.601052801765036e-05. Minimum gradient: -3.883903264068067e-05
Step 51/1000: loss=[[0.99992836]]. Maximum gradient: 3.351546547492035e-05. Minimum gradient: -5.5405471357516944e-05
Step 52/1000: loss=[[0.99990195]]. Maximum gradient: 4.4642067223321646e-05. Minimum gradient: -6.508854130515829e-05
Step 53/1000: loss=[[0.99984944]]. Maximum gradient: 7.057357288431376e-05. Minimum gradient: -0.00010238249524263665
Step 54/1000: loss=[[0.9997081]]. Maximum gradient: 0.00014037966320756823. Minimum gradient: -0.000200723807211034
Step 55/1000: loss=[[0.9989778]]. Maximum gradient: 0.00045278333709575236. Minimum gradient: -0.0007489514537155628
Step 56/1000: loss=[[0.9643442]]. Maximum gradient: 0.020121416077017784. Minimum gradient: -0.026629919186234474

Có thể thấy là sau bước 53, giá trị gradient tăng rất nhanh. Vì ta cố định learning rate là 50, ta sẽ thay đổi ảnh gốc rất nhiều sau mỗi bước. Điều này giải thích vì sao ảnh kết quả lại bị vỡ và có các mảng màu lạ như vậy.

Áp dụng adaptive learning rate

Để có kết quả tốt nhất, ta cần bắt đầu với learning rate lớn, sau đó giảm dần giá trị đó khi gradient lớn dần lên. Ta chọn learning rate sao cho sau mỗi bước, ta chỉ thay đổi các pixel trong ảnh gốc một giá trị không lớn hơn 1e-3.

MAX_CHANGE = 1e-3

def calculate_lr(grad_max, grad_min, max_change=MAX_CHANGE):
    max_abs_grad = max(np.abs(grad_min), np.abs(grad_max))
    return max_change / max_abs_grad

def training_step(tensor):
    with tf.GradientTape() as tape:
        tape.watch(tensor)
        loss = 1 - model(tensor)
    grad = tape.gradient(loss, tensor)
    grad_max = tf.math.reduce_max(grad).numpy()
    grad_min = tf.math.reduce_min(grad).numpy()

    learning_rate = calculate_lr(grad_max, grad_min) # Điều chỉnh learning rate sau mỗi bước
    tensor.assign_sub(grad * learning_rate)
    return loss

epsilon = 1e-4
for i in range(1000): # Thực hiện 1000 bước
    loss = training_step(tensor)

    if loss < epsilon: # Dừng training nếu loss nhỏ hơn epsilon
        print(f'Stop training at step {i}/1000: loss={loss}.')
        break

    if i % 25 == 0: # In ra giá trị loss sau mỗi 25 bước
        print(f'Step {i}/1000: loss={loss}.')

Lần này, kết quả trông khá hơn nhiều.

Much better result

Ta kiểm tra xem đã lừa được model hay chưa.

faked = tensor.numpy()
faked = np.clip(faked, 0, 1) # Normalize giá trị ảnh sau chỉnh sửa vào trong khoảng [0, 1]

pred = model.predict(faked) # Trả về 0.99962485

Lần này model của ta dự đoán ảnh là nữ với độ chắc chắn 99.96%. Có thể nói rằng ta đã lừa được model.

Dưới đây là ảnh gốc và ảnh sau chỉnh sửa đặt cạnh nhau. Tôi không thấy điểm khác biệt nào cả, còn bạn thì sao?

Compare adversarial example with the original

Kết thúc

Thoạt nhìn qua, những model deep learning thường có vẻ rất khó hiểu. Nhưng nếu ta hiểu được bản chất của quá trình training, ta có thể dễ dàng đánh lừa được chúng. FSGM là một giải thuật đơn giản. Ngoài ra còn có nhiều giải thuật khác phức tạp hơn nhiều, ví dụ như LinfDeepFoolAttack hay NewtonFool.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Tạo adversarial example bằng Tensorflow”

Leave a Reply