Menggunakan pre-trained model sebagai ekstraktor fitur

Keras Applications: Membuat Model Deteksi Gambar dengan Mudah

Dalam dua artikel sebelumnya, Serverless Scraping Semurah Tahu Tempe dengan AWS Lambda dan Managed Database Semurah Tahu Tempe dengan Amazon DynamoDB, kita sudah membahas metode “murahan” untuk scraping CCTV Transjakarta menggunakan AWS Lambda dan Amazon DynamoDB.

Sekarang saatnya membangun model machine learning yang bisa mendeteksi kemacetan dari jepretan CCTV. Artikel ini sangat terinspirasi (i.e. nyontek) artikel dari blog keras.io: Building Powerful Image Classification Models Using Very Little Data.

Jadi ceritanya, kami sudah mengumpulkan 53.000 jepretan dari 15 CCTV Transjakarta selama periode 1 November sampai 19 Desember 2016. Kami melabeli sendiri sekitar 15.000 jepretan yang diambil pukul 05.00-23.00 WIB, mana yang macet dan mana yang tidak. Hasil pengumpulan datanya kurang lebih seperti ini:

Statistik kemacetan per halte

Statistik kemacetan per halte Transjakarta. Halte paling macet tidak lain adalah Pondok Indah 1 Selatan, di mana 631 dari 1.052 (~60%) jepretan CCTV kami labeli “Macet”.

Dengan data di tangan, langsung saja kita buat model machine learning-nya!

Keras Applications

Keras Applications adalah salah satu modul dalam pustaka Keras yang menyediakan arsitektur berbagai model deep learning ternama. Asyiknya lagi, model-model itu sudah dilatih out of the box. Dengan kata lain, menggunakan Keras Applications kita bisa membuat sistem cerdas (misalnya deteksi gambar / image recognition) tanpa harus mengumpulkan data sama sekali atau melatih model berminggu-minggu pada GPU cluster. Model deteksi gambar Keras Applications juga bisa dipakai untuk mengekstraksi fitur-fitur dari gambar, yang akan dibahas lebih lanjut dalam artikel ini. Per Desember 2016, pilihan model yang tersedia dalam Keras Applications antara lain Xception, VGG16 dan VGG19, ResNet50, dan InceptionV3 untuk deteksi gambar serta MusicTaggerCRNN untuk deteksi musik. Keras sendiri sebenarnya “cuma” antarmuka yang memudahkan pemrograman; segala komputasi model dilakukan oleh pustaka backend, yaitu TensorFlow atau Theano.

Jadi, bagaimana cara memakai Keras Applications untuk melatih model machine learning? Seperti dijelaskan dalam blog Keras, terdapat 2 cara untuk ini. Yang paling mudah adalah dengan menggunakan model Keras Applications tanpa lapisan terakhir sebagai ekstraktor fitur. Karena hanya sampai lapisan sebelum terakhir alias bottleneck layer saja, keluaran yang kita terima dari model bukan berupa kelas gambar (anjing vs kucing vs burung, dan sebagainya), melainkan berupa tensor atau array multidimensi. Tensor ini mengandung informasi atau fitur mengenai gambar yang bisa dimanfaatkan lebih lanjut. Nah, cara pertama memakai Keras Applications adalah dengan mengekstraksi tensor dari sekumpulan gambar, yang kemudian dijadikan data latih model machine learning untuk task yang kita inginkan.

Menggunakan pre-trained model sebagai ekstraktor fitur

Menggunakan pre-trained model sebagai ekstraktor fitur

Cara kedua lebih sulit, yaitu dengan melatih ulang beberapa lapisan terakhir model Keras Applications (disebut juga retraining atau fine-tuning). Umumnya yang dilatih ulang hanya bottleneck layer-nya saja, karena semakin banyak lapisan yang dilatih, semakin banyak juga jumlah data dan kemampuan komputasi yang dibutuhkan. Namun jika kita punya data yang banyak dan komputer yang canggih, melatih ulang beberapa lapisan model biasanya akan memberi hasil yang lebih akurat.

Tentu saja (supaya gampang) artikel ini memakai cara pertama, hehehe.

Pemodelan Hasil ResNet50

