Konuyu görüntüle
IUCODERS FORUM > Programlama > Diğer (COBOL,asp php js..) > Fakirin TV Film Tavsiyecisi :)
Yazar
dotnetonur


avatar
Dersaadet
Kayıt: 21.11.2007
23.01.2008-01:52 #35812
Okurken çok zevk aldığım, uykudan uyanıp beni PC başına neden çektiğini şimdi daha iyi anladığım, Python ile Yapay Zeka uygulamalarına örnek teşkil edecek bir doküman...

Orjinal Adresi: http://ileriseviye.org/arasayfa.php?inode=tvrecommend.html

---------------------------------------------------

Bu makalede bilgisayarın basit bir keyif modellemesi için nasıl kullanılabileceği anlatılacaktır. Modellenecek keyif yazarın yani benim keyfim olup ortaya çıkmış olan yazılım kolayca bir başkasının keyfinin modellenmesi için de kullanılabilir.

Problem ifadesi: Hemen her akşam eve gittiğimde televizyonda keyfime uygun bir film olup olmadığını öğrenmek istiyorum. Bu o kadar da hızlı yapabildiğim bir iş değil çünkü:

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ç
22 Ocak 2008





Ortam sanal olsa da, islenen suc gercektir...

Del.icio.us
Digg
Facebook
Furl
Google
Blink
Simpy
Spurl
Y! MyWeb