- Internet'e bağlanıp ekolay televizyon sitesine girmem gerekiyor.
- Oradan yabancı filmler kısmına gitmem gerekiyor.
- Ardından tek tek film listesindeki linklere tıklayıp filmlere dair bilgi almam gerekiyor. Hangi kanalda hangi saatte olduğunu öğrendikten sonra bazen de IMDb puanına bakmam gerekiyor.
- Bundan sonra düşünüp taşınıp o gece için izleyeceğim bir ya da birkaç filmi seçmem gerekiyor.
Toby Segaran'ın "Programming Collective Intelligence" kitabını ve fazlamesai.net'teki röportajını okuduktan sonra yukarıdaki işi benim yerime bilgisayarın yapabileceğini düşündüm ve bunun için makina öğrenme (machine learning) tekniklerini kullanan bir yazılım geliştirmeye başladım. Böylece ortaya çıkan yazılım benim yerime gidip film listesini inceleyebilecek ve benim tercihlerimi önceden öğrenmiş olarak bana o gün için güzel tavsiyelerde bulunabilecekti. Böylece tvrecommend sistemi doğdu. Aşağıda bu programın geliştirilmesine dair detayları bulacaksınız.
Baş aktörler: Python, SQLite, BeautifulSoup, IMDbPY, libsvm
Yukarıda bahsi geçen "makina öğrenme" sistemi ile keyfimi modelleyeceğim yazılımı Python ile geliştirmeye karar verdim. Bunun başlıca sebebi Segaran'ın Programming Collective Intelligence kitabında seçtiği dilin bu olması idi. Kitapta karşılaştığım ve denediğim Python kodlarını hemen hiçbir Python kılavuzuna bakmadan anlayabildiğim için çok bir zorluk çekmeyeceğimi düşündüm. Bir başka motivasyon faktörü ise geliştireceğim sistemde kullanmak istediğim işlev kütüphanelerinin Python için bulunması ve kolayca kullanılabilmeleri idi.
Veritabanı olarak kitaptaki örneklerin pratikliğinden feyz alarak ben de
SQLite kullanmaya tercih ettim. SQLite veri
tabanı bu iş için yeterince hızlı, basit ve pratik görünüyordu. Üstelik benim
gibi günlük olarak .NET ve C# ortamında MS SQL Server Management tarzı gelişmiş
GUI araçları kullanmaya alışmış biri için
SQLite Studio,
SQLite Spy
gibi yönetim arayüzleri yeterince iyi sayılırdı (SQLite Spy tavsiyesi için
FM editörü Kıvılcım Hindistan'a teşekkürler). Kullanmakta olduğum Python sürümü
Python 2.5.1 olduğu için SQLite erişimi de standart olarak geliyordu ve
import sqlite3 as sqlite
ifadesi ile kolayca kullanılabiliyordu. Veritabanı yaratmak için alışık olduğum
SQL ifadelerini yazmam yeterli olmuştu:
def create_db(dbname = "movie.db"):
# Create the tables required for the movie database
con = sqlite.connect(dbname)
con.execute('CREATE TABLE Movie(movieID INTEGER PRIMARY KEY AUTOINCREMENT, title, \
original_title, mtime, mdate DATE, url, channel, imdb_rating DOUBLE, isWatch INTEGER)')
con.execute('CREATE INDEX Movieidx on Movie(movieID)')
con.commit()
con.close()
Şimdi sırada ekolay TV sitesine bağlanmak ve o günkü yabancı film listesini çekmek
vardı. Ardından da tek tek listedeki filmlerin web adreslerini öğrenip, o URLleri
ziyaret edip filme dair bilgileri elde etmek gerekiyordu. Burada da imdadıma Python
için yazılmış olan HTML 'parser'ı Beautiful
Soup yetişti. Beautiful Soup dosyasını çalıştığım dizine, yani tv.py dosyasının
bulunduğu dizine kopyalayıp Python dosyasının da başına from BeautifulSoup
import BeautifulSoup
yazmam yeterli olmuştu. Böylece HTML olarak çektiğim
film listesi sayfasında her türlü HTML ayıklama işini kolayca yapabilecektim:
today = datetime.date.today() title_url = today.strftime("http://www.ekolay.net/televizyon/hp_list.asp?tur=18&tarih=%d.%m.%Y") c = urllib2.urlopen(title_url) soup = BeautifulSoup(c.read()) for tr in soup.findAll(attrs = {'style' : 'padding-top:5px;'}): time = tr('td')[0].contents.__str__() channel = tr('td')[3].contents.__str__() for link in tr('a'): . . .
Yani belli şartları sağlayan 'attribute'ları olan tr
elementlerinin
üzerinden dönüp onların da içindeki a
yani link elementlerinin
üzerinden dönüp film detayları ile ilgili bilgi barındıran web sayfa adreslerini
ayıklamak mümkün olmuştu. Tabii diğer bilgiler için devreye biraz da kirli şekilde
düzenli ifade kalıplarını (regular expressions) sokmak gerekmişti ancak Perl, awk, sed,
JavaScript, vb. dillerden birini belli bir seviyenin üzerinde kullanmış hemen her
programcının düzenli ifadeleri bildiğini var sayarak bunun üzerinde çok durmuyorum;
tek gereklilik Python dosyasının başına import re
satırını eklemek.
Beautiful Soup HTML parser ve düzenli ifadeler ile temel bilgilerin bir kısmını çektikten sonra sırada filmin IMDb puanını öğrenmek vardı. Ancak gerçek hayat dikensiz gül bahçesi değildi ve IMDb puanının öğrenmek için filmin orjinal ismi gerekiyordu. Maalesef ekolay tv sitesinden gelen HTML içinde yabancı film bilgisi tutarlı bir formatta bulunmadığı gibi bazen de hiç yazılmıyordu! Elimden geldiğince düzenli ifade kalıpları ile farkına vardığım örüntüleri yakalamaya çalıştım ancak her zaman filmin özgün ismini bulmak mümkün değildi. Mümkün olduğu durumlarda ise IMDb puanı önemli kriterdi ve bunu da yine Python ile öğrenmek çok kolaydı çünkü IMDbPY hızır gibi imdadıma yetişmişti. Debian GNU/Linux ve MS Windows ortamlarına kolayca kurulabilen bu Python paketi ile birkaç satır yazarak Python içinden IMDB sitesi ile konuşmak çok kolaylaşmıştı:
# Create the object that will be used to access the IMDb's database.
ia = imdb.IMDb() # by default access the web.
# Search for a movie (get a list of Movie objects).
s_result = ia.search_movie(movie_name)
# Print the long imdb canonical title and movieID of the results.
for item in s_result:
print item['long imdb canonical title'], item.movieID
# Retrieves default information for the first result (a Movie object).
if len(s_result) > 0:
first_result = s_result[0]
ia.update(first_result)
if (first_result.has_key('rating')):
return first_result['rating']
.
.
.
Tüm gerekli bilgileri aldıktan sonra bunları Movie sınıfından bir nesne aracılığı ile taşımak ve veri tabanına kaydetmek mümkün hale gelmişti. Movie sınıfının temel yapısı şöyle idi:
class Movie:
#Initialize the crawler with the name of the database
def __init__(self, title = "", original_title = "", url = "", time = "", date = "", channel = ""):
self.title = title
self.original_title = original_title
self.url = url
self.time = time
self.date = date
self.channel = channel
# default IMDb rating is the mean value of [0.0, 10.0]
self.imdb_rating = 5.0
self.detail = ""
def persist(self, dbname = "movie.db"):
con = sqlite.connect(dbname)
if (self.original_title == ""):
con.execute("INSERT INTO Movie(title, original_title, mtime, mdate, url, channel, imdb_rating) \
VALUES ('%s', NULL, '%s', '%s', '%s', '%s', '%f')" %
(string.replace(self.title, "'", "_")
,self.time
,self.date
,self.url
,self.channel
,self.imdb_rating))
else:
con.execute("INSERT INTO Movie(title, original_title, mtime, mdate, url, channel, imdb_rating) \
VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%f')" %
(string.replace(self.title, "'", "_")
,string.replace(self.original_title, "'", "_")
,self.time
,self.date
,self.url
,self.channel
,self.imdb_rating))
con.commit()
con.close()
Yukarıdaki sınıf sayesinde Movie sınıfından bir movie nesnesi yaratıp içine gerekli bilgileri
doldurup sonra da movie.persist()
ile bunu veritabanına yazabiliyordum ve
şimdi sıra keyfimin modellenmesindeki kritik adımlardan birine gelmiştim: Sistemdeki veri
tabanından o günkü filmler mevcuttu ve bunlar aynı zamanda bir HTML dosyasına da yazılmıştı
ve web tarayıcımdaki bir bookmark ile sürekli güncel halini görebiliyordum ama bunlardan
hangisini izleyecektim? Sistem hepsinin üzerinden geçmeli ve bana tek tek ilgili filmi
izleyip izlemeyeceğimi sormalı ve bu bilgiyi de veritabanında isWatch
gibi
bir alanda saklamalıydı:
def update_db(dbname = "movie.db"):
# Update the Movie table by asking the user if he/she will watch the film
# http://initd.org/pub/software/pysqlite/doc/usage-guide.html#using-shortcut-methods
isWatch = {}
con = sqlite.connect(dbname)
con.row_factory = sqlite.Row
cur = con.cursor()
cur.execute("SELECT movieID, title, url, original_title, mtime, mdate, isWatch \
FROM Movie WHERE mdate = DATE('NOW')")
for row in cur:
result = raw_input(row["title"] + " isimli filmi seyredecek misiniz? (E/H): ")
if (string.upper(result.strip()) == 'E'):
isWatch[row["movieID"]] = 1
else:
isWatch[row["movieID"]] = -1
for key, value in isWatch.iteritems():
con.execute("UPDATE Movie SET isWatch = %s WHERE movieID = %s" % (value, key))
con.commit()
con.close()
Nihayet işin yapay zekâ / makina öğrenme ('machine learning') kısmı için gerekli tüm veri hazırdı. Artık bilgisayarı keyfimin kahyası haline getirebilir, keyfimi modelleyebilirdim. Bunun için kullanmayı seçtiğim yöntem SVM yani 'support vector machine' algoritması idi ve libsvm sayesinde de Python içinden kolayca kullanmak mümkündü (SVM'ye dair kısa Türkçe bilgi için: Ekşi Sözlük SVM maddesi). SVM ailesindeki sınıflandırma ve regresyon yöntemlerinin matematiğine dair gerçekten meraklı olanlar şu teknik kaynaklara bakabilirler: An Introduction to Support Vector Machines and Other Kernel-based Learning Methods ve Pattern Recognition and Machine Learning.
Eldeki veriyi SVM kullanarak sınıflandırmak için takip edilmesi gereken adımlar kısaca şu idi ( A practical guide to SVM classification [PDF] belgesinde de gayet güzel şekilde anlatıldığı gibi):
- Veriyi uygun şekilde vektör haline getir. Vektörün her bir elemanı reel sayı olmalıdır.
- Eğer kanal ismi gibi karakter tabanlı ve 'etiket' rolü gören bir özellik varsa bunu da sadece 1 ve 0'lardan oluşan bir vektör haline getir. Bu özellik içinde kaç farklı etiket varsa o kadar farklı vektör olacaktır, her vektörde sadece tek bir 1 olacaktır.
- Her özelliği aldığı tüm değerler üzerinden ölçekle ve [-1, 1] aralığına tasvir et. Böylece çok büyük aralıkta değişen değerler ile küçük aralıkta değişen değerler arasındaki fark mutlak değil oransal bir fark olsun. SVM böylece daha sağlıklı çalışacaktır.
- Böylece düzgün şekilde oluşturulmuş vektörlerin ait olduğu sınıfları sayısal değerlerini de sıralı şekilde bir araya getir (1: izle, -1: izleme) ve ilgili libsvm metodunu çağırıp modelin hesaplanmasını bekle
Yukarıdaki en kritik kısımlardan biri ölçekleme kısmı idi, bunun için Segaran'ın
"Programming Collective Intelligence" kitabındaki scaledata
fonksiyonunu
biraz modifiye etmem yeterli oldu:
def scaledata(rows):
low = [999999999.0] * len(rows[0])
high = [-999999999.0]* len(rows[0])
# Find the lowest and highest values
for row in rows:
d = row
for i in range(len(d)):
if d[i] < low[i]: low[i] = d[i]
if d[i] > high[i]: high[i] = d[i]
# Create a function that scales data
def scaleinput(d):
result = [0] * len(d)
for i in range(len(low)):
if (high[i] == low[i]):
result[i] = low[i]
else:
result[i] = (d[i] - low[i]) / (high[i] - low[i])
return result
# Scale all the data
newrows = [scaleinput(row) for row in rows]
return newrows, scaleinput
Bir başka kritik kısım ise 'CNBC-e', 'ATV', 'FOX TV' gibi kanal isimlerini vektör haline getirmekti. Bunu basit bir Python 'dictionary' nesnesi ile kolayca halletmek mümkün olsa da ilerideki modifikasyonlarda esneklik sağlaması için yavaş bir fonksiyon halinde kodlamayı tercih ettim:
def channel_to_vector(channel):
d = {}
d['CNBC-E'] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
d['TV 8'] = [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]
d['TRT2'] = [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0]
d['KANAL D'] = [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
d['FOX'] = [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]
d['ATV'] = [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
d['CINE-5'] = [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0]
d['SAMANYOLU'] = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0]
d['STAR'] = [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
d['TRT1'] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
d['SHOW'] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
return d[channel]
Eksik Veri ile Keyif Nasıl Modellenir?
Buraya kadar her şey iyi güzel ama hatırlarsanız yukarıda hayatın dikensiz gül bahçesi olmadığını belirtmiş ve her film verisinden orjinal ismi çekemediğimi dolayısı ile IMDb puanını öğrenemediğimi vurgulamıştım. Makina öğrenmesi alanındaki en önemli meselelerden biri de 'kayıp veri' meselesi idi. Yani vektörde IMDb sütununa karşılık gelen yere hangi sayıyı yazacaktım veri tabanında onun değerinin NULL olduğunu gördüğümde? Bunun için çeşitli yöntemler mevcuttu. En basiti içinde NULL geçen filmleri çöpe atmaktı ama o zaman keyif modellemesini pek sağlıklı gerçekleştiremeyecektim, böyle pek çok film olabilirdi. Bir başka yöntem eksik veri barındıran vektörleri ve tüm veriyi barındıran vektörleri kıyaslayıp IMDb puan verisinin nasıl bir istatistiksel dağılımda olduğunu analiz etmek bir nevi veri madenciliği ('data mining') yapmaktı. Ancak Statistical Analysis with Missing Data kitabına biraz göz atınca bunun da çok basit bir iş olmadığını gördüm. Bu durumda ne sisteme çok zarar verecek ne de çok fazla iş yapmayı gerektirecek bir orta yol seçtim ve IMDb puanının öğrenemediğim filmlerin puanı olarak 5.0 değerini belirledim sabit olarak.
Artık libsvm'ye istediği formatta sevdiğim ve sevmediğim filmleri verip
"benim sinema keyfimi öğren, çöz beni" diyebilecek hale gelmiştim (tabii
tv.py dosyasının başına from svm import *
satırını da eklediğimi
belirteyim):
def create_svm_data(dbname = "movie.db"):
rows = []
answers = []
con = sqlite.connect(dbname)
con.row_factory = sqlite.Row
cur = con.cursor()
cur.execute("SELECT movieID, title, url, original_title, mtime, mdate, imdb_rating, \
channel, isWatch FROM Movie")
for row in cur:
r = []
answers.append(row["isWatch"])
r.append(string.atof(string.replace(row["mtime"], ":", "")))
r.append(row["imdb_rating"])
r.extend(channel_to_vector(row["channel"]))
rows.append(r)
con.close()
return answers, rows
def create_svm_model():
a, r = create_svm_data()
newr, scaleinput = scaledata(r)
prob = svm_problem(a, newr)
param = svm_parameter(kernel_type = LINEAR, C = 10)
m = svm_model(prob, param)
m.save("test.model")
print "A support vector machine model has been created and saved as test.model."
return
Artık 4-5 gün boyunca biriktirdiğim ve "bunu izlerim, bunu asla izlemem" dediğim film verisinden yola çıkarak oluşmuş SVM modeli yani tabiri caizse 'keyfim' diske 'test.model' dosyası olarak yazıldığına göre sıra yeni bir günde, yeni bir film listesinde sistemi denemeye gelmişti.
Bilgisayarın bana mantıklı film önerileri sunup sunamayacağını anlamanın tek
yolu o günkü film verisini get_details()
ile çekmek ve sonra da
ilgili libsvm metoduna test.model dosyasını gösterip tek tek o günkü filmlerin
vektörleştirilmiş hallerini verip 1 mi (izle) yoksa -1 mi (izleme) döndürdüğüne
bakıp -1 olanları bir yere yazmak idi:
def predict_film(dbname='movie.db'):
# Retrieve data that is not today
# Use it to have scale function
# Retrieve today's data
# Scale it
# pass it to test.model's predict
rows = []
answers = []
predict_rows = []
predict_film_details = []
con = sqlite.connect(dbname)
con.row_factory = sqlite.Row
cur = con.cursor()
cur.execute("SELECT movieID, title, url, original_title, mtime, mdate, imdb_rating, channel, \
isWatch FROM Movie WHERE mdate < DATE('NOW') AND isWatch IS NOT NULL")
for row in cur:
r = []
answers.append(row["isWatch"])
r.append(string.atof(string.replace(row["mtime"], ":", "")))
r.append(row["imdb_rating"])
r.extend(channel_to_vector(row["channel"]))
rows.append(r)
con.close()
newr, scaleinput = scaledata(rows)
con = sqlite.connect(dbname)
con.row_factory = sqlite.Row
cur = con.cursor()
cur.execute("SELECT movieID, title, url, original_title, mtime, mdate, imdb_rating, channel, isWatch \
FROM Movie WHERE mdate = DATE('NOW')")
for row in cur:
r = []
answers.append(row["isWatch"])
r.append(string.atof(string.replace(row["mtime"], ":", "")))
r.append(row["imdb_rating"])
r.extend(channel_to_vector(row["channel"]))
predict_rows.append(r)
details = []
details.append(row["movieID"])
details.append(row["title"])
details.append(row["url"])
predict_film_details.append(details)
con.close()
m = svm_model("test.model")
for i in range(len(predict_rows)):
s = scaleinput(predict_rows[i])
isWatch = m.predict(s)
if isWatch > -1.0:
print "prediction : "
print predict_rows[i]
print predict_film_details[i]
Burada bahsi geçen tvrecommend sisteminin kurulum ve kullanım detaylarına http://tvrecommend.sourceforge.net adresinden erişebilirsiniz. Sistem halen yoğun olarak geliştirilme durumunda olduğu için sourceforge.net'teki 'download' işlevselliği yerine Subversion checkout ile edinmenizi öneririm.
İzlenimler ve Sonuçlar
Bu sistemi tamamen kişisel amaçlarım için günlük film tercihlerimi bilgisayara
öğretip sonra bana sonraki günlerde bana mantıklı film tavsiyesinde bulunması
için geliştirdim. Halihazırda sistem kullanım itibari ile yazılım geliştiricileri
/ uzman kullanıcıları hedefliyor. Komut satırından kullanılabilen sistem
metin tabanlı bir menü sunuyor ve filmle ilgili detayları bulunduğu dizindeki
bir HTML dosyasına yazıyor. Bunu cron
'a veya Windows'ta Task
Scheduler'a bağlamak mümkün (buna hizmet edecek bir tvservice.py
dosyası da mevcut).
Sistem devreye soktuğum günden beri bana makul günlük film önerilerinde bulunuyor ve benim keyfimi iyi öğrendiğini, modellediğini düşünüyorum. Yeterince iyi ve beni bir yükten kurtardı.
tvrecommend sistemini pek Python bilmeden geliştirmeye başladım. Geliştirirken Python ile ilgili hiç 'tutorial' okumadım sadece takıldığım bazı yerlerde Google üzerinden bulduğum birkaç belgeye baktım. Python'un IDLE çalışma ortamı (ve Emacs'ın python-mode'u) görebildiğim kadarı ile Common Lisp'e çok benzer çalışma ortamı sunuyor, yani Emacs + SLIME + CL üçlüsüne alışkın biri olarak yabancılık çekmedim. Python çalışma ortamı mesela bir SBCL ortamına kıyasla daha yavaş ancak teknik dokümantasyonu yeterli ve takip etmesi kolay, bu projede performans çok önemli değildi.
Bir başka nokta: SQLite kesinlikle bundan sonraki projelerimde göz önünde bulunduracağım bir veri tabanı.
Python'un 3. şahıslar tarafından yazılmış Beautiful Soup, IMDbPY gibi kütüphaneleri gayet güzel iş görüyor.
libsvm ve dolayısı ile 'support vector machines' yöntemi doğru metodoloji ile kullanıldığında çok güçlü bir makina öğrenme / yapay zeka yöntemi. Ancak doğru kullanmak için verinin nasıl vektör haline getirileceğine, ölçeklemeye, kayıp veri yerine ne konacağına çok dikkat edilmesi gerekiyor. libsvm içinden çıkan araçlar her türlü verinin SVM için uygunluğunu denetleyebilecek ve gerekli parametreleri önerebilecek türden araçlar, özellikle GNU/Linux ortamında çok faydaları dokunabilir.
Yapılması Gerekenler
Öncelikle kodun cilalanması ve daha bir son kullanıcıya yönelik hale getirilmesi gerekiyor. Ayrıca libsvm ile yapılan tahminlerin gerçekten de tatmin edici olup olmadığını anlamak için biraz daha test yapmak lazım.
Kendi adıma daha çok Python öğrenmeye heves ettim bu projeden sonra. Unicode ve Türkçe karakterler meselesi ile ilgili kodda çok kirli ve hızlıca olduğunu düşündüğüm bölümler var, onları bertaraf etmek faydalı bir çaba olacaktır.
ekolay tv sitesi yerine film bilgilerini doğru dürüst bir XML formatında veren bir RSS kaynağı bulmak sistemde çok daha eksiksiz veri birikmesini sağlayacağı için daha sağlıklı çalışmasını sağlayacaktır (böyle bir kaynak biliyorsanız lütfen haber verin! ;-)
Performans açısından C4.5 karar ağacı gibi daha kolay anlaşılabilen bir makina öğrenme algoritması kullanıp mevcut çözümün tahmin performansı ile kıyaslanabilir, bu da işin yapay zeka kısmı ile ilgili değerli bir çaba olacaktır.
Emre Sevinç (emre . sevinc at gmail nokta com)
22 Ocak 2008, Istanbul
Makalenin özgün adresi: http://ileriseviye.org/arasayfa.php?inode=tvrecommend.html