Kode berikut diadaptasi dari dokumentasi Keras Applications dan berfungsi untuk mengekstraksi fitur gambar (2.048 dimensi) menggunakan model ResNet50. Ketika pertama kali dijalankan, kode ini akan mengunduh parameter model ResNet50 yang sudah dilatih oleh pengembang Keras. Saking banyaknya parameter, ukuran berkas yang diunduh sampai 90 MB lho!

from keras.applications.resnet50 import ResNet50, preprocess_input
from keras.preprocessing import image
import numpy as np
resnet = ResNet50(include_top=False)
def extract_features(img_paths, batch_size=64):
""" This function extracts image features for each image in img_paths using ResNet50 bottleneck layer.
Returned features is a numpy array with shape (len(img_paths), 2048).
"""
global resnet
n = len(img_paths)
img_array = np.zeros((n, 224, 224, 3))
for i, path in enumerate(img_paths):
img = image.load_img(path, target_size=(224, 224))
img = image.img_to_array(img)
img = np.expand_dims(img, axis=0)
x = preprocess_input(img)
img_array[i] = x
X = resnet.predict(img_array, batch_size=batch_size, verbose=1)
X = X.reshape((n, 2048))
return X

Bisa dilihat, dengan Keras Applications kode deep learning jadi sangat sederhana bukan?

Menggunakan MacBook Pro keluaran pertengahan 2014 (Intel Core i5 2.6 GHz, RAM 8 GB, 4 thread, penyimpanan SSD, dan tidak ada GPU), kode di atas dapat mengekstraksi fitur sekitar 100 gambar setiap menitnya. Data kami seluruhnya ada 15.000 gambar, jadi total waktu yang diperlukan untuk ekstraksi fitur mencapai 2.5 jam 😱

Selanjutnya, hasil eksekusi fungsi extract_features tersebut menjadi data latih untuk membangun model deteksi kemacetan. Karena data latihnya tidak lagi berupa sekumpulan gambar melainkan sudah berbentuk matriks (n gambar * 2.048), kita sebetulnya bisa memakai model supervised learning apa pun untuk task ini, misalnya XGBoost atau SVM. Namun, kita ikuti saja cara dalam blog Keras, yaitu menggunakan feed-forward neural network sederhana: 1 hidden layer dengan 256 simpul ditambah 1 dropout layer. Untuk fungsi objektif dipilih binary cross entropy yang dioptimasi dengan algoritma Adam. (Banyak ya konsep-konsep neural network yang mungkin asing bagi pembaca. Semoga kapan-kapan kami bisa membahas konsep ini dalam artikel terpisah.)

Kode untuk melatih modelnya sebagai berikut:

from keras.layers import Dense, Dropout
from keras.models import Sequential
from sklearn.model_selection import train_test_split

# X, y = code for obtaining image features and labels
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8)

model = Sequential()
model.add(Dense(256, activation='relu', input_dim=2048))
model.add(Dropout(0.5))
model.add(Dense(1, activation='sigmoid'))
model.compile('adam', 'binary_crossentropy', metrics=['accuracy', 'fmeasure'])

model.fit(X_train, y_train, validation_data=(X_test, y_test))

Dengan komputer yang sama, kode di atas selesai dieksekusi dalam 1-2 menit saja! Berikut kira-kira pesan yang dikeluarkan Keras selama model dilatih.

Train on 11835 samples, validate on 3945 samples
Epoch 1/10
11835/11835 [==============================] - 8s - loss: 0.2065 - acc: 0.9189 - fmeasure: 0.7886 - val_loss: 0.1172 - val_acc: 0.9569 - val_fmeasure: 0.8924
Epoch 2/10
11835/11835 [==============================] - 7s - loss: 0.1358 - acc: 0.9492 - fmeasure: 0.8677 - val_loss: 0.0952 - val_acc: 0.9617 - val_fmeasure: 0.8994
...
Epoch 9/10
11835/11835 [==============================] - 6s - loss: 0.0846 - acc: 0.9674 - fmeasure: 0.9157 - val_loss: 0.0799 - val_acc: 0.9660 - val_fmeasure: 0.9125
Epoch 10/10
11835/11835 [==============================] - 6s - loss: 0.0774 - acc: 0.9715 - fmeasure: 0.9263 - val_loss: 0.0824 - val_acc: 0.9673 - val_fmeasure: 0.9097

Evaluasi

