BerandaComputers and TechnologyMembangun sistem pemberi rekomendasi esai dalam 10 hari

Membangun sistem pemberi rekomendasi esai dalam 10 hari

Saya baru saja menyelesaikan MVP untuk ide yang saya miliki bulan lalu: Esai Findka

, buletin yang menyesuaikan dengan preferensi Anda dengan pembelajaran mesin. Saya beralih ke ini dari aplikasi sistem pemberi rekomendasi yang jauh lebih rumit, jadi saya sudah terbiasa dengan (hampir) semua bagian yang diperlukan untuk membangunnya. Saya sangat senang dengan hasilnya: aplikasi baru ini sederhana, basis kodenya bersih, dan saya meluncurkannya dalam waktu kurang dari dua minggu. Dalam tradisi agung masyarakat kita, sekarang saatnya untuk panduan arsitektur + toolkit. Jangan ragu untuk melompat ke bagian mana pun yang paling Anda minati. Jika Anda hanya membaca satu bagian, saya sarankan [:script {:src “/js/ensure-logged-in.js”}] Rekomendasi .

    Catatan: Saya telah mengabstraksi banyak kode ke Pukulan

    , kerangka kerja web yang saya rilis beberapa bulan lalu. Saya akan sering merujuk Biff.

    Bahasa

    Sebagian besar aplikasi ditulis dalam Clojure (satu-satunya pengecualian adalah algoritme rekomendasi aktual, yang ditulis dengan Python). Clojure adalah bahasa luar biasa yang memiliki penekanan kuat pada kesederhanaan dan stabilitas, yang membuat basis kode mudah dipelihara dan diperpanjang dalam jangka panjang. Sebagai Lisp, ia juga memiliki umpan balik yang sangat ketat yang sangat bagus untuk pengembangan cepat.

    Ada perdagangan- off: karena Clojure berfokus pada kekhawatiran jangka panjang daripada masalah jangka pendek, perlu waktu beberapa saat untuk mempercepatnya (meskipun jauh lebih mudah jika Anda memiliki mentor). Sebagai contoh, untuk melakukan web dev di Clojure, Anda akan perlu belajar bagaimana mengumpulkan sekelompok perpustakaan individu — tidak ada kerangka kerja seperti Rails atau Django. ([{:keys [biff/node] Luminus mungkin yang terbaik titik awal, setelah Anda terbiasa dengan bahasa itu sendiri).

    Dalam banyak kasus, menurut saya perdagangan -off sangat berharga. Tetapi untuk aplikasi kecil atau eksperimental (seperti yang mungkin Anda buat saat memulai), kecepatan di awal sangat berharga. Jika Anda ingin bergerak cepat segera tetapi Anda belum merasa nyaman di ekosistem Clojure, Anda mungkin mengalami saat-saat yang buruk. Salah satu tujuan saya untuk Biff adalah membantu menguranginya.

Paling depan  Oke, sekarang untuk beberapa kode aktual. Findka Essays, tidak seperti pendahulunya, adalah aplikasi multi-halaman yang sederhana. Tidak ada React di sini. Jadi bagian depannya cukup sederhana.  

(Untuk singkatnya, saya akan menyebutnya "Findka" mulai sekarang ).

Crux tidak menerapkan skema apa pun, tetapi Biff melakukannya. Anda dapat menentukan skema menggunakan Spesifikasi Clojure : (membutuhkan ‘[trident.util :as u]) (u / sdefs :: event-type # {: submit: email: click: like: dislike} :: timestamp inst? :: string url? :: orang tua uuid? :: pengguna uuid? :: event (u / only-keys : req-un [::event-type ::timestamp ::user ::url] : opt-un [::parent])) (skema def {: peristiwa {: spec [uuid? ::event]}})

Skema ini mendefinisikan “tabel” untuk acara. The [uuid? ::event] bagian berarti itu kunci utama untuk dokumen acara harus berupa UUID, dan lainnya dokumen harus sesuai dengan spesifikasi yang diberikan untuk ::peristiwa di atas. Pada kasus ini, itu berarti dokumen harus memiliki jenis peristiwa, stempel waktu, ID pengguna dan URL, dan secara opsional dapat memiliki kunci "induk" (yang disetel ke kunci utama dari acara lain).

Seperti aplikasi multi-halaman standar, Findka memiliki titik akhir POST untuk menulis database dan GET endpoint untuk membaca (meskipun saya tidak tahu apakah file endpoint mematuhi REST atau tidak). Ini satu untuk mengirimkan esai:

(defn kirim-esai [{:keys [biff/node session/uid params]: sebagai sys}] (jika (nil? uid) {: status 302 : headers / Lokasi "/ login /"} (melakukan (simpul inti / await-tx (biff.crux / submit-tx sys {[:events] {: uid pengguna : stempel waktu: db / saat ini-waktu : jenis acara: kirim : url (: url params)}})) {: status 302 : headers / Location "/ settings"}))) (rute def [["/api/submit-essay" {:post submit-essay :name ::submit-essay :middleware [anti-forgery/wrap-anti-forgery]}] ...])

Dan inilah cuplikan dari / pengaturan . Panggilan ke inti / q tampil kueri datalog yang mengembalikan 10 kejadian terbaru pengguna saat ini, yang ditampilkan di UI:

(pengaturan defn [{:keys [biff/db session/uid]}] (biarkan [events (map first (crux/q db {:find '[event timestamp] : hasil lengkap? benar : args [{'user uid}] :dipesan oleh '[[timestamp :desc]] : batas 10 : dimana '[[event :event-type] [event :user user] [event :timestamp timestamp]]}))] ... [:div [:.h-5] [:.nunito-sans-bold.text-lg "Recent activity"] (untuk [[i {:keys [event-type url timestamp]}] (peristiwa vektor yang diindeks peta)] [:.p-2.leading-snug {:class (when (odd? i) "bg-gray-200")} [:.text-xs.text-gray-700 timestamp] [:div (case event-type :submit "Submitted: " :email "Received: " :click "Clicked: " :like "Added to favorites: " :dislike "Show less like this: ") [:a.text-blue-600 {:href url :target "_blank"} url]]])] ...)) (pengaturan defn [sys] {: body (rum / render-static-markup (statis / halaman dasar {: skrip [[:script {:src "https://apis.google.com/js/platform.js"}] [:script {:src "/js/ensure-logged-in.js"}] [:script {:src "/js/settings.js"}]]} (pengaturan sys))) : header / content-type "text / html"}) (rute def [["/settings" {:get settings :name ::settings :middleware [anti-forgery/wrap-anti-forgery]}] ...])

Rekomendasi

Dan inilah kami; seluruh alasan keberadaan Findka. Findka mengirimi Anda setiap hari atau email mingguan. Masing-masing menyertakan daftar tautan ke esai (yang dikirimkan oleh orang lain pengguna). Setiap kali Anda mengeklik tautan, Findka menyimpannya sebagai acara. Lembur, Findka mempelajari jenis esai yang Anda sukai. Data klik semua orang digabungkan menjadi model, yang digunakan Findka untuk memilih esai untuk Anda.

Sebagian besar pekerjaan dilakukan oleh Mengherankan

, Python perpustakaan yang mencakup beberapa algoritma pemfilteran kolaboratif yang berbeda. (Perpustakaan itulah mengapa saya menggunakan Python sama sekali, bukan hanya Clojure — tidak perlu menemukan kembali roda). Findka menggunakan baseline k-NN algoritma. Ini mencari esai yang sering disukai oleh orang yang suka sama esai sebagai Anda. Jika belum ada banyak data (seperti yang terjadi sekarang, sejak Findka diluncurkan baru-baru ini), defaultnya adalah merekomendasikan esai yang paling banyak disukai secara umum.

Saya telah menambahkan dua lapisan tambahan. Saya menyebut yang pertama sebagai “perataan popularitas”. Saya mengurutkan semua esai menurut berapa kali mereka direkomendasikan di masa lalu, dan saya mempartisi mereka menjadi 10 tempat sampah. Setiap kali saya memilih esai untuk dikirim seseorang, saya pertama kali memilih bin dengan probabilitas seragam, dan kemudian saya menggunakan k-NN untuk pilih esai dari tempat sampah itu. Dengan demikian, 10% esai terpopuler akan diambil hanya 10% dari rekomendasi (jika dibiarkan, item populer dapat berakhir mengambil lebih banyak dari rekomendasi mereka).

Lapisan lainnya adalah untuk eksplorasi vs. eksploitasi, yaitu perlu imbangi merekomendasikan esai yang relevan dengan merekomendasikan esai yang lebih beragam, di untuk mempelajari preferensi pengguna dengan lebih baik. Saya menggunakan “epsilon-greedy” strategi: untuk persentase waktu yang tetap, saya merekomendasikan esai acak alih-alih menggunakan k-NN. Saat ini persentasenya cukup tinggi: 50%. Sebagai Findka menumbuhkan dan mengumpulkan lebih banyak data, saya kemungkinan akan menurunkan persentasenya.

Khususnya, saya saat ini tidak menggunakan konten pemfilteran berbasis. Seringkali, artikel pemberi rekomendasi menganalisis teks artikel dan menggunakannya untuk mencari tahu yang mana mirip. Ini sangat masuk akal untuk berita, di mana Anda selalu berada merekomendasikan artikel baru yang mungkin belum memiliki banyak data interaksi pengguna. Namun, Findka ditujukan untuk artikel yang tetap relevan dari waktu ke waktu. Kita dapat mampu membiarkan artikel meningkatkan klik secara bertahap sebelum kami merekomendasikannya banyak pengguna. Pemfilteran berbasis konten kemungkinan besar masih akan membantu, jadi saya akan melakukannya bereksperimen dengannya di beberapa titik.

Untuk menjalankan kode Python, saya cukup menyebutnya sebagai subproses dari Clojure. Pertama, saya menghasilkan CSV dengan semua acara pengguna. Kode Python menyerap CSV dan lalu mengeluarkan CSV berbeda yang memiliki daftar URL untuk setiap pengguna. Dari Clojure, saya kemudian mengirimkan email dengan URL tersebut.

(memerlukan '[trident.util :as u]) (defn get-user-> recs [db] (tulis-acara-csv db) (u / sh "python3" (.getPath (io / resource "python / recommend.py"))) (baca-rekomendasi-csv))

Saya membaca file Python dari classpath JVM, yang memudahkan penerapan. saya cukup sertakan file dalam sumber daya aplikasi saya. Untuk penskalaan yang lebih baik, saya akan akhirnya pindahkan kode Python ke server khusus.

    Devops

    Ini adalah bagian favorit saya. Administrasi sistem Linux selalu menghangatkan hati saya, sejak bermain-main dengan Linux sebagai remaja memainkan peran besar di awal saya pendidikan komputasi. Saya masih ingat perasaan heran yang datang setelah saya terpelajar matikan -h sekarang (setidaknya, setelah saya tahu bahwa mengetik file kata sandi di terminal tidak membuat tanda bintang kecil muncul). Disana ada sesuatu tentang bisa mengganggu sistem secara langsung yang tertangkap perhatian saya.

    Bagaimanapun. Findka berjalan di atas tetesan DigitalOcean. Saya menggunakan Packer dan Terraform untuk penyediaan, dan saya menerapkan kode dari repositori git Findka menggunakan

ketergantungan git tools.deps . Untuk semua proyek saya, saya ingin menambahkan tugas script bash dengan form sebagai berikut:

     

    #! / bin / bash set -e foo () { … } batang () { … } “$ @”

    Dengan cara ini, Anda menambahkan tugas build hanya dengan menentukan fungsi baru. Saya menaruh alias t=’. / task’ di saya . bashrc , jadi saya dapat menjalankan tugas dengan mis t foo .

    Ini adalah file tugas Findka. Saya akan membahas setiap tugas: #! / bin / bash set -e init () { terraform init } build-image () { pembuat paket-var “do_key=$ DIGITALOCEAN_KEY” webserver.json curl -X GET -H “Otorisasi: Pembawa $ DIGITALOCEAN_KEY” “https://api.digitalocean.com/v2/images?private=true” | jq } tf () { terraform $ 1 -var “do_token=$ {DIGITALOCEAN_KEY}” -var “github_deploy_key=$ GITHUB_DEPLOY_KEY” } dev () { BIFF_ENV=dev clj -m biff.core } css () { npx tailwindcss membangun tailwind.css -o resources / www / findka-essays / css / custom.css } css-prod () { NODE_ENV=css produksi } terapkan () { git push origin master scp config.edn biff@essays.findka.com: config.edn scp blank-prod-deps.edn biff@essays.findka.com: deps.edn ssh biff@essays.findka.com systemctl restart biff ssh biff@essays.findka.com journalctl -u biff -f } prod-connect () { ssh -NL 7800: localhost: 7888 biff@essays.findka.com } “$ @”

    init : tidak banyak bicara. Anda harus menjalankan ini sekali.

    membangun-gambar : ini menyiapkan image Ubuntu untuk tetesan DigitalOcean. Ini file konfigurasinya, webserver.json :

    { “pembangun”: [ { “monitoring”: true, “type”: “digitalocean”, “size”: “s-1vcpu-1gb”, “region”: “nyc1”, “ssh_username”: “root”, “image”: “ubuntu-20-04-x64”, “api_token”: “{{user `do_key`}}”, “private_networking”: true, “snapshot_name”: “findka-essays-webserver” } ] , “penyedia”: [ { “type”: “shell”, “script”: “./provision.sh” } ], “variabel”: { “do_key”: “” }, “variabel-sensitif”: [ “do_key” ] }

    provision.sh adalah ~ 100 baris skrip yang:

  • Pemasangan paket (Clojure, Nginx, Python Surprise, Certbot)
  • Menyiapkan pengguna non-root
  • Membuat layanan Systemd yang memulai aplikasi saat boot
  • Mengonfigurasi Nginx, Letsencrypt, dan firewall
  • Setelah Packer selesai, membangun-gambar tugas mencetak ID untuk yang baru dibuat gambar, yang dibutuhkan Terraform.

    tf : tugas ini menyebarkan infrastruktur menurut webserver.tf file. Berikut cuplikannya:

    ... sumber daya "digitalocean_droplet" "webserver" { gambar=" ” name=”findka-essays-webserver” region=”nyc1″ size=”s-1vcpu-1gb” private_networking=true ssh_keys=[ data.digitalocean_ssh_key.Jacob.id ] koneksi { host=self.ipv4_address pengguna=”root” type=”ssh” batas waktu=”2m” } penyedia “file” { sumber=”config.edn” tujuan=”/home/biff/config.edn” } penyedia “file” { konten=var.github_deploy_key tujuan=”/home/biff/.ssh/id_rsa” } } …

    Tidak ditampilkan beberapa konfigurasi untuk catatan DNS dan Postgres yang dikelola database, yang digunakan Crux untuk ketekunan.

    dev , css : ini digunakan untuk pengembangan lokal. Setelah beberapa kode selesai, saya jalankan css-prod dan kemudian lakukan. Jika saya menggunakan CI / CD, saya akan melakukannya yang membangun artefak CSS alih-alih mengirimkannya ke git.

    menyebarkan: tugas paling lucu. Ini mendorong ke git, menyalin melalui file konfigurasi gitignored (yang mencakup rahasia, seperti kunci API), menyebarkan kode baru, dan kemudian mengawasi log.

    Clojure memiliki fitur di mana Anda dapat bergantung pada repositori git. Kapan Anda mulai aplikasi Anda, Clojure akan menggandakan repo dan menambahkan kodenya ke classpath. Kamu perlu menentukan hash komit, tetapi jika Anda menghilangkan hash, Anda dapat menjalankan perintah untuk mengambil hash untuk commit terbaru. Sehingga scp blank-prod-deps.edn ... tidak hanya itu:

    {: deps {github-jacobobryant / findka-essays {: git / url "git@github.com: jacobobryant / findka.git", : tag "HEAD", : deps / root "essays"}}}

    Setelah berjalan ssh biff@essays.findka.com systemctl restart biff , Systemd layanan akan mengambil hash komit terbaru (yang baru saja didorong ke master), tambahkan ke file dependensi, dan mulai aplikasi. (Ini bekerja dengan repo pribadi, omong-omong: itulah mengapa Terraform menyalin kunci penerapan Github saya ke server). Hal ini menyebabkan waktu henti sekitar satu menit, tetapi tidak masalah untuk itu Skala Findka.

    Tugas terakhir, prod-connect , memungkinkan Anda menjalankan kode Clojure sewenang-wenang kawat. Ini seperti SSHing di server Anda dan berjalan psql jadi kamu dapat meminta basis data produksi Anda, tetapi lebih canggih: Anda dapat menjalankan Clojure kode dalam proses JVM aplikasi produksi yang sedang berjalan. Berkat Clojure terlambat mengikat, Anda bahkan dapat mendefinisikan ulang fungsi — seperti penangan HTTP — dan membuat definisi baru segera berlaku. Semua dari kenyamanan Anda editor teks favorit (Vim, kan?).

    Saat pertama kali meluncurkan Findka, saya menggunakan fitur ini untuk mengirim email secara manual selama beberapa hari, sebelum menambahkannya ke tugas cron. Saya memiliki namespace Clojure yang terlihat seperti ini:

    (ns findka.essays.admin (:memerlukan [trident.util :as u] ...)) (defn dapatkan-data-email [db] ...) (komentar (u / pprint (biarkan [{:keys [biff/node]: sebagai sys} @ biff.core / system db (simpul inti / db] (dapatkan-email-data db)))

    Jika saya meletakkan kursor di u / pprint lalu ketik cpp sementara koneksi-prod adalah berjalan, itu akan mengeksekusi kode itu di server. Jadi saya bisa mencetak datanya yang akan diteruskan ke fungsi pengiriman email saya tanpa benar-benar mengirim email. Jika ada yang salah dalam data, saya dapat memodifikasi dapatkan-email -data berfungsi dan menjalankannya kembali tanpa harus menjalankan menyebarkan tugas. Kapan semuanya baik-baik saja, saya menjalankan fungsi berbeda yang mengirim email.

    Secara umum, ini bagus untuk:

    RELATED ARTICLES

    LEAVE A REPLY

    Please enter your comment!
    Please enter your name here

    Most Popular

    Recent Comments