İçindekiler
- Problem
- 2.0.0 nereden geliyor
- Tasarım kararları ve alternatifler
- Gerçek bir hata: kaydedilmiş quiz'e karesel zamanda dönmek
- Kullanım
- Sınırlar
Problem
Bir flashcard uygulamasının merkezinde tek bir zor soru vardır: bu kart ne zaman geri gelmeli? Erken gelirse öğrencinin zamanı boşa gider; geç gelirse kart unutulmuş olur.
Uzun süre yaygın cevap SM-2 oldu: önceki aralığı bir kolaylık katsayısıyla (easiness factor) çarpmak. Basittir, ama öğrencinin hatırlama olasılığını bilmez; bu yüzden "kartlarımın %90'ını hatırlamak istiyorum" diyebileceğiniz tek bir ayar yoktur. FSRS (Free Spaced Repetition Scheduler) hafızayı bunun yerine üç sayıyla modeller: stability, difficulty ve retrievability. Sonraki tekrar, tahmin edilen hatırlama olasılığının hedef orana düştüğü güne konur. FSRS-6, Anki'nin FSRS zamanlayıcısının arkasındaki sürümdür; referans uygulaması py-fsrs'tir.
"FSRS uyguluyor" demek README'de kolay, denetlemesi zordur. EducationAI 2.0.0, FSRS-6'yı uygulayan ve test paketinde py-fsrs 6.3.2 ile aynı sonucu verdiğini gösteren bir Swift paketidir. Zamanlayıcının etrafında bir çalışma uygulamasının ihtiyaç duyduğu parçalar da vardır: çalışma kuyruğu, SM-2 ve FSRS'e taşınması, yazılı cevap notlama, quiz'ler, zorluk ayarlayıcı, tahmin, istatistikler ve çalışma serileri.
Adına rağmen içinde sinir ağı yoktur: FSRS, 21 ağırlıklı istatistiksel bir hafıza modelidir.
2.0.0 nereden geliyor
Repo, bugün taşıdığı paketten daha eskidir. Ağustos 2025'te açıldı ve 1.0.0 olarak Alamofire, RxSwift, SnapKit ve SDWebImage'a bağımlı bir EducationAI sınıfıyla etiketlendi. Şubat 2026'da bağımlılıksız olarak, kilitle korunan sınıflar üzerinde SM-2, quiz, ilerleme, öneri ve oyunlaştırma motorları biçiminde yeniden yazıldı. Haziran 2026'da yerel bir klasöre bağımlılık, .package(path: "../SwiftAI"), ile yer tutucu "neural tutor" ve "enterprise" tipleri eklendi; paket başka hiç kimse için çözülemez oldu.
2.0.0 CHANGELOG'u o kodun sorunlarını sayar: motorlar saati kendileri okuyordu, aralıkları NaN'da çöken Int(days * 86_400) ile çeviriyordu ve "hard" cevabı unutma (lapse) sayıyordu. 15 Ekim 2026'da Haziran kodu kaldırıldı (7aff8be), motorların yerine yenisi geldi (6bc2b0f) ve 2.0.0, PR #31 ile yeni bir major sürüm olarak yayımlandı.
Tasarım kararları ve alternatifler
Ana zamanlayıcı FSRS-6, SM-2 korunuyor. FSRS'te uygulama tek bir sayıyı, desiredRetention'ı değiştirerek tekrar yükü ile hatırlama arasında denge kurar; eğitilmiş FSRS ağırlıkları olduğu gibi verilebilir. SM-2'de kalmak daha basit olurdu, kendi uydurduğum bir sezgisel kuralın ise arkasında hiçbir kanıt olmazdı. SM-2 gitmedi: SM2Scheduler onu yayımlandığı haliyle uygular (0–5 kaliteleri; 1 gün, 6 gün, sonra aralık çarpı kolaylık katsayısı), ReviewState(migrating:scheduler:sm2Retention:) da SM-2 kartlarını fsrs-rs'in kullandığı yöntemle taşır; mevcut ilerleme kaybolmaz.
Referansı işlem işlem izlemek. FSRSScheduler+Formulas.swift içindeki formüller py-fsrs 6.3.2'yi işlem işlem izler. exp(x) yerine pow(M_E, x) kullanılır, çünkü Python'daki math.e ** x bunu hesaplar; kaynak yorumu nedenini yazar: sonuçlar bit düzeyinde aynı çıkar ve .5 güne düşen aralıklar aynı yöne yuvarlanır. Geçen günler Python'un timedelta.days'i gibi aşağı yuvarlanır. Bedeli, idiomatik Swift'ten çok Python'a benzeyen koddur. Karşılığı ise test edilebilir bir iddiadır: scripts/generate-fsrs-fixtures.py, py-fsrs ile 2.400 tekrar üretir (sekiz yapılandırma, her birinde 25 vaka, vaka başına 12 tekrar); FSRSReferenceTests her birinden sonra faz, adım, stability, difficulty, vade tarihi ve retrievability değerlerini fuzz kapalıyken karşılaştırır.
Değerler, dışarıdan verilen tarih ve rastgelelik. FSRSScheduler, ReviewState, QuizSession ve DifficultyAdjuster, Sendable ve Codable struct'lardır. Her fonksiyon çalıştığı tarihi parametre olarak alır; aralık fuzz'ı bir RandomNumberGenerator'dan çekilir ve bu bir SeededRandomNumberGenerator olabilir. Alternatif, yani kilitle korunan bir servis sınıfı, 1.x'te vardı ve "bu karta gelecek salı ne olacak" sorusunu test edilemez yapıyordu.
Saklanan veriye güvenmemek. Bir ReviewState diskten, bir senkronizasyon servisinden ya da uygulamanın eski bir sürümünden gelir. Zamanlayıcı her hesaptan önce onu denetler: NaN ya da sonsuz tarihler, negatif sayaçlar, faza uymayan bir adım, 1–10 dışında bir difficulty. Bozuk bir durum çökme yerine SchedulingError.invalidState fırlatır. Tam sayıya dönüşümler SafeNumerics.roundedInt(_:clampedTo:) üzerinden geçer, sayaçlar Int.max'te durur. Bir precondition daha kısa olurdu, ama o zaman tek bir bozuk kart bütün uygulamayı çökertirdi.
Telefonun diline bağlı olmayan notlama. AnswerMatcher büyük-küçük harfi, aksanları, genişliği ve noktalamayı locale: nil ile katlar; aynı cevap her cihazda aynı biçimde notlanır. Yazım hataları ekleme, silme, değiştirme ve bitişik yer değiştirme olarak sayılır, yalnız köşegen çevresindeki bir şeritte hesaplanır; 500 karakterden uzun cevaplar karşılaştırılmaz. Tam bir edit distance tablosu daha basit ama düşmanca girdide daha yavaş olurdu; testler şeritli sonucu 3.000 rastgele çiftte tam tabloyla karşılaştırır.
Gerçek bir hata: kaydedilmiş quiz'e karesel zamanda dönmek
QuizSession Codable'dır ve decoder'ı kaydedilmiş sonuçlara güvenmez: kaydedilen her cevabı answer(_:to:) üzerinden yeniden oynatır; böylece düzenlenmiş bir dosya yanlış cevabı doğruya çeviremez. Bu kararın bedeli aramada ortaya çıktı. answer soruyu questions.first(where:) ile buluyordu, bu yüzden bir quiz'i decode etmek karesel zaman alıyordu. b334965 commit'i sayıları yazar: 25.000 cevap için yaklaşık 2 sn, 100.000 için yaklaşık 22 sn. Paketin güvenlik politikası, decode edilen hiçbir değerin boyutuyla orantısız zaman harcatmayacağını söyler; yani bu yalnız yavaş bir yol değil, tutulmayan bir sözdü.
Düzeltme, initializer yinelenen kimlikleri denetlerken soru kimliğinden konuma bir sözlük kurar; currentQuestion da her çağrıda listeyi baştan taramak yerine ileri giden bir konumu okur. İkisi de türetilmiş değerlerdir; kodlanmazlar ve kayıt biçimi değişmedi.
Bunun için yazılan ilk test "100.000 soru 5 sn içinde geri yüklenir" diyordu. İkinci ders buradan geldi. CI'daki iOS simülatöründe bütün koşu daha yavaştır; kontrol PR #33'ün merge commit'inde ve o pull request'in ilk koşusunda kırıldı, aynı kodun yeniden koşusunda geçti. PR #34 (4f88ede) sabit sınırın yerine bir oran koydu: 25.000 ve 100.000 soruyu decode et, büyük olan küçüğün on katından kısa sürsün. Doğrusal geri yükleme 4 civarında bir oran verir, eski tarama 16 civarında; oran makinenin hızına bağlı değildir. Mutlak süre sınırı olan bir performans testi kodu olduğu kadar runner'ı da ölçer; sürenin nasıl büyüdüğünü ölçen test ise kodu ölçer.
İlgili bir düzeltme, 7bf9e7e, stability'yi FSRSWeights.maximumStability (10^10 gün) ile sınırladı: saklanan 1e308 değerindeki bir stability doğrulamadan geçiyor, bir tekrarda sonsuza büyüyor ve o kartın sonraki tekrarı hata fırlatıyordu. Bir girdiyi kabul etmek ile çıktının geçerli kalması iki ayrı denetimdir.
Kullanım
Paketi Xcode'da https://github.com/muhittincamdali/educationai.git adresiyle ya da Package.swift içinde ekleyin:
swift
1dependencies: [2 .package(url: "https://github.com/muhittincamdali/educationai.git", from: "2.0.0")3],4targets: [5 .target(name: "YourApp", dependencies: [6 .product(name: "EducationAI", package: "educationai")7 ])8]Aşağıdaki örnekler, testlerin derleyip çalıştırdığı ReadmeSamples.swift dosyasından gelir. Cevap düğmelerini bir sonraki aralıkla etiketlemek dört puanın önizlemesini kullanır:
swift
1func buttonLabels(for card: Flashcard, scheduler: FSRSScheduler) throws -> [String] {2 let preview = try scheduler.preview(card.review)3 let format = Duration.UnitsFormatStyle(allowedUnits: [.days, .hours, .minutes], width: .narrow, maximumUnitCount: 1)4 return Rating.allCases.map { rating in5 let interval = Duration.seconds(preview[rating].interval)6 return "\(String(describing: rating).capitalized) · \(interval.formatted(format))"7 }8}Yeni bir kart için bu Again · 1m, Hard · 6m ve Good · 10m verir, Easy için yaklaşık bir hafta. Bir quiz, QuizResult.suggestedRating aracılığıyla zamanlayıcıya geri bağlanır: yanlış cevap Again, yazım hatalı cevap Hard, doğru cevap Good olarak zamanlanır:
swift
1func quizAndReschedule(deck: inout [Flashcard], answers: [String: String], at date: Date) throws -> Double {2 var quiz = try QuizSession(questions: QuizBuilder.typedAnswer(from: deck))3 while let question = quiz.currentQuestion {4 let result = try quiz.answer(.text(answers[question.id] ?? ""), to: question.id)5 if let index = deck.firstIndex(where: { $0.id == result.questionID }) {6 deck[index].review = try FSRSScheduler.default.review(deck[index].review, rating: result.suggestedRating, at: date).state7 }8 }9 return quiz.score10}StudyDeck örnek uygulaması (iOS ve macOS) bu parçaları bir araya getirir; DocC referansında dört rehber vardır. Test paketinde 11 suite içinde 124 test bulunur; 15 Ekim 2026'da v2.0.0 etiketinde swift test ile geçti.
Sınırlar
- İddia "referansla aynı sonuç"tur, "öğrenciler için en iyisi" değil. Gerçek öğrenci verisiyle yapılmış bir ölçüm yok.
- Optimizer yok. Ağırlıklar varsayılan değerlerdir ya da dışarıdan gelir; bunları öğrencinin tekrar kayıtlarından eğitmek yol haritasında. Yol haritasında ayrıca referans uygulamalar benimseyince FSRS-7, Anki kayıtlarını içe aktarma ve vade tarihlerinin günlere dengeli dağıtılması var.
- Fuzz
py-fsrsile karşılaştırılmıyor, çünküpy-fsrsonu Python'un kendi rastgele üretecinden çekiyor; fuzz yalnız aralık ve tekrarlanabilirlik açısından denetleniyor. - Depolama, senkronizasyon ya da UI kütüphanesi yok. Paket
Codabledeğerler döndürür; onları saklamak ve çakışmaları çözmek uygulamanın işidir. - Sınırlar açıktır ve bunlara takılınabilir: 500 karakterden uzun cevaplar karşılaştırılmaz, tahminler en fazla 3.650 günü kapsar, ayarlayıcı pencereleri en fazla 1.000 cevap tutar.
- Yerel ayardan bağımsız katlama birkaç Unicode durumunda test edildi, örneğin "İstanbul"; her yazı sistemi için ayrı bir test yok.
- Swift 6.2 (Xcode 26) ve iOS 17, macOS 14, tvOS 17, watchOS 10 ya da visionOS 1 gerektirir.
Kod, referans verileri ve onları üreten betik MIT lisansıyla GitHub üzerinde.