Nah, sekarang model kita sudah siap digunakan untuk memprediksi kemacetan! Tapi sebelumnya, kita evaluasi dulu seberapa bagus prediksinya, baik untuk data latih maupun data uji. Metrik-metrik yang kita perhatikan antara lain akurasi, presisi, recall, F1, dan negative predictive value (NPV).

Hasil evaluasi model

Hasil evaluasi model. Dengan total waktu latih hanya 1-2 menit, lumayan banget kan hasilnya?

Secara umum, hasil evaluasi pada data latih lebih baik dibanding pada data uji (ya iya lah!). Keseluruhan metrik juga tidak mengalami penurunan drastis pada data uji, dengan penurunan terbesar terjadi pada recall yang berkurang sekitar 4.4%. Bisa dibilang modelnya tidak overfit terlalu parah.

Contoh prediksi model

Contoh prediksi model. Memang cukup sulit ya menentukan kemacetan dari 1 jepretan saja

Evaluasi per halte

Sekilas hasil di atas tampak memuaskan. Sayangnya, begitu kami telaah hasil evaluasi data uji untuk masing-masing halte, ternyata pada sebagian halte F1-nya tidak sampai 85%:

Hasil evaluasi kurang bagus untuk sebagian halte (F1 tidak sampai 0.85)

Hasil evaluasi kurang bagus untuk sebagian halte 😞

Untuk halte-halte ini, presisi dan NPV-nya sebetulnya tidak begitu rendah, walau masih di bawah angka rata-rata seluruh halte. Yang bermasalah adalah recall-nya. Pada halte Kebon Jeruk Selatan, recall-nya bahkan hanya mencapai 31%!

Kami menduga hasil evaluasi untuk halte-halte tersebut tidak baik karena memang labelnya tidak seimbang. Buktinya, keenam halte tersebut termasuk dalam 7 halte dengan tingkat kemacetan terendah dalam data kami, dengan jumlah label positif kurang dari 10% (lihat kembali grafik batang persentase kemacetan di atas). Scatter plot di bawah juga menunjukkan hubungan yang cukup linear antara (logaritma) persentase kemacetan halte dengan angka F1 model pada data uji (korelasi ~75%).

Perbandingan (log) tingkat kemacetan dengan F1 pada data uji

Perbandingan tingkat kemacetan (skala-log) dengan F1 pada data uji

Jika dipikir-pikir, ini sebetulnya good problem ya, karena semakin sering suatu jalan macet, modelnya semakin akurat memprediksi kemacetan di jalan itu. Karena presisinya juga cukup baik, kita bisa benar-benar percaya kalau model mengatakan jalan sedang macet. Mungkin pembaca ada yang tertarik untuk menelusuri lagi?

Kemampuan generalisasi model

Evaluasi di atas dilakukan berdasarkan metode train-test split biasa. Sejatinya, kita menginginkan model yang memiliki kemampuan generalisasi yang baik: ketika dipakai untuk halte atau CCTV yang baru (tidak ada di data latih), akurasi model enggak jelek-jelek amat. Nah, percobaan terakhir yang kami lakukan adalah melatih model tanpa data suatu halte sama sekali, yang kemudian diujikan pada data halte tersebut. Contohnya, dilatih model tanpa data halte Pondok Indah Selatan, lalu diujikan pada data halte Pondok Indah Selatan; dilatih model lain tanpa data halte Mampang Prapatan Utara, lalu diujikan pada data halte Mampang Prapatan Utara, dan seterusnya. Jadi seperti cross-validation berdasarkan halte begitu.

Hasilnya seperti berikut:

Hasil evaluasi (F1) berdasarkan cross-validation per halte

Hasil evaluasi (F1) menggunakan metode cross-validation per halte

Ternyata modelnya berhasil mencapai F1 cross-validation lebih dari 90% hanya pada 4 dari 15 halte 😞, dan lebih dari 80% pada 9 dari 15 halte. Lagi-lagi halte yang “bermasalah” adalah halte dengan tingkat kemacetan rendah, seperti Pondok Indah 2 Selatan dan RS Medika Selatan. Yang menarik untuk diamati adalah halte Permata Hijau Utara dan Mampang Prapatan Utara, di mana tingkat kemacetannya rendah (masing-masing 6,84% dan 9,60%) namun F1-nya tinggi. Dan sebaliknya, halte Kebon Jeruk Utara tingkat kemacetannya tinggi (32,32%) namun F1-nya cukup rendah, sekitar 74%. Dari hasil di atas, agaknya masih perlu dilakukan perbaikan pemodelan agar kemampuan generalisasi model bisa jauh lebih baik, misalnya F1 cross-validation di atas 90% semua hehehe.

