“Try not to become a man of success,
but rather try to become a man of value.”—Albert Einstein
“อย่าพยายามเป็นคนประสบความสำเร็จ
แต่ให้พยายามเป็นคนที่มีคุณค่า.”—อัลเบิร์ต ไอน์สไตน์
จงสร้างข้อมูลหนึ่งมิติขึ้นมา (อาจใช้คำสั่ง เช่น np.random.normal
) และใช้วิธีการประมาณความหนาแน่นแก่น (สมการ \(\eqref{eq: KDE shell}\) และ \(\eqref{eq: KDE kernel}\)) เพื่อประมาณความหนาแน่นความน่าจะเป็น จากข้อมูลนั้น โดยทดลองค่า \(\sigma\) หลาย ๆ ค่า. สังเกตผล อภิปราย และสรุป.
รายการ [code: kde] แสดงโปรแกรมวิธีการประมาณความหนาแน่นแก่น. ตัวอย่างการเรียกใช้ เช่น
datax = np.random.normal(3, 2, 100).reshape((1,-1))
xs = np.linspace(-2, 8, 50).reshape((1,-1))
pdx = kde(xs, datax, sigma=1)
plt.plot(xs[0,:], pdx[0,:], 'k')
บรรทัดแรกเป็นคำสั่งเพื่อสร้างข้อมูล datax
ขึ้นมา จากการแจกแจงแบบเกาส์เซียน จำนวน \(100\) จุดข้อมูล โดยมีค่าเฉลี่ยเป็น \(3\) และค่าเบี่ยงเบนมาตราฐานเป็น \(2\). บรรทัดที่สอง สร้างค่า \(x\) ที่ต้องการถาม และบรรทัดที่สาม คือการเรียกโปรแกรม kde
เพื่อประมาณความหนาแน่นความน่าจะเป็นของข้อมูล datax
. บรรทัดสุดท้ายเป็นการวาดกราฟแสดงความหนาแน่นที่ค่าอินพุตต่าง ๆ. ดูตัวอย่างจากรูป [fig: kde sigma's].
def kde(x, kx, sigma=1):
'''
x: D x Nx
kx: D x Nk; D - # dimensions, Nk - # datapoints
'''
= x.shape[1]
N = np.zeros((1, N))
px = 1/N * 1/np.sqrt(2*np.pi*sigma**2)
norm
for n in range(N):
= np.sum((x[:,[n]] - kx)**2, axis=0) # (Nk, )
distn = np.exp(-distn/(2*sigma**2)) # (Nk,)
texn 0,n] = norm * np.sum(texn)
px[
return px
จงสร้างข้อมูลสองมิติขึ้นมา โดยอาจใช้คำสั่ง เช่น
mu = [6, 9]
cov = [[8, -5], [-5, 9]]
x = np.random.multivariate_normal(mu, cov, 500).reshape((2,-1))
เมื่อ mu
และ cov
แทนค่าเฉลี่ยและค่าความแปรปรวนร่วมเกี่ยว ตามลำดับ และสร้างเป็นข้อมูลสองมิติจำนวน \(500\) จุดข้อมูล. และใช้วิธีการประมาณความหนาแน่นแก่น (สมการ \(\eqref{eq: KDE shell}\) และ \(\eqref{eq: KDE kernel}\)) เพื่อประมาณความหนาแน่นความน่าจะเป็น จากข้อมูลนั้น โดยทดลองค่า \(\sigma\) หลาย ๆ ค่า. สังเกตผล อภิปราย และสรุป.
รูป 2 แสดงตัวอย่างข้อมูลสองมิติที่สร้างขึ้น และค่าความหนาแน่นที่ประมาณออกมา.
แบบฝึกหัดนี้ เราจะศึกษารูปปฐมของซัพพอร์ตเวกเตอร์แมชชีน. จงสร้างข้อมูลขึ้นมาจำนวน \(200\) จุดข้อมูล โดยเป็นกลุ่มบวกและกลุ่มลบอย่างละครึ่ง จุดข้อมูลอยู่ในปริภูมิสองมิติ และข้อมูลสามารถแบ่งแยกได้อย่างสมบูรณ์เชิงเส้น เช่น ข้อมูลที่แสดงในรูป 3. แล้วแก้ปัญหาในรูปปฐมของซัพพอร์ตเวกเตอร์แมชชีน เพื่อหาอภิระนาบแบ่งแยก นั่นคือ หาค่าพารามิเตอร์ \(\boldsymbol{w}\) และ \(b\) ดังแสดงในรูป 4.
ทบทวนปัญหาปฐม สำหรับข้อมูลที่สามารถแบ่งแยกได้โดยสมบูรณ์เชิงเส้น คือ \[\begin{eqnarray} \underset{\boldsymbol{w}, b}{\mathrm{minimize}} & \frac{1}{2} \boldsymbol{w}^T \boldsymbol{w} & \nonumber \\ \mbox{s.t.} & y_i (\boldsymbol{w}^T \boldsymbol{x}_i + b) \geq 1 & \mbox{ for } i =1, \ldots, N. \nonumber \end{eqnarray}\]
หมายเหตุ ปัญหานี้ เราจะใช้ฟังก์ชันเอกลักษณ์เป็นลักษณะสำคัญ นั่นคือ \(\boldsymbol{z} = \phi(\boldsymbol{x}) = \boldsymbol{x}\) (ซึ่งเทียบเท่าการใช้เคอร์เนลเชิงเส้น \(k(\boldsymbol{x}, \boldsymbol{x}') = \boldsymbol{x}^T \boldsymbol{x}'\)).
เราอาจสามารถแก้ปัญหาได้ด้วยวิธีการลงโทษ เช่นอาจกำหนดฟังก์ชันลงโทษ เป็น \[P(\boldsymbol{w}, b) = \sum_i \mathrm{relu}\left(1 - y_i \cdot (\boldsymbol{w}^T \boldsymbol{x}_i + b)\right)\] เมื่อ \[\begin{eqnarray} \mathrm{relu}(a) = \left\{ \begin{array}{ll} a & \mbox{ เมื่อ } a \geq 0, \\ 0 & \mbox{ หากเป็นกรณีอื่น} . \end{array} \right. \nonumber \end{eqnarray}\]
จากปัญหาปฐมของซัพพอร์ตเวกเตอร์แมชชีน ตัวอย่างคำสั่งข้างล่าง
la = 100
loss_adaptor = lambda wb: loss(wb[:2], wb[2], la)[0,0]
dloss_adaptor = lambda wb: dloss(wb[:2], wb[2], la)
wb0 = np.zeros((3,1))
wbo, gd_losses, wbs = gd(dloss_adaptor, wb0, loss_adaptor,
step_size = 0.0001, Nmax = 5000)
ใช้ค้นหาอภิระนาบ (ระบุด้วย \(\boldsymbol{w}\) และ \(b\) ซึ่งคือ wbo[:2]
และ wbo[2]
ตามลำดับ). โปรแกรม loss
และ dloss
รวมถึงโปรแกรมอื่นที่เกี่ยวข้องกำหนดดังแสดงในรายการ [code: svm primal gd]. โปรแกรม gd
(แสดงในรายการ [code: gd as function]) คำนวณวิธีลงเกรเดียนต์.
= lambda a: (a >= 0)*a
relu = lambda a: (a >= 0)*1
drelu
def loss(w, b, la):
= 0.5 * np.dot(w.transpose(), w)
term1 = relu(1 - datay * (np.dot(w.transpose(), datax) + b))
term2 return term1 + la * np.sum(term2)
def dloss(w, b, la):
= datax.shape[1]
N = np.vstack((w,0))
term1 = - datay * np.vstack((datax, np.ones( (1, N))))
dc = drelu(1 - datay*(np.dot(w.transpose(),datax) + b))*dc
term2 = np.sum(term2, axis=1).reshape((-1,1))
sum_dpenalty
return term1 + la * sum_dpenalty
def gd(grad, v0, g, step_size=0.01, Nmax=100, tol=1e-6):
'''
grad: gradient function
v0: initial value
g: objective function
'''
= []
losses = np.zeros(v0.shape)
vs = v0
v = grad(v)
gradv
for i in range(Nmax):
= v - step_size * gradv
v = grad(v)
gradv
= g(v)
loss
losses.append(loss)= np.hstack((vs, v))
vs
= np.linalg.norm(gradv)
eps if eps <= tol:
print('Reach termination criteria')
break
return v, losses, vs
หัวข้อนี้ แนะนำมอดูลไซไพ (Scipy) ซึ่งมีเครื่องมือหลายอย่างสำหรับงานคำนวณทางวิทยาศาสตร์ รวมถึงมอดูล optimize
. มอดูล optimize
มีเครื่องมือการหาค่าดีที่สุดอยู่หลายวิธี. แม้วิธีลงเกรเดียนต์ (หัวข้อ [sec: opt grad desc]) เป็นวิธีการที่ใช้งานได้ แต่ในทางปฏิบัติ มีวิธีที่มีประสิทธิภาพมากกว่าวิธีลงเกรเดียนต์อยู่มากมาย และด้วยแบบจำลอง optimize
เราสามารถไปใช้วิธีเหล่านั้นได้ โดยไม่ต้องใช้เวลาในการศึกษารายละเอียดของวิธีเหล่านั้นมากนั้น. มอดูล optimize
สามารถนำเข้าได้ด้วยคำสั่ง
from scipy import optimize
แบบฝึกหัด 1.0.0.1.1 จะแก้ปัญหาเดียวกับแบบฝึกหัด 1.0.0.0.3 เพียงแต่จะใช้เครื่องมือจากแบบจำลอง optimize
แทนวิธีลงเกรเดียนต์.
เช่นเดียวกับแบบฝึกหัด 1.0.0.0.3 จงสร้างข้อมูลขึ้นมาจำนวน \(200\) จุดข้อมูล โดยเป็นกลุ่มบวกและกลุ่มลบอย่างละครึ่ง จุดข้อมูลอยู่ในปริภูมิสองมิติ และข้อมูลสามารถแบ่งแยกได้อย่างสมบูรณ์เชิงเส้น แล้วแก้ปัญหาในรูปปฐมของซัพพอร์ตเวกเตอร์แมชชีน เพื่อหาอภิระนาบแบ่งแยก.
ศึกษาการเขียนโปรแกรมด้วยการใช้มอดูล optimize
(ดังแสดงในตัวอย่างของแบบฝึกหัดนี้) เปรียบเทียบกับด้วยวิธีลงเกรเดียนต์ (แบบฝึกหัด 1.0.0.0.3) ทดลองใช้งาน สังเกตผล อภิปราย และสรุป.
ตัวอย่างคำสั่งข้างล่าง ฝึกแบบจำลอง และรายงานผลอภิระนาบที่พบ
svm = primal_SVM()
svm.phi = lambda xq: xq # identity projection
res = svm.train(datax, datay)
print('w=', svm.wopt.T)
print('b=', svm.bopt)
โดย บรรทัดแรก เป็นการสร้างตัวแปรวัตถุ svm
จากคลาส primal_SVM
ซึ่งแสดงในรายการ [code: gd as function]. บรรทัดที่สอง กำหนดฟังก์ชันลักษณะสำคัญเป็นฟังก์ชันเอกลักษณ์. บรรทัดที่สาม เป็นการฝึกซัพพอร์ตเวกเตอร์แมชชีน ด้วยข้อมูล datax
และ datay
. ผลลัพธ์ที่ได้จากการฝึกจะสรุปออกมาเป็นค่าของพารามิเตอร์ svm.wopt
และ svm.bopt
ที่ใช้บรรยายอภิระนาบ. สังเกตว่า เมท็อด train
มีการเรียกใช้
res = optimize.minimize(minf, wb0, method='SLSQP', jac=gradf,
constraints=ineq_cons, options=options)
ซึ่งเป็นเครื่องมือการหาค่าดีที่สุด โดยระบุวิธีเป็น 'SLSQP'
(ศึกษารายละเอียดเพิ่มเติมจากเวปไซต์ของไซไพ หากสนใจ) โดยภายใน เมท็อด train
ได้กำหนดค่า minf
และ gradf
ซึ่งคือ ฟังก์ชันจุดประสงค์ และฟังก์ชันเกรเดียนต์ ตามลำดับ พร้อมทั้งข้อจำกัดแบบอสมการ ineq_cons
.
หมายเหตุ การใช้งานจริง สิ่งที่ต้องการ คือการทำนายกลุ่มของข้อมูล. เมท็อด decision_score
ใช้คำนวณค่าฟังก์ชันแบ่งแยก ซึ่งจะนำไปใช้ตัดสินกลุ่ม. ส่วนค่าของ \(\boldsymbol{w}\) และ \(b\) นั้น เป็นรายละเอียดภายใน ไม่จำเป็นต้องรายงาน และการใช้งานจริงของซัพพอร์ตเวกเตอร์แมชชีน ก็จะไม่มีการคำนวณค่า \(\boldsymbol{w}\) ออกมา เพราะว่า รูปแบบการคำนวณถูกแปลงไป เพื่อใช้ประโยชน์จากฟังก์ชันเคอร์เนล. ดูแบบฝึกหัด 1.0.0.1.2 สำหรับโปรแกรมซัพพอร์ตเวกเตอร์แมชชีนที่ใช้งานจริง.
class primal_SVM:
def __init__(self):
self.bopt = None
self.wopt = None
self.phi = None
def train(self, x, y, wb0,
={'ftol': 1e-9, 'disp': True}):
optionsassert (x.shape[1] == y.shape[1]) and (y.shape[0] == 1)
= x.shape[1]
N # projected x to z
= self.phi(x)
z assert z.shape[1] == x.shape[1]
= z.shape[0] # number of projected dimensions
D
if wb0 is None: # w: (D,1), b: scalar
= np.random.normal(0, 1, D+1)
wb0
def primal_ineq(w, b):
= w.reshape((-1,1))
w = np.asscalar(b)
b = y.reshape((1,-1)) * \
cineq -1))) + b) -1
(np.dot(w.T,z.reshape((D,return cineq.reshape((-1,)) # (M,)
def primal_dc(w, b):
= y.reshape((1,-1)) * z.reshape((D,-1))
gradw = y.reshape((1,-1))
gradb = np.hstack( (gradw.T, gradb.T) )
dc return dc.reshape((-1,D+1)) # (M,D+1)
# inequality constraints: y[i] * ( w.T x[i] + b)-1 >= 0
= {'type': 'ineq',
ineq_cons 'fun' : lambda wb: primal_ineq(wb[:-1], wb[-1]),
'jac' : lambda wb: primal_dc(wb[:-1], wb[-1])}
# Objective
def primal_f(wb): # primal loss: minimization form
= wb[:-1].reshape((-1,1)) # D x 1
w = 0.5 * np.dot(w.T, w) # scalar
L return np.asscalar(L)
def primal_grad(wb): # gradient of primal loss
= wb[:-1].reshape((-1,1)) # D x 1
w = np.vstack((w, 0))
pgrad return pgrad.reshape((-1,)) # (D,)
= lambda wb: primal_f(wb)
minf = lambda wb: primal_grad(wb)
gradf = optimize.minimize(minf, wb0, method='SLSQP',
res =gradf, constraints=ineq_cons, options=options)
jac
if not res.success:
return res
# Train succeeds.
self.wopt = res.x[:-1].reshape((-1,1))
self.bopt = res.x[-1]
return res
def decision_score(self, x):
# Project to feature space
= self.phi(x)
z assert z.shape[0] == self.wopt.shape[0]
# Compute the score
= np.dot(self.wopt.T, z) + self.bopt
yhat return yhat # 1 x N
ปัญหาเดียวกับแบบฝึกหัด 1.0.0.1.1 แต่แก้ปัญหาจากปัญหาคู่. \[\begin{eqnarray} \underset{\boldsymbol{\alpha}}{\mathrm{maximize}} & \sum_{i=1}^N \alpha_i - \frac{1}{2}\sum_{i=1}^N \sum_{j=1}^N \alpha_i \alpha_j y_i y_j k(\boldsymbol{x}_i, \boldsymbol{x}_j) \nonumber \\ \mbox{s.t.} & \nonumber \\ & \begin{array}{ll} \sum_{i=1}^N \alpha_i y_i = 0, & \\ 0 \leq \alpha_i \leq C & \mbox{ สำหรับ } i =1, \ldots, N. \end{array} \nonumber \end{eqnarray}\] เนื่องจาก ปัญหาคู่ สำหรับกรณีแบ่งแยกได้โดยสมบูรณ์ กับกรณีทั่วไปต่างกัน เพียงข้อกำหนด \(\alpha_i \leq C\) ซึ่งหากเลือกค่า \(C\) ขนาดใหญ่ก็จะให้ผลแบบเดียวกับกรณีแบ่งแยกได้โดยสมบูรณ์ ตัวอย่างข้างล่างนี้ จึงใช้รูปแบบที่มีอภิมานพารามิเตอร์ \(C\).
โปรแกรมในรายการ [code: cSVM] แสดงตัวอย่างโปรแกรมซัพพอร์ตเวกเตอร์แมชชีน. สังเกต วิธีการเขียนโปรแกรมจะใช้การทำเวคตอไรเซชั่นมากที่สุดเท่าที่จะทำได้ เนื่องจากประสิทธิภาพการคำนวณและความยืดหยุ่น. ตัวอย่าง เช่น หากการคำนวณค่าพารามิเตอร์ \(b\) คือ \(b_o = y_i - \sum_{j \in S} \alpha_j y_j k(\boldsymbol{x}_j, \boldsymbol{x}_i)\) สำหรับ \(i \in \{j: 0 < \alpha_j < C \}\). นั่นคือ เทียบเท่า \(b_o = y_i - (\boldsymbol{\alpha}^T \odot \boldsymbol{y}) \cdot \boldsymbol{K}_{:,i}\) สำหรับ \(i \in \{j: 0 < \alpha_j < C \}\). หมายเหตุ โปรแกรม [code: cSVM] คำนวณค่า \(b\) ด้วยค่าเฉลี่ย ซึ่งจะซับซ้อนกว่าตัวอย่างการทำเวคตอไรเซชั่นที่อภิปรายข้างต้นเล็กน้อย.
คล้ายกับตัวอย่างในแบบฝึึกหัด 1.0.0.1.1 คำสั่งข้างล่าง แสดงตัวอย่างการฝีก และการอนุมานด้วยซัพพอร์ตเวกเตอร์แมชชีน
svm = cSVM()
svm.kernel = lambda xq, xp: np.dot(xq.T, xp) # linear kernel
C = 1
res = svm.train(datax, datay, C=C)
svm.decision_score(testx)
ในตัวอย่าง ใช้อภิมานพารามิเตอร์ \(C=1\) ฝึกด้วยข้อมูล datax
และ datay
ที่ต้องเป็นชนิด np.array
สัดส่วน (D, N)
และ (1, N)
ตามลำดับ เมื่อ D
และ N
แทนจำนวนมิติของอินพุต และจำนวนจุดข้อมูล ตามลำดับ. คำสั่งสุดท้าย ใช้คำนวณค่าฟังก์ชันแบ่งแยกสำหรับ ข้อมูลทดสอบ testx
.
จงศึกษาวิธีการเขียนโปรแกรม ทดลองใช้งาน สังเกตผล อภิปราย และสรุป. ทดลองค่า \(C\) ต่าง ๆ และรายงานดังตัวอย่างในรูป [fig: csvm different Cs].
class cSVM:
def __init__(self):
self.epsilon = 0.001
self.bopt = None
self.sv = None
self.sy = None
self.salpha = None
self.kernel = None
def train(self, x, y, C=1, a0=None,
={'ftol': 1e-9, 'disp': True}):
optionsassert (x.shape[1] == y.shape[1]) and (y.shape[0] == 1)
= x.shape[1]
N if a0 is None:
= np.random.normal(0, 1, N)
a0
# parameter bounds: 0 <= alpha_i <= C for all i's
= optimize.Bounds([0 for i in range(N)],
bounds for i in range(N)])
[C
# inequality constraints: dummy
= {'type': 'ineq',
ineq_cons 'fun' : lambda a: np.array([0]), # (1,)
'jac' : lambda a: np.zeros((1,N))} # (M,N)
# equality constraint: sum_i y_i alpha_i = 0
= {'type': 'eq', 'fun' : lambda a: np.dot(y,
eq_cons -1, 1))).reshape((-1,)),
a.reshape(('jac' : lambda a: y.reshape((1,N))}
# Objective
= self.kernel(x, x) # N x N
K = np.dot(y.T, y) * K # N x N
H
def dual_minf(a, H): # dual loss in minimization form
= a.reshape((-1,1))
a = 0.5 * np.dot(a.T, np.dot(H, a)) - np.sum(a)
Q return np.asscalar(Q)
def dual_grad(a, H, N): # gradient of dual loss
= np.dot(H, a.reshape((-1,1))) - np.ones((N,1))
dQ return dQ.reshape((-1,)) # (N,)
= lambda a: dual_minf(a, H)
minf = lambda a: dual_grad(a, H, N)
gradf = optimize.minimize(minf, a0, method='SLSQP',
res =gradf, constraints=[ineq_cons, eq_cons],
jac=bounds, options=options)
bounds
if not res.success:
return res
= res.x.reshape((-1,1))
alpha = np.where(alpha > self.epsilon)[0]
sv_ids = np.where( np.logical_and(alpha > self.epsilon,
svp_ids < C - self.epsilon) )[0]
alpha = len(sv_ids)
Ns = len(svp_ids)
Np
if Np == 0: # No support vector on the edge
print('# No support vector on the edge.')
= sv_ids
svp_ids = Ns
Np
# support vectors
self.sv = x[:, sv_ids] # D x Ns
self.sy = y[0, sv_ids].reshape((1,-1)) # 1 x Ns
= y[0, svp_ids].reshape((1,-1)) # 1 x Np
spy
# support alphas
self.salpha = alpha[sv_ids]
# optimal b
= K[np.repeat(sv_ids, Np),
sK
np.tile(svp_ids, Ns)].reshape((Ns, Np)) self.bopt = np.mean(spy - \
self.salpha.T * self.sy, sK))
np.dot(return res
def decision_score(self, x):
assert x.shape[0] == self.sv.shape[0] # x in D x N
# Compute support kernels
= self.kernel(x, self.sv) # N x Ns
sK assert (sK.shape[0] == x.shape[1])
and (sK.shape[1] == self.sv.shape[1])
= np.dot(sK, self.salpha*self.sy.reshape((-1,1))) \
yhat + self.bopt
return yhat.T # 1 x N
จงสร้างข้อมูลที่ไม่สามารถแบ่งแยกสมบูรณ์ได้เชิงเส้น โดยจะสร้างให้มีลักษณะใดก็ได้ แต่ควรทำให้สามารถตรวจดูได้สะดวก เช่น อินพุตควรจะเป็นสองมิติ. ตัวอย่างอาจจะเช่นที่แสดงในรูป 5. ทดลองใช้ซัพพอร์ตเวกเตอร์แมชชีน กับเคอร์เนลเชิงเส้น และเคอร์เนลเกาส์เซียนที่ค่า \(\sigma\) ต่าง ๆ. ดูรูป [fig: csvm linear kernel on wave datapoints] กับ [fig: csvm gaussian kernels] สำหรับตัวอย่าง. ออกแบบการทดลอง เพื่อศึกษาผลของ \(C\) และ \(\sigma\). ทดลอง สังเกตผล อภิปราย และสรุป.
ตัวอย่าง คำสั่งข้างล่างแสดงการกำหนดค่าเคอร์เนลของตัวแปรวัตถุ (จากcSVM
) ให้เป็นเคอร์เนลเกาส์เซียน (ใช้ \(\sigma = 2\))
svm.kernel = lambda xq, xp: gaussian(xq, xp, sigma=2)
โดยโปรแกรม gaussian
แสดงในรายการ [code: gaussain kernel].
def gaussian(xq, xp, sigma=1):
assert xq.shape[0] == xp.shape[0] # xq: D x Nx, xp: D x Ns
= xq.shape[1]
Nx = xp.shape[1]
Ns = np.zeros((Nx, Ns))
K
= -1/(2*sigma**2)
c for i in range(Nx):
= xq[:,[i]] - xp # (D,1)-(D,Ns): broadcast to (D,Ns)
vi = np.exp(c*np.sum(vi**2, axis=0)) # (Ns,)
K[i,:]
return K # Nx x Ns