Pertanyaan generalisasi lainnya adalah: apakah model sama akuratnya untuk deteksi di siang hari ketimbang deteksi di malam hari? Karena saya sudah lelah, akan saya simpan saja pertanyaan ini untuk percobaan-percobaan selanjutnya (kalau saya tidak malas).

Penutup

Sebetulnya masih sangat banyak lagi yang bisa dieksplorasi untuk memperbaiki akurasi model, misalnya:

  • Implementasi metode khusus untuk menangani label yang tidak seimbang
  • Latih ulang beberapa lapisan terakhir model Keras Applications
  • Coba memakai model lain yang tersedia di Keras Applications, seperti InceptionV3
  • Kumpulkan lagi data dari lebih banyak halte
  • Kumpulkan lagi data yang sudah otomatis teranotasi, misalnya dengan API Flickr
  • Buat data sintetis dari data yang sudah ada, misalnya dengan modul pemrosesan gambar Keras
  • Gunakan feed-forward neural network yang lebih kompleks
  • Tuning lebih banyak lagi parameter neural network, misalnya tingkat regularisasi dan fungsi aktivasi

Ketiga artikel terakhir yang saya tulis menjelaskan metode pemodelan machine learning dari awal (pengumpulan data) sampai akhir (evaluasi model), menggunakan tools yang semuanya gratis atau “hampir” gratis. Saya sangat senang menulisnya, semoga bermanfaat dan pembaca juga senang membacanya.

Sampai jumpa di tahun 2017!

Contoh rekaman CCTV Transjakarta halte Pondok Indah 1

Managed Database Semurah Tahu Tempe dengan Amazon DynamoDB

Kembali lagi dengan pos Semurah Tahu Tempe. Sebelumnya kita sudah membahas serverless scraping dengan AWS Lambda. Kali ini kita akan membahas managed database semurah tahu tempe dengan Amazon DynamoDB.

Bagi yang belum familiar, managed database adalah layanan yang memungkinkan kita menggunakan fitur-fitur sistem basis data tanpa perlu melakukan instalasi software atau hardware apapun. Bisa dibilang managed database adalah database tanpa repot: kita tidak perlu menyediakan server, kita tidak perlu melakukan pembaruan versi database, kita bisa dengan mudah scale up dan scale down kapasitas database, dan jatuhnya juga bisa jadi lebih murah dibanding pakai database “konvensional”.

Salah satu managed database yang disediakan AWS adalah Amazon DynamoDB. DynamoDB berbasis NoSQL, di mana data disimpan dalam format key-value layaknya JSON. Memang fitur DynamoDB tidak selengkap database lainnya; misalnya, kita tidak bisa melakukan query agregasi di DynamoDB. 😦 Namun, karena kinerjanya yang baik serta kepraktisannya untuk digunakan dan di-scale up scale down, DynamoDB bisa menjadi pilihan yang tepat untuk aplikasi yang tidak memerlukan fitur lengkap dari database SQL. Lebih lengkapnya mengenai usage pattern DynamoDB bisa dibaca di pos ini.

Nah, seperti AWS Lambda, DynamoDB juga memiliki free tier tanpa ekspirasi. Kita bisa menyimpan data hingga 25 GB dengan kapasitas IO hingga 25 item * 100 kB per detik.

Free tier Amazon DynamoDB

Free tier Amazon DynamoDB

Melanjutkan pos sebelumnya, mari kita buat scraper menggunakan pasangan emas DynamoDB dan AWS Lambda.

Scraping CCTV Transjakarta

Sudah beberapa tahun ini Jakarta Smart City memiliki web yang menampilkan kondisi berbagai fasilitas umum di Jakarta secara real-time dalam bentuk peta. Salah satu informasi yang bisa diakses dalam peta ini adalah live streaming CCTV di halte-halte Transjakarta. Contohnya, kita bisa menonton kondisi jalan di depan halte busway Pondok Indah di alamat ini (hint: jalannya sering banget macet).

Contoh rekaman CCTV Transjakarta halte Pondok Indah 1

Contoh rekaman CCTV Transjakarta halte Pondok Indah 1. Dulu saya mengarungi kemacetan ini setiap hari 😦

Karena sudah ada data terbuka yang bisa diakses secara real-time, bisa nggak ya kita buat program otomatis untuk mendeteksi kemacetan di berbagai lokasi di Jakarta? Supaya bisa menjawab pertanyaan ini, kita akan mengumpulkan screenshot dari video CCTV-CCTV Transjakarta dan menggunakannya sebagai data latih machine learning.

Menyetel tabel DynamoDB

Sebelum mulai scraping, kita perlu menyiapkan basis data di mana kita akan menyimpan hasil scraping nantinya. Data (i.e. item) dalam tabel DynamoDB disimpan dalam format JSON dan antar-item tidak diharuskan memiliki skema yang sama (ingat, DynamoDB adalah NoSQL). Hanya, setiap item wajib memiliki Partition Key yang digunakan (di balik layar) oleh AWS untuk membagi isi tabel ke dalam lokasi-lokasi terpisah dalam rangka meningkatkan kinerja. Selain Partition Key, kita juga bisa mendefinisikan Sort Key untuk mengurutkan item. Jadi, peletakan item dilakukan berdasarkan Partition Key, dan item-item dengan Partition Key yang sama diurutkan berdasarkan Sort Key. Partition Key (ditambah Sort Key) berperan seperti primary key dalam basis data SQL.

Ilustrasi skema penyimpanan tabel DynamoDB

Ilustrasi skema penyimpanan tabel DynamoDB

Untuk data screenshot CCTV, kita definisikan loc sebagai Partition Key dan timestamp sebagai Sort Key. loc adalah kode lokasi CCTV (misalnya “Pondok Indah 1 N”) dan timestamp adalah waktu kita menjalankan scraping (misalnya “2016-11-30 08:19:30”). Data screenshot CCTV-nya sendiri akan disimpan dalam field img_bytes, namun dalam DynamoDB kita tidak perlu (bahkan tidak bisa) menspesifikasikan field selain Partition Key dan Sort Key.

Tabel DynamoDB bisa dibuat dengan meng-klik Create Table pada halaman utama DynamoDB di AWS. Kita gunakan pengaturan seperti di bawah ini, kemudian klik Create untuk membuat tabel.

Pengaturan tabel DynamoDB untuk scraping CCTV Transjakarta

Pengaturan tabel DynamoDB untuk scraping CCTV Transjakarta

Lebih kurang 1 menit AWS akan selesai mem-provision tabel kita. Kita bisa melihat berbagai informasi mengenai tabel seperti Partition Key & Sort Key, jumlah item dalam tabel, dan total ukuran data yang disimpan. Untuk sekarang, catat Amazon Resource Name (ARN) tabel karena nantinya akan diperlukan untuk mengakses tabel dari fungsi Lambda.

Read Capacity dan Write Capacity

Jika kita menginstalasi basis data biasa (non-managed), sebelum tahap pendefinisian tabel tentu kita sudah menspesifikasikan kapasitas basis data tersebut: berapa besar penyimpanannya, berapa jumlah CPU-nya, berapa besar RAM-nya, dan lain-lain. Bagaimana kita melakukan ini pada DynamoDB?

Pada gambar pengaturan tabel di atas, bisa kita baca keterangan “Provisioned capacity set to 5 reads and 5 writes”. Inilah yang menjadi spesifikasi kapasitas tabel DynamoDB kita. Singkatnya, dengan 1 Read Capacity kita dapat membaca 1 item per detik dari tabel (ukuran item hingga 100 kB). Begitu juga dengan 1 Write Capacity kita dapat menulis 1 item per detik ke dalam tabel. Jika kita membaca atau menulis item lebih cepat atau lebih besar dari kapasitas yang kita setel, AWS akan mengirim error berupa ProvisionedThroughputExceededException. Tarif DynamoDB sendiri dihitung dari total Read Capacity dan Write Capacity di semua tabel yang kita miliki, ditambah total ukuran data tersimpan.

Hak Akses DynamoDB dari Fungsi Lambda

Selanjutnya, kita perlu membuka akses tulis DynamoDB dari fungsi Lambda yang kita gunakan untuk scraping. Caranya, buka halaman IAM (Identity and Access Management), lalu klik Roles. Di situ akan muncul lambda-role, yaitu role akses yang telah kita buat sebelumnya saat mendefinisikan fungsi Lambda.

Kita akan memasang policy baru pada role ini yang memungkinkan kita mengakses DynamoDB dari fungsi Lambda. Pada bagian Inline Policies, klik link “To create one, click here” lalu pilih Policy Generator. Di halaman berikutnya (Edit Permissions), pilih Allow pada field Effect, Amazon DynamoDB pada AWS Service, Put Item pada Actions, dan ARN tabel pada Amazon Resource Name (ARN). Klik Add Statement lalu klik Next Step. AWS akan men-generate kode policy berdasarkan pengaturan yang kita masukkan tadi. Klik Apply Policy untuk memasang policy ini pada fungsi Lambda.

Pengaturan akses tulis DynamoDB

Pengaturan akses tulis DynamoDB

Scraping!

Fiuh, setelah (sedikit) capek menyetel DynamoDB, mari kita buat fungsi scraping CCTV Transjakarta di AWS Lambda. Kodenya begini:

from datetime import datetime, timedelta
import urllib2
from urllib2 import URLError
import boto3
START_BYTE = b'\xff\xd8'
END_BYTE = b'\xff\xd9'
ITER_LIMIT = 10000
def capture_img(url):
stream = urllib2.urlopen(url, timeout=15)
all_bytes = ''
i = 0
while True and (i < ITER_LIMIT):
# Read MJPEG stream until start byte and end byte are found, or until ITER_LIMIT reads
i += 1
all_bytes += stream.read(1024)
a = all_bytes.find(START_BYTE)
b = all_bytes.find(END_BYTE)
if a != -1 and b != -1:
return all_bytes[a : b+2]
def write_to_dynamo(item):
dynamodb = boto3.resource('dynamodb', region_name='ap-southeast-1')
table = dynamodb.Table('cctv-jak')
table.put_item(Item=item)
def lambda_handler(event, context):
loc_urls = {
"Pondok Indah 1 N": "http://202.51.112.91:727/image2&quot;,
"Pondok Indah 1 S": "http://202.51.112.91:728/image2&quot;,
# other locations to scrape
}
records = {}
for loc, url in loc_urls.iteritems():
try:
img_byte_str = capture_img(url)
if not img_byte_str:
# Stream format not as expected, continue to next CCTV
continue
img_byte_str = img_byte_str.decode("ISO-8859-1") # for storage in DynamoDB
# +7 hour: handle timezone difference from server time to WIB time
timestamp = str(datetime.now() + timedelta(hours=7))[:19]
records[loc] = {
'loc': loc,
'timestamp': timestamp,
'img_bytes': img_byte_str
}
write_to_dynamo(records[loc])
except URLError:
# Skip on timeout error
pass
return records
view raw cctv-scraper.py hosted with ❤ by GitHub

Klik Test untuk coba mengeksekusi fungsi, dan voila, Lambda akan men-scrape screenshot CCTV, menyimpannya di DynamoDB, dan mengembalikan hasil eksekusi.

Hasil eksekusi fungsi Lambda untuk scraping CCTV Transjakarta

Hasil eksekusi fungsi Lambda untuk scraping CCTV Transjakarta

Menurut hasil di atas, untuk men-scrape 2 screenshot CCTV dibutuhkan waktu 7,3 detik, jadi jangan lupa menyesuaikan pengaturan timeout fungsi Lambda kalau mau men-scrape lebih banyak CCTV sekaligus. Sebagai informasi, saya menyetel timeout Lambda 2 menit dan 8 Write Capacity DynamoDB untuk men-scrape 8 CCTV.

Getting the data out

Terakhir, saat nanti data screenshot yang kita scrape sudah banyak, kita tentu mau mengunduh screenshot-screenshot tersebut. Kode untuk mengunduhnya lumayan panjang, dan karena pos ini juga sudah panjang, kodenya saya taruh di Gist saja ya.


Dalam 2 pos terakhir kita bereksplorasi men-scrape screenshot CCTV Transjakarta menggunakan AWS Lambda + Amazon DynamoDB. Seperti saya tulis di pos pertama, saya sudah mencoba cara ini, dan dengan biaya $0.15 saja saya bisa men-scrape plus mengunduh 37.000 screenshot CCTV (setara 1,5 GB).

Bagaimana dengan kamu? Sudah pernah pakai AWS Lambda atau Amazon DynamoDB? Untuk apa? Jangan lupa sharing ya di komentar!